feat(deploy): implement lib_ownership, registry merge, and deployment test suite
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
"""Deploy freshness — the deploy/ scripts must actually ship the latest assets.
|
||||
|
||||
``deploy/install.sh`` treats only ``.agents/skills/**`` as framework-owned. Every
|
||||
other shipped asset (``.agents/hooks.json``, ``.agents/hooks/*.sh``,
|
||||
``.agents/MULTI_AGENT_RULES*.md``, ``.agents/INSTALL.md``) takes the
|
||||
``elif [ ! -e "$dest" ]`` branch, so it is copied once and never refreshed.
|
||||
``deploy/remove.sh``'s manifest-less fallback list is likewise frozen at the
|
||||
pre-loop skill set, so it strands those same assets plus the whole
|
||||
``multi-agent-mux-loop`` skill.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
|
||||
REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
# Framework assets that live outside .agents/skills/ and must still be
|
||||
# refreshed. Mapped as {installed path: path in the source tree} because the
|
||||
# user manual is installed from deploy/, not from .agents/.
|
||||
NON_SKILL_ASSETS = {
|
||||
".agents/hooks.json": ".agents/hooks.json",
|
||||
".agents/hooks/loop_delegation_guard.sh": ".agents/hooks/loop_delegation_guard.sh",
|
||||
".agents/MULTI_AGENT_RULES.md": ".agents/MULTI_AGENT_RULES.md",
|
||||
".agents/MULTI_AGENT_RULES.ko.md": ".agents/MULTI_AGENT_RULES.ko.md",
|
||||
".agents/INSTALL.md": "deploy/INSTALL.md",
|
||||
}
|
||||
|
||||
MARK = "UPSTREAM-RELEASE-MARKER"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def src_and_target():
|
||||
"""A pristine copy of the working tree plus an empty install target.
|
||||
|
||||
Runtime directories are excluded so the fixture stays fast and so the
|
||||
source can never be confused with an already-installed workspace.
|
||||
"""
|
||||
tmp = tempfile.mkdtemp(prefix="mam_deploy_fresh_")
|
||||
src, tgt = os.path.join(tmp, "src"), os.path.join(tmp, "tgt")
|
||||
shutil.copytree(REPO_ROOT, src,
|
||||
ignore=shutil.ignore_patterns(".git", ".venv", ".mam",
|
||||
".mam_deploy", "__pycache__"),
|
||||
symlinks=True)
|
||||
os.makedirs(tgt)
|
||||
try:
|
||||
yield src, tgt
|
||||
finally:
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
|
||||
def _install(src, tgt, **extra):
|
||||
env = dict(os.environ)
|
||||
env.update({"MAM_REPO_URL": src, "MAM_SKIP_VENV": "1",
|
||||
"MAM_SKIP_GITIGNORE": "1"})
|
||||
env.update(extra)
|
||||
return subprocess.run(["bash", os.path.join(src, "deploy", "install.sh"), tgt],
|
||||
env=env, capture_output=True, text=True)
|
||||
|
||||
|
||||
def _bump(src, rel):
|
||||
"""Simulate an upstream release that changed ``rel``."""
|
||||
path = os.path.join(src, rel)
|
||||
assert os.path.exists(path), "no such source asset: %s" % rel
|
||||
if rel.endswith(".json"):
|
||||
with open(path) as f:
|
||||
data = json.load(f)
|
||||
data["_release"] = MARK
|
||||
with open(path, "w") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
else:
|
||||
with open(path, "a") as f:
|
||||
f.write("\n# %s\n" % MARK)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# D-1 — a refresh install must deliver upstream changes to EVERY framework
|
||||
# asset, not only to those under .agents/skills/.
|
||||
# --------------------------------------------------------------------------
|
||||
def test_d1_refresh_updates_non_skill_framework_assets(src_and_target):
|
||||
src, tgt = src_and_target
|
||||
assert _install(src, tgt).returncode == 0
|
||||
|
||||
for source_rel in NON_SKILL_ASSETS.values():
|
||||
_bump(src, source_rel)
|
||||
_bump(src, ".agents/skills/lib.sh") # control: this one is known to work
|
||||
|
||||
res = _install(src, tgt)
|
||||
assert res.returncode == 0, res.stderr
|
||||
|
||||
with open(os.path.join(tgt, ".agents/skills/lib.sh")) as f:
|
||||
assert MARK in f.read(), (
|
||||
"control failed: even a skills/ file did not refresh, so this test "
|
||||
"is not measuring what it claims")
|
||||
|
||||
stale = []
|
||||
for rel in NON_SKILL_ASSETS:
|
||||
with open(os.path.join(tgt, rel)) as f:
|
||||
if MARK not in f.read():
|
||||
stale.append(rel)
|
||||
assert not stale, (
|
||||
"install.sh refreshed .agents/skills/ but left these framework assets "
|
||||
"at their originally-installed version: %s. The O-3 guard and the "
|
||||
"MULTI_AGENT_RULES protocol therefore never reach an existing "
|
||||
"workspace." % stale)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# D-2 — remove.sh's manifest-less fallback must clear every asset the
|
||||
# installers write, or the next install is blocked by the survivors
|
||||
# (install.sh only copies a non-skill asset when it does not exist).
|
||||
# --------------------------------------------------------------------------
|
||||
def test_d2_manifestless_removal_strands_no_framework_assets(src_and_target):
|
||||
src, tgt = src_and_target
|
||||
assert _install(src, tgt).returncode == 0
|
||||
|
||||
# An install_mam.sh deployment leaves no manifest; force that code path.
|
||||
os.remove(os.path.join(tgt, ".mam", "install_manifest.txt"))
|
||||
|
||||
res = subprocess.run(["bash", os.path.join(tgt, ".mam_deploy", "remove.sh"),
|
||||
"--force", tgt], capture_output=True, text=True)
|
||||
assert res.returncode == 0, res.stderr
|
||||
|
||||
survivors = []
|
||||
for root, _, files in os.walk(os.path.join(tgt, ".agents")):
|
||||
for f in files:
|
||||
survivors.append(os.path.relpath(os.path.join(root, f), tgt))
|
||||
assert not survivors, (
|
||||
"the manifest-less fallback left framework assets behind: %s. Because "
|
||||
"install.sh copies a non-skill asset only when it is absent, these "
|
||||
"survivors permanently pin the workspace to the old version."
|
||||
% sorted(survivors))
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# D-3 — install_mam.sh must record a manifest, so that remove.sh/update.sh
|
||||
# take the exact-reversal path instead of the frozen fallback list.
|
||||
# --------------------------------------------------------------------------
|
||||
def test_d3_install_mam_records_a_manifest():
|
||||
body = open(os.path.join(REPO_ROOT, "deploy", "install_mam.sh")).read()
|
||||
assert "install_manifest.txt" in body, (
|
||||
"deploy/install_mam.sh writes no .mam/install_manifest.txt, so every "
|
||||
"workspace it deploys is uninstalled and updated through remove.sh's "
|
||||
"frozen fallback list")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# D-5 — the fetch-validation gate must cover the assets a broken fetch would
|
||||
# plausibly drop, including the O-3 hook pair.
|
||||
# --------------------------------------------------------------------------
|
||||
def test_d5_asset_presence_gate_covers_the_hook_pair():
|
||||
body = open(os.path.join(REPO_ROOT, "deploy", "install.sh")).read()
|
||||
start = body.index("check_assets_present()")
|
||||
gate = body[start:body.index("}", body.index("return 0", start))]
|
||||
for rel in (".agents/hooks.json", ".agents/hooks/loop_delegation_guard.sh"):
|
||||
assert rel in gate, (
|
||||
"check_assets_present() does not require %s, so a fetch that "
|
||||
"silently dropped the O-3 guard still passes validation and "
|
||||
"installs a workspace whose guard is inert" % rel)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# D-6 — asset_hashes.txt drives both the refresh 3-way merge and remove.sh's
|
||||
# "preserve modified files" backup. Framework files outside skills/ get
|
||||
# neither today.
|
||||
# --------------------------------------------------------------------------
|
||||
def test_d6_hash_db_covers_non_skill_framework_assets(src_and_target):
|
||||
src, tgt = src_and_target
|
||||
assert _install(src, tgt).returncode == 0
|
||||
|
||||
with open(os.path.join(tgt, ".mam", "asset_hashes.txt")) as f:
|
||||
db = f.read()
|
||||
missing = [rel for rel in NON_SKILL_ASSETS if rel not in db]
|
||||
assert not missing, (
|
||||
"these framework assets are absent from .mam/asset_hashes.txt: %s. "
|
||||
"Local edits to them are silently destroyed by remove.sh instead of "
|
||||
"being backed up." % missing)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# D-7 — every variable the installer writes into a fresh .mam.env must be
|
||||
# documented in the committed template.
|
||||
# --------------------------------------------------------------------------
|
||||
def test_d7_env_template_documents_installer_applied_defaults():
|
||||
installer = open(os.path.join(REPO_ROOT, "deploy", "install.sh")).read()
|
||||
start_idx = installer.index("cat <<EOF > \"$MAM_ENV\"")
|
||||
end_idx = installer.index("EOF", start_idx + 15)
|
||||
block = installer[start_idx:end_idx]
|
||||
written = [ln.split("=", 1)[0].strip() for ln in block.splitlines()
|
||||
if "=" in ln and not ln.strip().startswith("#")]
|
||||
|
||||
template = open(os.path.join(REPO_ROOT, ".mam.env.example")).read()
|
||||
undocumented = [v for v in written if v not in template]
|
||||
assert not undocumented, (
|
||||
"install.sh seeds %s into every new .mam.env, but .mam.env.example "
|
||||
"never mentions them, so users cannot discover or correct them"
|
||||
% undocumented)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# D-8 — CI must lint the shipped hook and run the test suite that every
|
||||
# recent feature commit claims as its evidence.
|
||||
# --------------------------------------------------------------------------
|
||||
def test_d8_ci_lints_the_hook_and_runs_the_tests():
|
||||
ci = open(os.path.join(REPO_ROOT, "deploy", "gitea-ci.yml")).read()
|
||||
assert ".agents/hooks/loop_delegation_guard.sh" in ci, (
|
||||
"gitea-ci.yml shellchecks every other shipped bash entrypoint but not "
|
||||
"the O-3 hook")
|
||||
assert "pytest" in ci, (
|
||||
"gitea-ci.yml runs no pytest job, so none of the 17 files under tests/ "
|
||||
"ever gate a release")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# D-9 — .agents/INSTALL.md is shipped from two different sources: the .agents/
|
||||
# walk and the later `cp deploy/INSTALL.md`, whose `[ ! -e ]` guard can
|
||||
# therefore never fire. One of the two must not exist, or the herdr
|
||||
# migration's doc update silently loses to the tmux-era copy.
|
||||
# --------------------------------------------------------------------------
|
||||
def test_d9_installed_manual_is_not_the_stale_tmux_era_copy(src_and_target):
|
||||
src, tgt = src_and_target
|
||||
assert _install(src, tgt).returncode == 0
|
||||
|
||||
with open(os.path.join(tgt, ".agents", "INSTALL.md")) as f:
|
||||
shipped = f.read()
|
||||
assert "tmux" not in shipped, (
|
||||
"the installed .agents/INSTALL.md is the pre-herdr copy. install.sh "
|
||||
"stages .agents/INSTALL.md from the repo first, so its later "
|
||||
"`cp deploy/INSTALL.md` -- guarded by `[ ! -e ]` -- never runs and the "
|
||||
"up-to-date manual is never delivered")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# D-10 — a preserved customization must stay preserved across REPEATED
|
||||
# refreshes. The hash DB was rebuilt from the working copy, so the
|
||||
# second refresh saw target == db, classified the file as unmodified,
|
||||
# and overwrote it with no notice.
|
||||
# --------------------------------------------------------------------------
|
||||
def test_d10_customization_survives_repeated_refresh(src_and_target):
|
||||
src, tgt = src_and_target
|
||||
assert _install(src, tgt).returncode == 0
|
||||
|
||||
skill = os.path.join(tgt, ".agents", "skills", "lib.sh")
|
||||
with open(skill, "a") as f:
|
||||
f.write("\n# MY_LOCAL_TWEAK\n")
|
||||
|
||||
for n in (2, 3, 4):
|
||||
if n == 4: # upstream also moves on
|
||||
_bump(src, ".agents/skills/lib.sh")
|
||||
res = _install(src, tgt)
|
||||
assert res.returncode == 0, res.stderr
|
||||
with open(skill) as f:
|
||||
body = f.read()
|
||||
assert "MY_LOCAL_TWEAK" in body, (
|
||||
"the local customization was destroyed on refresh #%d. install.sh "
|
||||
"rebuilds .mam/asset_hashes.txt from the working copy, so after "
|
||||
"the first PRESERVE the modified hash becomes the canonical one "
|
||||
"and the next refresh silently overwrites it." % n)
|
||||
assert "Local modification detected" in res.stderr, (
|
||||
"refresh #%d overwrote nothing but also reported nothing; the "
|
||||
"user gets no signal that their edit is diverging" % n)
|
||||
@@ -16,6 +16,7 @@ class TestDeployLayout(unittest.TestCase):
|
||||
self.env = os.environ.copy()
|
||||
self.env["MAM_REPO_URL"] = self.repo_root
|
||||
self.env["MAM_SKIP_VENV"] = "1"
|
||||
self.env["MAM_INSTALLER_URL"] = "file://" + os.path.join(self.repo_root, "deploy", "install.sh")
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
"""Deploy key-level registry merge tests for .agents/hooks.json.
|
||||
|
||||
Tests for Rev.2 key-level 3-way merge rules (R-1 to R-10).
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
|
||||
REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ws():
|
||||
"""A pristine source copy plus empty target directory."""
|
||||
tmp = tempfile.mkdtemp(prefix="mam_deploy_reg_")
|
||||
src, tgt = os.path.join(tmp, "src"), os.path.join(tmp, "tgt")
|
||||
shutil.copytree(REPO_ROOT, src,
|
||||
ignore=shutil.ignore_patterns(".git", ".venv", ".mam",
|
||||
".mam_deploy", "__pycache__"),
|
||||
symlinks=True)
|
||||
os.makedirs(tgt)
|
||||
try:
|
||||
yield src, tgt
|
||||
finally:
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
|
||||
def _install(src, tgt, **extra):
|
||||
env = dict(os.environ)
|
||||
env.update({"MAM_REPO_URL": src, "MAM_SKIP_VENV": "1",
|
||||
"MAM_SKIP_GITIGNORE": "1"})
|
||||
env.update(extra)
|
||||
return subprocess.run(["bash", os.path.join(src, "deploy", "install.sh"), tgt],
|
||||
env=env, capture_output=True, text=True)
|
||||
|
||||
|
||||
def _install_mam(src, tgt):
|
||||
return subprocess.run(["bash", os.path.join(src, "deploy", "install_mam.sh"),
|
||||
"--target", tgt], capture_output=True, text=True)
|
||||
|
||||
|
||||
def _read_json(path):
|
||||
with open(path, encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def _write_json(path, data):
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
|
||||
def _add_user_hook(tgt, name="custom-user-hook"):
|
||||
p = os.path.join(tgt, ".agents", "hooks.json")
|
||||
d = _read_json(p)
|
||||
d[name] = {"PreToolUse": [{"matcher": "user_action", "hooks": [{"type": "command", "command": "echo user"}]}]}
|
||||
_write_json(p, d)
|
||||
return name
|
||||
|
||||
|
||||
def _ship_new_hook(src, name="mam-loop-timeout-guard"):
|
||||
script = os.path.join(src, ".agents", "hooks", "loop_timeout_guard.sh")
|
||||
with open(script, "w") as f:
|
||||
f.write('#!/usr/bin/env bash\necho \'{"decision":"allow"}\'\n')
|
||||
os.chmod(script, 0o755)
|
||||
p = os.path.join(src, ".agents", "hooks.json")
|
||||
d = _read_json(p)
|
||||
d[name] = {"PreToolUse": [{"matcher": "file_change", "hooks": [
|
||||
{"type": "command", "command": "./hooks/loop_timeout_guard.sh", "timeout": 10}]}]}
|
||||
_write_json(p, d)
|
||||
return name
|
||||
|
||||
|
||||
def test_r1_user_hook_does_not_block_new_framework_hook(ws):
|
||||
src, tgt = ws
|
||||
assert _install(src, tgt).returncode == 0
|
||||
|
||||
user_hook = _add_user_hook(tgt)
|
||||
new_hook = _ship_new_hook(src)
|
||||
|
||||
res = _install(src, tgt)
|
||||
assert res.returncode == 0, res.stderr
|
||||
|
||||
tgt_data = _read_json(os.path.join(tgt, ".agents", "hooks.json"))
|
||||
assert user_hook in tgt_data, "User's custom hook was lost"
|
||||
assert new_hook in tgt_data, "New framework hook was not delivered"
|
||||
|
||||
|
||||
def test_r2_upstream_fix_to_an_existing_hook_lands(ws):
|
||||
src, tgt = ws
|
||||
assert _install(src, tgt).returncode == 0
|
||||
|
||||
_add_user_hook(tgt)
|
||||
|
||||
p_src = os.path.join(src, ".agents", "hooks.json")
|
||||
d_src = _read_json(p_src)
|
||||
key = list(d_src.keys())[0]
|
||||
d_src[key]["updated_by_upstream"] = True
|
||||
_write_json(p_src, d_src)
|
||||
|
||||
res = _install(src, tgt)
|
||||
assert res.returncode == 0, res.stderr
|
||||
|
||||
tgt_data = _read_json(os.path.join(tgt, ".agents", "hooks.json"))
|
||||
assert tgt_data.get(key, {}).get("updated_by_upstream") is True
|
||||
|
||||
|
||||
def test_r3_user_modified_hook_is_preserved_and_reported(ws):
|
||||
src, tgt = ws
|
||||
assert _install(src, tgt).returncode == 0
|
||||
|
||||
p_tgt = os.path.join(tgt, ".agents", "hooks.json")
|
||||
d_tgt = _read_json(p_tgt)
|
||||
key = list(d_tgt.keys())[0]
|
||||
d_tgt[key]["user_custom_setting"] = 123
|
||||
_write_json(p_tgt, d_tgt)
|
||||
|
||||
p_src = os.path.join(src, ".agents", "hooks.json")
|
||||
d_src = _read_json(p_src)
|
||||
d_src[key]["upstream_competing_setting"] = 456
|
||||
_write_json(p_src, d_src)
|
||||
|
||||
res = _install(src, tgt)
|
||||
assert res.returncode == 0, res.stderr
|
||||
|
||||
tgt_data = _read_json(p_tgt)
|
||||
assert tgt_data[key].get("user_custom_setting") == 123, "User edit was overwritten"
|
||||
assert "kept yours" in res.stderr or "kept your" in res.stderr or "Registry merged" in res.stderr
|
||||
|
||||
|
||||
def test_r4_retired_upstream_hook_removed_if_unmodified(ws):
|
||||
src, tgt = ws
|
||||
assert _install(src, tgt).returncode == 0
|
||||
|
||||
p_src = os.path.join(src, ".agents", "hooks.json")
|
||||
d_src = _read_json(p_src)
|
||||
retired_key = list(d_src.keys())[0]
|
||||
del d_src[retired_key]
|
||||
_write_json(p_src, d_src)
|
||||
|
||||
res = _install(src, tgt)
|
||||
assert res.returncode == 0, res.stderr
|
||||
|
||||
tgt_data = _read_json(os.path.join(tgt, ".agents", "hooks.json"))
|
||||
assert retired_key not in tgt_data, "Retired unmodified hook was not removed"
|
||||
|
||||
|
||||
def test_r4b_retired_upstream_hook_kept_if_user_modified(ws):
|
||||
src, tgt = ws
|
||||
assert _install(src, tgt).returncode == 0
|
||||
|
||||
p_tgt = os.path.join(tgt, ".agents", "hooks.json")
|
||||
d_tgt = _read_json(p_tgt)
|
||||
retired_key = list(d_tgt.keys())[0]
|
||||
d_tgt[retired_key]["modified_by_user"] = True
|
||||
_write_json(p_tgt, d_tgt)
|
||||
|
||||
p_src = os.path.join(src, ".agents", "hooks.json")
|
||||
d_src = _read_json(p_src)
|
||||
del d_src[retired_key]
|
||||
_write_json(p_src, d_src)
|
||||
|
||||
res = _install(src, tgt)
|
||||
assert res.returncode == 0, res.stderr
|
||||
|
||||
tgt_data = _read_json(p_tgt)
|
||||
assert retired_key in tgt_data, "User-modified retired hook should be kept"
|
||||
|
||||
|
||||
def test_r5_merge_preserves_backup_and_stderr_notification(ws):
|
||||
src, tgt = ws
|
||||
assert _install(src, tgt).returncode == 0
|
||||
|
||||
_add_user_hook(tgt)
|
||||
_ship_new_hook(src)
|
||||
|
||||
res = _install(src, tgt)
|
||||
assert res.returncode == 0, res.stderr
|
||||
assert "Registry merged" in res.stderr or "added" in res.stderr
|
||||
|
||||
backup_dir = os.path.join(tgt, ".mam", "skill-backups")
|
||||
assert os.path.exists(backup_dir) and len(os.listdir(backup_dir)) > 0
|
||||
|
||||
|
||||
def test_r6_missing_base_falls_back_to_additive_merge(ws):
|
||||
src, tgt = ws
|
||||
assert _install(src, tgt).returncode == 0
|
||||
|
||||
shutil.rmtree(os.path.join(tgt, ".mam", "base"), ignore_errors=True)
|
||||
|
||||
user_hook = _add_user_hook(tgt)
|
||||
new_hook = _ship_new_hook(src)
|
||||
|
||||
res = _install(src, tgt)
|
||||
assert res.returncode == 0, res.stderr
|
||||
|
||||
tgt_data = _read_json(os.path.join(tgt, ".agents", "hooks.json"))
|
||||
assert user_hook in tgt_data
|
||||
assert new_hook in tgt_data
|
||||
|
||||
|
||||
def test_r7_corrupt_registry_preserved_and_install_continues(ws):
|
||||
src, tgt = ws
|
||||
assert _install(src, tgt).returncode == 0
|
||||
|
||||
p_tgt = os.path.join(tgt, ".agents", "hooks.json")
|
||||
with open(p_tgt, "w") as f:
|
||||
f.write("INVALID JSON {{{")
|
||||
|
||||
res = _install(src, tgt)
|
||||
assert res.returncode == 0, res.stderr
|
||||
|
||||
with open(p_tgt) as f:
|
||||
assert "INVALID JSON" in f.read()
|
||||
|
||||
|
||||
def test_r8_merge_is_idempotent(ws):
|
||||
src, tgt = ws
|
||||
assert _install(src, tgt).returncode == 0
|
||||
|
||||
_add_user_hook(tgt)
|
||||
_ship_new_hook(src)
|
||||
|
||||
res1 = _install(src, tgt)
|
||||
assert res1.returncode == 0
|
||||
t1_content = open(os.path.join(tgt, ".agents", "hooks.json")).read()
|
||||
|
||||
res2 = _install(src, tgt)
|
||||
assert res2.returncode == 0
|
||||
t2_content = open(os.path.join(tgt, ".agents", "hooks.json")).read()
|
||||
|
||||
assert t1_content == t2_content
|
||||
|
||||
|
||||
def test_r9_install_mam_records_hash_db_and_base(ws):
|
||||
src, tgt = ws
|
||||
res = _install_mam(src, tgt)
|
||||
assert res.returncode == 0, res.stderr
|
||||
|
||||
assert os.path.exists(os.path.join(tgt, ".mam", "asset_hashes.txt"))
|
||||
assert os.path.exists(os.path.join(tgt, ".mam", "base", ".agents", "hooks.json"))
|
||||
|
||||
|
||||
def test_r10_missing_ownership_rules_abort_rather_than_degrade(ws):
|
||||
src, tgt = ws
|
||||
lib = os.path.join(src, "deploy", "lib_ownership.sh")
|
||||
assert os.path.exists(lib), "deploy/lib_ownership.sh is not shipped"
|
||||
os.remove(lib)
|
||||
|
||||
res = _install(src, tgt)
|
||||
assert res.returncode != 0
|
||||
assert "lib_ownership.sh" in res.stderr
|
||||
Reference in New Issue
Block a user