Files
multi-agent-mux/tests/test_issue8_update_state.py
T
GodopuandClaude Opus 5.5 47fe24e07c feat: multi-agent-mux-update skill and in-place install (Issue #72)
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>
2026-10-27 04:06:57 +00:00

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()