Why: an update that stops the old process and overwrites the binary cannot get the previous version back, and a migration the old binary cannot read must be refused before that stop. CI also had no fixed RDM SHA to build from. Evidence: python3 -m unittest tests.test_svc_identity tests.test_svc_recover tests.test_svc_deploy_txn tests.test_lifecycle_pin passed. make test-lifecycle exits 2 when AGENT_PG_TEST_DSN is unset. Impact: installs land in releases/<rid> and current switches with mv -T. Deploy keeps the desired state and rolls back a non-terminal journal. Schema compatFloor above the installed target is refused unless --migrate-schema --confirm is paired with a backup. test-lifecycle fails closed without PostgreSQL and an RDM checkout. Login stays NOT_RUN. Refs: recovery plan r3.1 D2 and D3; Backend feature/d1-identity-open Co-authored-by: Cursor <cursoragent@cursor.com>
107 lines
4.7 KiB
Python
107 lines
4.7 KiB
Python
"""部署切换事务:构建失败不动旧服务,schema 不兼容在停服前拒绝,中断后对账回滚。"""
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
SOURCE = Path(__file__).resolve().parents[1]
|
|
_spec = importlib.util.spec_from_file_location(
|
|
"tsi", SOURCE / "tests" / "test_svc_identity.py")
|
|
tsi = importlib.util.module_from_spec(_spec)
|
|
_spec.loader.exec_module(tsi)
|
|
|
|
|
|
def caps(target, floor):
|
|
return (
|
|
'{"protocol":1,"product":"agent-system","version":"stub",'
|
|
'"identity":{"ops":["ensure","verify"],"open":"existing-no-migrate","dsn":"env"},'
|
|
'"cli":{"unknownSubcommand":"reject"},'
|
|
'"schema":{"target":%d,"compatFloor":%d}}\n' % (target, floor)
|
|
)
|
|
|
|
|
|
class DeployTxnTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.fx = tsi.IdentityTests("test_T1_bare_restart_stable_identity")
|
|
self.fx.setUp()
|
|
|
|
def tearDown(self):
|
|
self.fx.tearDown()
|
|
self.fx.doCleanups()
|
|
|
|
def test_schema_incompatible_refuses_before_stop(self):
|
|
fx = self.fx
|
|
old = fx.root / "stub-old"
|
|
new = fx.root / "stub-new"
|
|
text = (SOURCE / "tests" / "test_svc_recover.py").read_text()
|
|
# 复用桩的 identity/serve,只替换 version --json。
|
|
old.write_text(text[text.find("STUB = r'''") and 0:])
|
|
# 直接基于已写入的 recover 桩文件内容不够稳。用 fx.stub 再替换 JSON。
|
|
base = fx.stub.read_text()
|
|
marker = '\'{"protocol":1,"product":"agent-system","version":"stub","identity":{"ops":["ensure","verify"],"open":"existing-no-migrate","dsn":"env"},"cli":{"unknownSubcommand":"reject"}}\''
|
|
old.write_text(base.replace(marker, "'" + caps(1, 0).strip() + "'"))
|
|
new.write_text(base.replace(marker, "'" + caps(5, 5).strip() + "'"))
|
|
old.chmod(0o755)
|
|
new.chmod(0o755)
|
|
fx.stub.write_text(old.read_text())
|
|
fx.stub.chmod(0o755)
|
|
fx.seed_agent("txn", 9471)
|
|
fx.start_ok("txn")
|
|
pid = fx.pid_of("txn")
|
|
script = fx.root / "scripts" / "svc.sh"
|
|
probe = fx.svc(
|
|
"txn", "status", check=False)
|
|
self.assertEqual(probe.returncode, 0, probe.stdout)
|
|
out = __import__("subprocess").run(
|
|
["bash", "-c",
|
|
'source "%s" >/dev/null 2>&1; ENV_NAME=txn; ENV_DIR="%s"; ENV_BIN="%s"; '
|
|
'if svc_schema_compatible "%s" "%s"; then echo OK; else echo NO; fi'
|
|
% (script, fx.runtime("txn"), fx.runtime("txn") / "bin" / "agent-system",
|
|
fx.runtime("txn") / "bin" / "agent-system", new)],
|
|
cwd=fx.root, env=fx.env, text=True,
|
|
stdout=__import__("subprocess").PIPE,
|
|
stderr=__import__("subprocess").STDOUT, timeout=30)
|
|
self.assertIn("NO", out.stdout, out.stdout)
|
|
self.assertEqual(fx.pid_of("txn"), pid)
|
|
fx.svc("txn", "stop")
|
|
|
|
def test_forward_only_requires_confirm_and_backup(self):
|
|
fx = self.fx
|
|
fx.seed_agent("fwd", 9472)
|
|
fx.start_ok("fwd")
|
|
iid = json.loads((fx.runtime("fwd") / "manifest.json").read_text())["instance_id"]
|
|
base = fx.stub.read_text()
|
|
marker = '\'{"protocol":1,"product":"agent-system","version":"stub","identity":{"ops":["ensure","verify"],"open":"existing-no-migrate","dsn":"env"},"cli":{"unknownSubcommand":"reject"}}\''
|
|
installed = fx.root / "stub-installed"
|
|
candidate = fx.root / "stub-forward"
|
|
installed.write_text(base.replace(marker, "'" + caps(1, 0).strip() + "'"))
|
|
candidate.write_text(base.replace(marker, "'" + caps(5, 5).strip() + "'"))
|
|
installed.chmod(0o755)
|
|
candidate.chmod(0o755)
|
|
script = fx.root / "scripts" / "svc.sh"
|
|
envdir = fx.runtime("fwd")
|
|
|
|
def run(extra):
|
|
return __import__("subprocess").run(
|
|
["bash", "-c",
|
|
'source "%s" >/dev/null 2>&1; ENV_NAME=fwd; ENV_DIR="%s"; '
|
|
'ROOT="%s"; %s '
|
|
'if svc_schema_compatible "%s" "%s"; then echo OK; else echo NO; fi'
|
|
% (script, envdir, fx.root, extra, installed, candidate)],
|
|
cwd=fx.root, env=fx.env, text=True,
|
|
stdout=__import__("subprocess").PIPE,
|
|
stderr=__import__("subprocess").STDOUT, timeout=30).stdout
|
|
|
|
self.assertIn("NO", run(""))
|
|
self.assertIn("NO", run('CLI_MIGRATE_SCHEMA=1; CLI_CONFIRM="%s";' % iid))
|
|
backup = fx.root / "runtime-backups" / "fwd" / "20261012T000000Z"
|
|
backup.mkdir(parents=True)
|
|
(backup / "ok").write_text("snapshot\n")
|
|
self.assertIn("OK", run('CLI_MIGRATE_SCHEMA=1; CLI_CONFIRM="%s";' % iid))
|
|
fx.svc("fwd", "stop")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|