Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
362 changes: 362 additions & 0 deletions tests/cli_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -728,3 +728,365 @@ def test_33_terminal_run_import_check():
assert "Compiled" in output, "Expected 'Compiled' not found in output."
assert "Finished Executing" in output, "Expected 'Finished Executing' not found in output."
assert Path("sqlite_sample.py").exists(), "Expected compiled .py file not found."

def test_34_update_basic(tmp_path):
"""
Update end-to-end:
1) init + install flask
2) append a local marker to flask_components.py
3) 'xircuits update flask' should create timestamped .bak and remove marker
4) second update is idempotent (no new .bak)
"""
os.chdir(tmp_path)

# init + install
stdout, stderr, rc = run_command("xircuits init", timeout=30)
stdout, stderr, rc = run_command("xircuits install flask", timeout=180)
install_out = (stdout + stderr).lower()
assert "library flask ready to use." in install_out

lib_dir = Path("xai_components") / "xai_flask"
target_file = lib_dir / "flask_components.py"
assert target_file.exists(), f"Missing: {target_file}"

# local change
marker = "# LOCAL_CHANGE_MARKER_FOR_UPDATE_TEST\n"
with open(target_file, "a", encoding="utf-8", errors="ignore") as f:
f.write(marker)
with open(target_file, "r", encoding="utf-8", errors="ignore") as f:
assert marker in f.read(), "Marker inject failed."

# first update should back up and restore
stdout, stderr, rc = run_command("xircuits update flask", timeout=300)
out1 = (stdout or "") + (stderr or "")
assert "xai_flask update (" in out1, "Missing update summary on first run."

bak_suffix = r"\.\d{8}-\d{6}\.bak$"
backup_re = re.compile(r"^flask_components\.py" + bak_suffix)
backups_after_first = {p.name for p in target_file.parent.iterdir()
if p.is_file() and backup_re.match(p.name)}
assert backups_after_first, "No timestamped backup created on first update."

with open(target_file, "r", encoding="utf-8", errors="ignore") as f:
assert marker not in f.read(), "Marker still present after update; file not restored."

# dependencies installed in default mode
assert "dependencies for xai_flask installed." in out1.lower()

# second update should not create new .bak
stdout, stderr, rc = run_command("xircuits update flask", timeout=300)
out2 = (stdout or "") + (stderr or "")
assert "xai_flask update (" in out2, "Missing update summary on second run."

backups_after_second = {p.name for p in target_file.parent.iterdir()
if p.is_file() and backup_re.match(p.name)}
assert backups_after_second == backups_after_first, \
"Second update created additional backups; should be idempotent."


def test_35_update_dry_run(tmp_path):
"""
Dry-run Update:
- modify file
- run 'xircuits update flask --dry-run'
- no NEW .bak created
- marker remains
- diff file mentioned; no deps install
"""
os.chdir(tmp_path)

stdout, stderr, rc = run_command("xircuits init", timeout=30)
stdout, stderr, rc = run_command("xircuits install flask", timeout=180)

lib_dir = Path("xai_components") / "xai_flask"
target_file = lib_dir / "flask_components.py"
assert target_file.exists()

backup_re = re.compile(r"^flask_components\.py\.\d{8}-\d{6}\.bak$")
pre_backups = {
p.name for p in target_file.parent.iterdir()
if p.is_file() and backup_re.match(p.name)
}

marker = "# LOCAL_CHANGE_MARKER_FOR_UPDATE_TEST_DRYRUN\n"
with open(target_file, "a", encoding="utf-8", errors="ignore") as f:
f.write(marker)

stdout, stderr, rc = run_command("xircuits update flask --dry-run", timeout=180)
output = (stdout or "") + (stderr or "")
assert rc == 0, f"Dry-run failed.\n{output}"

# parse "would backup as: <name>"
m = re.search(r"would backup as:\s*(flask_components\.py\.\d{8}-\d{6}\.bak)", output)
assert m, f"Couldn't parse 'would backup as' from output.\n{output}"
would_backup_name = m.group(1)

post_backups = {
p.name for p in target_file.parent.iterdir()
if p.is_file() and backup_re.match(p.name)
}

assert would_backup_name not in post_backups, \
f"Dry-run created backup unexpectedly: {would_backup_name}"
assert post_backups == pre_backups, \
f"Dry-run should not create backups.\nBefore: {sorted(pre_backups)}\nAfter: {sorted(post_backups)}"

with open(target_file, "r", encoding="utf-8", errors="ignore") as f:
assert marker in f.read(), "Marker should remain after dry-run."

unexpected_dep = ["Installing Python dependencies", "Dependencies for xai_flask installed"]
assert not any(h.lower() in output.lower() for h in unexpected_dep), \
"Dry-run should not install deps."
assert "dry-run.diff.txt" in output.lower(), \
"Expected dry-run diff mention in output."



def test_36_update_specific_ref(tmp_path):
"""
Specific ref to v1.17.0:
- '(ref=v1.17.0)' appears in output
- requirements.txt contains 'flask-cors==4.0.0'
"""
os.chdir(tmp_path)

stdout, stderr, rc = run_command("xircuits init", timeout=30)
stdout, stderr, rc = run_command("xircuits install flask", timeout=180)

lib_dir = Path("xai_components") / "xai_flask"
assert lib_dir.exists()
reqs = lib_dir / "requirements.txt"
assert reqs.exists()

stdout, stderr, rc = run_command("xircuits update flask --ref v1.17.0", timeout=300)
output = (stdout or "") + (stderr or "")
assert rc == 0, f"Update with ref failed.\n{output}"

req_text = reqs.read_text(encoding="utf-8", errors="ignore").lower()
assert ("(ref=v1.17.0)" in output.replace(" ", "")) and ("flask-cors==4.0.0" in req_text), \
(
"Strict ref update check failed:\n"
"- Expected '(ref=v1.17.0)' in CLI output AND 'flask-cors==4.0.0' in requirements.txt\n"
f"--- CLI output ---\n{output}\n"
f"--- requirements.txt ---\n{req_text}"
)


def test_37_update_prune_archives_locals(tmp_path):
"""
Prune:
- add local-only file/dir
- 'xircuits update flask --prune' archives with timestamped .bak and removes originals
- subsequent dry-run must not suggest backups for these local names
"""
os.chdir(tmp_path)
stdout, stderr, rc = run_command("xircuits init", timeout=30)
stdout, stderr, rc = run_command("xircuits install flask", timeout=180)

lib_dir = Path("xai_components") / "xai_flask"
assert lib_dir.exists()

local_file = lib_dir / "LOCAL_ONLY.md"
local_dir = lib_dir / "local_extra" / "subdir"
local_dir.mkdir(parents=True, exist_ok=True)
local_file.write_text("local-only note", encoding="utf-8")
(local_dir / "keep.txt").write_text("keep me", encoding="utf-8")

assert local_file.exists()
assert local_dir.exists()

stdout, stderr, rc = run_command("xircuits update flask --prune", timeout=300)
output = (stdout or "") + (stderr or "")
assert "xai_flask update (" in output

bak_suffix = r"\.\d{8}-\d{6}\.bak$"

# file archived
file_bak_exists = any(
re.match(r"LOCAL_ONLY\.md" + bak_suffix, p.name) for p in lib_dir.iterdir() if p.is_file()
)
assert file_bak_exists, "Expected a timestamped .bak for LOCAL_ONLY.md"
assert not local_file.exists(), "Original LOCAL_ONLY.md should be removed"

# dir archived
dir_bak_exists = any(
(p.is_dir() and re.match(r"local_extra" + bak_suffix, p.name))
for p in lib_dir.iterdir()
)
assert dir_bak_exists, "Expected a timestamped .bak directory for local_extra/"
assert not (lib_dir / "local_extra").exists(), "Original local_extra/ should be removed"

stdout, stderr, rc = run_command("xircuits update flask --dry-run", timeout=180)
dry = (stdout or "") + (stderr or "")
assert rc == 0, f"dry-run after prune failed:\n{dry}"

for forbidden in ["LOCAL_ONLY.md", "local_extra/"]:
assert forbidden not in dry, f"Dry-run suggests local artifacts still present: {forbidden}"


def test_38_update_without_installing_deps(tmp_path):
"""
--install-deps=false:
- no pip output
- files updated (timestamped .bak) and marker removed
"""
os.chdir(tmp_path)

stdout, stderr, rc = run_command("xircuits init", timeout=30)
stdout, stderr, rc = run_command("xircuits install flask", timeout=180)

lib = Path("xai_components") / "xai_flask"
tgt = lib / "flask_components.py"
assert lib.exists() and tgt.exists()

marker = "# LOCAL_MARKER_FOR_INSTALL_DEPS_FALSE\n"
with tgt.open("a", encoding="utf-8", errors="ignore") as w:
w.write(marker)

stdout, stderr, rc = run_command("xircuits update flask --install-deps=false", timeout=300)
out = (stdout or "") + (stderr or "")
assert rc == 0, f"update --install-deps=false failed:\n{out}"

forbidden = [
"Installing Python dependencies",
"Installing collected packages",
"Requirement already satisfied",
"Successfully installed",
"Uninstalling",
]
assert not any(s in out for s in forbidden), (
"Expected NO pip activity with --install-deps=false, but found:\n" +
"\n".join(s for s in forbidden if s in out)
)

bak_suffix = r"\.\d{8}-\d{6}\.bak$"
bak_re = re.compile(r"^flask_components\.py" + bak_suffix)
baks = {p.name for p in lib.iterdir() if p.is_file() and bak_re.match(p.name)}
assert baks, "Expected timestamped .bak (files updated) but none found."

now = tgt.read_text(encoding="utf-8", errors="ignore")
assert marker not in now, "Local marker still present; backup/restore failed."


def test_39_update_repo_override_with_ref(tmp_path):
"""
--repo + --ref:
- update from an explicit repo/ref should create .bak and remove local marker
"""
import re
from pathlib import Path
import os

os.chdir(tmp_path)

run_command("xircuits init", timeout=30)
run_command("xircuits install flask", timeout=180)

lib = Path("xai_components") / "xai_flask"
tgt = lib / "flask_components.py"
assert tgt.exists()

marker = "# LOCAL_MARKER_FOR_REPO_OVERRIDE\n"
tgt.write_text(tgt.read_text(encoding="utf-8", errors="ignore") + marker, encoding="utf-8")

repo = "https://github.com/XpressAI/xai-flask"
cmd = f"xircuits update flask --repo {repo} --ref main"
stdout, stderr, rc = run_command(cmd, timeout=300)
out = (stdout or "") + (stderr or "")
assert rc == 0, f"--repo update failed:\n{out}"

bak_suffix = r"\.\d{8}-\d{6}\.bak$"
bak_re = re.compile(r"^flask_components\.py" + bak_suffix)
baks = {p.name for p in lib.iterdir() if p.is_file() and bak_re.match(p.name)}
assert baks, "Expected .bak after repo override"

assert marker not in tgt.read_text(encoding="utf-8", errors="ignore")

def test_40_core_update_no_overwrite(tmp_path):
"""
Core libs: 'xircuits update <core>' fails (non-cloneable /tree URL),
no backups created, and local edits remain intact.
"""
os.chdir(tmp_path)

# init
stdout, stderr, rc = run_command("xircuits init", timeout=30)

# use xai_utils as a core example
lib_dir = Path("xai_components") / "xai_utils"
target = lib_dir / "utils.py"
assert target.exists(), f"Missing core file: {target}"

# add a local marker
marker = "# CORE_UPDATE_MARKER\n"
with target.open("a", encoding="utf-8", errors="ignore") as w:
w.write(marker)

# snapshot existing .bak (should stay unchanged)
bak_re = re.compile(r"^utils\.py\.\d{8}-\d{6}\.bak$")
pre_baks = {p.name for p in lib_dir.iterdir() if p.is_file() and bak_re.match(p.name)}

# run update (expected to FAIL for core libs)
stdout, stderr, rc = run_command("xircuits update utils", timeout=120)
out = (stdout or "") + (stderr or "")

# expect non-zero exit and the tree-url failure hint
assert rc != 0, "Core update should fail with current /tree URL behavior."
assert ("tree/master" in out) or ("not found" in out.lower()) or ("returned non-zero exit status" in out.lower()), \
f"Unexpected failure output:\n{out}"

# no new backups created
post_baks = {p.name for p in lib_dir.iterdir() if p.is_file() and bak_re.match(p.name)}
assert post_baks == pre_baks, "Failure must not create .bak files for core libs."

# local marker still there (no overwrite happened)
now = target.read_text(encoding="utf-8", errors="ignore")
assert marker in now, "Core file was modified unexpectedly on failed update."

def test_41_update_preserves_local_artifacts_without_prune(tmp_path):
"""
Update (no --prune):
- local-only file/dir should remain (no .bak, no removal)
- tracked file with marker gets backed up & restored
"""
os.chdir(tmp_path)

# init + install remote lib
stdout, stderr, rc = run_command("xircuits init", timeout=30)
stdout, stderr, rc = run_command("xircuits install flask", timeout=180)

lib_dir = Path("xai_components") / "xai_flask"
target = lib_dir / "flask_components.py"
assert target.exists()

# local-only artifacts
local_file = lib_dir / "LOCAL_ONLY.md"
local_dir = lib_dir / "local_extra"
local_file.write_text("local note", encoding="utf-8")
(local_dir / "keep.txt").parent.mkdir(parents=True, exist_ok=True)
(local_dir / "keep.txt").write_text("keep me", encoding="utf-8")

# marker on a tracked file to force update path
marker = "# LOCAL_MARKER_NO_PRUNE\n"
with target.open("a", encoding="utf-8", errors="ignore") as w:
w.write(marker)

# run normal update (NO --prune)
stdout, stderr, rc = run_command("xircuits update flask", timeout=300)
out = (stdout or "") + (stderr or "")
assert "xai_flask update (" in out

# tracked file: expect timestamped .bak and marker removed
bak_re = re.compile(r"^flask_components\.py\.\d{8}-\d{6}\.bak$")
baks = {p.name for p in lib_dir.iterdir() if p.is_file() and bak_re.match(p.name)}
assert baks, "Expected .bak for tracked file"
assert marker not in target.read_text(encoding="utf-8", errors="ignore")

# local-only artifacts: must still exist; no .bak created for them
assert local_file.exists(), "LOCAL_ONLY.md should remain without --prune"
assert local_dir.exists(), "local_extra/ should remain without --prune"
ts_suffix = r"\.\d{8}-\d{6}\.bak$"
assert not any(re.match(r"LOCAL_ONLY\.md" + ts_suffix, p.name) for p in lib_dir.iterdir()), \
"Unexpected .bak for LOCAL_ONLY.md without --prune"
assert not any(p.is_dir() and re.match(r"local_extra" + ts_suffix, p.name) for p in lib_dir.iterdir()), \
"Unexpected .bak dir for local_extra/ without --prune"
Loading