The update is a skill (decision 4); deploy/update.sh and the installed .mam_deploy/update.sh are gone. - Phase 1 (installed code, update.sh): publish the update marker under the exclusive admission lock; refuse before any change while a running row, a live herdr agent in the workspace on any running server, an open job (unreadable JSON included, dry_run not), a live loop marker or a monitor.lock held by a monitor the skills did not start (U3) is there - the calling agent (server + pane, its row and one running job) excepted (lib_py/update.py precheck, decisions 1 and 7). Stop the monitor (TERM, then KILL and restart it if it does not end; decision 2), handle a leftover backup by its state (U4), back up .agents, .mam_deploy, root files, .venv, the installer's .mam files and the registry (SQLite backup API under the registry lock) into .mam/update-backup/, then exec a copy of install_phase.sh outside the tree (decision 5). - The copy (no lib.sh/lib_py, bash 3.2) runs the installer in place; on failure it restores the backup and restarts the monitor from the restored tree; on success it runs phase 2, post_update.sh of the new tree, which converts the registry with lib_py.registry_format and restarts the monitor if it ran. The backup and the marker are removed at the end. - install.sh updates in place (decisions 3, 4, 9): install_state keys are set, one line each; no updater is installed and an old one (.mam_deploy/update.sh, or a root update.sh the manifest lists) is removed; framework files the new version no longer ships are deleted when unmodified, kept and listed when modified, and dropped from base/, the manifest and asset_hashes.txt; a local source's .mam is not staged. remove.sh knows the new skill; CI shellchecks its scripts; docs point at the skill. - test_issue8_update_state and test_deploy_layout td6/td27 run the skill. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
81 lines
3.4 KiB
Python
81 lines
3.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Issue #8 — an update must keep the installer's fresh version.txt and
|
|
asset_hashes.txt instead of restoring the pre-update copies. Since Issue #72
|
|
the update is the multi-agent-mux-update skill (in place, no remove.sh) and
|
|
no .mam_deploy/update.sh is installed.
|
|
|
|
Restoring them left version.txt naming the previous commit and made remove.sh
|
|
compare against stale hashes, so every updated-but-pristine file was backed up
|
|
as "locally modified". install_state is the one file whose old keys are
|
|
merged back (gitignore_created from the original install).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
|
|
|
|
@pytest.fixture
|
|
def installed_workspace():
|
|
temp = tempfile.mkdtemp(prefix="mam_issue8_")
|
|
ws = Path(temp) / "workspace"
|
|
ws.mkdir()
|
|
env = os.environ.copy()
|
|
env["MAM_REPO_URL"] = str(REPO_ROOT)
|
|
env["MAM_SKIP_VENV"] = "1"
|
|
env["MAM_INSTALLER_URL"] = "file://" + str(REPO_ROOT / "deploy" / "install.sh")
|
|
res = subprocess.run(["bash", str(REPO_ROOT / "deploy" / "install.sh"), str(ws)],
|
|
env=env, capture_output=True, text=True, timeout=600)
|
|
assert res.returncode == 0, res.stderr
|
|
try:
|
|
yield ws, env
|
|
finally:
|
|
shutil.rmtree(temp, ignore_errors=True)
|
|
|
|
|
|
def test_update_keeps_fresh_version_and_hashes_and_merges_install_state(installed_workspace):
|
|
ws, env = installed_workspace
|
|
mam = ws / ".mam"
|
|
fresh_version = (mam / "version.txt").read_text()
|
|
assert "commit=" in fresh_version
|
|
|
|
# Simulate the previous install: stale stamps and an install_state key the
|
|
# re-install will not write again.
|
|
(mam / "version.txt").write_text("source=old\ncommit=stale0000\nfetched_at=then\nmethod=git\n")
|
|
(mam / "asset_hashes.txt").write_text("stale-hash-db\n")
|
|
with open(mam / "install_state", "a") as fh:
|
|
fh.write("custom_marker=kept\n")
|
|
|
|
assert not (ws / ".mam_deploy" / "update.sh").exists()
|
|
upd_env = {k: v for k, v in env.items() if not k.startswith("HERDR_")
|
|
and k not in ("WORKSPACE_ROOT", "AGENT_SESSIONS_YAML", "MAM_JOBS_DIR")}
|
|
upd_env["MAM_NO_SUBSCRIBER_DAEMON"] = "1"
|
|
# The update lists the live herdr agents; never ask a real server here.
|
|
fake = ws.parent / "fake-herdr-bin"
|
|
fake.mkdir()
|
|
(fake / "herdr").write_text('#!/bin/sh\necho \'{"sessions": []}\'\n')
|
|
(fake / "herdr").chmod(0o755)
|
|
upd_env["PATH"] = "%s:%s" % (fake, upd_env["PATH"])
|
|
res = subprocess.run(["bash", str(ws / ".agents/skills/multi-agent-mux-update/scripts/update.sh"), "--yes"],
|
|
cwd=str(ws), env=upd_env, capture_output=True, text=True, timeout=600,
|
|
stdin=subprocess.DEVNULL)
|
|
assert res.returncode == 0, res.stderr + res.stdout
|
|
|
|
version = (mam / "version.txt").read_text()
|
|
assert "stale0000" not in version, version
|
|
assert "commit=" in version and version.strip() != ""
|
|
hashes = (mam / "asset_hashes.txt").read_text()
|
|
assert "stale-hash-db" not in hashes and hashes.strip(), "asset_hashes.txt must be the installer's fresh ledger"
|
|
|
|
state = (mam / "install_state").read_text()
|
|
assert "custom_marker=kept" in state, state
|
|
assert state.count("gitignore_created=1") == 1, "merged keys must not be duplicated:\n" + state
|
|
assert not (ws / ".mam_deploy" / "update.sh").exists()
|