Why: Replace repeated submodule source pointers with consumer-owned language versions and fixed application sources, keeping independent repository edits and existing branches intact. Evidence: SELF task receipts contain startup/sync evidence, real Go and npm consumption, Native overlay/unlink, safe transition/ref preservation, credential boundary and historical build regressions. Exact committed full composition and RDM checks follow this checkpoint; formal acceptance and GATE CI remain pending. Impact: Add isolated build/test composition, source provenance and safe owner refresh, adapt governance and service build callers, remove replaced index links while retaining original Git metadata, and adopt reusable dependency standards. Refs: Documents approved dependency-workspace r1/R1-R15 and U1-U7 implementation plan; feature/dependency-workspace task card.
139 lines
8.9 KiB
Python
139 lines
8.9 KiB
Python
"""Source adapters exercise real Git/Go/npm builds without starting services."""
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import unittest
|
|
import zipfile
|
|
import test_composition
|
|
|
|
SOURCE = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
@unittest.skipUnless(all(shutil.which(command) for command in ("git", "go", "node", "npm")), "Git/Go/Node/npm required")
|
|
class SvcBuildCutoverTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.fixture = test_composition.CompositionTests(methodName="runTest")
|
|
self.fixture.setUp()
|
|
self.addCleanup(self.fixture.doCleanups)
|
|
self.base, self.root, self.env = self.fixture.base, self.fixture.root, self.fixture.env
|
|
self.env = self.env.copy()
|
|
for key in ("COMPOSITION_ROOT", "DEV_ROOT", "COMPOSITION_SOURCE_MAP"):
|
|
self.env.pop(key, None)
|
|
(self.root / "scripts/workspace").mkdir(parents=True)
|
|
shutil.copy2(SOURCE / "scripts/build-composition.py", self.root / "scripts/build-composition.py")
|
|
shutil.copy2(SOURCE / "scripts/workspace/transport.py", self.root / "scripts/workspace/transport.py")
|
|
(self.root / ".gitignore").write_text("bin/\nprivate/\n")
|
|
(self.root / "Makefile").write_text("build frontend:\n\tpython3 scripts/build-composition.py --root . --target $@\n")
|
|
self.sha = self.fixture.commit(self.root)
|
|
self.cache = self.base / "cache"
|
|
self.call(self.base, "git", "clone", str(self.root), str(self.cache))
|
|
self.runtime = self.base / "runtime"
|
|
self.runtime.mkdir()
|
|
wrappers = self.base / "tools"
|
|
wrappers.mkdir()
|
|
git = shutil.which("git")
|
|
(wrappers / "git").write_text('#!/usr/bin/env bash\nfor arg in "$@"; do if [ "$arg" = submodule ]; then echo "recursive submodule acquisition forbidden" >&2; exit 63; fi; done\nexec ' + git + ' "$@"\n')
|
|
(wrappers / "git").chmod(0o755)
|
|
self.env["PATH"] = str(wrappers) + os.pathsep + self.env["PATH"]
|
|
|
|
def call(self, cwd, *args, check=True, env=None):
|
|
result = subprocess.run(args, cwd=cwd, env=env or self.env, text=True,
|
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=120)
|
|
if check:
|
|
self.assertEqual(result.returncode, 0, result.stdout)
|
|
return result
|
|
|
|
def adapter(self, commands, check=True, env=None):
|
|
code = 'source "$1"\nROOT="$2"; SRC_DIR="$3"; RUNTIME="$4"; ENV_NAME=fixture\n' + commands
|
|
return self.call(self.base, "bash", "-c", code, "cutover", str(SOURCE / "scripts/svc.sh"), str(self.root), str(self.cache), str(self.runtime), check=check, env=env)
|
|
|
|
def test_versioned_dev_materialization_uses_composer_without_submodules(self):
|
|
self.adapter('materialize_dev_commit "' + self.sha + '" dev build')
|
|
binary = self.runtime / ".worktrees/dev/bin/agent-system"
|
|
self.assertEqual(self.call(self.base, str(binary)).stdout, "immutable-page")
|
|
resolved = json.loads((binary.parent / "resolved-manifest.json").read_text())
|
|
self.assertEqual(resolved["root"]["commit"], self.sha)
|
|
|
|
def test_versioned_static_adapter_resolves_published_frontend_artifact(self):
|
|
self.adapter('wt=$(materialize_dev_commit "' + self.sha + '" frontend frontend)\nsvc_frontend_dist "$wt"')
|
|
dist = self.runtime / ".worktrees/frontend/bin/frontend-dist"
|
|
self.assertEqual((dist / "index.html").read_text(), "immutable-page")
|
|
|
|
def test_mismatched_existing_cache_is_rejected_before_candidate_build(self):
|
|
path = self.runtime / ".worktrees/reused"
|
|
self.call(self.base, "git", "-C", str(self.cache), "worktree", "add", "--detach", str(path), self.sha)
|
|
(self.root / "new-marker").write_text("new candidate\n")
|
|
new_sha = self.fixture.commit(self.root)
|
|
self.call(self.base, "git", "-C", str(self.cache), "fetch", "origin")
|
|
result = self.adapter('materialize_dev_commit "' + new_sha + '" reused build', check=False)
|
|
self.assertNotEqual(result.returncode, 0, result.stdout)
|
|
self.assertEqual(self.call(path, "git", "rev-parse", "HEAD").stdout.strip(), self.sha)
|
|
self.assertFalse((path / "bin").exists())
|
|
|
|
def test_cache_recycling_preserves_ignored_unique_files(self):
|
|
path = self.runtime / ".worktrees/unique"
|
|
self.call(self.base, "git", "-C", str(self.cache), "worktree", "add", "--detach", str(path), self.sha)
|
|
(path / "private").mkdir()
|
|
(path / "private/unique").write_text("owner data\n")
|
|
self.adapter('recycle_worktree unique')
|
|
self.assertTrue((path / "private/unique").is_file())
|
|
self.assertEqual((path / "private/unique").read_text(), "owner data\n")
|
|
|
|
def test_versioned_rdm_adapter_passes_fixed_dev_root_to_product_wrapper(self):
|
|
rdm = self.base / "rdm"
|
|
rdm.mkdir()
|
|
self.call(rdm, "git", "init", "-q", "-b", "main")
|
|
self.call(rdm, "git", "config", "user.email", "fixture@example.invalid")
|
|
self.call(rdm, "git", "config", "user.name", "Fixture")
|
|
version = "v0.0.0-" + self.call(self.fixture.backend, "git", "show", "-s", "--format=%cd", "--date=format-local:%Y%m%d%H%M%S", self.fixture.backend_sha).stdout.strip() + "-" + self.fixture.backend_sha[:12]
|
|
proxy = self.base / "proxy/example.com/backend/@v"
|
|
proxy.mkdir(parents=True)
|
|
(proxy / (version + ".mod")).write_text((self.fixture.backend / "go.mod").read_text())
|
|
(proxy / (version + ".info")).write_text(json.dumps({"Version": version, "Time": "2026-10-13T00:00:00Z"}))
|
|
(proxy / "list").write_text(version + "\n")
|
|
with zipfile.ZipFile(proxy / (version + ".zip"), "w") as archive:
|
|
for relative in self.call(self.fixture.backend, "git", "ls-files").stdout.splitlines():
|
|
archive.write(self.fixture.backend / relative, "example.com/backend@" + version + "/" + relative)
|
|
(rdm / "go.mod").write_text("module git-ssh.u1680832.nyat.app/weichen/Agent-System-RDM.git\n\ngo 1.23\nrequire example.com/backend " + version + "\n")
|
|
(rdm / "main.go").write_text('package main\nimport("fmt";"example.com/backend/web")\nfunc main(){fmt.Print(web.Page())}\n')
|
|
(rdm / "Makefile").write_text('build:\n\t@test -n "$(COMPOSITION_ROOT)"\n\tpython3 "$(COMPOSITION_ROOT)/scripts/build-composition.py" --root "$(COMPOSITION_ROOT)" --target build --rdm-source . --output bin\n\tcp bin/agent-system-rdm-product bin/agent-system-rdm\n')
|
|
env = self.env | {"GOPROXY": (self.base / "proxy").as_uri(), "GOMODCACHE": str(self.base / "module-cache")}
|
|
self.call(rdm, "go", "mod", "download", "all", env=env)
|
|
rdm_sha = self.fixture.commit(rdm)
|
|
self.adapter('RDM_SRC_DIR="' + str(rdm) + '"\nmaterialize_rdm_commit "' + rdm_sha + '" rdm', env=env)
|
|
binary = self.runtime / ".worktrees/rdm-rdm/bin/agent-system-rdm"
|
|
self.assertEqual(self.call(self.base, str(binary)).stdout, "immutable-page")
|
|
self.assertEqual(json.loads((binary.parent / "resolved-manifest.json").read_text())["root"]["commit"], self.sha)
|
|
|
|
def test_e2e_build_only_uses_versioned_sources_without_runtime_or_api_calls(self):
|
|
self.test_versioned_rdm_adapter_passes_fixed_dev_root_to_product_wrapper()
|
|
rdm = self.base / "rdm"
|
|
rdm_sha = self.call(rdm, "git", "rev-parse", "HEAD").stdout.strip()
|
|
self.call(rdm, "git", "update-ref", "refs/remotes/origin/main", rdm_sha)
|
|
shutil.copytree(SOURCE / "scripts/workspace", self.root / "scripts/workspace", dirs_exist_ok=True)
|
|
for name in ("workspace.py", "component-source.py", "svc.sh", "e2e.sh"):
|
|
shutil.copy2(SOURCE / "scripts" / name, self.root / "scripts" / name)
|
|
(self.root / ".gitignore").write_text("bin/\nprivate/\n.workspace/\ngo.work\n")
|
|
self.fixture.commit(self.root)
|
|
curl = self.base / "tools/curl"
|
|
curl.write_text('#!/usr/bin/env bash\necho "runtime API must not execute in build-only mode" >&2\nexit 71\n')
|
|
curl.chmod(0o755)
|
|
output = self.base / "e2e"
|
|
env = self.env | {"RDM_REPO": str(rdm), "E2E_OUT": str(output), "GOPROXY": (self.base / "proxy").as_uri(), "GOMODCACHE": str(self.base / "module-cache")}
|
|
unrelated = subprocess.Popen(["sleep", "60"])
|
|
self.addCleanup(lambda: (unrelated.terminate(), unrelated.wait()) if unrelated.poll() is None else None)
|
|
env.update(API_PID=str(unrelated.pid), UI_PID=str(unrelated.pid))
|
|
result = self.call(self.root, "bash", "scripts/e2e.sh", "--build-only", env=env)
|
|
self.assertIsNone(unrelated.poll(), "build-only must not kill inherited process identities")
|
|
self.assertIn("runtime/browser/model acceptance NOT_RUN", result.stdout)
|
|
self.assertEqual(self.call(self.base, str(output / "agent-system-rdm")).stdout, "immutable-page")
|
|
self.assertEqual((self.root / "bin/frontend-dist/index.html").read_text(), "immutable-page")
|
|
self.assertFalse((output / "data").exists())
|
|
self.assertFalse((output / "cookies.txt").exists())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|