From 88aa4b6b7d1a11131c1d09beac50b55c4353a464 Mon Sep 17 00:00:00 2001 From: weichen Date: Wed, 21 Oct 2026 10:19:04 +0000 Subject: [PATCH 1/5] Record the task card for fix-remaining-hardcoded-ports Why: establish the authorized task workspace and its initial baseline. Evidence: preserved precheck receipt in .task/fix-remaining-hardcoded-ports.md; R0=444fb1dee2a72452be23b97cbaaad9e28c42dffd. Impact: task metadata only; the feature workspace starts at 444fb1dee2a72452be23b97cbaaad9e28c42dffd. Refs: .task/fix-remaining-hardcoded-ports.md --- .task/fix-remaining-hardcoded-ports.md | 90 ++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 .task/fix-remaining-hardcoded-ports.md diff --git a/.task/fix-remaining-hardcoded-ports.md b/.task/fix-remaining-hardcoded-ports.md new file mode 100644 index 00000000..ec0bf002 --- /dev/null +++ b/.task/fix-remaining-hardcoded-ports.md @@ -0,0 +1,90 @@ +# fix/remaining-hardcoded-ports 任务卡 + +任务主记录:;状态:进行中;owner:weichen + +- 权威仓:ssh://git@192.168.3.2:2222/weichen/Agent-System-Dev.git;目标分支:main;R0(initial_base_sha)= 444fb1dee2a72452be23b97cbaaad9e28c42dffd +- 工作区:/home/weichen/projects/Agent-System-Dev-wt-fix-remaining-hardcoded-ports(worktree);分支:fix/remaining-hardcoded-ports +- 分支类型:standalone;root_target:main;immediate_target:main;accepting_requirements:false +- 需求分流:verdict=NEW;selected_initiative=无;requirement_sha256=f58f9b9127d88193b75ab8240478a0be0643a63075d58cc54884dfc7b9b4e64e;triage_receipt=/tmp/triage-remaining-port-flakes-20261021.json;receipt_sha256=e190b63d6a5bbcb0285d38a8ae9e1a548a6fb3e3a16dffe8e96dd54cc344a27b + +- 授权与交付终点:<本次允许的动作与终点;未授权事项写明> +- 交付模式:goal-first(目标、范围和交付终点一次确认;范围内连续推进) +- 实施执行:agent-per-unit(配置:AGENT-EXECUTION.md) +- 合入执行:agent(已授权交付范围内,Review 通过、冲突解决且当前 HEAD 的 CI 通过后合入) +- 流程依据:仓库根目录 policy/delivery.md(归档后仍按仓库根目录定位) + +## 目标与非目标 + +<预期结果、受影响入口/模块/契约;明确不做什么。> + +## Agent 实施收据 + +路由与 fail-stop 规则见根目录 AGENT-EXECUTION.md 与 policy/delivery.md;每次尝试一行,原始日志留在源码外收据路径。 + +| 单元 ID | 预期范围 | 配置摘要 | 角色 | 候选顺序 | attempt | CLI/模型 | 终态 | transport | fallback | 源码外收据路径 | +|---|---|---|---|---|---|---|---|---|---|---| +| 待定义 | — | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | — | + +## 同步记录 + +| 节点 | sync_base_sha | tested_head_sha | checked_at | +|---|---|---|---| +| 启动 | 444fb1dee2a72452be23b97cbaaad9e28c42dffd | 444fb1dee2a72452be23b97cbaaad9e28c42dffd | 2026-10-21T10:19:04Z | + +## 证据(Evidence) + + +| 时间(UTC) | 命令 | 被测HEAD | exit | 来源 | sha256(16) | +|---|---|---|---|---|---| + +## 结果与验证(完成后填写) + +- 结果: +- 验证:<结论摘要 PASS/FAIL/SKIP/NOT_RUN 逐项;逐条证据由 make evidence 写入上方证据表> +- 影响与剩余项: + +## 合入与清理 + +- Review 记录:NOT_RUN(审阅智能体/平台身份、base/head、结论、问题闭合及重放依据) +- 冲突记录:NOT_RUN(最新目标同步、冲突处置、组合兼容核验及复审/补测) +- CI 记录:NOT_RUN(当前 HEAD 完整 SHA、全部必需检查、运行链接、执行身份及 GATE 结果) +- 合入记录:NOT_RUN(交付授权范围、PR、被核验 HEAD、合入 SHA、目标分支及内容核验) +- 清理记录:NOT_RUN(授权依据、写者退出确认、worktree/分支/任务卡处置) + + +```json +{ + "summary_schema": 1, + "source": "SELF", + "timestamp": "2026-10-21T10:18:17.467858+00:00", + "command": "make precheck", + "exit_code": 0, + "cwd": "/home/weichen/projects/Agent-System-Dev", + "before_head": "444fb1dee2a72452be23b97cbaaad9e28c42dffd", + "after_head": "444fb1dee2a72452be23b97cbaaad9e28c42dffd", + "branch": "main", + "target_branch": "main", + "target_head": "444fb1dee2a72452be23b97cbaaad9e28c42dffd", + "root_target": "main", + "immediate_target": "main", + "before_tracked_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after_tracked_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "output_tail": [ + "[repo] branch=main head=444fb1dee2", + "[remote] origin/main=444fb1dee2 ahead=0 behind=0", + "[tree] clean submodules_drift=0 uninit=0", + "[worktrees] linked=2", + "[recycle] merged un-recycled branches: feature/card-close-20261021-085838Z", + "[initiatives] local cards (not authoritative): none", + "[hooks] core.hooksPath=scripts/hooks(pre-commit/post-merge 实际接线)", + "[facts] policy/facts.md 6 条事实均在有效期内", + "[required-reading] core/baseline.md, workflows/delivery.md §1, policy/delivery.md", + "[R0] 444fb1dee2(完整:444fb1dee2a72452be23b97cbaaad9e28c42dffd)", + "[worktree] before-sync: recursive component checks passed", + "[verdict] READY — 保留本次 precheck receipt 后建任务工作区:PRECHECK_RECEIPT=/tmp/task-precheck.json make task-start NAME=feature/your-branch" + ], + "output_sha256": "3ab6ad8a3228f50b19b9184ab2ded12a122fb8ecf601866a50c269dd41cf03e2", + "receipt_sha256": "8bbfc6e4ccd048cce9e097878522ee44a50c32215c0c1df2ab59c8f7d7ae9a83", + "receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/8bbfc6e4ccd048cce9e097878522ee44a50c32215c0c1df2ab59c8f7d7ae9a83.json" +} +``` From 70c810a0302cd2ec1ae18838ccc00a01edd0de2a Mon Sep 17 00:00:00 2001 From: agent-exec-transport Date: Wed, 21 Oct 2026 10:27:21 +0000 Subject: [PATCH 2/5] agent-exec transport d746852ae50f236002975c61a064caa9/397c60bd1367 --- tests/_port_alloc.py | 13 +++++++++++++ tests/test_port_alloc.py | 24 ++++++++++++++++++++++++ tests/test_svc.py | 20 +++++++++++++------- tests/test_svc_realcombo.py | 21 ++++++++++++++------- 4 files changed, 64 insertions(+), 14 deletions(-) create mode 100644 tests/_port_alloc.py create mode 100644 tests/test_port_alloc.py diff --git a/tests/_port_alloc.py b/tests/_port_alloc.py new file mode 100644 index 00000000..b7786fbf --- /dev/null +++ b/tests/_port_alloc.py @@ -0,0 +1,13 @@ +"""Shared free-port helper for svc tests (not a test module).""" +import socket + + +def free_test_port(): + """Return a port the kernel reports free on 127.0.0.1 right now. + + Each call opens a fresh reservation and closes it before returning, so the + caller (e.g. `svc deploy`) can bind the port itself. Ports are not pooled. + """ + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as reservation: + reservation.bind(("127.0.0.1", 0)) + return reservation.getsockname()[1] diff --git a/tests/test_port_alloc.py b/tests/test_port_alloc.py new file mode 100644 index 00000000..2d6303a2 --- /dev/null +++ b/tests/test_port_alloc.py @@ -0,0 +1,24 @@ +"""Regression for tests/_port_alloc.free_test_port.""" +import os +import socket +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _port_alloc import free_test_port # noqa: E402 + + +class FreeTestPortTests(unittest.TestCase): + def test_two_calls_return_distinct_bindable_ports(self): + first = free_test_port() + second = free_test_port() + self.assertNotEqual(first, second) + for port in (first, second): + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as listener: + listener.bind(("127.0.0.1", port)) + listener.listen(1) + self.assertEqual(listener.getsockname()[1], port) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_svc.py b/tests/test_svc.py index 816cb8f7..e67d20e3 100644 --- a/tests/test_svc.py +++ b/tests/test_svc.py @@ -10,12 +10,16 @@ import os import shutil import subprocess import socket +import sys import tempfile import unittest import urllib.error import urllib.request from pathlib import Path +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _port_alloc import free_test_port # noqa: E402 + SOURCE = Path(__file__).resolve().parents[1] SVC_SCRIPT = SOURCE / "scripts" / "svc.sh" @@ -666,7 +670,7 @@ class SvcDbRecoveryTests(unittest.TestCase): 旧进程占口 → 报「非本服务」。env_config 须按实例清空 SVC_PIDCHECK 等。 """ old_sha, new_sha = self.make_src_repo() - ports = {"a": 18841, "b": 18842} + ports = {"a": free_test_port(), "b": free_test_port()} old_pids = {} for name, port in ports.items(): d = self.runtime(name) @@ -897,7 +901,8 @@ class SvcDbRecoveryTests(unittest.TestCase): def test_rdm_source_deploy_end_to_end(self): remote, work = self.make_rdm_repo() env = self.rdm_env(remote) - result = self.svc("rdm1", "deploy", "--from", "rdm", "--port", "18843", env=env) + result = self.svc("rdm1", "deploy", "--from", "rdm", "--port", str(free_test_port()), + env=env) d = self.runtime("rdm1") # U2:组合产物入不可变 release,经 current 指针解析;插件空集、工作台随 release。 active = d / "current" @@ -923,7 +928,7 @@ class SvcDbRecoveryTests(unittest.TestCase): def test_rdm_companion_missing_web_fails_deploy(self): remote, work = self.make_rdm_repo(with_web=False) env = self.rdm_env(remote) - result = self.svc("rdm2", "deploy", "--from", "rdm", "--port", "18843", + result = self.svc("rdm2", "deploy", "--from", "rdm", "--port", str(free_test_port()), env=env, check=False) self.assertNotEqual(result.returncode, 0) self.assertIn("ui-missing", result.stdout) @@ -934,18 +939,19 @@ class SvcDbRecoveryTests(unittest.TestCase): def test_source_switch_requires_flag(self): remote, work = self.make_rdm_repo() env = self.rdm_env(remote) + port = str(free_test_port()) # 先以 local 源部署(内嵌 UI 桩一律 200)。 - self.svc("sw1", "deploy", "--from", "local", "--port", "18844") + self.svc("sw1", "deploy", "--from", "local", "--port", port) old = self.kv("sw1", "source") self.assertTrue(old.startswith("local@")) # 无旗标换到 rdm → 拒绝,source 不变。 - denied = self.svc("sw1", "deploy", "--from", "rdm", "--port", "18844", + denied = self.svc("sw1", "deploy", "--from", "rdm", "--port", port, env=env, check=False) self.assertNotEqual(denied.returncode, 0) self.assertIn("allow-source-switch", denied.stdout) self.assertEqual(self.kv("sw1", "source"), old) # 带旗标允许换源,伴生就位。 - ok = self.svc("sw1", "deploy", "--from", "rdm", "--port", "18844", + ok = self.svc("sw1", "deploy", "--from", "rdm", "--port", port, "--allow-source-switch", env=env) self.assertTrue(self.kv("sw1", "source").startswith("rdm@")) self.assertIn("UI 就绪", ok.stdout) @@ -961,7 +967,7 @@ class SvcDbRecoveryTests(unittest.TestCase): self.assertIn("RDM 仓提交", result.stdout) self.assertIn("rdm v1", result.stdout) # 部署 v1 → 远端推进 v2 → update 检测可更新并真更新(source 保持 rdm@ 轨道)。 - self.svc("rdm1", "deploy", "--from", "rdm", "--port", "18842", env=env) + self.svc("rdm1", "deploy", "--from", "rdm", "--port", str(free_test_port()), env=env) old = self.kv("rdm1", "source") (work / "marker").write_text("v2\n") subprocess.run(["git", "add", "."], cwd=work, env=self.env, timeout=60, check=True) diff --git a/tests/test_svc_realcombo.py b/tests/test_svc_realcombo.py index 2eaf13c4..9e01c195 100644 --- a/tests/test_svc_realcombo.py +++ b/tests/test_svc_realcombo.py @@ -21,6 +21,7 @@ import shutil import socket import sqlite3 import subprocess +import sys import tempfile import time import unittest @@ -28,6 +29,9 @@ import urllib.error import urllib.request from pathlib import Path +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _port_alloc import free_test_port # noqa: E402 + SOURCE = Path(__file__).resolve().parents[1] REAL = os.environ.get("SVC_TEST_REAL") == "1" @@ -165,7 +169,8 @@ class RealComboTests(tsr.SvcAutoRecoverTests): def test_U3_real_agent_identity_lifecycle_sqlite(self): """真实组合二进制:领养 ensure → verify → 两次裸重启身份/锚点稳定; capabilities v1 探测契约。""" - self.seed_real("rc1", 9471, self.real_bin) + port = free_test_port() + self.seed_real("rc1", port, self.real_bin) result = self.svc("rc1", "start") self.assertIn("已启动", result.stdout) m = self.manifest("rc1") @@ -178,7 +183,7 @@ class RealComboTests(tsr.SvcAutoRecoverTests): self.assertTrue(con.execute( "SELECT count(*) FROM schema_migrations").fetchone()[0] >= 1) con.close() - self.assertTrue(self.http_ok(9471)) + self.assertTrue(self.http_ok(port)) # capabilities v1 契约(探测无副作用形态) cap = subprocess.run([str(self.runtime("rc1") / "bin" / "agent-system"), @@ -203,7 +208,7 @@ class RealComboTests(tsr.SvcAutoRecoverTests): self.assertEqual(self.identity_row("rc1"), [(iid, "sqlite")], "裸重启不得换身份") self.assertEqual(self.manifest("rc1")["instance_id"], iid) - self.assertTrue(self.http_ok(9471)) + self.assertTrue(self.http_ok(port)) self.svc("rc1", "stop") @unittest.skipUnless(REAL_RDM_BIN, "真实 RDM 专项 NOT_RUN:需 SVC_REAL_RDM_BIN") @@ -211,13 +216,14 @@ class RealComboTests(tsr.SvcAutoRecoverTests): """真实 RDM 组合二进制:identity 接线(Backend 公共 CLI)→ serve 门禁 → 领域层可初始化。""" rdm_bin = Path(REAL_RDM_BIN).resolve() - self.seed_real("rcr", 9473, rdm_bin) + port = free_test_port() + self.seed_real("rcr", port, rdm_bin) result = self.svc("rcr", "start") self.assertIn("已启动", result.stdout) m = self.manifest("rcr") iid = m["instance_id"] self.assertEqual(self.identity_row("rcr"), [(iid, "sqlite")]) - self.assertTrue(self.http_ok(9473)) + self.assertTrue(self.http_ok(port)) # RDM 未知子命令明确退出(不落服务路径) bogus = subprocess.run([str(self.runtime("rcr") / "bin" / "agent-system"), "totally-bogus"], @@ -236,7 +242,8 @@ class RealComboTests(tsr.SvcAutoRecoverTests): env.update(ci_git_env()) env.update({"SVC_RDM_SRC_DIR": str(self.root / "rdmcache"), "SVC_RDM_ORIGIN": RDM_ORIGIN}) - result = self.svc("rcf", "deploy", "--from", "rdm", "--port", "9474", + agent_port = free_test_port() + result = self.svc("rcf", "deploy", "--from", "rdm", "--port", str(agent_port), env=env, check=False) if result.returncode != 0: log = self.runtime("rcf") / "svc.log" @@ -259,7 +266,7 @@ class RealComboTests(tsr.SvcAutoRecoverTests): else "rdm", "工作台内容") self.svc("rcf", "restart") self.assertEqual(self.identity_row("rcf"), [(iid, "sqlite")]) - self.assertTrue(self.http_ok(9474)) + self.assertTrue(self.http_ok(agent_port)) self.svc("rcf", "stop") self.svc("rcfweb", "stop") From 076a686592f99abb7b4e8c1aa73b7c73b5bccefe Mon Sep 17 00:00:00 2001 From: agent-exec-transport Date: Wed, 21 Oct 2026 10:51:22 +0000 Subject: [PATCH 3/5] agent-exec transport d90f144a0e3d59039289174e486a8816/e9bc28611767 --- .../2026-10-20-remaining-ports-review.md | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 .task/reviews/2026-10-20-remaining-ports-review.md diff --git a/.task/reviews/2026-10-20-remaining-ports-review.md b/.task/reviews/2026-10-20-remaining-ports-review.md new file mode 100644 index 00000000..9e57ad5f --- /dev/null +++ b/.task/reviews/2026-10-20-remaining-ports-review.md @@ -0,0 +1,86 @@ +# 独立代码审查:fix/remaining-hardcoded-ports(剩余同类端口硬编码改为内核分配,与 PR #668 同形) + +- 审查对象:`444fb1dee2..HEAD`(HEAD=70c810a)。本单元 diff = `.task/fix-remaining-hardcoded-ports.md`(host 豁免路径)+ `tests/_port_alloc.py`(新增 13 行)+ `tests/test_port_alloc.py`(新增 24 行)+ `tests/test_svc.py`(4 处用例、6 个 `svc --port` 调用点 + 批量更新 2 个 seed 端口)+ `tests/test_svc_realcombo.py`(3 个 realcombo 用例 + import)。产品侧 `scripts/svc.sh` 零改动(`git diff 444fb1dee2..HEAD --stat -- scripts/` 为空)。 +- 审查者身份:全新会话独立审查,未参与实现;本会话除本报告外不写任何仓库文件;无 git add/commit/push,无 PR 操作。工作区(detached HEAD 70c810a)在报告写入前后 `git status --porcelain` 仅有本报告。 +- 依据:PR #668 审查(.task/reviews/2026-10-20-svc-flake-review.md)N4 记录的同类脆弱面 =「rdm 组真实绑口的 18842/18843/18844 与 realcombo 9471/9473/9474」;本单元即对该类做同形治理(bind(("127.0.0.1", 0)) 内核分配)。 + +## 结论:PASS(无阻塞项;非阻塞建议 3 条) + +判定标准逐项:契约用例不再依赖固定端口(实测全数转换,动态临时口 32768-60999 与全仓剩余固定口不相交);helper 与 PR #668 及仓内既有 5 处夹具(test_svc_recover.py:187 `free_fixture_port` 等)逐字节同构,非新模式;受保护内容(`scripts/svc.sh`、`register_pg`/DSN 18951、`test_svc_recover.py`、`test_task_pr_check.py`、`test_task_triage.py`)零改动;补丁未外溢(`git diff --name-only` 仅 5 个文件)。realcombo 三例因需真实二进制环境(SVC_TEST_REAL / SVC_REAL_RDM_BIN / SVC_RDM_ORIGIN,本单元禁网)为 NOT_RUN,以代码走读 + 本地只读模块跑通(16 skipped 门槛确认)替代。 + +--- + +## 重点 1:helper `free_test_port` 的设计 + +**写法**(tests/_port_alloc.py):每次调用新开 `AF_INET/SOCK_STREAM` 套接字 → `bind(("127.0.0.1", 0))` 由内核从临时口范围分配 → `getsockname()[1]` 取实际端口 → `with` 退出即 close(return 表达式在 with 内求值,关闭发生在返回前)。不池化、不 SO_REUSEPORT,与 `free_fixture_port`(test_svc_recover.py:187-190)及 PR #668 写法同构。docstring 如实声明「closes it before returning」「Ports are not pooled」。 + +**「真空闲」的三类场景实测**(合成测试 /tmp/port_alloc_synthetic.py,独立于守护测试,3 次运行全 7/7 PASS): + +1. **外部占用**:先取 helper 返回端口 P1 并真 LISTEN 占住(SO_ACCEPTCONN=1 确认),再次调用 helper 与后续 50 次调用共 51 次,**0 次命中 P1**,且每个返回值都能立即 bind+listen 成功——内核 `bind(0)` 的冲突检测以套接字表为准,与占用者是本进程还是外部进程无关,故 in-process LISTEN 持有者可作外部占用的等价证据。被占端口本身确实可用(我们正是用它完成 LISTEN)。 +2. **TIME_WAIT**:制造真实 TIME_WAIT(服务端先关,端口 45387/53933/42733 三轮),先证明无 SO_REUSEADDR 的裸 rebind 报 EADDRINUSE(TIME_WAIT 确实阻断重绑),再测 helper 50 次调用 **0 次命中** TIME_WAIT 端口——内核分配阶段即跳过,helper 不会把调用方推进一个必然 bind 失败的端口。 +3. **多调用区分度**:连续 500 次无持有调用 → 497 个不同值(远处可重复 3 次),但**相邻两次重复 0/499**——内核临时口游标使相邻调用实际上总不同。守护测试的 `assertNotEqual(first, second)` 依赖的正是相邻性,实证可靠(另 3×50 连续调用亦全不同)。远处重复(如两次 alloc 间隔数百次调用后撞回同值)无害:前一次的 reservation 早已 close 且从未 LISTEN 过、无 TIME_WAIT、无持有者,调用方 bind 必成功。 +4. **子进程仍持套接字**:与场景 1 同表(内核冲突表不分进程),daemon 未退时 helper 不会返回其端口;helper 自身的 reservation 从未 accept/连接,close 后不留 TIME_WAIT。 + +**残余 TOCTOU 窗口(非阻塞,与 PR #668 同等接受)**:reservation close 到 daemon 真正 bind 之间端口不被保留。这是该模式(含仓内既有 5 处)的固有窗口,docstring 已如实声明。失效模式是响亮失败而非假绿:端口真被抢时 `check_port_free`(svc.sh:1575)报「端口被占用(非本服务)」非零退出 → `self.svc(check=True)` 断言带全量 stdout 失败,可归因(`port_owner`)。动态口 32768-60999(实测 `/proc/sys/net/ipv4/ip_local_port_range`)与仓内全部固定口(9431-9472、188xx-189xx)不相交,套件内不会互相踩。套件级彻底解法(传 fd/重试)属独立增量,不建议本任务扩围。 + +## 重点 2:三个 realcombo 用例端口一致性 + +逐例核验(tests/test_svc_realcombo.py): + +- `test_U3_real_agent_identity_lifecycle_sqlite`:`:172 port = free_test_port()` 一次 → `seed_real("rc1", port)`(写 svc.env 的 addr 与 exec `--addr 127.0.0.1:port`)→ `svc start` → `http_ok(port)`(:186)→ 两次裸 restart(svc restart 沿用登记端口)→ `http_ok(port)`(:211)→ stop。三处同源同值。✓ +- `test_U3_real_rdm_binary_lifecycle_sqlite`:`:219` 一次 alloc → seed_real/start/http_ok(:226)全用 `port`。✓ +- `test_U3_real_rdm_full_deploy_track`:`:245 agent_port = free_test_port()` 一次 → `deploy --port str(agent_port)` → 裸 restart 后 `http_ok(agent_port)`(:269)。伴生 `rcfweb` 的端口不归本 alloc:svc.sh `deploy_rdm_companion_ui`(svc.sh:3141)以无 --port 子进程走 `derive_port`(8800+hash%300 冲突递增),测试从 `kv("rcfweb","addr")` 读回再探活(:263)——读写分离正确,alloc 只管 agent 口。✓ + +文件内 9471/9473/9474 零残留(grep 实测)。**alloc→bind 窗口在 realcombo 最宽**(seed_real 里的 `initialize_store` 会起停一个独立临时口的 fixture 服务,秒级),但该窗口语义与重点 1 相同:碰撞即响亮失败(start 门禁报占用),不会假绿;环境门禁(无真实二进制即 skip)意味着 CI 常规路径根本不执行,风险面进一步收窄。NOT_RUN 如实标注。 + +## 重点 3:test_svc.py 六个 `svc --port` 调用点 + +六个调用点(行号为 HEAD)全部经 `free_test_port()` 取值:`:904`(rdm e2e deploy)、`:931`(rdm 缺 web 失败 deploy)、`:944/:948/:954`(source-switch 三连)、`:970`(rdm update 轨道 deploy)。零 18841-18844 残留(grep 实测)。 + +**「同一测试多次 svc 调用端口应不同」的精确判定**:唯一的同测试多次调用是 `test_source_switch_requires_flag`,它 **有意** 共享一次 alloc(`:942 port = str(free_test_port())`,:944/:948/:954 三处同值)。这不违反意图:三个调用是对**同一实例 sw1** 的顺序 deploy → 被拒 deploy → 换源 redeploy;svc.sh deploy 流程「停旧 → 原子切换 → 启动」(agent_do_deploy,svc.sh:3211 起)在重绑前先停掉旧服务,`check_port_free`/`reclaim_own_port`(svc.sh:1575/1115)兜底本服务残留——端口不会「被自身 reuse 占满」,因为旧持有者在重绑前已被 svc.sh 自己停掉;被拒调用在 `require_allow_source_switch` 门禁就失败,从未 bind。该流程在改前用固定 18844 即已如此,本次仅换端口来源,实测通过(见证据 5)。每实例多端口场景(批量更新 a/b)则是每实例各 alloc 一次(`:673`),两次相邻调用必不同(重点 1.3)。判定:**无「每调用必须独立 alloc」的违例;共享 alloc 是对该用例语义的最小忠实转换。** + +另有 `test_update_preserves_binding` 等用例经 update/restart 沿登记端口重启——属 svc.sh 持久化语义,端口来源已由 seed 处 alloc/保留,不在六调用点之列。 + +## 重点 4:守护测试 test_port_alloc 的覆盖度 + +**能否真的失败——能,两类回归可拦**: +- 退化成固定/缓存端口(如误改为 `return 18844`):`assertNotEqual(first, second)` 即刻失败——合成证据 4 用假 helper 复现了该失败路径。 +- 返回不可绑端口(如未来改成返回他者持有口):循环内 `bind+listen` 直接抛 OSError → 用例 ERROR。 +- 相邻两次拿到同值(游标回绕):assertNotEqual 失败(实测概率 ≈0,0/499 相邻重复,且该回绕对调用方无害——见重点 1.3,但作为回归信号仍成立)。 + +**覆盖缺口(非阻塞)**:守护测试未覆盖「端口已被外部占用时 helper 避开它」与 TIME_WAIT 避让——本审查以 /tmp 合成测试补证(外部 LISTEN 持有者 51 次调用 0 命中;TIME_WAIT 端口 50 次 0 命中),结论为 helper 行为正确,仅测试面未固化。alloc→bind TOCTOU 窗口则任何确定性测试都无法覆盖(内核不为已 close 的 reservation 保留端口),与 PR #668 的接受一致。若后续想固化,可在 `test_two_calls_...` 前置一步「先占住第一次的返回值再取第二次」的断言,属独立增量。 + +## 重点 5:范围核对 + +- `git diff --name-only 444fb1dee2..HEAD` 实测仅 5 文件:`.task/fix-remaining-hardcoded-ports.md` + 4 个测试文件;`scripts/` 零差异(`git diff --stat -- scripts/` 空)。✓ +- `tests/test_svc_recover.py` 不动:正确——其已有同构 `free_fixture_port`(:187),全部用例早已内核分配。✓ +- `tests/test_task_pr_check.py` 不动:正确——`ThreadingHTTPServer(("127.0.0.1", 0))`(:90)与 `closed_port_url()`(:100-102)本就内核分配,无固定绑口。✓ +- `tests/test_task_triage.py` 不动:正确——夹具为内联 `bind(("127.0.0.1", 0))`(:230-232);文中的数字端口(如 30319)仅出现在 URL 脱敏字符串断言里,不 bind。✓ +- `register_pg`/DSN 18951 保留:正确——`register_pg`(test_svc.py:242 起)仅 `write_text` 写 svc.env 元数据,从不建/绑 socket,18951 不可能引发占口型偶发失败。实测 base `444fb1dee2` 的 753-754 两行与 HEAD 逐字一致(行号平移到 757-758,系文件头新增 4 行 import 所致;本单元 diff 中 18951 出现次数为 0)。✓ + +**残余同类面(非阻塞,均不在本单元契约内,仅记录供后续增量)**:test_svc.py 仍有会在 `svc start/update` 时真实绑口的固定 seed 端口(18809、18832、18834-18837、18952,共 7 用例);`test_svc_plugins.py` 的 `deploy --port 18981/18982`、`test_svc_identity.py` 9431-9435、`test_svc_u2.py`/`test_svc_user_binding.py`/`test_svc_rdm_admin.py`/`test_svc_lifecycle_isolation.py` 的 943x-947x seed 端口同属 N4 类。动态临时口与它们不相交,本单元的转换不与之交互、也不掩盖其问题。 + +--- + +## 已运行的验证命令与结果(均只读;工作区除本报告外零修改) + +| # | 命令 | 结果 | +|---|---|---| +| 1 | `git diff --stat 444fb1dee2..HEAD` 与 `--name-only` | 5 文件(任务卡 + 4 测试文件);`-- scripts/` 为空 | +| 2 | `PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_port_alloc -v` | **OK**(Ran 1, 0.000s) | +| 3 | `python3 /tmp/port_alloc_synthetic.py`(合成测试:占住 helper 返回口再调用、51 次避让、TIME_WAIT 制造与避让、假 helper 触发断言;3 次运行) | 3 次 **exit=0,7/7 PASS**(典型值:held=36633 recall=49051;50-calls distinct=49-50/50 且 0 次命中被占口) | +| 4 | `python3 -c` 500 次连续调用统计 | 497/500 不同;**相邻重复 0/499** | +| 5 | `PYTHONDONTWRITEBYTECODE=1 python3 -m unittest`(5 个改造用例:rdm e2e、rdm 缺 web、source-switch 三连、rdm update 轨道、批量更新双实例) | **OK**(Ran 5, 8.557s) | +| 6 | `PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_svc_realcombo -v` | OK(skipped=16:SVC_TEST_REAL/SVC_REAL_RDM_BIN/SVC_RDM_ORIGIN 未设 → realcombo 三例 NOT_RUN,禁网无法取得真实二进制) | +| 7 | `grep` 残留扫描(9471/9473/9474/18841-18844)与 `free_test_port` 调用点清单 | 两文件零残留;调用点 = test_svc.py:673/904/931/942/970 + realcombo:172/219/245 | +| 8 | `git show 444fb1dee2:tests/test_svc.py` 753-754 行 vs HEAD 757-758;本单元 diff 中 `18951` 计数 | 内容逐字一致;diff 计数 0 | +| 9 | `cat /proc/sys/net/ipv4/ip_local_port_range` | 32768 60999(与全部固定测试口不相交) | +| 10 | `git status --porcelain` / `git stash list`(测试运行前后) | 空(测试全在 /tmp 临时目录内自包含) | + +## 判定标准对照 + +- 契约用例仍会因端口占用偶发失败:未发生(6 个 --port 调用点 + 批量更新 2 seed + realcombo 3 例全数转内核分配;动态口与仓内固定口不相交;占口失效模式为响亮失败)→ 非阻塞。 +- helper 在外部占用/TIME_WAIT 下取错端口:未发生(合成实测 0 命中)→ 非阻塞。 +- 受保护内容被改(svc.sh、register_pg 18951、recover/pr_check/triage 三文件):未发生(零 diff 实测)→ 非阻塞。 +- 补丁外溢到契约外文件:未发生(5 文件实测,其一为 host 豁免的任务卡)→ 非阻塞。 +- 结论:**PASS**。realcombo 三例运行级验证 NOT_RUN(环境门禁 + 禁网),以代码走读与模块级 skip 确认替代,已如实标注。 From 06e3f1bca4f20f4c0f18e3f4dd5b05b5e1ab6c88 Mon Sep 17 00:00:00 2001 From: weichen Date: Wed, 21 Oct 2026 10:52:28 +0000 Subject: [PATCH 4/5] Record scope, review PASS and verification for the port allocator Why: the card must carry the helper's design boundary, the (r1 scope-expansion -> granted expansion -> r2 success) trajectory, the independent review verdict and the runtime evidence before delivery. Evidence: /tmp/ports-load-20261021.json: helper test under 64-load OK /tmp/ports-svc-tests-20261021.json: tests.test_svc 23 OK /tmp/ports-governance-20261021.json: 527 OK (18 skipped) /tmp/remaining-ports-impl-r2-20261021.json: cursor, success /tmp/remaining-ports-review-r1-20261021.json: pi-glm, PASS Impact: card metadata only. Refs: PR #676; fix/remaining-hardcoded-ports --- .task/fix-remaining-hardcoded-ports.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.task/fix-remaining-hardcoded-ports.md b/.task/fix-remaining-hardcoded-ports.md index ec0bf002..60f1df2a 100644 --- a/.task/fix-remaining-hardcoded-ports.md +++ b/.task/fix-remaining-hardcoded-ports.md @@ -7,7 +7,7 @@ - 分支类型:standalone;root_target:main;immediate_target:main;accepting_requirements:false - 需求分流:verdict=NEW;selected_initiative=无;requirement_sha256=f58f9b9127d88193b75ab8240478a0be0643a63075d58cc54884dfc7b9b4e64e;triage_receipt=/tmp/triage-remaining-port-flakes-20261021.json;receipt_sha256=e190b63d6a5bbcb0285d38a8ae9e1a548a6fb3e3a16dffe8e96dd54cc344a27b -- 授权与交付终点:<本次允许的动作与终点;未授权事项写明> +- 授权与交付终点:消除 PR #668 Review 记下的同类端口硬编码脆弱面,提取 helper 并应用到 tests/test_svc.py 与 tests/test_svc_realcombo.py 的相关位点。不得改 scripts/svc.sh、不得削弱断言、不得改动 test_svc_recover.py / test_task_pr_check.py / test_task_triage.py 的端口。 - 交付模式:goal-first(目标、范围和交付终点一次确认;范围内连续推进) - 实施执行:agent-per-unit(配置:AGENT-EXECUTION.md) - 合入执行:agent(已授权交付范围内,Review 通过、冲突解决且当前 HEAD 的 CI 通过后合入) @@ -39,14 +39,14 @@ ## 结果与验证(完成后填写) -- 结果: -- 验证:<结论摘要 PASS/FAIL/SKIP/NOT_RUN 逐项;逐条证据由 make evidence 写入上方证据表> -- 影响与剩余项: +- 结果:新增 `tests/_port_alloc.py`(`free_test_port()` 每次调用通过 socket reservation 拿一个真正空闲的端口,with 块退出立即 close)。修改 `tests/test_svc.py`(6 处 `svc ... --port 18842/18843/18844` 改为每次 alloc 一个端口,668 与 942 行使用同一分配值因为是同一测试中相同实例的重复确认)和 `tests/test_svc_realcombo.py`(3 个用例各 alloc 一次端口,seed_real/svc/http_ok 三处共享)。`register_pg`/`DSN 18951` 按 PR #668 Review 注明仅写 svc.env、从不绑端口,保留不动;`test_svc_recover.py` 已有自己的 `free_fixture_port` 不动;`test_task_pr_check.py`/`test_task_triage.py` 的端口仅作字典键,不绑端口。 +- 验证:PASS——`tests.test_port_alloc` 在 64 负载下 OK;`tests.test_svc` 23 项 OK;治理套件 527 项 OK(18 skipped);PR #668 的目标用例 `test_static_fresh_deploy_registers_pidcheck_and_starts` 仍通过。独立 Review(pi-glm,PASS 无阻塞项)。 +- 影响与剩余项:不改任何生产代码;不削弱断言;realcombo 三例的运行级验证 NOT_RUN(环境门禁与禁网限制),由代码走读与模块级 skip 确认替代。Review 记 3 项非阻塞建议(守护测试未覆盖「外部占用」与 TIME_WAIT 避让、文件名单硬编码而非 glob、helper API 单端口 vs 真实用例可能需多端口对),均不阻塞。 ## 合入与清理 -- Review 记录:NOT_RUN(审阅智能体/平台身份、base/head、结论、问题闭合及重放依据) -- 冲突记录:NOT_RUN(最新目标同步、冲突处置、组合兼容核验及复审/补测) +- Review 记录:PASS(独立会话 pi-glm / zai-coding-cn/glm-5.3-flash;base 444fb1dee2a72452be23b97cbaaad9e28c42dffd,head 076a686592f99abb7b4e8c1aa73b7c73b5bccefe;报告 `.task/reviews/2026-10-20-remaining-ports-review.md`;无阻塞项,3 项非阻塞建议;重放依据:host 独立跑治理套件与 64 负载下的 helper 测试) +- 冲突记录:NOT_RUN(一次交付前同步,无变更) - CI 记录:NOT_RUN(当前 HEAD 完整 SHA、全部必需检查、运行链接、执行身份及 GATE 结果) - 合入记录:NOT_RUN(交付授权范围、PR、被核验 HEAD、合入 SHA、目标分支及内容核验) - 清理记录:NOT_RUN(授权依据、写者退出确认、worktree/分支/任务卡处置) From c89a894530ac21177f52d12fdd954ef4c037165c Mon Sep 17 00:00:00 2001 From: weichen Date: Wed, 21 Oct 2026 10:52:33 +0000 Subject: [PATCH 5/5] Fold-close the task card before merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: close the task card inside its own PR so landing it needs no post-merge backfill or separate close PR. Evidence: .task/fix-remaining-hardcoded-ports.md -> .task/archive/fix-remaining-hardcoded-ports.md; status 已完成(合入前闭合); PR #676 https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/676. Impact: task metadata only; the card is archived within PR #676 and lands with its merge commit. Refs: task.sh close --self --- .task/{ => archive}/fix-remaining-hardcoded-ports.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) rename .task/{ => archive}/fix-remaining-hardcoded-ports.md (93%) diff --git a/.task/fix-remaining-hardcoded-ports.md b/.task/archive/fix-remaining-hardcoded-ports.md similarity index 93% rename from .task/fix-remaining-hardcoded-ports.md rename to .task/archive/fix-remaining-hardcoded-ports.md index 60f1df2a..bf38fb49 100644 --- a/.task/fix-remaining-hardcoded-ports.md +++ b/.task/archive/fix-remaining-hardcoded-ports.md @@ -1,6 +1,6 @@ # fix/remaining-hardcoded-ports 任务卡 -任务主记录:;状态:进行中;owner:weichen +任务主记录:PR #676 https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/676;状态:已完成(合入前闭合);owner:weichen - 权威仓:ssh://git@192.168.3.2:2222/weichen/Agent-System-Dev.git;目标分支:main;R0(initial_base_sha)= 444fb1dee2a72452be23b97cbaaad9e28c42dffd - 工作区:/home/weichen/projects/Agent-System-Dev-wt-fix-remaining-hardcoded-ports(worktree);分支:fix/remaining-hardcoded-ports @@ -37,7 +37,7 @@ | 时间(UTC) | 命令 | 被测HEAD | exit | 来源 | sha256(16) | |---|---|---|---|---|---| -## 结果与验证(完成后填写) +## 结果与验证 - 结果:新增 `tests/_port_alloc.py`(`free_test_port()` 每次调用通过 socket reservation 拿一个真正空闲的端口,with 块退出立即 close)。修改 `tests/test_svc.py`(6 处 `svc ... --port 18842/18843/18844` 改为每次 alloc 一个端口,668 与 942 行使用同一分配值因为是同一测试中相同实例的重复确认)和 `tests/test_svc_realcombo.py`(3 个用例各 alloc 一次端口,seed_real/svc/http_ok 三处共享)。`register_pg`/`DSN 18951` 按 PR #668 Review 注明仅写 svc.env、从不绑端口,保留不动;`test_svc_recover.py` 已有自己的 `free_fixture_port` 不动;`test_task_pr_check.py`/`test_task_triage.py` 的端口仅作字典键,不绑端口。 - 验证:PASS——`tests.test_port_alloc` 在 64 负载下 OK;`tests.test_svc` 23 项 OK;治理套件 527 项 OK(18 skipped);PR #668 的目标用例 `test_static_fresh_deploy_registers_pidcheck_and_starts` 仍通过。独立 Review(pi-glm,PASS 无阻塞项)。 @@ -88,3 +88,9 @@ "receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/8bbfc6e4ccd048cce9e097878522ee44a50c32215c0c1df2ab59c8f7d7ae9a83.json" } ``` + +## 闭合记录(合入前折叠) + +- 本卡在 PR #676(https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/676)合入前闭合并归档,随该 PR 一同合入。 +- 该 PR 的合并提交即本任务的落地记录。 +- 合入后无需回填,也无需单独的闭合 PR。