From 94671ad8a136f22988b75d7583a875ce8f7ec48f Mon Sep 17 00:00:00 2001 From: rabea-Al Date: Wed, 15 Oct 2025 01:14:31 +0800 Subject: [PATCH 1/2] Add CLI update tests (PR #450) --- tests/cli_tests.py | 273 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 273 insertions(+) diff --git a/tests/cli_tests.py b/tests/cli_tests.py index 3a01bcc8..8727a0f7 100644 --- a/tests/cli_tests.py +++ b/tests/cli_tests.py @@ -728,3 +728,276 @@ 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: " + 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") From 543aa683a1c2993e8969be28a65b50852d6c1093 Mon Sep 17 00:00:00 2001 From: rabea-Al Date: Wed, 15 Oct 2025 03:04:06 +0800 Subject: [PATCH 2/2] over core libs no-overwrite + preserve locals w/o prune --- tests/cli_tests.py | 89 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/tests/cli_tests.py b/tests/cli_tests.py index 8727a0f7..dc8551ca 100644 --- a/tests/cli_tests.py +++ b/tests/cli_tests.py @@ -1001,3 +1001,92 @@ def test_39_update_repo_override_with_ref(tmp_path): 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 ' 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"