Integrate reviewed recovery and source visibility fixes

Why: Complete the approved U2/U3 fix pass and prevent shared host fixture collisions.

Evidence: Native SELF receipts record complete Backend suite, Frontend 221 tests, meaningful startup regression failures and the bounded baseline model batch.

Impact: Pin the corrected Backend and Frontend and allocate owned recovery fixture ports; no existing service deployment.

Refs: modules/documents/docs/proposals/2026-10-13-fast-slow-u2-u3-plan.md
This commit is contained in:
2026-10-13 03:55:35 +00:00
parent f485818aaa
commit 0f624e1834
4 changed files with 290 additions and 16 deletions
File diff suppressed because one or more lines are too long
+22 -14
View File
@@ -184,6 +184,12 @@ esac
"""
def free_fixture_port():
with socket.socket() as reservation:
reservation.bind(("127.0.0.1", 0))
return reservation.getsockname()[1]
class SvcAutoRecoverTests(unittest.TestCase):
def setUp(self):
tmp = tempfile.mkdtemp(prefix="svc-autorecover-")
@@ -330,14 +336,14 @@ class SvcAutoRecoverTests(unittest.TestCase):
# ---- 1. 期望态:start 成功置位 / 显式 stop 清除(含进程已死的 stop)----
def test_start_records_autostart_and_stop_clears(self):
self.seed_agent("dev", 18801)
self.seed_agent("dev", free_fixture_port())
self.start_ok("dev")
self.assertEqual(self.kv("dev", "autostart"), "1")
self.svc("dev", "stop")
self.assertIsNone(self.kv("dev", "autostart"))
def test_stop_on_dead_process_clears_autostart(self):
self.seed_agent("dev", 18802)
self.seed_agent("dev", free_fixture_port())
self.start_ok("dev")
self.kill9("dev")
# 进程已死但 autostart 保留(等待 recover);显式 stop 表示"保持停止"。
@@ -348,7 +354,8 @@ class SvcAutoRecoverTests(unittest.TestCase):
# ---- 2. recover:崩溃拉回 / 运行中不动 / 显式停止不复活 / 未启动不碰 ----
def test_recover_restarts_sigkilled_service(self):
self.seed_agent("dev", 18803)
port = free_fixture_port()
self.seed_agent("dev", port)
self.start_ok("dev")
old_pid = self.kill9("dev")
result = self.svc("recover", "--quiet")
@@ -361,10 +368,10 @@ class SvcAutoRecoverTests(unittest.TestCase):
ok_events = self.history_events("dev", "recover")
self.assertTrue(any('"result":"ok"' in l for l in ok_events), ok_events)
# 恢复用的仍是持久化 exec=(svc.env 登记未漂移)。
self.assertIn("--addr 127.0.0.1:18803", self.kv("dev", "exec"))
self.assertIn("--addr 127.0.0.1:%d" % port, self.kv("dev", "exec"))
def test_recover_noop_while_running(self):
self.seed_agent("dev", 18804)
self.seed_agent("dev", free_fixture_port())
self.start_ok("dev")
pid = self.pid_of("dev")
result = self.svc("recover")
@@ -373,7 +380,7 @@ class SvcAutoRecoverTests(unittest.TestCase):
self.assertEqual(self.history_events("dev", "recover"), [])
def test_recover_respects_explicit_stop(self):
self.seed_agent("dev", 18805)
self.seed_agent("dev", free_fixture_port())
self.start_ok("dev")
self.svc("dev", "stop")
result = self.svc("recover")
@@ -382,7 +389,7 @@ class SvcAutoRecoverTests(unittest.TestCase):
self.assertEqual(self.history_events("dev", "recover"), [])
def test_recover_ignores_never_started_service(self):
self.seed_agent("dev", 18806) # 从未 start:无 autostart 键
self.seed_agent("dev", free_fixture_port()) # 从未 start:无 autostart 键
result = self.svc("recover")
self.assertEqual(result.returncode, 0, result.stdout)
self.assertFalse(self.pid_alive(self.pid_of("dev")))
@@ -391,7 +398,7 @@ class SvcAutoRecoverTests(unittest.TestCase):
# ---- 3. 并发防护:生命周期锁被持有时 recover 跳过 ----
def test_recover_skips_when_lock_held(self):
self.seed_agent("dev", 18807)
self.seed_agent("dev", free_fixture_port())
self.start_ok("dev")
self.kill9("dev")
lock = self.runtime("dev") / ".start.lock"
@@ -411,11 +418,12 @@ class SvcAutoRecoverTests(unittest.TestCase):
# ---- 4. 恢复失败路径:端口被外部进程占用 → fail 事件 + 非零退出 ----
def test_recover_fails_when_port_stolen(self):
self.seed_agent("dev", 18808)
port = free_fixture_port()
self.seed_agent("dev", port)
self.start_ok("dev")
self.kill9("dev")
thief = subprocess.Popen(
["python3", "-m", "http.server", "18808", "--bind", "127.0.0.1"],
["python3", "-m", "http.server", str(port), "--bind", "127.0.0.1"],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
self.addCleanup(thief.terminate)
# 等 thief 真正完成 bind 再 recover——慢环境里 Popen 返回时端口尚未监听,
@@ -426,7 +434,7 @@ class SvcAutoRecoverTests(unittest.TestCase):
s = socket.socket()
s.settimeout(0.2)
try:
s.connect(("127.0.0.1", 18808))
s.connect(("127.0.0.1", port))
bound = True
except OSError:
pass
@@ -479,7 +487,7 @@ class SvcAutoRecoverTests(unittest.TestCase):
"""模拟 update 误清 pidfile 后本 bin 仍占口:裸 start 应回收并拉起新进程。"""
if not self._ss_can_attribute_pid():
self.skipTest("ss -ltnp 无法归因 PID(reclaim_own_port 需此能力)")
port = 18812
port = free_fixture_port()
self.seed_agent("dev", port)
self.start_ok("dev")
old = self.pid_of("dev")
@@ -500,7 +508,7 @@ class SvcAutoRecoverTests(unittest.TestCase):
def test_start_rejects_foreign_port_occupant(self):
"""外部 python http.server 占口:start 必须失败并提示非本服务。"""
port = 18813
port = free_fixture_port()
self.seed_agent("dev", port)
thief = subprocess.Popen(
["python3", "-m", "http.server", str(port), "--bind", "127.0.0.1"],
@@ -537,7 +545,7 @@ class SvcAutoRecoverTests(unittest.TestCase):
self.assertFalse((unit_dir / "agent-system-svc-recover.service").exists())
def test_recover_reserved_name_rejected(self):
self.seed_agent("recover", 18809) # 保留字不可作服务名
self.seed_agent("recover", free_fixture_port()) # 保留字不可作服务名
result = self.svc("recover", "recover", check=False)
self.assertEqual(result.returncode, 2, result.stdout)