agent-exec transport 13e9c9807fd03b453a03da384041d045/c2d965af28b4

This commit is contained in:
agent-exec-transport
2026-10-19 09:28:51 +00:00
parent 198d1891e4
commit c26af5456b
12 changed files with 9668 additions and 25 deletions
File diff suppressed because one or more lines are too long
+1
View File
@@ -66,3 +66,4 @@
- [DHF 模板接入 RDM 文档生成管线](modules/documents/docs/proposals/2026-10-14-dhf-template-generation-plan.md):DHF 模板资产合同与文档生成转换。 - [DHF 模板接入 RDM 文档生成管线](modules/documents/docs/proposals/2026-10-14-dhf-template-generation-plan.md):DHF 模板资产合同与文档生成转换。
- [RDM 模板与两树一网对话文档生成](modules/documents/docs/proposals/2026-10-11-rdm-template-dialog-generation.md):历史资产盘点、模板版本与章节实例化、持久提案、文档级审签及 DOCX。 - [RDM 模板与两树一网对话文档生成](modules/documents/docs/proposals/2026-10-11-rdm-template-dialog-generation.md):历史资产盘点、模板版本与章节实例化、持久提案、文档级审签及 DOCX。
- [RDM 最终目标核对与完整交付](modules/documents/docs/proposals/2026-10-11-rdm-final-target-completion.md):系统级项目、权限覆盖、私域协作、全部文档三权与完整业务链的需求映射。 - [RDM 最终目标核对与完整交付](modules/documents/docs/proposals/2026-10-11-rdm-final-target-completion.md):系统级项目、权限覆盖、私域协作、全部文档三权与完整业务链的需求映射。
- [快慢通道 U2/U3 实施计划](modules/documents/docs/proposals/2026-10-13-fast-slow-u2-u3-plan.md):r0.5 [书面设计](modules/documents/docs/proposals/2026-10-14-fast-slow-u3-r05-terminal-evidence-and-fast-feedback-design.md)与[实施计划](modules/documents/docs/proposals/2026-10-14-fast-slow-u3-r05-terminal-evidence-and-fast-feedback-implementation-plan.md)已评审;r0.4 A01 FAIL,r0.5 真实 A01 NOT_RUN。r0.6 [传输完成性设计](modules/documents/docs/proposals/2026-10-14-fast-slow-u3-r06-transport-completeness-design.md)与[实施计划](modules/documents/docs/proposals/2026-10-14-fast-slow-u3-r06-transport-evidence-implementation-plan.md)已获人工批准,零付费实施中;付费探针与 A01 须另行资源提案。八个 PR 仍为草稿,见[核验报告](modules/documents/docs/reports/2026-10-13-fast-slow-u2-u3-validation.md)。
+7 -3
View File
@@ -9,6 +9,7 @@
import os import os
import shutil import shutil
import subprocess import subprocess
import socket
import tempfile import tempfile
import unittest import unittest
import urllib.error import urllib.error
@@ -362,9 +363,12 @@ class SvcDbRecoveryTests(unittest.TestCase):
def test_external_dsn_and_conffile_recover_on_bare_start(self): def test_external_dsn_and_conffile_recover_on_bare_start(self):
d = self.runtime("dev") d = self.runtime("dev")
self.seed_agent("dev", 18795, with socket.socket() as reservation:
"--addr 127.0.0.1:18795 --data %s --plugins %s --auth auto" reservation.bind(("127.0.0.1", 0))
% (d / "data", d / "plugins")) port = reservation.getsockname()[1]
self.seed_agent("dev", port,
"--addr 127.0.0.1:%d --data %s --plugins %s --auth auto"
% (port, d / "data", d / "plugins"))
external = "postgresql://extu:ext-pw-4@10.9.8.7:5432/extdb" external = "postgresql://extu:ext-pw-4@10.9.8.7:5432/extdb"
self.clear_serve_record() self.clear_serve_record()
self.svc("dev", "deploy", "--from", "local", "--dsn", external) self.svc("dev", "deploy", "--from", "local", "--dsn", external)
+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): class SvcAutoRecoverTests(unittest.TestCase):
def setUp(self): def setUp(self):
tmp = tempfile.mkdtemp(prefix="svc-autorecover-") tmp = tempfile.mkdtemp(prefix="svc-autorecover-")
@@ -330,14 +336,14 @@ class SvcAutoRecoverTests(unittest.TestCase):
# ---- 1. 期望态:start 成功置位 / 显式 stop 清除(含进程已死的 stop)---- # ---- 1. 期望态:start 成功置位 / 显式 stop 清除(含进程已死的 stop)----
def test_start_records_autostart_and_stop_clears(self): 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.start_ok("dev")
self.assertEqual(self.kv("dev", "autostart"), "1") self.assertEqual(self.kv("dev", "autostart"), "1")
self.svc("dev", "stop") self.svc("dev", "stop")
self.assertIsNone(self.kv("dev", "autostart")) self.assertIsNone(self.kv("dev", "autostart"))
def test_stop_on_dead_process_clears_autostart(self): 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.start_ok("dev")
self.kill9("dev") self.kill9("dev")
# 进程已死但 autostart 保留(等待 recover);显式 stop 表示"保持停止"。 # 进程已死但 autostart 保留(等待 recover);显式 stop 表示"保持停止"。
@@ -348,7 +354,8 @@ class SvcAutoRecoverTests(unittest.TestCase):
# ---- 2. recover:崩溃拉回 / 运行中不动 / 显式停止不复活 / 未启动不碰 ---- # ---- 2. recover:崩溃拉回 / 运行中不动 / 显式停止不复活 / 未启动不碰 ----
def test_recover_restarts_sigkilled_service(self): 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") self.start_ok("dev")
old_pid = self.kill9("dev") old_pid = self.kill9("dev")
result = self.svc("recover", "--quiet") result = self.svc("recover", "--quiet")
@@ -361,10 +368,10 @@ class SvcAutoRecoverTests(unittest.TestCase):
ok_events = self.history_events("dev", "recover") ok_events = self.history_events("dev", "recover")
self.assertTrue(any('"result":"ok"' in l for l in ok_events), ok_events) self.assertTrue(any('"result":"ok"' in l for l in ok_events), ok_events)
# 恢复用的仍是持久化 exec=(svc.env 登记未漂移)。 # 恢复用的仍是持久化 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): def test_recover_noop_while_running(self):
self.seed_agent("dev", 18804) self.seed_agent("dev", free_fixture_port())
self.start_ok("dev") self.start_ok("dev")
pid = self.pid_of("dev") pid = self.pid_of("dev")
result = self.svc("recover") result = self.svc("recover")
@@ -373,7 +380,7 @@ class SvcAutoRecoverTests(unittest.TestCase):
self.assertEqual(self.history_events("dev", "recover"), []) self.assertEqual(self.history_events("dev", "recover"), [])
def test_recover_respects_explicit_stop(self): def test_recover_respects_explicit_stop(self):
self.seed_agent("dev", 18805) self.seed_agent("dev", free_fixture_port())
self.start_ok("dev") self.start_ok("dev")
self.svc("dev", "stop") self.svc("dev", "stop")
result = self.svc("recover") result = self.svc("recover")
@@ -382,7 +389,7 @@ class SvcAutoRecoverTests(unittest.TestCase):
self.assertEqual(self.history_events("dev", "recover"), []) self.assertEqual(self.history_events("dev", "recover"), [])
def test_recover_ignores_never_started_service(self): 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") result = self.svc("recover")
self.assertEqual(result.returncode, 0, result.stdout) self.assertEqual(result.returncode, 0, result.stdout)
self.assertFalse(self.pid_alive(self.pid_of("dev"))) self.assertFalse(self.pid_alive(self.pid_of("dev")))
@@ -391,7 +398,7 @@ class SvcAutoRecoverTests(unittest.TestCase):
# ---- 3. 并发防护:生命周期锁被持有时 recover 跳过 ---- # ---- 3. 并发防护:生命周期锁被持有时 recover 跳过 ----
def test_recover_skips_when_lock_held(self): 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.start_ok("dev")
self.kill9("dev") self.kill9("dev")
lock = self.runtime("dev") / ".start.lock" lock = self.runtime("dev") / ".start.lock"
@@ -411,11 +418,12 @@ class SvcAutoRecoverTests(unittest.TestCase):
# ---- 4. 恢复失败路径:端口被外部进程占用 → fail 事件 + 非零退出 ---- # ---- 4. 恢复失败路径:端口被外部进程占用 → fail 事件 + 非零退出 ----
def test_recover_fails_when_port_stolen(self): 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.start_ok("dev")
self.kill9("dev") self.kill9("dev")
thief = subprocess.Popen( 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) stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
self.addCleanup(thief.terminate) self.addCleanup(thief.terminate)
# 等 thief 真正完成 bind 再 recover——慢环境里 Popen 返回时端口尚未监听, # 等 thief 真正完成 bind 再 recover——慢环境里 Popen 返回时端口尚未监听,
@@ -426,7 +434,7 @@ class SvcAutoRecoverTests(unittest.TestCase):
s = socket.socket() s = socket.socket()
s.settimeout(0.2) s.settimeout(0.2)
try: try:
s.connect(("127.0.0.1", 18808)) s.connect(("127.0.0.1", port))
bound = True bound = True
except OSError: except OSError:
pass pass
@@ -479,7 +487,7 @@ class SvcAutoRecoverTests(unittest.TestCase):
"""模拟 update 误清 pidfile 后本 bin 仍占口:裸 start 应回收并拉起新进程。""" """模拟 update 误清 pidfile 后本 bin 仍占口:裸 start 应回收并拉起新进程。"""
if not self._ss_can_attribute_pid(): if not self._ss_can_attribute_pid():
self.skipTest("ss -ltnp 无法归因 PID(reclaim_own_port 需此能力)") self.skipTest("ss -ltnp 无法归因 PID(reclaim_own_port 需此能力)")
port = 18812 port = free_fixture_port()
self.seed_agent("dev", port) self.seed_agent("dev", port)
self.start_ok("dev") self.start_ok("dev")
old = self.pid_of("dev") old = self.pid_of("dev")
@@ -500,7 +508,7 @@ class SvcAutoRecoverTests(unittest.TestCase):
def test_start_rejects_foreign_port_occupant(self): def test_start_rejects_foreign_port_occupant(self):
"""外部 python http.server 占口:start 必须失败并提示非本服务。""" """外部 python http.server 占口:start 必须失败并提示非本服务。"""
port = 18813 port = free_fixture_port()
self.seed_agent("dev", port) self.seed_agent("dev", port)
thief = subprocess.Popen( thief = subprocess.Popen(
["python3", "-m", "http.server", str(port), "--bind", "127.0.0.1"], ["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()) self.assertFalse((unit_dir / "agent-system-svc-recover.service").exists())
def test_recover_reserved_name_rejected(self): 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) result = self.svc("recover", "recover", check=False)
self.assertEqual(result.returncode, 2, result.stdout) self.assertEqual(result.returncode, 2, result.stdout)