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.
824 lines
49 KiB
Python
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()
|