Files
Agent-System-Dev/tests/test_workspace.py
T
weichen a408e49167 Prove interrupted nested transitions preserve owner recovery data
Why:
An interrupted multi-component transition must retain original staged, ignored and external-worktree data after some independent copies have completed.

Evidence:
SELF scratch fixture interrupts a real nested copy after App is saved and Logging is partially copied; originals and external Git metadata remain identical, recorded partial data persists and retry refuses to discard it.

Impact:
Add failure-path coverage and clarify reachability checks before component branch cleanup; no transition implementation change.

Refs:
Dependency-workspace R9/R15 and independent reliability coverage followup.
2026-10-13 18:33:21 +00:00

824 lines
49 KiB
Python

"""Workspace behavior against disposable real Git repositories and Go modules."""
import hashlib
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
import unittest
from unittest.mock import patch
import zipfile
SOURCE = Path(__file__).resolve().parents[1]
class WorkspaceTests(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory(prefix="workspace-test-")
self.addCleanup(self.tmp.cleanup)
self.base = Path(self.tmp.name)
self.root = self.base / "project"
self.root.mkdir()
self.env = os.environ.copy()
for key in list(self.env):
if key.startswith("GIT_CONFIG_") or key in ("GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GOWORK"):
self.env.pop(key, None)
self.env.update({"GIT_CONFIG_NOSYSTEM": "1", "GIT_CONFIG_GLOBAL": "/dev/null",
"GIT_ALLOW_PROTOCOL": "file", "GIT_TERMINAL_PROMPT": "0",
"GIT_AUTHOR_NAME": "Workspace Test", "GIT_AUTHOR_EMAIL": "test@example.invalid",
"GIT_COMMITTER_NAME": "Workspace Test", "GIT_COMMITTER_EMAIL": "test@example.invalid",
"GOTOOLCHAIN": "local", "GOPROXY": "off", "GOSUMDB": "off"})
self.git(self.root, "init", "--initial-branch=main")
(self.root / ".gitignore").write_text(".workspace/\ngo.work\ngo.work.sum\n")
self.git(self.root, "add", ".gitignore")
self.git(self.root, "commit", "-m", "Create project")
self.remote, self.author, self.first = self.component("App")
self.manifest = {"schema": 1, "components": [self.entry("App", self.remote)]}
self.lock = {"schema": 1, "components": {"App": {"commit": self.first}}}
self.save_manifest()
def command(self, cwd, *args, check=True, env=None, separate_stderr=False):
result = subprocess.run(args, cwd=cwd, env=env or self.env, text=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE if separate_stderr else subprocess.STDOUT, timeout=60)
if check:
self.assertEqual(result.returncode, 0, result.stdout + (result.stderr or ""))
return result
def git(self, cwd, *args, **kwargs):
return self.command(cwd, "git", *args, **kwargs)
def cli(self, *args, check=True, env=None):
return self.command(self.root, sys.executable, str(SOURCE / "scripts/workspace.py"),
"--root", str(self.root), *args, check=check, env=env)
def component(self, name, files=None):
remote = self.base / (name + ".git")
author = self.base / (name + "-author")
self.git(self.base, "init", "--bare", "--initial-branch=main", str(remote))
self.git(remote, "config", "gc.auto", "0")
self.git(remote, "config", "receive.autogc", "false")
self.git(self.base, "init", "--initial-branch=main", str(author))
for name, value in (files or {"content": "first\n", ".gitignore": "private/\n"}).items():
path = author / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(value)
self.git(author, "add", ".")
self.git(author, "commit", "-m", "First component")
self.git(author, "remote", "add", "origin", str(remote))
self.git(author, "push", "-u", "origin", "main")
return remote, author, self.git(author, "rev-parse", "HEAD").stdout.strip()
def entry(self, name, remote, role="application", modules=None):
return {"id": name, "repo": str(remote), "path": ".workspace/" + name,
"legacy_path": "modules/" + name, "role": role,
"modules": modules or [], "target": "main"}
def save_manifest(self):
(self.root / "components.json").write_text(json.dumps(self.manifest))
(self.root / "components.lock.json").write_text(json.dumps(self.lock))
def state(self):
return json.loads((self.root / ".workspace/state.json").read_text())
def test_prepare_pins_fixed_commit_and_reuses_without_advancing(self):
(self.author / "content").write_text("latest\n")
self.git(self.author, "commit", "-am", "Advance remote")
self.git(self.author, "push", "origin", "main")
self.cli("prepare")
clone = self.root / ".workspace/App"
self.assertEqual(self.git(clone, "rev-parse", "HEAD").stdout.strip(), self.first)
self.assertEqual((clone / "content").read_text(), "first\n")
self.cli("prepare")
self.assertEqual(self.state()["components"]["App"]["baseline"], self.first)
self.assertTrue((self.root / ".workspace/resolved.json").is_file())
def test_edit_registers_branch_and_prepare_preserves_work(self):
self.cli("edit", "--name", "App", "--branch", "feature/change")
clone = self.root / ".workspace/App"
(clone / "content").write_text("my change\n")
self.git(clone, "commit", "-am", "Local work")
self.cli("prepare")
self.assertEqual(self.git(clone, "branch", "--show-current").stdout.strip(), "feature/change")
self.assertEqual((clone / "content").read_text(), "my change\n")
self.cli("unlink", "--name", "App")
self.assertTrue(clone.is_dir())
def test_rejects_unknown_component_and_bad_branch_without_source_creation(self):
result = self.cli("edit", "--name", "missing", check=False)
self.assertNotEqual(result.returncode, 0)
self.assertIn("unknown component", result.stdout.lower())
result = self.cli("edit", "--name", "App", "--branch", "feature/../../escape", check=False)
self.assertNotEqual(result.returncode, 0)
self.assertFalse((self.root / ".workspace/App").exists())
def test_rejects_paths_and_symlink_escape(self):
for path in ("../escape", "/tmp/escape", ".workspace/../escape"):
with self.subTest(path=path):
self.manifest["components"][0]["path"] = path
self.save_manifest()
result = self.cli("prepare", check=False)
self.assertNotEqual(result.returncode, 0)
self.assertFalse((self.base / "escape").exists())
self.manifest["components"][0]["path"] = ".workspace/App"
self.save_manifest()
(self.root / ".workspace").symlink_to(self.author, target_is_directory=True)
result = self.cli("prepare", check=False)
self.assertNotEqual(result.returncode, 0)
self.assertFalse((self.author / "App").exists())
def test_rejects_mismatched_origin_dirty_and_git_lock_without_changes(self):
self.cli("prepare")
clone = self.root / ".workspace/App"
self.git(clone, "remote", "set-url", "origin", str(self.base / "wrong.git"))
self.assertNotEqual(self.cli("prepare", check=False).returncode, 0)
self.git(clone, "remote", "set-url", "origin", str(self.remote))
(clone / "unique").write_text("unsaved\n")
self.assertNotEqual(self.cli("edit", "--name", "App", check=False).returncode, 0)
self.assertEqual((clone / "unique").read_text(), "unsaved\n")
(clone / "unique").unlink()
lock = clone / ".git/index.lock"
lock.write_text("owned by another operation\n")
self.assertNotEqual(self.cli("edit", "--name", "App", check=False).returncode, 0)
self.assertEqual(lock.read_text(), "owned by another operation\n")
def test_ignored_unique_files_and_shared_worktrees_block_branch_changes(self):
self.cli("prepare")
clone = self.root / ".workspace/App"
(clone / "private").mkdir()
(clone / "private/key").write_text("unique\n")
self.assertNotEqual(self.cli("edit", "--name", "App", check=False).returncode, 0)
shutil.rmtree(clone / "private")
external = self.base / "other-owner"
self.git(clone, "worktree", "add", "-b", "feature/other", str(external))
result = self.cli("edit", "--name", "App", check=False)
self.assertNotEqual(result.returncode, 0)
self.assertEqual(self.git(external, "branch", "--show-current").stdout.strip(), "feature/other")
def test_workspace_operation_lock_and_user_go_work_are_preserved(self):
area = self.root / ".workspace"
area.mkdir()
lock = area / ".operation.lock"
lock.write_text("other writer\n")
result = self.cli("prepare", check=False)
self.assertNotEqual(result.returncode, 0)
self.assertEqual(lock.read_text(), "other writer\n")
lock.unlink()
(self.root / "go.work").write_text("go 1.27\n// manual\n")
result = self.cli("edit", "--name", "App", check=False)
self.assertNotEqual(result.returncode, 0)
self.assertEqual((self.root / "go.work").read_text(), "go 1.27\n// manual\n")
def test_partial_initialization_is_retained_and_retry_is_safe(self):
self.lock["components"]["App"]["commit"] = "a" * 40
self.save_manifest()
result = self.cli("prepare", check=False)
self.assertNotEqual(result.returncode, 0)
clone = self.root / ".workspace/App"
self.assertTrue((clone / ".git").exists(), result.stdout)
self.assertFalse((self.root / ".workspace/.operation.lock").exists())
self.lock["components"]["App"]["commit"] = self.first
self.save_manifest()
self.cli("prepare")
self.assertEqual(self.git(clone, "rev-parse", "HEAD").stdout.strip(), self.first)
def legacy(self):
self.git(self.root, "submodule", "add", str(self.remote), "modules/App")
self.git(self.root, "commit", "-am", "Pin legacy component")
return self.root / "modules/App"
def fingerprint(self, path):
files = {}
for file in path.rglob("*"):
if ".git" not in file.relative_to(path).parts and file.is_file():
files[str(file.relative_to(path))] = hashlib.sha256(file.read_bytes()).hexdigest()
return (self.git(path, "show-ref").stdout, self.git(path, "status", "--porcelain=v1").stdout, files)
def test_transition_report_is_readonly_apply_preserves_unique_commits_refs_and_files(self):
legacy = self.legacy()
self.git(legacy, "checkout", "-b", "feature/legacy")
(legacy / "content").write_text("unpublished commit\n")
self.git(legacy, "commit", "-am", "Unpublished")
unique_head = self.git(legacy, "rev-parse", "HEAD").stdout.strip()
self.git(legacy, "branch", "feature/other-ref")
(legacy / "content").write_text("uncommitted\n")
(legacy / "unique").write_text("untracked\n")
(legacy / "private").mkdir()
(legacy / "private/data").write_text("ignored\n")
before = self.fingerprint(legacy)
report = json.loads(self.cli("transition", "--name", "App").stdout)
self.assertEqual(report["components"]["App"]["legacy"]["head"], unique_head)
self.assertFalse((self.root / ".workspace").exists())
self.cli("transition", "--name", "App", "--apply")
clone = self.root / ".workspace/App"
self.assertEqual(self.git(clone, "rev-parse", "HEAD").stdout.strip(), unique_head)
self.assertEqual(self.git(clone, "remote", "get-url", "origin").stdout.strip(), str(self.remote))
self.assertEqual((clone / "content").read_text(), "uncommitted\n")
self.assertEqual((clone / "unique").read_text(), "untracked\n")
self.assertEqual((clone / "private/data").read_text(), "ignored\n")
self.assertEqual(self.git(clone, "rev-parse", "refs/migration/heads/feature/other-ref").stdout.strip(), unique_head)
self.assertEqual(self.fingerprint(legacy), before)
self.assertFalse((clone / ".git/objects/info/alternates").exists())
def test_transition_rejects_external_symlinks_preserving_both_locations(self):
legacy = self.legacy()
outside = self.base / "outside"
outside.write_text("owner data\n")
(legacy / "escape").symlink_to(outside)
before = self.fingerprint(legacy)
result = self.cli("transition", "--name", "App", "--apply", check=False)
self.assertNotEqual(result.returncode, 0)
self.assertEqual(outside.read_text(), "owner data\n")
self.assertEqual(self.fingerprint(legacy), before)
self.assertFalse((self.root / ".workspace/App").exists())
def test_library_legacy_baseline_uses_gitlink_not_current_head(self):
legacy = self.legacy()
(legacy / "content").write_text("local newer commit\n")
self.git(legacy, "commit", "-am", "Local advance")
self.manifest["components"][0]["role"] = "go-library"
self.lock["components"] = {}
self.save_manifest()
self.cli("edit", "--name", "App")
self.assertEqual(self.git(self.root / ".workspace/App", "rev-parse", "HEAD").stdout.strip(), self.first)
self.assertEqual((legacy / "content").read_text(), "local newer commit\n")
def test_status_reports_dirty_and_verify_rejects_it(self):
self.cli("prepare")
clone = self.root / ".workspace/App"
(clone / "content").write_text("unsaved\n")
result = json.loads(self.cli("status").stdout)
self.assertTrue(result["components"]["App"]["actual"]["dirty"])
self.assertNotEqual(self.cli("verify", check=False).returncode, 0)
self.assertEqual((clone / "content").read_text(), "unsaved\n")
def test_transition_preserves_staged_blob_internal_symlink_and_reflog_commit(self):
legacy = self.legacy()
(legacy / "content").write_text("orphan commit\n")
self.git(legacy, "commit", "-am", "Reflog-only work")
orphan = self.git(legacy, "rev-parse", "HEAD").stdout.strip()
self.git(legacy, "checkout", "--detach", self.first)
(legacy / "content").write_text("staged value\n")
self.git(legacy, "add", "content")
(legacy / "content").write_text("working value\n")
(legacy / "link").symlink_to("content")
before = self.fingerprint(legacy)
self.cli("transition", "--name", "App", "--apply")
clone = self.root / ".workspace/App"
self.assertEqual(self.git(clone, "show", ":content").stdout, "staged value\n")
self.assertEqual((clone / "content").read_text(), "working value\n")
self.assertTrue((clone / "link").is_symlink())
self.assertEqual(os.readlink(clone / "link"), "content")
self.assertEqual(self.git(clone, "rev-parse", "refs/migration/reflog/" + orphan).stdout.strip(), orphan)
self.assertEqual(self.fingerprint(legacy), before)
def test_transition_refuses_initialized_nested_repository(self):
legacy = self.legacy()
nested = legacy / "child"
nested.mkdir()
self.git(nested, "init", "--initial-branch=main")
(nested / "unique").write_text("owned by nested repository\n")
result = self.cli("transition", "--name", "App", "--apply", check=False)
self.assertNotEqual(result.returncode, 0)
self.assertIn("nested repository", result.stdout)
self.assertFalse((self.root / ".workspace/App").exists())
self.assertEqual((nested / "unique").read_text(), "owned by nested repository\n")
def nested_legacy(self):
legacy = self.legacy()
errors_remote, _, _ = self.component("Errors", {
"go.mod": "module example.invalid/errors\n\ngo 1.27\n",
"value.go": 'package errors\nfunc Value() string { return "errors" }\n',
".gitignore": "private/\n"})
logging_remote, _, _ = self.component("Logging", {
"go.mod": "module example.invalid/logging\n\ngo 1.27\n",
"value.go": 'package logging\nfunc Value() string { return "logging" }\n'})
self.git(legacy, "submodule", "add", str(errors_remote), "deps/errors")
self.git(legacy, "submodule", "add", str(logging_remote), "deps/logging")
(legacy / "go.mod").write_text("module example.invalid/native\n\ngo 1.27\n\nrequire (\n example.invalid/errors v0.0.0\n example.invalid/logging v0.0.0\n)\nreplace example.invalid/errors => ./deps/errors\nreplace example.invalid/logging => ./deps/logging\n")
(legacy / "main.go").write_text('package main\nimport ("fmt"; errors "example.invalid/errors"; logging "example.invalid/logging")\nfunc main() { fmt.Print(errors.Value()+":"+logging.Value()) }\n')
self.git(legacy, "add", ".")
self.git(legacy, "commit", "-m", "Add registered nested dependencies")
self.manifest["components"][0]["modules"] = [{"path": "example.invalid/native", "directory": "."}]
self.manifest["components"] += [
self.entry("Errors", errors_remote, "go-library", [{"path": "example.invalid/errors", "directory": "."}]),
self.entry("Logging", logging_remote, "go-library", [{"path": "example.invalid/logging", "directory": "."}])]
self.save_manifest()
return legacy, legacy / "deps/errors", legacy / "deps/logging"
def test_transition_known_nested_repositories_preserves_external_worktree_and_runs_go(self):
legacy, errors, logging = self.nested_legacy()
self.git(errors, "checkout", "-b", "feature/nested")
(errors / "value.go").write_text('package errors\nfunc Value() string { return "committed" }\n')
self.git(errors, "commit", "-am", "Unpushed nested commit")
errors_head = self.git(errors, "rev-parse", "HEAD").stdout.strip()
(errors / "value.go").write_text('package errors\nfunc Value() string { return "staged" }\n')
self.git(errors, "add", "value.go")
(errors / "value.go").write_text('package errors\nfunc Value() string { return "working" }\n')
(errors / "private").mkdir()
(errors / "private/unique").write_text("nested ignored data\n")
external = self.base / "nested-dependent-worktree"
self.git(errors, "worktree", "add", "-b", "feature/external", str(external))
before = {str(path): self.fingerprint(path) for path in (legacy, errors, logging, external)}
self.cli("transition", "--name", "App", "--apply")
clone = self.root / ".workspace/Errors"
self.assertEqual(self.git(clone, "rev-parse", "HEAD").stdout.strip(), errors_head)
self.assertIn('return "staged"', self.git(clone, "show", ":value.go").stdout)
self.assertEqual((clone / "private/unique").read_text(), "nested ignored data\n")
self.assertTrue((clone / ".git").is_dir())
self.assertFalse((clone / ".git/objects/info/alternates").exists())
self.assertFalse((self.root / ".workspace/App/deps/errors/.git").exists())
self.assertEqual(self.command(self.root / ".workspace/App", "go", "run", ".").stdout, "working:logging")
for path in (legacy, errors, logging, external):
self.assertEqual(self.fingerprint(path), before[str(path)])
self.assertTrue(Path(self.git(external, "rev-parse", "--absolute-git-dir").stdout.strip()).exists())
mapping = self.state()["components"]["App"]["source"]["nested_components"]
self.assertEqual({item["id"] for item in mapping}, {"Errors", "Logging"})
def test_conflicting_duplicate_nested_component_stops_before_cloning(self):
legacy, errors, logging = self.nested_legacy()
remote = next(item["repo"] for item in self.manifest["components"] if item["id"] == "Errors")
self.git(legacy, "submodule", "add", remote, "deps/duplicate-errors")
self.git(legacy, "commit", "-am", "Add duplicate logical dependency")
duplicate = legacy / "deps/duplicate-errors"
(duplicate / "value.go").write_text("conflicting unsaved source\n")
before = self.fingerprint(legacy)
result = self.cli("transition", "--name", "App", "--apply", check=False)
self.assertNotEqual(result.returncode, 0)
self.assertIn("conflicting duplicate", result.stdout)
self.assertIn("Errors", result.stdout)
self.assertFalse((self.root / ".workspace/App").exists())
self.assertFalse((self.root / ".workspace/Errors").exists())
self.assertEqual(self.fingerprint(legacy), before)
def test_partial_nested_transition_keeps_originals_and_owner_recovery_state(self):
legacy, errors, logging = self.nested_legacy()
self.git(errors, "checkout", "-b", "feature/partial-source")
(errors / "value.go").write_text('package errors\nfunc Value() string { return "staged" }\n')
self.git(errors, "add", "value.go")
(errors / "value.go").write_text('package errors\nfunc Value() string { return "working" }\n')
(errors / "private").mkdir()
(errors / "private/unique").write_text("ignored owner data")
external = self.base / "external-owner"
self.git(errors, "worktree", "add", "--detach", str(external), "HEAD")
originals = {str(path): self.fingerprint(path) for path in (legacy, errors, logging, external)}
metadata = {str(path): self.git_metadata(path) for path in (legacy, errors, logging)}
sys.path.insert(0, str(SOURCE / "scripts"))
self.addCleanup(sys.path.remove, str(SOURCE / "scripts"))
from workspace.cli import main
copy = shutil.copy2
copied = []
def interrupt(source, destination, *args, **kwargs):
result = copy(source, destination, *args, **kwargs)
if Path(source).is_relative_to(logging):
copied.append(str(source))
raise OSError("injected partial nested copy failure")
return result
with patch.dict(os.environ, self.env, clear=True), patch("workspace.transition.shutil.copy2", side_effect=interrupt):
result = main(["--root", str(self.root), "transition", "--name", "App", "--apply"])
self.assertNotEqual(result, 0)
self.assertTrue(copied)
state = self.state()["components"]
self.assertEqual(state["App"]["mode"], "editable")
# Errors has not been copied when Logging fails; its complete original
# index/worktree/ignored data is still the owner's recovery source.
self.assertIn("staged", self.git(errors, "show", ":value.go").stdout)
self.assertIn("working", (errors / "value.go").read_text())
self.assertEqual((errors / "private/unique").read_text(), "ignored owner data")
partial = self.root / ".workspace/Logging"
self.assertTrue((partial / ".git").is_dir())
self.assertIn("partial nested copy", state["Logging"]["error"])
self.assertNotEqual(self.cli("transition", "--name", "App", "--apply", check=False).returncode, 0)
self.assertTrue(partial.exists(), "owner recovery data must not be removed on retry")
for path in (legacy, errors, logging, external):
self.assertEqual(self.fingerprint(path), originals[str(path)])
for path in (legacy, errors, logging):
self.assertEqual(self.git_metadata(path), metadata[str(path)])
def test_equivalent_recursive_nested_sources_share_an_independent_component(self):
legacy, errors, logging = self.nested_legacy()
remote = next(item["repo"] for item in self.manifest["components"] if item["id"] == "Errors")
self.git(logging, "submodule", "add", remote, "deps/errors")
(logging / "go.mod").write_text("module example.invalid/logging\n\ngo 1.27\n\nrequire example.invalid/errors v0.0.0\nreplace example.invalid/errors => ./deps/errors\n")
(logging / "value.go").write_text('package logging\nimport errors "example.invalid/errors"\nfunc Value() string { return "logging:"+errors.Value() }\n')
self.git(logging, "add", ".")
self.git(logging, "commit", "-m", "Add recursive registered dependency")
self.git(legacy, "add", "deps/logging")
self.git(legacy, "commit", "-m", "Pin recursive logging candidate")
duplicate = logging / "deps/errors"
before = {str(path): self.fingerprint(path) for path in (legacy, errors, logging, duplicate)}
self.cli("transition", "--name", "App", "--apply")
self.assertEqual(len(self.state()["components"]["Errors"]["legacy_snapshots"]), 2)
self.assertEqual(self.command(self.root / ".workspace/App", "go", "run", ".").stdout, "errors:logging:errors")
for path in (legacy, errors, logging, duplicate):
self.assertEqual(self.fingerprint(path), before[str(path)])
def git_metadata(self, path):
common = Path(self.git(path, "rev-parse", "--path-format=absolute", "--git-common-dir").stdout.strip())
return {str(file.relative_to(common)): hashlib.sha256(file.read_bytes()).hexdigest()
for file in common.rglob("*") if file.is_file()}
def preserved_source_refs(self, path):
output = self.git(path, "for-each-ref", "--format=%(refname) %(objectname)", "refs/migration/sources/").stdout
return dict(row.split() for row in output.splitlines())
def test_repeated_transition_keeps_alias_tag_versions_and_snapshot_history(self):
legacy, errors, logging = self.nested_legacy()
remote = next(item["repo"] for item in self.manifest["components"] if item["id"] == "Errors")
self.git(logging, "submodule", "add", remote, "deps/errors")
self.git(logging, "commit", "-am", "Add equivalent dependency alias")
self.git(legacy, "add", "deps/logging")
self.git(legacy, "commit", "-m", "Pin alias source")
alias = logging / "deps/errors"
self.git(alias, "tag", "-a", "alias-note", "-m", "First unique alias annotation")
first_tag = self.git(alias, "rev-parse", "refs/tags/alias-note").stdout.strip()
self.cli("transition", "--name", "App", "--apply")
clone = self.root / ".workspace/Errors"
saved_refs = self.preserved_source_refs(clone)
self.assertIn(first_tag, saved_refs.values())
saved_snapshots = self.state()["components"]["Errors"]["legacy_snapshots"]
self.git(alias, "tag", "-f", "-a", "alias-note", "-m", "Second unique alias annotation")
second_tag = self.git(alias, "rev-parse", "refs/tags/alias-note").stdout.strip()
self.assertNotEqual(first_tag, second_tag)
original = {str(path): self.git_metadata(path) for path in (legacy, errors, logging, alias)}
self.cli("transition", "--name", "App", "--apply")
current_refs = self.preserved_source_refs(clone)
self.assertEqual({name: current_refs.get(name) for name in saved_refs}, saved_refs,
"a new source snapshot must not overwrite any previous preserved reference")
self.assertIn(second_tag, current_refs.values())
history = self.state()["components"]["Errors"]["legacy_snapshots"]
for snapshot in saved_snapshots:
self.assertIn(snapshot, history, "prior captured metadata must remain available")
# This object was present only through the secondary alias's annotated tag.
# Disposable GC proves it remains reachable after the source tag moves.
self.git(clone, "reflog", "expire", "--expire=now", "--all")
self.git(clone, "gc", "--prune=now")
self.assertEqual(self.git(clone, "cat-file", "-t", first_tag).stdout.strip(), "tag")
self.assertEqual(self.git(clone, "cat-file", "-t", second_tag).stdout.strip(), "tag")
for path in (legacy, errors, logging, alias):
self.assertEqual(self.git_metadata(path), original[str(path)])
def test_repeated_transition_preserves_dirty_index_ignored_and_external_git_metadata(self):
legacy = self.legacy()
self.git(legacy, "checkout", "-b", "feature/legacy")
(legacy / "content").write_text("staged source\n")
self.git(legacy, "add", "content")
(legacy / "content").write_text("working source\n")
(legacy / "private").mkdir()
(legacy / "private/unique").write_text("ignored source\n")
external = self.base / "external-owner"
self.git(legacy, "worktree", "add", "-b", "feature/external", str(external))
(external / "content").write_text("external owner work\n")
metadata = self.git_metadata(legacy)
self.cli("transition", "--name", "App", "--apply")
clone = self.root / ".workspace/App"
refs = self.preserved_source_refs(clone)
self.cli("transition", "--name", "App", "--apply")
self.assertEqual(self.git(clone, "show", ":content").stdout, "staged source\n")
self.assertEqual((clone / "content").read_text(), "working source\n")
self.assertEqual((clone / "private/unique").read_text(), "ignored source\n")
self.assertEqual((external / "content").read_text(), "external owner work\n")
self.assertEqual(self.git_metadata(legacy), metadata)
self.assertEqual(self.preserved_source_refs(clone), refs)
# A unique destination file belongs to its current owner and blocks reuse.
(clone / "private/new-owner-file").write_text("new workspace work\n")
state_before = (self.root / ".workspace/state.json").read_bytes()
result = self.cli("transition", "--name", "App", "--apply", check=False)
self.assertNotEqual(result.returncode, 0)
self.assertIn("conflicting duplicate destination", result.stdout)
self.assertEqual((clone / "private/new-owner-file").read_text(), "new workspace work\n")
self.assertEqual((self.root / ".workspace/state.json").read_bytes(), state_before)
self.assertEqual(self.git_metadata(legacy), metadata)
def test_repeated_transition_rejects_a_new_source_head_without_switching_destination(self):
legacy = self.legacy()
self.cli("transition", "--name", "App", "--apply")
clone = self.root / ".workspace/App"
refs = self.preserved_source_refs(clone)
state_before = (self.root / ".workspace/state.json").read_bytes()
(legacy / "content").write_text("new legacy head\n")
self.git(legacy, "commit", "-am", "Advance original owner branch")
new_head = self.git(legacy, "rev-parse", "HEAD").stdout.strip()
original_metadata = self.git_metadata(legacy)
result = self.cli("transition", "--name", "App", "--apply", check=False)
self.assertNotEqual(result.returncode, 0)
self.assertIn("conflicting duplicate destination", result.stdout)
self.assertEqual(self.git(clone, "rev-parse", "HEAD").stdout.strip(), self.first)
self.assertEqual(self.git(legacy, "rev-parse", "HEAD").stdout.strip(), new_head)
self.assertEqual(self.preserved_source_refs(clone), refs)
self.assertEqual((self.root / ".workspace/state.json").read_bytes(), state_before)
self.assertEqual(self.git_metadata(legacy), original_metadata)
def npm_fixture(self):
remote, author, head = self.component("UI", {"package.json": '{"name":"@agent-system/ui","version":"0.0.0"}\n'})
(author / "package.json").write_text('{"name":"@agent-system/ui","version":"0.0.1"}\n')
self.git(author, "commit", "-am", "Advance UI remote beyond consumer lock")
self.git(author, "push", "origin", "main")
(self.author / "package-lock.json").write_text(json.dumps({"name": "frontend", "lockfileVersion": 3,
"packages": {"": {"dependencies": {"@agent-system/ui": "git+file://" + str(remote) + "#" + head}},
"node_modules/@agent-system/ui": {"version": "0.0.0", "resolved": "git+file://" + str(remote) + "#" + head}}}))
self.git(self.author, "add", "package-lock.json")
self.git(self.author, "commit", "-m", "Lock UI package")
self.git(self.author, "push", "origin", "main")
self.lock["components"]["App"]["commit"] = self.git(self.author, "rev-parse", "HEAD").stdout.strip()
entry = self.entry("UI", remote, "npm-library")
entry["package_name"] = "@agent-system/ui"
self.manifest["components"].append(entry)
self.save_manifest()
return remote, author, head
def test_npm_library_edit_uses_consumer_lock_git_sha_not_remote_main(self):
_, _, head = self.npm_fixture()
self.cli("edit", "--name", "UI")
clone = self.root / ".workspace/UI"
self.assertEqual(self.git(clone, "rev-parse", "HEAD").stdout.strip(), head)
self.assertEqual(json.loads((clone / "package.json").read_text())["version"], "0.0.0")
self.assertEqual(self.state()["components"]["UI"]["source"]["kind"], "npm-lock")
self.assertFalse((self.root / "go.work").exists())
def test_npm_library_rejects_wrong_lock_origin_and_competing_manual_lock(self):
remote, _, head = self.npm_fixture()
self.lock["components"]["UI"] = {"commit": head}
self.save_manifest()
result = self.cli("edit", "--name", "UI", check=False)
self.assertNotEqual(result.returncode, 0)
self.assertIn("competing", result.stdout)
self.lock["components"].pop("UI")
self.save_manifest()
self.cli("prepare")
lock_path = self.root / ".workspace/App/package-lock.json"
document = json.loads(lock_path.read_text())
document["packages"]["node_modules/@agent-system/ui"]["resolved"] = "git+file://" + str(self.base / "unapproved.git") + "#" + head
lock_path.write_text(json.dumps(document))
result = self.cli("edit", "--name", "UI", check=False)
self.assertNotEqual(result.returncode, 0)
self.assertIn("origin", result.stdout)
self.assertFalse((self.root / ".workspace/UI").exists())
def test_unlink_refuses_manual_overlay_changes_without_changing_registration(self):
self.go_fixture()
self.cli("prepare")
consumer = self.root / ".workspace/Consumer"
self.command(consumer, "go", "mod", "download", "all", env=self.env | {"GOWORK": "off"})
self.cli("edit", "--name", "Native")
overlay = self.root / "go.work"
overlay.write_text(overlay.read_text() + "// manual edit\n")
result = self.cli("unlink", "--name", "Native", check=False)
self.assertNotEqual(result.returncode, 0)
self.assertTrue(self.state()["components"]["Native"]["linked"])
self.assertTrue(overlay.read_text().endswith("// manual edit\n"))
def test_missing_fixed_source_never_falls_back_to_remote_main(self):
self.lock["components"] = {}
self.save_manifest()
result = self.cli("prepare", check=False)
self.assertNotEqual(result.returncode, 0)
self.assertFalse((self.root / ".workspace/App").exists())
def test_existing_fixed_checkout_is_not_switched_after_lock_change(self):
self.cli("prepare")
(self.author / "content").write_text("advanced version\n")
self.git(self.author, "commit", "-am", "Advance")
self.git(self.author, "push", "origin", "main")
self.lock["components"]["App"]["commit"] = self.git(self.author, "rev-parse", "HEAD").stdout.strip()
self.save_manifest()
result = self.cli("prepare", check=False)
self.assertNotEqual(result.returncode, 0)
self.assertEqual(self.git(self.root / ".workspace/App", "rev-parse", "HEAD").stdout.strip(), self.first)
def advance_app_lock(self):
(self.author / "content").write_text("updated locked version\n")
counter = self.author / "revision-counter"
counter.write_text((counter.read_text() if counter.exists() else "") + "next\n")
self.git(self.author, "add", "revision-counter")
self.git(self.author, "commit", "-am", "Advance fixed source")
self.git(self.author, "push", "origin", "main")
head = self.git(self.author, "rev-parse", "HEAD").stdout.strip()
self.lock["components"]["App"]["commit"] = head
self.save_manifest()
return head
def test_explicit_fixed_refresh_preserves_old_commit_refs_and_reflog(self):
self.cli("prepare")
clone = self.root / ".workspace/App"
refs_before = self.git(clone, "for-each-ref", "--format=%(refname) %(objectname)").stdout.splitlines()
reflog_before = self.git(clone, "reflog", "--format=%H", "HEAD").stdout.splitlines()
head = self.advance_app_lock()
self.cli("prepare", "--name", "App", "--refresh-fixed")
self.assertEqual(self.git(clone, "rev-parse", "HEAD").stdout.strip(), head)
self.assertEqual(self.git(clone, "branch", "--show-current").stdout, "")
self.assertEqual((clone / "content").read_text(), "updated locked version\n")
self.assertEqual(self.state()["components"]["App"]["baseline"], head)
for row in refs_before:
self.assertIn(row, self.git(clone, "for-each-ref", "--format=%(refname) %(objectname)").stdout.splitlines())
for row in reflog_before:
self.assertIn(row, self.git(clone, "reflog", "--format=%H", "HEAD").stdout.splitlines())
history = self.state()["components"]["App"]["previous_sources"]
self.assertEqual(history[0]["commit"], self.first)
self.assertEqual(self.git(clone, "rev-parse", history[0]["ref"]).stdout.strip(), self.first)
self.cli("prepare", "--name", "App", "--refresh-fixed")
self.assertEqual(len(self.state()["components"]["App"]["previous_sources"]), 1)
def test_explicit_fixed_refresh_refuses_dirty_ignored_edited_and_locked_sources(self):
self.cli("prepare")
clone = self.root / ".workspace/App"
self.advance_app_lock()
state_before = (self.root / ".workspace/state.json").read_bytes()
(clone / "content").write_text("unsaved owner change\n")
self.assertNotEqual(self.cli("prepare", "--refresh-fixed", check=False).returncode, 0)
self.assertEqual((clone / "content").read_text(), "unsaved owner change\n")
(clone / "content").write_text("first\n")
(clone / "private").mkdir()
(clone / "private/unique").write_text("ignored owner data\n")
self.assertNotEqual(self.cli("prepare", "--refresh-fixed", check=False).returncode, 0)
self.assertEqual((clone / "private/unique").read_text(), "ignored owner data\n")
(clone / "private/unique").unlink()
(clone / "private").rmdir()
lock = clone / ".git/index.lock"
lock.write_text("another Git owner\n")
self.assertNotEqual(self.cli("prepare", "--refresh-fixed", check=False).returncode, 0)
self.assertEqual(lock.read_text(), "another Git owner\n")
lock.unlink()
self.assertEqual((self.root / ".workspace/state.json").read_bytes(), state_before)
self.lock["components"]["App"]["commit"] = self.first
self.save_manifest()
self.cli("edit", "--name", "App", "--branch", "feature/owner")
self.advance_app_lock()
result = self.cli("prepare", "--refresh-fixed", check=False)
self.assertNotEqual(result.returncode, 0)
self.assertEqual(self.git(clone, "branch", "--show-current").stdout.strip(), "feature/owner")
self.assertEqual(self.git(clone, "rev-parse", "HEAD").stdout.strip(), self.first)
def test_explicit_fixed_refresh_refuses_shared_or_unregistered_ownership(self):
self.cli("prepare")
clone = self.root / ".workspace/App"
self.advance_app_lock()
external = self.base / "fixed-external-owner"
self.git(clone, "worktree", "add", "-b", "feature/external", str(external))
(external / "unique").write_text("external owner data\n")
result = self.cli("prepare", "--refresh-fixed", check=False)
self.assertNotEqual(result.returncode, 0)
self.assertEqual(self.git(clone, "rev-parse", "HEAD").stdout.strip(), self.first)
self.assertEqual((external / "unique").read_text(), "external owner data\n")
(external / "unique").unlink()
self.git(clone, "worktree", "remove", str(external))
data = self.state()
data["components"].pop("App")
(self.root / ".workspace/state.json").write_text(json.dumps(data))
result = self.cli("prepare", "--refresh-fixed", check=False)
self.assertNotEqual(result.returncode, 0)
self.assertEqual(self.git(clone, "rev-parse", "HEAD").stdout.strip(), self.first)
def test_failed_explicit_fixed_refresh_keeps_old_binding_and_can_retry(self):
self.cli("prepare")
clone = self.root / ".workspace/App"
self.lock["components"]["App"]["commit"] = "a" * 40
self.save_manifest()
result = self.cli("prepare", "--refresh-fixed", check=False)
self.assertNotEqual(result.returncode, 0)
self.assertEqual(self.state()["components"]["App"]["baseline"], self.first)
self.assertEqual(self.state()["components"]["App"]["mode"], "versioned")
self.assertEqual(self.git(clone, "rev-parse", "HEAD").stdout.strip(), self.first)
head = self.advance_app_lock()
self.cli("prepare", "--refresh-fixed")
self.assertEqual(self.git(clone, "rev-parse", "HEAD").stdout.strip(), head)
def test_explicit_fixed_refresh_refuses_index_flags_hiding_owner_changes(self):
self.cli("prepare")
clone = self.root / ".workspace/App"
self.git(clone, "update-index", "--skip-worktree", "content")
(clone / "content").write_text("hidden owner change\n")
self.advance_app_lock()
result = self.cli("prepare", "--refresh-fixed", check=False)
self.assertNotEqual(result.returncode, 0)
self.assertEqual(self.git(clone, "rev-parse", "HEAD").stdout.strip(), self.first)
self.assertEqual((clone / "content").read_text(), "hidden owner change\n")
def test_private_git_metadata_symlink_is_not_adopted(self):
self.cli("prepare")
clone = self.root / ".workspace/App"
external = self.base / "other-owner-metadata"
(clone / ".git").rename(external)
(clone / ".git").symlink_to(external, target_is_directory=True)
result = self.cli("edit", "--name", "App", check=False)
self.assertNotEqual(result.returncode, 0)
self.assertFalse((external / "refs/heads/feature").exists())
def test_git_operation_is_reported_and_not_removed(self):
self.cli("prepare")
operation = self.root / ".workspace/App/.git/MERGE_HEAD"
operation.write_text(self.first + "\n")
result = self.cli("edit", "--name", "App", check=False)
self.assertNotEqual(result.returncode, 0)
self.assertEqual(operation.read_text(), self.first + "\n")
def test_status_does_not_expose_credentials_from_an_unmatched_actual_origin(self):
self.cli("prepare")
clone = self.root / ".workspace/App"
self.git(clone, "remote", "set-url", "origin", "https://owner:workspace-fake-secret@example.invalid/repo.git")
result = self.cli("status")
self.assertNotIn("workspace-fake-secret", result.stdout)
self.assertIn("redacted", result.stdout)
def test_transition_preserves_committed_symlink_deletion_and_file_type_change(self):
legacy = self.legacy()
(legacy / "old-link").symlink_to("content")
(legacy / "regular").write_text("committed file\n")
self.git(legacy, "add", "old-link", "regular")
self.git(legacy, "commit", "-m", "Add two file types")
(legacy / "old-link").unlink()
(legacy / "regular").unlink()
(legacy / "regular").symlink_to("content")
before = self.fingerprint(legacy)
self.cli("transition", "--name", "App", "--apply")
clone = self.root / ".workspace/App"
self.assertFalse((clone / "old-link").is_symlink())
self.assertTrue((clone / "regular").is_symlink())
self.assertEqual(self.git(clone, "diff", "--raw").stdout, self.git(legacy, "diff", "--raw").stdout)
self.assertEqual(self.fingerprint(legacy), before)
def go_fixture(self):
module = "example.invalid/Native"
library_remote, author, head = self.component("Native", {
"go.mod": "module example.invalid/Native\n\ngo 1.27\n",
"value.go": 'package native\nfunc Value() string { return "versioned" }\n'})
version = "v0.0.0-20261013000000-" + head[:12]
proxy = self.base / "proxy/example.invalid/!native/@v"
proxy.mkdir(parents=True)
(proxy / (version + ".mod")).write_text((author / "go.mod").read_text())
# Go's real proxy protocol supplies Origin.Hash, avoiding a fake Go executable.
(proxy / (version + ".info")).write_text(json.dumps({"Version": version,
"Time": "2026-10-13T00:00:00Z", "Origin": {"VCS": "git", "URL": str(library_remote), "Hash": head}}))
with zipfile.ZipFile(proxy / (version + ".zip"), "w") as archive:
for filename in ("go.mod", "value.go"):
archive.write(author / filename, module + "@" + version + "/" + filename)
app_remote, _, app_head = self.component("Consumer", {
"go.mod": "module example.invalid/consumer\n\ngo 1.27\n\nrequire " + module + " " + version + "\n",
"main.go": 'package main\nimport ("fmt"; native "example.invalid/Native")\nfunc main() { fmt.Print(native.Value()) }\n'})
self.manifest["components"] = [self.entry("Consumer", app_remote, modules=[{"path": "example.invalid/consumer", "directory": "."}]),
self.entry("Native", library_remote, "go-library", [{"path": module, "legacy": "native", "directory": "."}])]
self.lock["components"] = {"Consumer": {"commit": app_head}}
self.save_manifest()
self.env.update({"GOPROXY": (self.base / "proxy").as_uri(), "GOMODCACHE": str(self.base / "modcache"),
"GOPATH": str(self.base / "gopath")})
return head
def test_actual_go_version_baseline_overlay_and_unlink_restore_version_consumption(self):
head = self.go_fixture()
self.cli("prepare")
consumer = self.root / ".workspace/Consumer"
native = self.root / ".workspace/Native"
self.command(consumer, "go", "mod", "download", "all", env=self.env | {"GOWORK": "off"})
self.assertEqual(self.command(consumer, "go", "run", "-mod=readonly", ".", env=self.env | {"GOWORK": "off"}).stdout, "versioned")
self.cli("edit", "--name", "Native", "--branch", "feature/value")
self.assertEqual(self.git(native, "rev-parse", "HEAD").stdout.strip(), head)
(native / "value.go").write_text('package native\nfunc Value() string { return "edited" }\n')
self.assertEqual(self.command(consumer, "go", "run", ".").stdout, "edited")
self.assertEqual(self.command(consumer, "go", "run", "-mod=readonly", ".", env=self.env | {"GOWORK": "off"}).stdout, "versioned")
self.cli("unlink", "--name", "Native")
self.assertFalse((self.root / "go.work").exists())
self.assertEqual(self.command(consumer, "go", "run", "-mod=readonly", ".").stdout, "versioned")
self.assertEqual((native / "value.go").read_text(), 'package native\nfunc Value() string { return "edited" }\n')
def test_fresh_library_edit_resolves_fixed_consumer_and_preserves_consumer_manifest(self):
head = self.go_fixture()
self.cli("edit", "--name", "Native")
native = self.root / ".workspace/Native"
consumer = self.root / ".workspace/Consumer"
self.assertEqual(self.git(native, "rev-parse", "HEAD").stdout.strip(), head)
self.assertFalse((consumer / "go.sum").exists(), "source resolution must not write a consumer's go.sum")
self.assertEqual(self.git(consumer, "status", "--porcelain=v1").stdout, "")
def test_library_baseline_resolves_transitive_selected_module(self):
head = self.go_fixture()
native_version = "v0.0.0-20261013000000-" + head[:12]
bridge_remote, bridge_author, bridge_head = self.component("Bridge", {
"go.mod": "module example.invalid/Bridge\n\ngo 1.27\n\nrequire example.invalid/Native " + native_version + "\n",
"value.go": 'package bridge\nimport native "example.invalid/Native"\nfunc Value() string { return native.Value() }\n'})
bridge_version = "v0.0.0-20261013000000-" + bridge_head[:12]
proxy = self.base / "proxy/example.invalid/!bridge/@v"
proxy.mkdir(parents=True)
(proxy / (bridge_version + ".mod")).write_text((bridge_author / "go.mod").read_text())
(proxy / (bridge_version + ".info")).write_text(json.dumps({"Version": bridge_version,
"Time": "2026-10-13T00:00:00Z", "Origin": {"VCS": "git", "URL": str(bridge_remote), "Hash": bridge_head}}))
with zipfile.ZipFile(proxy / (bridge_version + ".zip"), "w") as archive:
for filename in ("go.mod", "value.go"):
archive.write(bridge_author / filename, "example.invalid/Bridge@" + bridge_version + "/" + filename)
author = self.base / "Consumer-author"
(author / "go.mod").write_text("module example.invalid/consumer\n\ngo 1.27\n\nrequire example.invalid/Bridge " + bridge_version + "\n")
(author / "main.go").write_text('package main\nimport ("fmt"; bridge "example.invalid/Bridge")\nfunc main() { fmt.Print(bridge.Value()) }\n')
self.git(author, "commit", "-am", "Consume Native transitively")
self.git(author, "push", "origin", "main")
self.lock["components"]["Consumer"]["commit"] = self.git(author, "rev-parse", "HEAD").stdout.strip()
self.save_manifest()
self.cli("edit", "--name", "Native")
self.assertEqual(self.git(self.root / ".workspace/Native", "rev-parse", "HEAD").stdout.strip(), head)
self.assertFalse((self.root / ".workspace/Consumer/go.sum").exists())
(self.root / ".workspace/Native/value.go").write_text('package native\nfunc Value() string { return "transitive edit" }\n')
self.assertEqual(self.command(self.root / ".workspace/Consumer", "go", "run", ".", separate_stderr=True).stdout, "transitive edit")
if __name__ == "__main__":
unittest.main()