From 241bea6ce7c99d9da56988a72278624956ecddaa Mon Sep 17 00:00:00 2001 From: weichen Date: Mon, 12 Oct 2026 13:52:29 +0000 Subject: [PATCH] Add immutable release dirs with switch journal and failure rollback Why: deploy overwrote live artifacts after stopping the old service, so a failed start or workbench check left instances in a mixed registration-vs-artifact state with no recovery path (2026-10-12 rdm1/rdmcand); the report's U2 requires restorable updates. Evidence: make test-governance 250 OK; five svc suites 105 OK (rows in the task card); new tests cover layout/pidcheck stability, rollback on start failure with desired-state restore, build failure keeping the old service running, interrupted-switch journal resolution, and schema- advance blocking rollback without touching the database. Impact: agent deploys stage immutable releases// and switch the current symlink atomically; deploy.journal persists the switch and is resolved at start/restart/deploy/update/recover entries; legacy layouts are adopted on first deploy and remain readable; release/rdm tracks verify sha256 before staging. Refs: .task/feature-svc-u2-release-recovery.md; user goal directive 2026-10-12; Dev #474 (U1 gate); Documents unified-deploy plan r2. --- .task/feature-svc-u2-release-recovery.md | 68 +++- scripts/svc.sh | 434 +++++++++++++++++++---- tests/test_svc.py | 10 +- tests/test_svc_u2.py | 228 ++++++++++++ 4 files changed, 666 insertions(+), 74 deletions(-) create mode 100644 tests/test_svc_u2.py diff --git a/.task/feature-svc-u2-release-recovery.md b/.task/feature-svc-u2-release-recovery.md index c536cc8c..220e1636 100644 --- a/.task/feature-svc-u2-release-recovery.md +++ b/.task/feature-svc-u2-release-recovery.md @@ -7,15 +7,22 @@ - 权威仓:ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Agent-System-Dev.git;目标分支:main;R0(initial_base_sha)= 9b5be3926eaa394a9e520fd72ffdf6d0a48d1aef - 工作区:/home/weichen/projects/Agent-System-Dev-wt-feature-svc-u2-release-recovery(worktree);分支:feature/svc-u2-release-recovery -- 授权与交付终点:<本次允许的动作与终点;未授权事项写明> +- 授权与交付终点:用户 2026-10-12 goal 指令(按序实施未做项);本任务 = 统一部署方案 U2(更新失败恢复能力)的 svc.sh 实现;交付终点 = PR 可合入 - 交付模式:manual(逐阶段人工评审);明确授权后可记为 authorized-continuous - 自动推进授权:无;授权范围:无;授权来源:无;授权终点:无;停止条件:无(未授权) -- 当前阶段:需求拆解;当前增量:I1(待定义) +- 当前阶段:实施与自检(待人工评审);当前增量:I1(releases/current/journal/回退)+ I2(回归测试) - 流程依据:仓库根目录 policy/delivery.md(归档后仍按仓库根目录定位) ## 目标与非目标 -<预期结果、受影响入口/模块/契约;明确不做什么。> +目标(诊断报告 U2 完成条件):更新具备失败恢复能力—— +1. 制品(二进制/插件/工作台)按不可变版本目录 releases// 保存;current 符号链接原子切换(temp+rename);进程 cmdline 保留未解析路径 → pidcheck 跨版本稳定,exe=(deleted) 覆盖孤儿类问题根除; +2. 切换记录持久化(deploy.journal:staging→switched→done/rolledback/blocked + prev/desired);任意步骤中断由 svc_deploy_journal_resolve 在 start/restart/deploy/update/recover 入口判定并恢复; +3. 成功提交实际版本(source=release、schema_at_commit 记录、release 裁剪 gc);失败(启动即退/工作台缺失/UI 探活)回退上一完整版本并恢复原期望运行态; +4. schema 兼容:库 schema 超过上一 release 提交时点 → 阻断回退(journal=blocked),绝不触碰业务数据库(不能靠覆盖库「回滚」);blocked 由显式 deploy 跨越; +5. legacy 布局(bin/ 直落服务目录)首次 deploy 领养为 release;legacy 目录永不删(旧进程 pidcheck 兼容);未迁移实例裸生命周期照旧。 + +非目标:U3 真实组合 CI 门禁;制品签名/远端分发;postgres static kind 不涉及;RDM 仓改动。 ## 关联方向与规划 @@ -27,13 +34,18 @@ | ID | 描述 | 项目符合度及依据 | 优先级 | 验收条件 | 依赖 | 结论 | |---|---|---|---|---|---|---| -| R1 | 待填写 | 待评估 | 待定 | 待定义 | 待确认 | 待评审 | +| R1 | 不可变 release + 原子切换 | 符合:install 覆盖改为 release 暂存+指针切换 | P0 | 布局断言 + cmdline 走 current + exec/pidcheck 一致 | 无 | 已实现 | +| R2 | 切换记录持久化、中断可恢复 | 符合:journal 五态 + 五入口 resolve | P0 | journal switched 伪造 → start 回退并恢复运行 | 无 | 已实现 | +| R3 | 启动/工作台失败回退旧版本并恢复期望态 | 符合:事故形态即「停服→覆盖→拒启」 | P0 | die-stub 部署失败 → 运行于 v1、制品=v1、history rollback | R1 | 已实现 | +| R4 | 构建失败旧服务保持运行 | 符合:U1 已前置构建,本任务补齐断言 | P0 | 坏 Makefile → 同 pid 运行、current 不动 | 无 | 已实现 | +| R5 | schema 前进阻断回退、不碰库 | 符合:报告约束「不能自动覆盖业务数据库」 | P0 | 库 v5>prev v3 → 拒绝且库不变、journal=blocked、restart 拒绝 | 无 | 已实现 | +| R6 | legacy 兼容 | 符合:存量实例未迁移不受影响 | P0 | 裸 start/restart 走原路径(既有 105 测全绿) | 无 | 已实现 | ## 方案与计划 -- 方案引用:待填写(含对象版本) -- 实施计划引用:待填写(含增量与需求 ID) -- 测试计划引用:待填写(含需求 ID 与验收条件) +- 方案引用:用户诊断报告 U2(2026-10-12)+ Documents 统一部署方案 r2 +- 实施计划:I1 = svc.sh(release/stage/switch/gc、journal、rollback、resolve 五入口接入、deploy 全轨改走 release、exec/pidcheck current 解析);I2 = tests/test_svc_u2.py 六用例 + test_svc.py rdm e2e 断言更新 +- 测试计划:R1–R6 ↔ test_svc_u2 T1–T5 + 既有五套件(105 用例)+ make test-governance(250 用例) ## 阶段评审 @@ -126,3 +138,45 @@ "receipt_sha256": "bbcdb9b5e38841c02dd42321675aee50d11052f8cb60e147d428460d57f6d1ef" } ``` + + +```json +{ + "schema": 1, + "source": "SELF", + "timestamp": "2026-10-12T13:49:32.617045+00:00", + "command": "make test-governance", + "command_sha256": "9776a8dfcfb8f4c95abc94c40cae3a2c0e91e8f4b91fe95d98085c290e929158", + "cwd": "/home/weichen/projects/Agent-System-Dev-wt-feature-svc-u2-release-recovery", + "common_dir": "/home/weichen/projects/Agent-System-Dev/.git", + "before": { + "head": "2076994b16f11e3290ffaacfe89d6c26bfbbb731", + "branch": "feature/svc-u2-release-recovery", + "target_branch": "main", + "target_head": "3b9cf82cd6fb53f973f26e2039275f7858b0c654", + "status": " M scripts/svc.sh\n M tests/test_svc.py\n?? tests/test_svc_u2.py\n", + "tracked_diff_sha256": "aa6e20c600686b48739e618edee7d5083365862a050ec94a4848e4eb1216e41d", + "untracked": { + "tests/test_svc_u2.py": "21584ddc98648c6176e6033fc013b8d92292fc20a5a0b2ef4f92ede073859fc7" + }, + "submodules": " 4f583ca3eeeb07a56a714c5086de84dcd6ad2980 deps/agent-command (heads/main)\n ed2af3e6048928b1fd0ba6da1f9d0c1bafec82d4 deps/agent-command/deps/development-guidelines (remotes/origin/feature/delivery-mechanism-5-ged2af3e)\n 343b1e6d9a5b58e47bf7e594826221036ae8175c deps/agent-intent (heads/main)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-intent/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 deps/agent-intent/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-intent/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 deps/agent-intent/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-intent/deps/logging-system/deps/development-guidelines (26b562d)\n c348441691e8bdab784a03aa9be64d53dacff341 deps/agent-native (heads/main)\n 343b1e6d9a5b58e47bf7e594826221036ae8175c deps/agent-native/deps/agent-intent (heads/main)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-native/deps/agent-intent/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 deps/agent-native/deps/agent-intent/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-native/deps/agent-intent/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 deps/agent-native/deps/agent-intent/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-native/deps/agent-intent/deps/logging-system/deps/development-guidelines (26b562d)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-native/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 deps/agent-native/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-native/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 deps/agent-native/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-native/deps/logging-system/deps/development-guidelines (26b562d)\n 490b0b2dfa1567cb16ff7a7230fd78e52bc2938e deps/agent-squad (heads/main)\n 343b1e6d9a5b58e47bf7e594826221036ae8175c deps/agent-squad/deps/agent-intent (heads/main)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/agent-intent/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 deps/agent-squad/deps/agent-intent/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/agent-intent/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 deps/agent-squad/deps/agent-intent/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/agent-intent/deps/logging-system/deps/development-guidelines (26b562d)\n c348441691e8bdab784a03aa9be64d53dacff341 deps/agent-squad/deps/agent-native (heads/main)\n 343b1e6d9a5b58e47bf7e594826221036ae8175c deps/agent-squad/deps/agent-native/deps/agent-intent (heads/main)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/agent-native/deps/agent-intent/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 deps/agent-squad/deps/agent-native/deps/agent-intent/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/agent-native/deps/agent-intent/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 deps/agent-squad/deps/agent-native/deps/agent-intent/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/agent-native/deps/agent-intent/deps/logging-system/deps/development-guidelines (26b562d)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/agent-native/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 deps/agent-squad/deps/agent-native/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/agent-native/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 deps/agent-squad/deps/agent-native/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/agent-native/deps/logging-system/deps/development-guidelines (26b562d)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 deps/agent-squad/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 deps/agent-squad/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/logging-system/deps/development-guidelines (26b562d)\n af199eeaa23c5b39e08b4649f2513d7572dc5274 deps/development-guidelines (heads/main)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/logging-system/deps/development-guidelines (26b562d)\n acabb85e2ae970f834defae916c7539d0a47993d modules/backend (v0.3.0-130-gacabb85)\n 4f583ca3eeeb07a56a714c5086de84dcd6ad2980 modules/backend/deps/agent-command (heads/main)\n ed2af3e6048928b1fd0ba6da1f9d0c1bafec82d4 modules/backend/deps/agent-command/deps/development-guidelines (remotes/origin/feature/delivery-mechanism-5-ged2af3e)\n 343b1e6d9a5b58e47bf7e594826221036ae8175c modules/backend/deps/agent-intent (heads/main)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-intent/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 modules/backend/deps/agent-intent/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-intent/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 modules/backend/deps/agent-intent/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-intent/deps/logging-system/deps/development-guidelines (26b562d)\n c348441691e8bdab784a03aa9be64d53dacff341 modules/backend/deps/agent-native (heads/main)\n 343b1e6d9a5b58e47bf7e594826221036ae8175c modules/backend/deps/agent-native/deps/agent-intent (heads/main)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-native/deps/agent-intent/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 modules/backend/deps/agent-native/deps/agent-intent/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-native/deps/agent-intent/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 modules/backend/deps/agent-native/deps/agent-intent/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-native/deps/agent-intent/deps/logging-system/deps/development-guidelines (26b562d)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-native/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 modules/backend/deps/agent-native/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-native/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 modules/backend/deps/agent-native/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-native/deps/logging-system/deps/development-guidelines (26b562d)\n 490b0b2dfa1567cb16ff7a7230fd78e52bc2938e modules/backend/deps/agent-squad (heads/main)\n 343b1e6d9a5b58e47bf7e594826221036ae8175c modules/backend/deps/agent-squad/deps/agent-intent (heads/main)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/agent-intent/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 modules/backend/deps/agent-squad/deps/agent-intent/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/agent-intent/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 modules/backend/deps/agent-squad/deps/agent-intent/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/agent-intent/deps/logging-system/deps/development-guidelines (26b562d)\n c348441691e8bdab784a03aa9be64d53dacff341 modules/backend/deps/agent-squad/deps/agent-native (heads/main)\n 343b1e6d9a5b58e47bf7e594826221036ae8175c modules/backend/deps/agent-squad/deps/agent-native/deps/agent-intent (heads/main)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/agent-native/deps/agent-intent/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 modules/backend/deps/agent-squad/deps/agent-native/deps/agent-intent/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/agent-native/deps/agent-intent/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 modules/backend/deps/agent-squad/deps/agent-native/deps/agent-intent/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/agent-native/deps/agent-intent/deps/logging-system/deps/development-guidelines (26b562d)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/agent-native/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 modules/backend/deps/agent-squad/deps/agent-native/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/agent-native/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 modules/backend/deps/agent-squad/deps/agent-native/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/agent-native/deps/logging-system/deps/development-guidelines (26b562d)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 modules/backend/deps/agent-squad/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 modules/backend/deps/agent-squad/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/logging-system/deps/development-guidelines (26b562d)\n ba289e7b1780a29855aef8477bf1ac49bd563eb2 modules/backend/deps/agent-system-core (v0.3.0-10-gba289e7)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 modules/backend/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 modules/backend/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/logging-system/deps/development-guidelines (26b562d)\n ba289e7b1780a29855aef8477bf1ac49bd563eb2 modules/core (v0.3.0-10-gba289e7)\n 3cc6438030803b8e2355d2867766e9654203f265 modules/documents (heads/main)\n 1edd872c554f666ba43353da7e6f0605ecec311b modules/frontend (v0.3.0-54-g1edd872)\n 83e80f5648649c3cc070f4dbc6bf118996828ae8 modules/ui (v0.3.0-20-g83e80f5)\n" + }, + "after": { + "head": "2076994b16f11e3290ffaacfe89d6c26bfbbb731", + "branch": "feature/svc-u2-release-recovery", + "target_branch": "main", + "target_head": "3b9cf82cd6fb53f973f26e2039275f7858b0c654", + "status": " M scripts/svc.sh\n M tests/test_svc.py\n?? tests/test_svc_u2.py\n", + "tracked_diff_sha256": "aa6e20c600686b48739e618edee7d5083365862a050ec94a4848e4eb1216e41d", + "untracked": { + "tests/test_svc_u2.py": "21584ddc98648c6176e6033fc013b8d92292fc20a5a0b2ef4f92ede073859fc7" + }, + "submodules": " 4f583ca3eeeb07a56a714c5086de84dcd6ad2980 deps/agent-command (heads/main)\n ed2af3e6048928b1fd0ba6da1f9d0c1bafec82d4 deps/agent-command/deps/development-guidelines (remotes/origin/feature/delivery-mechanism-5-ged2af3e)\n 343b1e6d9a5b58e47bf7e594826221036ae8175c deps/agent-intent (heads/main)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-intent/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 deps/agent-intent/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-intent/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 deps/agent-intent/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-intent/deps/logging-system/deps/development-guidelines (26b562d)\n c348441691e8bdab784a03aa9be64d53dacff341 deps/agent-native (heads/main)\n 343b1e6d9a5b58e47bf7e594826221036ae8175c deps/agent-native/deps/agent-intent (heads/main)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-native/deps/agent-intent/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 deps/agent-native/deps/agent-intent/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-native/deps/agent-intent/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 deps/agent-native/deps/agent-intent/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-native/deps/agent-intent/deps/logging-system/deps/development-guidelines (26b562d)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-native/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 deps/agent-native/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-native/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 deps/agent-native/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-native/deps/logging-system/deps/development-guidelines (26b562d)\n 490b0b2dfa1567cb16ff7a7230fd78e52bc2938e deps/agent-squad (heads/main)\n 343b1e6d9a5b58e47bf7e594826221036ae8175c deps/agent-squad/deps/agent-intent (heads/main)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/agent-intent/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 deps/agent-squad/deps/agent-intent/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/agent-intent/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 deps/agent-squad/deps/agent-intent/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/agent-intent/deps/logging-system/deps/development-guidelines (26b562d)\n c348441691e8bdab784a03aa9be64d53dacff341 deps/agent-squad/deps/agent-native (heads/main)\n 343b1e6d9a5b58e47bf7e594826221036ae8175c deps/agent-squad/deps/agent-native/deps/agent-intent (heads/main)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/agent-native/deps/agent-intent/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 deps/agent-squad/deps/agent-native/deps/agent-intent/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/agent-native/deps/agent-intent/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 deps/agent-squad/deps/agent-native/deps/agent-intent/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/agent-native/deps/agent-intent/deps/logging-system/deps/development-guidelines (26b562d)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/agent-native/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 deps/agent-squad/deps/agent-native/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/agent-native/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 deps/agent-squad/deps/agent-native/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/agent-native/deps/logging-system/deps/development-guidelines (26b562d)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 deps/agent-squad/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 deps/agent-squad/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/logging-system/deps/development-guidelines (26b562d)\n af199eeaa23c5b39e08b4649f2513d7572dc5274 deps/development-guidelines (heads/main)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/logging-system/deps/development-guidelines (26b562d)\n acabb85e2ae970f834defae916c7539d0a47993d modules/backend (v0.3.0-130-gacabb85)\n 4f583ca3eeeb07a56a714c5086de84dcd6ad2980 modules/backend/deps/agent-command (heads/main)\n ed2af3e6048928b1fd0ba6da1f9d0c1bafec82d4 modules/backend/deps/agent-command/deps/development-guidelines (remotes/origin/feature/delivery-mechanism-5-ged2af3e)\n 343b1e6d9a5b58e47bf7e594826221036ae8175c modules/backend/deps/agent-intent (heads/main)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-intent/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 modules/backend/deps/agent-intent/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-intent/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 modules/backend/deps/agent-intent/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-intent/deps/logging-system/deps/development-guidelines (26b562d)\n c348441691e8bdab784a03aa9be64d53dacff341 modules/backend/deps/agent-native (heads/main)\n 343b1e6d9a5b58e47bf7e594826221036ae8175c modules/backend/deps/agent-native/deps/agent-intent (heads/main)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-native/deps/agent-intent/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 modules/backend/deps/agent-native/deps/agent-intent/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-native/deps/agent-intent/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 modules/backend/deps/agent-native/deps/agent-intent/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-native/deps/agent-intent/deps/logging-system/deps/development-guidelines (26b562d)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-native/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 modules/backend/deps/agent-native/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-native/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 modules/backend/deps/agent-native/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-native/deps/logging-system/deps/development-guidelines (26b562d)\n 490b0b2dfa1567cb16ff7a7230fd78e52bc2938e modules/backend/deps/agent-squad (heads/main)\n 343b1e6d9a5b58e47bf7e594826221036ae8175c modules/backend/deps/agent-squad/deps/agent-intent (heads/main)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/agent-intent/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 modules/backend/deps/agent-squad/deps/agent-intent/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/agent-intent/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 modules/backend/deps/agent-squad/deps/agent-intent/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/agent-intent/deps/logging-system/deps/development-guidelines (26b562d)\n c348441691e8bdab784a03aa9be64d53dacff341 modules/backend/deps/agent-squad/deps/agent-native (heads/main)\n 343b1e6d9a5b58e47bf7e594826221036ae8175c modules/backend/deps/agent-squad/deps/agent-native/deps/agent-intent (heads/main)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/agent-native/deps/agent-intent/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 modules/backend/deps/agent-squad/deps/agent-native/deps/agent-intent/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/agent-native/deps/agent-intent/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 modules/backend/deps/agent-squad/deps/agent-native/deps/agent-intent/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/agent-native/deps/agent-intent/deps/logging-system/deps/development-guidelines (26b562d)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/agent-native/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 modules/backend/deps/agent-squad/deps/agent-native/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/agent-native/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 modules/backend/deps/agent-squad/deps/agent-native/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/agent-native/deps/logging-system/deps/development-guidelines (26b562d)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 modules/backend/deps/agent-squad/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 modules/backend/deps/agent-squad/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/logging-system/deps/development-guidelines (26b562d)\n ba289e7b1780a29855aef8477bf1ac49bd563eb2 modules/backend/deps/agent-system-core (v0.3.0-10-gba289e7)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 modules/backend/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 modules/backend/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/logging-system/deps/development-guidelines (26b562d)\n ba289e7b1780a29855aef8477bf1ac49bd563eb2 modules/core (v0.3.0-10-gba289e7)\n 3cc6438030803b8e2355d2867766e9654203f265 modules/documents (heads/main)\n 1edd872c554f666ba43353da7e6f0605ecec311b modules/frontend (v0.3.0-54-g1edd872)\n 83e80f5648649c3cc070f4dbc6bf118996828ae8 modules/ui (v0.3.0-20-g83e80f5)\n" + }, + "after_error": null, + "exit_code": 0, + "output": "make[1]: Entering directory '/home/weichen/projects/Agent-System-Dev-wt-feature-svc-u2-release-recovery'\nPYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s tests -p 'test_*.py' -v\ntest_candidate_cannot_read_checkout_credentials (test_ci_credentials.CheckoutTests.test_candidate_cannot_read_checkout_credentials) ... ok\ntest_candidate_submodule_update_commands_are_rejected (test_ci_credentials.CheckoutTests.test_candidate_submodule_update_commands_are_rejected) ... ok\ntest_credential_destination_confined_to_injected_server (test_ci_credentials.CheckoutTests.test_credential_destination_confined_to_injected_server) ... ok\ntest_external_or_unlisted_urls_are_rejected_before_component_fetch (test_ci_credentials.CheckoutTests.test_external_or_unlisted_urls_are_rejected_before_component_fetch) ... ok\ntest_inherited_template_hook_does_not_execute_with_credentials (test_ci_credentials.CheckoutTests.test_inherited_template_hook_does_not_execute_with_credentials) ... ok\ntest_nested_external_url_is_rejected_before_authenticated_fetch (test_ci_credentials.CheckoutTests.test_nested_external_url_is_rejected_before_authenticated_fetch) ... ok\ntest_recursive_gitlinks_materialize_exact_sha_without_persisting_credentials (test_ci_credentials.CheckoutTests.test_recursive_gitlinks_materialize_exact_sha_without_persisting_credentials) ... ok\ntest_redirects_do_not_forward_credential_or_fetch_unlisted_target (test_ci_credentials.CheckoutTests.test_redirects_do_not_forward_credential_or_fetch_unlisted_target) ... ok\ntest_after_snapshot_failure_keeps_command_exit_and_output (test_evidence.EvidenceTests.test_after_snapshot_failure_keeps_command_exit_and_output) ... ok\ntest_all_persisted_and_displayed_text_is_sanitized (test_evidence.EvidenceTests.test_all_persisted_and_displayed_text_is_sanitized) ... ok\ntest_authorization_forms_are_redacted_in_capture_console_and_import (test_evidence.EvidenceTests.test_authorization_forms_are_redacted_in_capture_console_and_import) ... ok\ntest_authorization_forms_are_redacted_on_direct_card_capture (test_evidence.EvidenceTests.test_authorization_forms_are_redacted_on_direct_card_capture) ... ok\ntest_before_after_heads_do_not_relabel_changed_command (test_evidence.EvidenceTests.test_before_after_heads_do_not_relabel_changed_command) ... ok\ntest_capture_does_not_overwrite_an_existing_receipt (test_evidence.EvidenceTests.test_capture_does_not_overwrite_an_existing_receipt) ... ok\ntest_capture_without_card_and_import_without_rerun (test_evidence.EvidenceTests.test_capture_without_card_and_import_without_rerun) ... ok\ntest_child_failure_code_is_preserved (test_evidence.EvidenceTests.test_child_failure_code_is_preserved) ... ok\ntest_command_line_token_arguments_are_redacted (test_evidence.EvidenceTests.test_command_line_token_arguments_are_redacted) ... ok\ntest_dirty_and_untracked_contents_have_object_digests (test_evidence.EvidenceTests.test_dirty_and_untracked_contents_have_object_digests) ... ok\ntest_failed_child_exit_survives_receipt_write_failure (test_evidence.EvidenceTests.test_failed_child_exit_survives_receipt_write_failure) ... ok\ntest_full_output_is_preserved_not_only_tail (test_evidence.EvidenceTests.test_full_output_is_preserved_not_only_tail) ... ok\ntest_import_missing_card_fails_without_losing_receipt (test_evidence.EvidenceTests.test_import_missing_card_fails_without_losing_receipt) ... ok\ntest_import_rejects_unredacted_authorization_with_valid_checksums (test_evidence.EvidenceTests.test_import_rejects_unredacted_authorization_with_valid_checksums) ... ok\ntest_import_retry_is_idempotent (test_evidence.EvidenceTests.test_import_retry_is_idempotent) ... ok\ntest_invalid_field_types_and_forged_source_are_rejected (test_evidence.EvidenceTests.test_invalid_field_types_and_forged_source_are_rejected) ... ok\ntest_known_secret_environment_value_is_redacted_without_label (test_evidence.EvidenceTests.test_known_secret_environment_value_is_redacted_without_label) ... ok\ntest_linked_worktree_import_preserves_source_identity (test_evidence.EvidenceTests.test_linked_worktree_import_preserves_source_identity) ... ok\ntest_missing_card_does_not_execute (test_evidence.EvidenceTests.test_missing_card_does_not_execute) ... ok\ntest_missing_required_snapshot_fields_are_rejected (test_evidence.EvidenceTests.test_missing_required_snapshot_fields_are_rejected) ... ok\ntest_nested_cwd_records_untracked_files_from_repository_root (test_evidence.EvidenceTests.test_nested_cwd_records_untracked_files_from_repository_root) ... ok\ntest_output_digest_checked_even_if_receipt_digest_recomputed (test_evidence.EvidenceTests.test_output_digest_checked_even_if_receipt_digest_recomputed) ... ok\ntest_pipeline_failure_is_recorded_and_returned (test_evidence.EvidenceTests.test_pipeline_failure_is_recorded_and_returned) ... ok\ntest_quoted_credentials_with_spaces_are_fully_redacted (test_evidence.EvidenceTests.test_quoted_credentials_with_spaces_are_fully_redacted) ... ok\ntest_receipt_write_failure_cannot_claim_success (test_evidence.EvidenceTests.test_receipt_write_failure_cannot_claim_success) ... ok\ntest_recorder_environment_does_not_leak_into_nested_commands (test_evidence.EvidenceTests.test_recorder_environment_does_not_leak_into_nested_commands) ... ok\ntest_source_cannot_be_promoted_by_environment (test_evidence.EvidenceTests.test_source_cannot_be_promoted_by_environment) ... ok\ntest_tamper_is_rejected_before_card_append (test_evidence.EvidenceTests.test_tamper_is_rejected_before_card_append) ... ok\ntest_unrelated_repo_cannot_import (test_evidence.EvidenceTests.test_unrelated_repo_cannot_import) ... ok\ntest_validate_precheck_checks_result_and_current_base (test_evidence.EvidenceTests.test_validate_precheck_checks_result_and_current_base) ... ok\ntest_validate_precheck_rejects_successful_arbitrary_command (test_evidence.EvidenceTests.test_validate_precheck_rejects_successful_arbitrary_command) ... ok\ntest_addr_change_keeps_database_and_auth (test_svc.SvcDbRecoveryTests.test_addr_change_keeps_database_and_auth) ... ok\ntest_auth_preflight_aligns_with_effective_env_dsn (test_svc.SvcDbRecoveryTests.test_auth_preflight_aligns_with_effective_env_dsn) ... ok\ntest_batch_update_stops_second_service_despite_prior_pidcheck (test_svc.SvcDbRecoveryTests.test_batch_update_stops_second_service_despite_prior_pidcheck)\n批量 update 不得用上一服务的 SVC_PIDCHECK 误判下一服务进程。 ... ok\ntest_dbref_binding_survives_bare_restart_and_redeploy (test_svc.SvcDbRecoveryTests.test_dbref_binding_survives_bare_restart_and_redeploy) ... ok\ntest_explicit_unbind_blocks_residual_env_dsn (test_svc.SvcDbRecoveryTests.test_explicit_unbind_blocks_residual_env_dsn) ... ok\ntest_external_dsn_and_conffile_recover_on_bare_start (test_svc.SvcDbRecoveryTests.test_external_dsn_and_conffile_recover_on_bare_start) ... ok\ntest_legacy_exec_dsn_survives_consecutive_restarts (test_svc.SvcDbRecoveryTests.test_legacy_exec_dsn_survives_consecutive_restarts) ... ok\ntest_mock_conflict_and_legacy_binary_refuse (test_svc.SvcDbRecoveryTests.test_mock_conflict_and_legacy_binary_refuse) ... ok\ntest_rdm_companion_missing_web_fails_deploy (test_svc.SvcDbRecoveryTests.test_rdm_companion_missing_web_fails_deploy) ... ok\ntest_rdm_source_commits_and_update_track (test_svc.SvcDbRecoveryTests.test_rdm_source_commits_and_update_track) ... [main 2ca317a] rdm v2\n 1 file changed, 1 insertion(+)\n create mode 100644 marker\nTo /tmp/svc-db-regress-chlmrahe/rdmremote.git\n 9662372..2ca317a main -> main\nok\ntest_rdm_source_deploy_end_to_end (test_svc.SvcDbRecoveryTests.test_rdm_source_deploy_end_to_end) ... ok\ntest_refusals_keep_registration_without_sqlite_fallback (test_svc.SvcDbRecoveryTests.test_refusals_keep_registration_without_sqlite_fallback) ... ok\ntest_restart_refusal_happens_before_stopping_running_service (test_svc.SvcDbRecoveryTests.test_restart_refusal_happens_before_stopping_running_service) ... ok\ntest_seed_owner_override_skips_account_lookup (test_svc.SvcDbRecoveryTests.test_seed_owner_override_skips_account_lookup) ... ok\ntest_seed_puts_with_temporary_bearer_key_when_auth_on (test_svc.SvcDbRecoveryTests.test_seed_puts_with_temporary_bearer_key_when_auth_on) ... ok\ntest_seed_puts_without_credentials_when_auth_off (test_svc.SvcDbRecoveryTests.test_seed_puts_without_credentials_when_auth_off) ... ok\ntest_sequential_restarts_no_cross_dsn_and_no_secret_leak (test_svc.SvcDbRecoveryTests.test_sequential_restarts_no_cross_dsn_and_no_secret_leak) ... ok\ntest_source_switch_requires_flag (test_svc.SvcDbRecoveryTests.test_source_switch_requires_flag) ... ok\ntest_static_fresh_deploy_registers_pidcheck_and_starts (test_svc.SvcDbRecoveryTests.test_static_fresh_deploy_registers_pidcheck_and_starts)\n全新 static --dir:登记须含 pidcheck;首启探活不得把 http.server 判死。 ... ok\ntest_update_preserves_binding (test_svc.SvcDbRecoveryTests.test_update_preserves_binding) ... ok\ntest_backup_custom_out_not_pruned (test_svc_backup.SvcAgentBackupTests.test_backup_custom_out_not_pruned) ... ok\ntest_backup_event_recorded (test_svc_backup.SvcAgentBackupTests.test_backup_event_recorded) ... ok\ntest_backup_live_writer_consistent_and_outside_runtime (test_svc_backup.SvcAgentBackupTests.test_backup_live_writer_consistent_and_outside_runtime)\n活跃 WAL 写入者:快照可读、行在、integrity ok;-wal/-shm 不入快照。 ... ok\n/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.8/lib/python3.14/unittest/suite.py:107: ResourceWarning: unclosed file <_io.TextIOWrapper name=3 encoding='utf-8'>\n for index, test in enumerate(self):\nResourceWarning: Enable tracemalloc to get the object allocation traceback\ntest_backup_manifest_sha_matches (test_svc_backup.SvcAgentBackupTests.test_backup_manifest_sha_matches) ... ok\ntest_backup_non_sqlite_db_kept_raw_without_leftovers (test_svc_backup.SvcAgentBackupTests.test_backup_non_sqlite_db_kept_raw_without_leftovers) ... ok\ntest_backup_rejects_postgres_with_db_hint (test_svc_backup.SvcAgentBackupTests.test_backup_rejects_postgres_with_db_hint) ... ok\ntest_backup_requires_registration (test_svc_backup.SvcAgentBackupTests.test_backup_requires_registration) ... ok\ntest_backup_target_exists_refused (test_svc_backup.SvcAgentBackupTests.test_backup_target_exists_refused) ... ok\ntest_backup_without_sqlite3_refuses (test_svc_backup.SvcAgentBackupTests.test_backup_without_sqlite3_refuses)\nPATH 无 sqlite3 → 拒绝(不回退裸拷贝)。重建与真实 PATH 等价的工具目录、 ... ok\ntest_retention_and_listing (test_svc_backup.SvcAgentBackupTests.test_retention_and_listing) ... ok\ntest_rm_purge_removes_snapshots (test_svc_backup.SvcAgentBackupTests.test_rm_purge_removes_snapshots) ... ok\ntest_replace_failure_keeps_original (test_svc_identity.AtomicWriteTests.test_replace_failure_keeps_original) ... ok\ntest_I3_start_ensures_sqlite_identity (test_svc_identity.IdentityTests.test_I3_start_ensures_sqlite_identity) ... ok\ntest_I3_wrong_db_identity_blocks_start (test_svc_identity.IdentityTests.test_I3_wrong_db_identity_blocks_start) ... ok\ntest_T1_bare_restart_stable_identity (test_svc_identity.IdentityTests.test_T1_bare_restart_stable_identity) ... ok\ntest_T2_port_change_keeps_anchors (test_svc_identity.IdentityTests.test_T2_port_change_keeps_anchors) ... ok\ntest_T3_data_drift_rejected (test_svc_identity.IdentityTests.test_T3_data_drift_rejected) ... ok\ntest_T4a_leftover_new_file_ignored (test_svc_identity.IdentityTests.test_T4a_leftover_new_file_ignored) ... ok\ntest_T4b_corrupt_manifest_blocks (test_svc_identity.IdentityTests.test_T4b_corrupt_manifest_blocks) ... ok\ntest_T5a_missing_marker_blocks (test_svc_identity.IdentityTests.test_T5a_missing_marker_blocks) ... ok\ntest_T5b_foreign_marker_blocks (test_svc_identity.IdentityTests.test_T5b_foreign_marker_blocks) ... ok\ntest_T5c_lost_manifest_blocks_not_rebuild (test_svc_identity.IdentityTests.test_T5c_lost_manifest_blocks_not_rebuild) ... ok\ntest_T6_cwd_independent_identity (test_svc_identity.IdentityTests.test_T6_cwd_independent_identity) ... ok\ntest_U1_bare_start_missing_row_is_event_init_recovers (test_svc_identity.IdentityTests.test_U1_bare_start_missing_row_is_event_init_recovers)\n普通启动只验证不补行:DB 行缺失 → 拒绝且不建空行(缺行即事件); ... ok\ntest_U1_capability_probe_no_false_positive_no_hang (test_svc_identity.IdentityTests.test_U1_capability_probe_no_false_positive_no_hang)\n能力探测无副作用:含 ensure 字样的服务态门禁报错不算能力(旧实现 ... ok\ntest_U1_update_ensures_missing_row_before_stop (test_svc_identity.IdentityTests.test_U1_update_ensures_missing_row_before_stop)\n领养时未能补行的实例(旧产物无 identity):候选有能力时,update 在 ... ok\ntest_U1_update_refuses_identityless_candidate_keeps_old_state (test_svc_identity.IdentityTests.test_U1_update_refuses_identityless_candidate_keeps_old_state)\nmanifest 治理实例 × 无 identity 候选(RDM 旧形态):停服前拒绝—— ... ok\ntest_Z_real_pg_not_run (test_svc_identity.IdentityTests.test_Z_real_pg_not_run) ... skipped '真实 PG 身份专项 NOT_RUN:需真实实例(SVC_TEST_PG=1 + AGENT_PG_TEST_DSN)'\ntest_recover_fails_when_port_stolen (test_svc_identity.IdentityTests.test_recover_fails_when_port_stolen) ... ok\ntest_recover_ignores_never_started_service (test_svc_identity.IdentityTests.test_recover_ignores_never_started_service) ... ok\ntest_recover_noop_while_running (test_svc_identity.IdentityTests.test_recover_noop_while_running) ... ok\ntest_recover_reserved_name_rejected (test_svc_identity.IdentityTests.test_recover_reserved_name_rejected) ... ok\ntest_recover_respects_explicit_stop (test_svc_identity.IdentityTests.test_recover_respects_explicit_stop) ... ok\ntest_recover_restarts_sigkilled_service (test_svc_identity.IdentityTests.test_recover_restarts_sigkilled_service) ... ok\ntest_recover_skips_when_lock_held (test_svc_identity.IdentityTests.test_recover_skips_when_lock_held) ... ok\ntest_start_reclaims_own_orphan_without_pidfile (test_svc_identity.IdentityTests.test_start_reclaims_own_orphan_without_pidfile)\n模拟 update 误清 pidfile 后本 bin 仍占口:裸 start 应回收并拉起新进程。 ... ok\ntest_start_records_autostart_and_stop_clears (test_svc_identity.IdentityTests.test_start_records_autostart_and_stop_clears) ... ok\ntest_start_rejects_foreign_port_occupant (test_svc_identity.IdentityTests.test_start_rejects_foreign_port_occupant)\n外部 python http.server 占口:start 必须失败并提示非本服务。 ... ok\ntest_stop_on_dead_process_clears_autostart (test_svc_identity.IdentityTests.test_stop_on_dead_process_clears_autostart) ... ok\ntest_watchdog_print_plan (test_svc_identity.IdentityTests.test_watchdog_print_plan) ... ok\ntest_addr_defaults_to_default_addr (test_svc_precheck.AgentListenRejectedPrecheck.test_addr_defaults_to_default_addr) ... ok\ntest_incident_combo_not_rejected (test_svc_precheck.AgentListenRejectedPrecheck.test_incident_combo_not_rejected) ... ok\ntest_insecure_not_rejected (test_svc_precheck.AgentListenRejectedPrecheck.test_insecure_not_rejected) ... ok\ntest_loopback_not_rejected (test_svc_precheck.AgentListenRejectedPrecheck.test_loopback_not_rejected) ... ok\ntest_nonloop_auth_off_with_dsn_rejected (test_svc_precheck.AgentListenRejectedPrecheck.test_nonloop_auth_off_with_dsn_rejected) ... ok\ntest_nonloop_no_dsn_rejected (test_svc_precheck.AgentListenRejectedPrecheck.test_nonloop_no_dsn_rejected) ... ok\ntest_auth_off_beats_dsn (test_svc_precheck.DbAuthEnabledMatrix.test_auth_off_beats_dsn) ... ok\ntest_auth_on_explicit (test_svc_precheck.DbAuthEnabledMatrix.test_auth_on_explicit) ... ok\ntest_auto_with_effective_dsn_is_authed (test_svc_precheck.DbAuthEnabledMatrix.test_auto_with_effective_dsn_is_authed) ... ok\ntest_auto_without_dsn_is_denied (test_svc_precheck.DbAuthEnabledMatrix.test_auto_without_dsn_is_denied) ... ok\ntest_no_auth_no_dsn_denied (test_svc_precheck.DbAuthEnabledMatrix.test_no_auth_no_dsn_denied) ... ok\ntest_broken_dbref_refuses (test_svc_precheck.ResolveDbConfigFixtures.test_broken_dbref_refuses) ... ok\ntest_legacy_exec_dsn_migrates_to_recoverable_keys (test_svc_precheck.ResolveDbConfigFixtures.test_legacy_exec_dsn_migrates_to_recoverable_keys) ... ok\ntest_legacy_exec_dsn_without_instance_goes_to_dsn_key (test_svc_precheck.ResolveDbConfigFixtures.test_legacy_exec_dsn_without_instance_goes_to_dsn_key) ... ok\ntest_migration_is_idempotent (test_svc_precheck.ResolveDbConfigFixtures.test_migration_is_idempotent) ... ok\ntest_no_dsn_no_dbref_is_sqlite (test_svc_precheck.ResolveDbConfigFixtures.test_no_dsn_no_dbref_is_sqlite) ... ok\ntest_post_g17_exec_restores_dsn_from_dbref (test_svc_precheck.ResolveDbConfigFixtures.test_post_g17_exec_restores_dsn_from_dbref) ... ok\ntest_env_config_resets_identity_env (test_svc_precheck.ResolverStateIsolation.test_env_config_resets_identity_env) ... ok\ntest_env_config_resets_pidcheck_runtime_state (test_svc_precheck.ResolverStateIsolation.test_env_config_resets_pidcheck_runtime_state) ... ok\ntest_env_config_resets_resolver_state (test_svc_precheck.ResolverStateIsolation.test_env_config_resets_resolver_state) ... ok\ntest_recover_fails_when_port_stolen (test_svc_recover.SvcAutoRecoverTests.test_recover_fails_when_port_stolen) ... ok\ntest_recover_ignores_never_started_service (test_svc_recover.SvcAutoRecoverTests.test_recover_ignores_never_started_service) ... ok\ntest_recover_noop_while_running (test_svc_recover.SvcAutoRecoverTests.test_recover_noop_while_running) ... ok\ntest_recover_reserved_name_rejected (test_svc_recover.SvcAutoRecoverTests.test_recover_reserved_name_rejected) ... ok\ntest_recover_respects_explicit_stop (test_svc_recover.SvcAutoRecoverTests.test_recover_respects_explicit_stop) ... ok\ntest_recover_restarts_sigkilled_service (test_svc_recover.SvcAutoRecoverTests.test_recover_restarts_sigkilled_service) ... ok\ntest_recover_skips_when_lock_held (test_svc_recover.SvcAutoRecoverTests.test_recover_skips_when_lock_held) ... ok\ntest_start_reclaims_own_orphan_without_pidfile (test_svc_recover.SvcAutoRecoverTests.test_start_reclaims_own_orphan_without_pidfile)\n模拟 update 误清 pidfile 后本 bin 仍占口:裸 start 应回收并拉起新进程。 ... ok\ntest_start_records_autostart_and_stop_clears (test_svc_recover.SvcAutoRecoverTests.test_start_records_autostart_and_stop_clears) ... ok\ntest_start_rejects_foreign_port_occupant (test_svc_recover.SvcAutoRecoverTests.test_start_rejects_foreign_port_occupant)\n外部 python http.server 占口:start 必须失败并提示非本服务。 ... ok\ntest_stop_on_dead_process_clears_autostart (test_svc_recover.SvcAutoRecoverTests.test_stop_on_dead_process_clears_autostart) ... ok\ntest_watchdog_print_plan (test_svc_recover.SvcAutoRecoverTests.test_watchdog_print_plan) ... ok\ntest_I3_start_ensures_sqlite_identity (test_svc_u2.U2ReleaseTests.test_I3_start_ensures_sqlite_identity) ... ok\ntest_I3_wrong_db_identity_blocks_start (test_svc_u2.U2ReleaseTests.test_I3_wrong_db_identity_blocks_start) ... ok\ntest_T1_bare_restart_stable_identity (test_svc_u2.U2ReleaseTests.test_T1_bare_restart_stable_identity) ... ok\ntest_T2_port_change_keeps_anchors (test_svc_u2.U2ReleaseTests.test_T2_port_change_keeps_anchors) ... ok\ntest_T3_data_drift_rejected (test_svc_u2.U2ReleaseTests.test_T3_data_drift_rejected) ... ok\ntest_T4a_leftover_new_file_ignored (test_svc_u2.U2ReleaseTests.test_T4a_leftover_new_file_ignored) ... ok\ntest_T4b_corrupt_manifest_blocks (test_svc_u2.U2ReleaseTests.test_T4b_corrupt_manifest_blocks) ... ok\ntest_T5a_missing_marker_blocks (test_svc_u2.U2ReleaseTests.test_T5a_missing_marker_blocks) ... ok\ntest_T5b_foreign_marker_blocks (test_svc_u2.U2ReleaseTests.test_T5b_foreign_marker_blocks) ... ok\ntest_T5c_lost_manifest_blocks_not_rebuild (test_svc_u2.U2ReleaseTests.test_T5c_lost_manifest_blocks_not_rebuild) ... ok\ntest_T6_cwd_independent_identity (test_svc_u2.U2ReleaseTests.test_T6_cwd_independent_identity) ... ok\ntest_U1_bare_start_missing_row_is_event_init_recovers (test_svc_u2.U2ReleaseTests.test_U1_bare_start_missing_row_is_event_init_recovers)\n普通启动只验证不补行:DB 行缺失 → 拒绝且不建空行(缺行即事件); ... ok\ntest_U1_capability_probe_no_false_positive_no_hang (test_svc_u2.U2ReleaseTests.test_U1_capability_probe_no_false_positive_no_hang)\n能力探测无副作用:含 ensure 字样的服务态门禁报错不算能力(旧实现 ... ok\ntest_U1_update_ensures_missing_row_before_stop (test_svc_u2.U2ReleaseTests.test_U1_update_ensures_missing_row_before_stop)\n领养时未能补行的实例(旧产物无 identity):候选有能力时,update 在 ... ok\ntest_U1_update_refuses_identityless_candidate_keeps_old_state (test_svc_u2.U2ReleaseTests.test_U1_update_refuses_identityless_candidate_keeps_old_state)\nmanifest 治理实例 × 无 identity 候选(RDM 旧形态):停服前拒绝—— ... ok\ntest_U2_build_failure_keeps_old_running (test_svc_u2.U2ReleaseTests.test_U2_build_failure_keeps_old_running) ... ok\ntest_U2_journal_interrupt_resolved_on_start (test_svc_u2.U2ReleaseTests.test_U2_journal_interrupt_resolved_on_start) ... ok\ntest_U2_layout_switch_and_rollback_on_start_failure (test_svc_u2.U2ReleaseTests.test_U2_layout_switch_and_rollback_on_start_failure) ... ok\ntest_U2_legacy_layout_bare_lifecycle_still_works (test_svc_u2.U2ReleaseTests.test_U2_legacy_layout_bare_lifecycle_still_works) ... ok\ntest_U2_schema_advance_blocks_rollback (test_svc_u2.U2ReleaseTests.test_U2_schema_advance_blocks_rollback) ... ok\ntest_Z_real_pg_not_run (test_svc_u2.U2ReleaseTests.test_Z_real_pg_not_run) ... skipped '真实 PG 身份专项 NOT_RUN:需真实实例(SVC_TEST_PG=1 + AGENT_PG_TEST_DSN)'\ntest_recover_fails_when_port_stolen (test_svc_u2.U2ReleaseTests.test_recover_fails_when_port_stolen) ... ok\ntest_recover_ignores_never_started_service (test_svc_u2.U2ReleaseTests.test_recover_ignores_never_started_service) ... ok\ntest_recover_noop_while_running (test_svc_u2.U2ReleaseTests.test_recover_noop_while_running) ... ok\ntest_recover_reserved_name_rejected (test_svc_u2.U2ReleaseTests.test_recover_reserved_name_rejected) ... ok\ntest_recover_respects_explicit_stop (test_svc_u2.U2ReleaseTests.test_recover_respects_explicit_stop) ... ok\ntest_recover_restarts_sigkilled_service (test_svc_u2.U2ReleaseTests.test_recover_restarts_sigkilled_service) ... ok\ntest_recover_skips_when_lock_held (test_svc_u2.U2ReleaseTests.test_recover_skips_when_lock_held) ... ok\ntest_start_reclaims_own_orphan_without_pidfile (test_svc_u2.U2ReleaseTests.test_start_reclaims_own_orphan_without_pidfile)\n模拟 update 误清 pidfile 后本 bin 仍占口:裸 start 应回收并拉起新进程。 ... ok\ntest_start_records_autostart_and_stop_clears (test_svc_u2.U2ReleaseTests.test_start_records_autostart_and_stop_clears) ... ok\ntest_start_rejects_foreign_port_occupant (test_svc_u2.U2ReleaseTests.test_start_rejects_foreign_port_occupant)\n外部 python http.server 占口:start 必须失败并提示非本服务。 ... ok\ntest_stop_on_dead_process_clears_autostart (test_svc_u2.U2ReleaseTests.test_stop_on_dead_process_clears_autostart) ... ok\ntest_watchdog_print_plan (test_svc_u2.U2ReleaseTests.test_watchdog_print_plan) ... ok\ntest_T1_user_list_binds_manifest_data (test_svc_user_binding.UserBindingTests.test_T1_user_list_binds_manifest_data) ... ok\ntest_T2_reject_override_data_flag (test_svc_user_binding.UserBindingTests.test_T2_reject_override_data_flag) ... ok\ntest_T2b_reject_literal_default_data (test_svc_user_binding.UserBindingTests.test_T2b_reject_literal_default_data) ... ok\ntest_T3_missing_data_root_rejected (test_svc_user_binding.UserBindingTests.test_T3_missing_data_root_rejected) ... ok\ntest_T4_no_flock_rejects_user_and_deploy (test_svc_user_binding.UserBindingTests.test_T4_no_flock_rejects_user_and_deploy) ... ok\ntest_T5_identity_regression_smoke (test_svc_user_binding.UserBindingTests.test_T5_identity_regression_smoke) ... ok\ntest_recover_fails_when_port_stolen (test_svc_user_binding.UserBindingTests.test_recover_fails_when_port_stolen) ... ok\ntest_recover_ignores_never_started_service (test_svc_user_binding.UserBindingTests.test_recover_ignores_never_started_service) ... ok\ntest_recover_noop_while_running (test_svc_user_binding.UserBindingTests.test_recover_noop_while_running) ... ok\ntest_recover_reserved_name_rejected (test_svc_user_binding.UserBindingTests.test_recover_reserved_name_rejected) ... ok\ntest_recover_respects_explicit_stop (test_svc_user_binding.UserBindingTests.test_recover_respects_explicit_stop) ... ok\ntest_recover_restarts_sigkilled_service (test_svc_user_binding.UserBindingTests.test_recover_restarts_sigkilled_service) ... ok\ntest_recover_skips_when_lock_held (test_svc_user_binding.UserBindingTests.test_recover_skips_when_lock_held) ... ok\ntest_start_reclaims_own_orphan_without_pidfile (test_svc_user_binding.UserBindingTests.test_start_reclaims_own_orphan_without_pidfile)\n模拟 update 误清 pidfile 后本 bin 仍占口:裸 start 应回收并拉起新进程。 ... ok\ntest_start_records_autostart_and_stop_clears (test_svc_user_binding.UserBindingTests.test_start_records_autostart_and_stop_clears) ... ok\ntest_start_rejects_foreign_port_occupant (test_svc_user_binding.UserBindingTests.test_start_rejects_foreign_port_occupant)\n外部 python http.server 占口:start 必须失败并提示非本服务。 ... ok\ntest_stop_on_dead_process_clears_autostart (test_svc_user_binding.UserBindingTests.test_stop_on_dead_process_clears_autostart) ... ok\ntest_watchdog_print_plan (test_svc_user_binding.UserBindingTests.test_watchdog_print_plan) ... ok\ntest_full_help_mentions_user_binding (test_svc_user_help.UserHelpTests.test_full_help_mentions_user_binding) ... ok\ntest_help_accounts_lists_six_commands (test_svc_user_help.UserHelpTests.test_help_accounts_lists_six_commands) ... ok\ntest_topics_and_short_and_lifecycle_mention_accounts (test_svc_user_help.UserHelpTests.test_topics_and_short_and_lifecycle_mention_accounts) ... ok\ntest_ide_lock_in_git_dir_does_not_block_finish (test_task.FinishGuardFixTests.test_ide_lock_in_git_dir_does_not_block_finish) ... ok\ntest_local_default_branch_at_origin_main_does_not_block (test_task.FinishGuardFixTests.test_local_default_branch_at_origin_main_does_not_block)\n克隆默认分支停在 origin/main 顶端(pin 为 feature 顶端时不在其历史内) ... ok\ntest_local_unique_commit_still_preserved (test_task.FinishGuardFixTests.test_local_unique_commit_still_preserved) ... ok\ntest_real_git_lock_still_blocks_finish (test_task.FinishGuardFixTests.test_real_git_lock_still_blocks_finish) ... ok\ntest_remote_tracking_refs_do_not_block_finish (test_task.FinishGuardFixTests.test_remote_tracking_refs_do_not_block_finish) ... ok\ntest_finish_abandon_retains_local_remote_tips_and_reason (test_task.TaskLifecycleTests.test_finish_abandon_retains_local_remote_tips_and_reason) ... ok\ntest_finish_abandon_without_reason_preserves_checkout_and_refs (test_task.TaskLifecycleTests.test_finish_abandon_without_reason_preserves_checkout_and_refs) ... ok\ntest_finish_clean_worktree_has_accurate_remote_result (test_task.TaskLifecycleTests.test_finish_clean_worktree_has_accurate_remote_result) ... ok\ntest_finish_deletes_only_merged_remote_branch (test_task.TaskLifecycleTests.test_finish_deletes_only_merged_remote_branch) ... ok\ntest_finish_preserves_component_commits_off_the_pinned_head (test_task.TaskLifecycleTests.test_finish_preserves_component_commits_off_the_pinned_head) ... ok\ntest_finish_preserves_component_untracked_file (test_task.TaskLifecycleTests.test_finish_preserves_component_untracked_file) ... ok\ntest_finish_preserves_linked_component_worktrees_at_the_pinned_head (test_task.TaskLifecycleTests.test_finish_preserves_linked_component_worktrees_at_the_pinned_head) ... ok\ntest_finish_preserves_linked_nested_component_worktree (test_task.TaskLifecycleTests.test_finish_preserves_linked_nested_component_worktree) ... ok\ntest_finish_preserves_locked_worktree (test_task.TaskLifecycleTests.test_finish_preserves_locked_worktree) ... ok\ntest_finish_preserves_main_and_active_index_lock (test_task.TaskLifecycleTests.test_finish_preserves_main_and_active_index_lock) ... ok\ntest_finish_preserves_modified_untracked_and_ignored_files (test_task.TaskLifecycleTests.test_finish_preserves_modified_untracked_and_ignored_files) ... ok\ntest_finish_preserves_remote_unmerged_tip (test_task.TaskLifecycleTests.test_finish_preserves_remote_unmerged_tip) ... ok\ntest_finish_recycles_clean_submodule_without_shared_config_changes (test_task.TaskLifecycleTests.test_finish_recycles_clean_submodule_without_shared_config_changes) ... ok\ntest_finish_rejects_subdirectory_without_deleting_it (test_task.TaskLifecycleTests.test_finish_rejects_subdirectory_without_deleting_it) ... ok\ntest_finish_remote_deletion_lease_preserves_concurrent_update (test_task.TaskLifecycleTests.test_finish_remote_deletion_lease_preserves_concurrent_update) ... ok\ntest_finish_requires_writer_exit_confirmation (test_task.TaskLifecycleTests.test_finish_requires_writer_exit_confirmation) ... ok\ntest_post_merge_is_silent_outside_main_checkout (test_task.TaskLifecycleTests.test_post_merge_is_silent_outside_main_checkout) ... ok\ntest_post_merge_main_merge_only_reminds_owner (test_task.TaskLifecycleTests.test_post_merge_main_merge_only_reminds_owner) ... ok\ntest_post_merge_main_pull_only_reminds_owner (test_task.TaskLifecycleTests.test_post_merge_main_pull_only_reminds_owner) ... ok\ntest_precheck_feature_behind_requires_sync (test_task.TaskLifecycleTests.test_precheck_feature_behind_requires_sync) ... ok\ntest_precheck_rejects_unreachable_authority (test_task.TaskLifecycleTests.test_precheck_rejects_unreachable_authority) ... ok\ntest_precheck_routes_existing_project_delivery_policy (test_task.TaskLifecycleTests.test_precheck_routes_existing_project_delivery_policy) ... ok\ntest_start_imports_precheck_and_creates_four_part_commit (test_task.TaskLifecycleTests.test_start_imports_precheck_and_creates_four_part_commit) ... ok\ntest_start_offline_does_not_create_worktree (test_task.TaskLifecycleTests.test_start_offline_does_not_create_worktree) ... ok\ntest_start_preserves_merged_local_and_remote_branch_names (test_task.TaskLifecycleTests.test_start_preserves_merged_local_and_remote_branch_names) ... ok\ntest_start_rejects_receipt_when_authoritative_base_changed (test_task.TaskLifecycleTests.test_start_rejects_receipt_when_authoritative_base_changed) ... ok\ntest_start_requires_precheck_receipt_before_creating_worktree (test_task.TaskLifecycleTests.test_start_requires_precheck_receipt_before_creating_worktree) ... ok\ntest_start_stacked_branch_records_parent_and_main_baseline (test_task.TaskLifecycleTests.test_start_stacked_branch_records_parent_and_main_baseline) ... ok\ntest_sync_commit_failure_never_reports_synced (test_task.TaskLifecycleTests.test_sync_commit_failure_never_reports_synced) ... ok\ntest_sync_does_not_execute_json_receipt_command (test_task.TaskLifecycleTests.test_sync_does_not_execute_json_receipt_command) ... ok\ntest_sync_keeps_json_evidence_fresh_after_metadata_only_changes (test_task.TaskLifecycleTests.test_sync_keeps_json_evidence_fresh_after_metadata_only_changes) ... ok\ntest_sync_keeps_unchanged_component_branch (test_task.TaskLifecycleTests.test_sync_keeps_unchanged_component_branch) ... ok\ntest_sync_materializes_nested_detached_components (test_task.TaskLifecycleTests.test_sync_materializes_nested_detached_components) ... ok\ntest_sync_materializes_updated_component (test_task.TaskLifecycleTests.test_sync_materializes_updated_component) ... ok\ntest_sync_preserves_component_feature_branch (test_task.TaskLifecycleTests.test_sync_preserves_component_feature_branch) ... ok\ntest_sync_preserves_dirty_child (test_task.TaskLifecycleTests.test_sync_preserves_dirty_child) ... ok\ntest_sync_preserves_nested_feature_branch_when_parent_pin_changes (test_task.TaskLifecycleTests.test_sync_preserves_nested_feature_branch_when_parent_pin_changes) ... ok\ntest_sync_reads_legacy_commands_with_escaped_pipes (test_task.TaskLifecycleTests.test_sync_reads_legacy_commands_with_escaped_pipes) ... ok\ntest_sync_still_warns_for_stale_legacy_table_evidence (test_task.TaskLifecycleTests.test_sync_still_warns_for_stale_legacy_table_evidence) ... ok\ntest_sync_warns_for_malformed_json_evidence (test_task.TaskLifecycleTests.test_sync_warns_for_malformed_json_evidence) ... ok\ntest_sync_warns_for_missing_json_receipt_fields_even_with_valid_checksum (test_task.TaskLifecycleTests.test_sync_warns_for_missing_json_receipt_fields_even_with_valid_checksum) ... ok\ntest_sync_warns_for_unresolved_json_evidence_head (test_task.TaskLifecycleTests.test_sync_warns_for_unresolved_json_evidence_head) ... ok\ntest_sync_warns_for_unresolved_legacy_evidence_head (test_task.TaskLifecycleTests.test_sync_warns_for_unresolved_legacy_evidence_head) ... ok\ntest_sync_warns_when_json_after_state_contains_uncommitted_code (test_task.TaskLifecycleTests.test_sync_warns_when_json_after_state_contains_uncommitted_code) ... ok\ntest_sync_warns_when_json_before_state_contains_uncommitted_code (test_task.TaskLifecycleTests.test_sync_warns_when_json_before_state_contains_uncommitted_code) ... ok\ntest_sync_warns_when_json_evidence_predates_noncard_changes (test_task.TaskLifecycleTests.test_sync_warns_when_json_evidence_predates_noncard_changes) ... ok\ntest_verify_rejects_recursive_drift_even_when_root_has_local_edits (test_task.TaskLifecycleTests.test_verify_rejects_recursive_drift_even_when_root_has_local_edits) ... ok\ntest_verify_rejects_uninitialized_component (test_task.TaskLifecycleTests.test_verify_rejects_uninitialized_component) ... ok\ntest_deleting_main_symlink_to_outside_is_denied (test_write_guard.WriteGuardTests.test_deleting_main_symlink_to_outside_is_denied) ... ok\ntest_detached_and_bad_branches_are_denied (test_write_guard.WriteGuardTests.test_detached_and_bad_branches_are_denied) ... ok\ntest_every_patch_file_and_move_destination_is_checked (test_write_guard.WriteGuardTests.test_every_patch_file_and_move_destination_is_checked) ... ok\ntest_feature_missing_parent_is_allowed (test_write_guard.WriteGuardTests.test_feature_missing_parent_is_allowed) ... ok\ntest_invalid_json_is_denied (test_write_guard.WriteGuardTests.test_invalid_json_is_denied) ... ok\ntest_missing_parent_on_main_is_denied (test_write_guard.WriteGuardTests.test_missing_parent_on_main_is_denied) ... ok\ntest_missing_unknown_and_malformed_write_input_are_denied (test_write_guard.WriteGuardTests.test_missing_unknown_and_malformed_write_input_are_denied) ... ok\ntest_non_git_destination_is_explicitly_outside_scope (test_write_guard.WriteGuardTests.test_non_git_destination_is_explicitly_outside_scope) ... ok\ntest_nonwrite_tools_are_outside_scope (test_write_guard.WriteGuardTests.test_nonwrite_tools_are_outside_scope) ... ok\ntest_relative_path_uses_hook_cwd_not_process_cwd (test_write_guard.WriteGuardTests.test_relative_path_uses_hook_cwd_not_process_cwd) ... ok\ntest_symlink_into_main_with_missing_parents_is_denied (test_write_guard.WriteGuardTests.test_symlink_into_main_with_missing_parents_is_denied) ... ok\ntest_valid_multi_file_patch_on_feature_is_allowed (test_write_guard.WriteGuardTests.test_valid_multi_file_patch_on_feature_is_allowed) ... ok\n\n----------------------------------------------------------------------\nRan 250 tests in 151.781s\n\nOK (skipped=2)\nmake[1]: Leaving directory '/home/weichen/projects/Agent-System-Dev-wt-feature-svc-u2-release-recovery'\n", + "output_sha256": "ba079d3cdc0a4320a42f2252863fcf2a7e762072f1de026685c887c931f373b9", + "receipt_sha256": "8a9fe60bad7333805b9e18aeb7ef9ea11f14e91bd88cbad0411c5737544ae3de" +} +``` diff --git a/scripts/svc.sh b/scripts/svc.sh index 920e1ed2..1066f6d0 100755 --- a/scripts/svc.sh +++ b/scripts/svc.sh @@ -1253,7 +1253,17 @@ env_config() { ENV_DIR="$RUNTIME/$name" ENV_PID="$ENV_DIR/svc.pid" ENV_LOG="$ENV_DIR/svc.log" - ENV_BIN_DIR="$ENV_DIR/bin" + # U2:releases/ 不可变版本目录 + current 原子指针。已切换实例经 current 解析 + # 产物(进程 cmdline 保留未解析路径——指针切换不破坏 pidcheck 匹配,也彻底 + # 消除 exe=(deleted) 覆盖孤儿);legacy 实例沿用原路径(首次 deploy 领养, + # svc_release_adopt_legacy)。 + if [ -L "$ENV_DIR/current" ]; then + ENV_BIN_DIR="$ENV_DIR/current/bin" + ENV_PLUGINS="$ENV_DIR/current/plugins" + else + ENV_BIN_DIR="$ENV_DIR/bin" + ENV_PLUGINS="$ENV_DIR/plugins" + fi ENV_BIN="$ENV_BIN_DIR/agent-system" # 按实例重置内存态——批量 update/restart 逐服务解析时不串用上一服务。 # DB_*:防跨服务串库;SVC_PIDCHECK/EXEC/VERSION/SOURCE:防 pid_alive 与 @@ -1599,6 +1609,256 @@ install_artifacts() { # SRCBIN SRCPLUGINS:agent 产物复制进服务目录。 c_ok "[$ENV_NAME] 产物就位:$ENV_BIN(plugins:$ENV_DIR/plugins)" } +# ---- U2:不可变版本目录(releases/)+ current 原子指针 + 切换 journal + 失败回退 ---- +# 布局:releases//{bin/agent-system, plugins/, htdocs/, release.json}; +# current → releases/(相对符号链接,temp+rename 原子切换);deploy.journal +# 记录切换过程(staging→switched→done/rolledback/blocked),任意步骤中断后由 +# svc_deploy_journal_resolve 判定并恢复实际状态。legacy 目录永不删除:旧进程 +# cmdline 指向 legacy 路径,pidcheck 兼容到自然换版。 + +svc_release_cur() { [ -L "$ENV_DIR/current" ] && readlink "$ENV_DIR/current" || true; } +svc_release_cur_id() { local c; c="$(svc_release_cur)"; printf '%s' "${c##*/}"; } + +# svc_release_adopt_legacy:legacy 布局(bin/ 直接落服务目录)→ 领养为首个 +# release(幂等:已有 current 即跳过;无产物即跳过——全新部署无需领养)。 +svc_release_adopt_legacy() { + [ -L "$ENV_DIR/current" ] && return 0 + [ -x "$ENV_DIR/bin/agent-system" ] || return 0 + # 注意:bash 5.3 set -u 下同一 local 行不得引用本行先声明变量(拆行)。 + local id="legacy-$(date -u +%Y%m%dT%H%M%SZ)" + local dir="$ENV_DIR/releases/$id" + mkdir -p "$dir/bin" || return 1 + cp -f "$ENV_DIR/bin/agent-system" "$dir/bin/agent-system" || return 1 + if [ -d "$ENV_DIR/plugins" ]; then + cp -r "$ENV_DIR/plugins" "$dir/plugins" || return 1 + else + mkdir -p "$dir/plugins" + fi + [ -d "$ENV_DIR/htdocs" ] && cp -r "$ENV_DIR/htdocs" "$dir/htdocs" || true + printf '{"id":"%s","source":"legacy","created":"%s"}\n' "$id" "$(date -u +%FT%TZ)" > "$dir/release.json" + ln -s "releases/$id" "$ENV_DIR/.current.new" && mv -Tf "$ENV_DIR/.current.new" "$ENV_DIR/current" || { + rm -f "$ENV_DIR/.current.new"; return 1; } + c_info "[$ENV_NAME] 领养 legacy 制品为首个 release:releases/$id" +} + +# svc_release_id:源标签净化的版本目录名(rdm-ddc09eb-20261012T131500)。 +svc_release_id() { + local label + label="$(printf '%s' "${SRC_LABEL:-manual}" | tr '@' '-' | tr -cd 'a-zA-Z0-9._-' | cut -c1-40)" + printf '%s-%s' "$label" "$(date -u +%Y%m%dT%H%M%SZ)" +} + +# svc_release_stage ID SRCBIN SRCPLUGINS [HTDOCS]:候选产物入不可变版本目录。 +# stdout 回显 release 目录(供门禁/探测引用)。 +svc_release_stage() { + local id="$1" srcbin="$2" srcplug="$3" htdocs="${4:-}" + local dir="$ENV_DIR/releases/$id" + rm -rf "$dir" + mkdir -p "$dir/bin" || return 1 + cp -f "$srcbin" "$dir/bin/agent-system" || return 1 + if [ -d "$srcplug" ]; then + cp -r "$srcplug" "$dir/plugins" || return 1 + else + mkdir -p "$dir/plugins" + fi + if [ -n "$htdocs" ] && [ -d "$htdocs" ]; then + cp -r "$htdocs" "$dir/htdocs" || return 1 + fi + printf '{"id":"%s","source":"%s","created":"%s"}\n' \ + "$id" "${SRC_LABEL:-}" "$(date -u +%FT%TZ)" > "$dir/release.json" + printf '%s' "$dir" +} + +# svc_release_switch ID:原子切换 current(temp 符号链接 + rename)。 +svc_release_switch() { + ln -s "releases/$1" "$ENV_DIR/.current.new" || return 1 + mv -Tf "$ENV_DIR/.current.new" "$ENV_DIR/current" || { + rm -f "$ENV_DIR/.current.new"; return 1; } +} + +# svc_release_gc [KEEP]:裁剪历史 release(保留最近 KEEP 个,缺省 5;current 与 +# legacy 永不删除)。失败静默——目录非关键路径。 +svc_release_gc() { + local keep="${1:-5}" cur id dirs=() + [ -d "$ENV_DIR/releases" ] || return 0 + cur="$(svc_release_cur_id)" + while IFS= read -r d; do dirs+=("$(basename "$d")"); done \ + < <(ls -1dt "$ENV_DIR/releases"/*/ 2>/dev/null) + local n=${#dirs[@]} i rmv + for ((i = keep; i < n; i++)); do + rmv="${dirs[$i]}" + [ "$rmv" = "$cur" ] && continue + case "$rmv" in legacy-*) continue ;; esac + rm -rf "$ENV_DIR/releases/$rmv" + done + return 0 +} + +# ---- 切换 journal(deploy.journal):python3 原子写;缺 python3 时降级为无 journal ---- +svc_journal_path() { printf '%s' "$ENV_DIR/deploy.journal"; } + +svc_journal_write() { # PHASE [k=v ...] + local phase="$1"; shift + command -v python3 >/dev/null 2>&1 || return 0 + python3 - "$phase" "$(svc_journal_path)" "$@" <<'PYJ' +import json, os, sys, time +phase, out, args = sys.argv[1], sys.argv[2], sys.argv[3:] +j = {"phase": phase, "ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())} +for kv in args: + if "=" in kv: + k, v = kv.split("=", 1) + j[k] = v +tmp = out + ".new" +with open(tmp, "w") as f: + f.write(json.dumps(j, ensure_ascii=False) + "\n") +os.replace(tmp, out) +PYJ +} + +svc_journal_get() { # KEY → 值(journal 缺失/损坏 → 空) + local f; f="$(svc_journal_path)" + [ -f "$f" ] || return 0 + python3 -c 'import json,sys +try: + d = json.load(open(sys.argv[1])); v = d.get(sys.argv[2], "") + print(v if isinstance(v, str) else json.dumps(v)) +except Exception: + pass' "$f" "$1" 2>/dev/null +} +svc_journal_phase() { svc_journal_get phase; } +svc_journal_clear() { rm -f "$(svc_journal_path)"; } + +# svc_db_schema_version:SQLite 档当前 schema(schema_migrations 最大 id; +# 无库/无表/pg → 空)。只读连接,绝不写库。 +svc_db_schema_version() { + local db="$ENV_DATA/agent-system.db" + [ -f "$db" ] || return 0 + python3 -c 'import sqlite3,sys +try: + con = sqlite3.connect("file:" + sys.argv[1] + "?mode=ro", uri=True) + print(con.execute("select max(id) from schema_migrations").fetchone()[0]) + con.close() +except Exception: + pass' "$db" 2>/dev/null +} + +# svc_release_schema_supported ID:release.json 的 schema_at_commit(无则空)。 +svc_release_schema_supported() { + local f="$ENV_DIR/releases/$1/release.json" + [ -f "$f" ] || return 0 + python3 -c 'import json,sys +try: + print(json.load(open(sys.argv[1])).get("schema_at_commit") or "") +except Exception: + pass' "$f" 2>/dev/null +} + +# svc_release_commit_schema ID:成功提交时把当前库 schema 记入 release.json +# (回退时据此判定旧制品可否运行;pg/无库静默跳过)。 +svc_release_commit_schema() { + local f="$ENV_DIR/releases/$1/release.json" v + v="$(svc_db_schema_version)" + [ -n "$v" ] || return 0 + python3 -c 'import json,sys +try: + d = json.load(open(sys.argv[1])); d["schema_at_commit"] = sys.argv[2] + open(sys.argv[1], "w").write(json.dumps(d, ensure_ascii=False) + "\n") +except Exception: + pass' "$f" "$v" 2>/dev/null || true +} + +# svc_rollback_deploy REASON:切换失败 → 回退上一完整版本并恢复期望运行态。 +# schema 阻断(库 schema 超过上一 release 提交时点)不自动回退启动、绝不触碰 +# 业务数据库——登记 blocked 待受控迁移(U2 约束:不能靠覆盖数据库「回滚」)。 +svc_rollback_deploy() { + local reason="$1" + local prev new desired pid cur_schema prev_schema + prev="$(svc_journal_get prev)" + new="$(svc_journal_get new)" + desired="$(svc_journal_get desired)" + pid="$(read_pid)" + if [ -n "$pid" ] && pid_alive "$pid"; then + c_warn "[$ENV_NAME] 回退前停止新制品进程 pid=$pid" + proc_stop "$pid" || true + rm -f "$ENV_PID" + fi + if [ -z "$prev" ] || [ ! -d "$ENV_DIR/releases/$prev" ]; then + c_err "[$ENV_NAME] 无可回退版本(prev 缺失,$reason)——保持停止;清理:rm 前先取证" + svc_journal_write blocked reason="无 prev release:$reason" + hist_event deploy fail "$pid" "回退无 prev($reason)" + return 1 + fi + cur_schema="$(svc_db_schema_version)" + prev_schema="$(svc_release_schema_supported "$prev")" + if [ -n "$cur_schema" ] && [ -n "$prev_schema" ] \ + && [ "$cur_schema" -gt "$prev_schema" ] 2>/dev/null; then + c_err "[$ENV_NAME] schema 已前进(库 v$cur_schema > 上一 release 提交 v$prev_schema):旧制品无法运行,禁止自动回退" + c_err " 不触碰业务数据库。处理:deploy 换可运行制品(受控迁移后),或人工处置;登记 blocked" + svc_journal_write blocked reason="schema v$cur_schema > prev v$prev_schema($reason)" + hist_event deploy fail "$pid" "回退阻断:schema 前进($reason)" + return 1 + fi + svc_release_switch "$prev" || { + svc_journal_write blocked reason="指针回退失败:$reason" + return 1 + } + c_ok "[$ENV_NAME] 已回退上一版本:releases/$prev($reason)" + # 指针已切回:重解析产物路径到回退后的 release。 + ENV_BIN_DIR="$ENV_DIR/current/bin" + ENV_PLUGINS="$ENV_DIR/current/plugins" + ENV_BIN="$ENV_BIN_DIR/agent-system" + hist_event deploy rollback "$pid" "new=${new:-?} prev=$prev($reason)" + svc_journal_write rolledback "prev=$prev" "new=${new:-}" "reason=$reason" + if [ "$desired" = "running" ]; then + c_info "[$ENV_NAME] 按原期望态恢复运行(上一版本)" + # 登记一致性:清掉新部署的源标签——回退后的 record_state 须回落到 svc.env + # 里上一版的 source(否则实际跑旧制品、登记却写新标签)。 + SRC_LABEL="" + SVC_SOURCE="$(svc_get source)" + agent_do_start || { svc_journal_clear; return 1; } + fi + svc_journal_clear + return 0 +} + +# svc_deploy_journal_resolve [ACTION]:生命周期入口处理中断的切换。 +# switched/verifying:指针已切但未经完整验证 → 回退上一完整版本(无 prev 的 +# 首次部署中断 → 保持停止)。blocked:拒绝除 deploy 外的动作(显式换制品即 +# 解除路径)。staging:未切换,清记录(孤儿 release 目录无害,随 gc 裁剪)。 +svc_deploy_journal_resolve() { + local action="${1:-}" phase + [ -f "$(svc_journal_path)" ] || return 0 + phase="$(svc_journal_phase)" + case "$phase" in + ""|done|rolledback) + svc_journal_clear; return 0 ;; + staging) + c_warn "[$ENV_NAME] 发现中断的暂存(staging,未切换)——忽略" + svc_journal_clear; return 0 ;; + blocked) + if [ "$action" = "deploy" ]; then + c_warn "[$ENV_NAME] 存在 blocked 切换记录,本次 deploy 尝试跨越(换可运行制品)" + return 0 + fi + c_err "[$ENV_NAME] 切换被阻断($(svc_journal_get reason)):先 deploy 换可运行制品,或受控迁移后再操作" + return 1 ;; + switched|verifying) + c_warn "[$ENV_NAME] 发现中断的切换($phase,new=$(svc_journal_get new))——回退上一完整版本" + svc_rollback_deploy "中断的切换(journal=$phase)" || return 1 + return 0 ;; + esac + return 0 +} + +# svc_deploy_recycle_wt:deploy 各失败/成功出口统一回收构建 worktree(尽力而为)。 +svc_deploy_recycle_wt() { + case "${SRC_KINDSRC:-}" in + dev) [ -n "${wt:-}" ] && recycle_worktree "$SRC_SHA" ;; + rdm) [ -n "${wt:-}" ] && recycle_worktree "rdm-$SRC_SHA" "$RDM_SRC_DIR" ;; + esac + return 0 +} + # materialize_dev_commit FULL SHA TARGET:隔离 worktree 物化+构建;stdout 只回显 worktree # 路径(提示信息走 stderr)。TARGET=build(agent)| frontend(static)。 materialize_dev_commit() { @@ -1868,33 +2128,26 @@ agent_do_deploy() { return 1 fi hist_event deploy begin "$(read_pid)" "from=$SRC_LABEL" - # U1/U2(构建前置):先构建/物化候选并过身份门禁,再停旧服务——构建失败或候选 - # 不合格时旧服务继续运行、旧制品与登记不动(2026-10-12 rdm1/rdmcand 事故:停服 - # →覆盖→启动即拒,只留「登记版本≠磁盘制品」的混合状态且无恢复路径)。 - local wt RDM_UI_OK=0 + # U2(不可变版本目录 + 失败回退):构建 → 暂存 release → 门禁(身份/环境 DSN) + # → 停旧 → current 原子切换 → 启动与 UI 验证 → 成功提交(schema 记录 + journal + # 清理 + release 裁剪);任一步失败回退上一完整版本并恢复期望运行态 + # (svc_rollback_deploy;库 schema 已前进时阻断,绝不覆盖业务数据库)。 + local wt relid staged srcbin srcplug srchtdocs="" prev_id desired + svc_release_adopt_legacy \ + || { hist_event deploy fail "$(read_pid)" "legacy 领养失败($SRC_LABEL)"; return 1; } case "$SRC_KINDSRC" in local) c_info "[$ENV_NAME] 构建当前工作区(make build)——唯一写主检出构建产物的路径" - make -C "$ROOT" build || { hist_event deploy fail "$(read_pid)" "构建失败($SRC_LABEL)"; return 1; } - svc_deploy_identity_gate "$BIN" \ - || { hist_event deploy fail "$(read_pid)" "候选身份门禁拒绝($SRC_LABEL)"; return 1; } - do_stop_quiet - install_artifacts "$BIN" "$ROOT/plugins" \ - || { hist_event deploy fail "$(read_pid)" "产物复制失败"; return 1; } - ;; + make -C "$ROOT" build || { + c_err "[$ENV_NAME] 构建失败($SRC_LABEL)——旧服务保持运行、制品与登记不变" + hist_event deploy fail "$(read_pid)" "构建失败($SRC_LABEL)" + return 1 + } + srcbin="$BIN"; srcplug="$ROOT/plugins" ;; dev) wt="$(materialize_dev_commit "$SRC_FULL" "$SRC_SHA" build)" \ || { hist_event deploy fail "$(read_pid)" "物化/构建失败($SRC_LABEL)"; return 1; } - if ! svc_deploy_identity_gate "$wt/bin/agent-system"; then - hist_event deploy fail "$(read_pid)" "候选身份门禁拒绝($SRC_LABEL)" - recycle_worktree "$SRC_SHA" - return 1 - fi - do_stop_quiet - install_artifacts "$wt/bin/agent-system" "$wt/plugins" \ - || { hist_event deploy fail "$(read_pid)" "产物复制失败"; return 1; } - recycle_worktree "$SRC_SHA" - ;; + srcbin="$wt/bin/agent-system"; srcplug="$wt/plugins" ;; release) local relbin="$REL_ROOT/build/$REL_VERSION/agent-system" if [ ! -f "$relbin" ]; then @@ -1903,65 +2156,98 @@ agent_do_deploy() { hist_event deploy fail "$(read_pid)" "制品缺失($SRC_LABEL)" return 1 fi - svc_deploy_identity_gate "$relbin" \ - || { hist_event deploy fail "$(read_pid)" "候选身份门禁拒绝($SRC_LABEL)"; return 1; } - do_stop_quiet - materialize_release || { hist_event deploy fail "$(read_pid)" "物化/校验失败($SRC_LABEL)"; return 1; } - ;; + local mf="$REL_ROOT/versions/$REL_VERSION/manifest.json" want have + if [ -f "$mf" ]; then + want="$(sed -n 's/.*"agent-system": *"\([0-9a-f]\{64\}\)".*/\1/p' "$mf" | head -1)" + have="$(sha256sum "$relbin" | cut -d' ' -f1)" + if [ -n "$want" ] && [ "$want" != "$have" ]; then + c_err "[$ENV_NAME] sha256 校验失败:制品与 manifest 不符($relbin)——拒绝部署" + hist_event deploy fail "$(read_pid)" "sha256 校验失败($SRC_LABEL)" + return 1 + fi + [ -n "$want" ] && c_ok "[$ENV_NAME] sha256 校验通过(manifest:$REL_VERSION)" + else + c_warn "[$ENV_NAME] 无 manifest 可校验:$mf" + fi + srcbin="$relbin"; srcplug="$REL_ROOT/build/$REL_VERSION/plugins" ;; rdm) wt="$(materialize_rdm_commit "$SRC_FULL" "$SRC_SHA")" \ || { hist_event deploy fail "$(read_pid)" "物化/构建失败($SRC_LABEL)"; return 1; } - # RDM 组合插件编译进二进制(provides 无声明式资产)——插件目录按空集安装。 + # RDM 组合插件编译进二进制(provides 无声明式资产)——插件目录按空集。 mkdir -p "$wt/.svc-empty-plugins" - if ! svc_deploy_identity_gate "$wt/bin/agent-system-rdm"; then - hist_event deploy fail "$(read_pid)" "候选身份门禁拒绝($SRC_LABEL)" - recycle_worktree "rdm-$SRC_SHA" "$RDM_SRC_DIR" - return 1 - fi - do_stop_quiet - install_artifacts "$wt/bin/agent-system-rdm" "$wt/.svc-empty-plugins" \ - || { hist_event deploy fail "$(read_pid)" "产物复制失败"; return 1; } - # RDM 自带零构建工作台(web/)落 htdocs/;收尾自动部署伴生 ${name}web。 - if [ -d "$wt/web" ]; then - rm -rf "$ENV_DIR/htdocs" - if cp -r "$wt/web" "$ENV_DIR/htdocs"; then - RDM_UI_OK=1 - c_ok "[$ENV_NAME] RDM 工作台就位:$ENV_DIR/htdocs(将自动部署伴生 $(rdm_companion_name "$ENV_NAME"))" - else - c_warn "[$ENV_NAME] RDM web/ 工作台复制失败" - fi - else - c_warn "[$ENV_NAME] RDM 源无 web/ 工作台目录" - fi - recycle_worktree "rdm-$SRC_SHA" "$RDM_SRC_DIR" - ;; + srcbin="$wt/bin/agent-system-rdm"; srcplug="$wt/.svc-empty-plugins" + [ -d "$wt/web" ] && srchtdocs="$wt/web" ;; esac - # 新产物承接能力复核(环境 DSN 交付):不支持即失败——绑定保持可恢复,不降级启动。 - if [ "$DB_MODE" = pg ] && [ "$DB_SOURCE" != conffile ] && ! bin_env_dsn_supported "$ENV_BIN"; then + relid="$(svc_release_id)" + staged="$(svc_release_stage "$relid" "$srcbin" "$srcplug" ${srchtdocs:+"$srchtdocs"})" \ + || { hist_event deploy fail "$(read_pid)" "产物暂存失败($SRC_LABEL)"; svc_deploy_recycle_wt; return 1; } + if ! svc_deploy_identity_gate "$staged/bin/agent-system"; then + hist_event deploy fail "$(read_pid)" "候选身份门禁拒绝($SRC_LABEL)" + rm -rf "$staged" + svc_deploy_recycle_wt + return 1 + fi + # 新产物承接能力复核(环境 DSN 交付;候选上、停服前):不支持即失败——绑定 + # 保持可恢复,不降级启动。 + if [ "$DB_MODE" = pg ] && [ "$DB_SOURCE" != conffile ] && ! bin_env_dsn_supported "$staged/bin/agent-system"; then c_err "[$ENV_NAME] 新产物不消费环境变量 DSN(AGENT_SYSTEM_DSN)——部署中止;旧绑定保留在 svc.env" c_err " 恢复:换部署版本,或改用携带 dsn 的配置文件(scripts/svc.sh $ENV_NAME config )" hist_event deploy fail "$(read_pid)" "新产物不支持环境 DSN($SRC_LABEL)" + rm -rf "$staged" + return 1 + fi + prev_id="$(svc_release_cur_id)" + desired="stopped"; pid_alive "$(read_pid)" 2>/dev/null && desired="running" + svc_journal_write staging "new=$relid" "prev=$prev_id" "desired=$desired" + do_stop_quiet + svc_release_switch "$relid" || { + svc_journal_write blocked reason="current 指针切换失败" + hist_event deploy fail "$(read_pid)" "current 指针切换失败($SRC_LABEL)" + return 1 + } + svc_journal_write switched "new=$relid" "prev=$prev_id" "desired=$desired" + c_ok "[$ENV_NAME] 已切换 release:releases/$relid(prev:${prev_id:-无})" + # 指针已切:重解析产物路径(env_config 解析时 current 可能尚不存在)。 + ENV_BIN_DIR="$ENV_DIR/current/bin" + ENV_PLUGINS="$ENV_DIR/current/plugins" + ENV_BIN="$ENV_BIN_DIR/agent-system" + if ! agent_do_start; then + svc_rollback_deploy "新制品启动失败($SRC_LABEL)" || true + hist_event deploy fail "$(read_pid)" "启动失败已回退($SRC_LABEL)" return 1 fi - agent_do_start || { hist_event deploy fail "$(read_pid)" "启动失败($SRC_LABEL)"; return 1; } case "$SRC_KINDSRC" in rdm) - if [ "$RDM_UI_OK" != 1 ]; then - c_err "[$ENV_NAME] RDM 工作台未就位(ui-missing):缺少可伺服的 htdocs/" - hist_event deploy fail "$(read_pid)" "ui-missing(htdocs 未就位)" - return 1 - fi - if ! deploy_rdm_companion_ui "$(rdm_companion_name "$ENV_NAME")" "$ENV_DIR/htdocs"; then - hist_event deploy fail "$(read_pid)" "ui-missing(伴生部署失败)" + # 顶层 htdocs → current/htdocs 符号链接:伴生经它伺服,指针切换自动换内容 + # (legacy 实体目录内容已随领养保入 release,替换不丢)。 + if [ -n "$srchtdocs" ]; then + if [ ! -L "$ENV_DIR/htdocs" ]; then + rm -rf "$ENV_DIR/htdocs" + ln -s current/htdocs "$ENV_DIR/htdocs" + fi + c_ok "[$ENV_NAME] RDM 工作台就位:releases/$relid/htdocs → $ENV_DIR/htdocs(伴生 $(rdm_companion_name "$ENV_NAME"))" + if ! deploy_rdm_companion_ui "$(rdm_companion_name "$ENV_NAME")" "$ENV_DIR/htdocs"; then + svc_rollback_deploy "RDM 伴生工作台部署失败(ui-missing)" || true + hist_event deploy fail "$(read_pid)" "ui-missing(伴生部署失败,已回退)" + return 1 + fi + else + svc_rollback_deploy "RDM 源无 web/ 工作台(ui-missing)" || true + hist_event deploy fail "$(read_pid)" "ui-missing(源无 web/,已回退)" return 1 fi ;; esac if ! assert_ui_ready; then - hist_event deploy fail "$(read_pid)" "ui-missing" + svc_rollback_deploy "UI 探活失败(ui-missing)" || true + hist_event deploy fail "$(read_pid)" "ui-missing(已回退)" return 1 fi - hist_event deploy ok "$(read_pid)" "from=$SRC_LABEL" + svc_release_commit_schema "$relid" + svc_journal_clear + svc_release_gc + svc_deploy_recycle_wt + hist_event deploy ok "$(read_pid)" "from=$SRC_LABEL release=$relid" return 0 } @@ -2380,6 +2666,9 @@ agent_do_start() { local exec_args if ! agent_has_explicit_cfg && [ -n "$(svc_get exec)" ]; then exec_args="$(svc_get exec)" + # U2:release 治理实例的持久化 exec 里 legacy plugins 路径跟随 current + #(指针切换后插件目录随 release 解析)。 + [ -L "$ENV_DIR/current" ] && exec_args="${exec_args//$ENV_DIR\/plugins/$ENV_PLUGINS}" c_info "[$ENV_NAME] 复用上次配置(svc.env exec=;显式参数可覆盖):$(mask_dsn "$exec_args")" else local args="" explicit_auth="${CLI_AUTH:-$(env_var "$ENV_NAME" AUTH)}" @@ -2398,7 +2687,7 @@ agent_do_start() { c_warn "[$ENV_NAME] 该产物不支持部分启动参数(旧版本二进制),已自动省略:${args:-<无>} → ${adjusted:-<无>}" fi args="$adjusted" - exec_args="--addr $ENV_ADDR --data $ENV_DATA --plugins ${CLI_PLUGINS:-$ENV_DIR/plugins} $args $CLI_EXTRA" + exec_args="--addr $ENV_ADDR --data $ENV_DATA --plugins ${CLI_PLUGINS:-$ENV_PLUGINS} $args $CLI_EXTRA" exec_args="$(printf '%s' "$exec_args" | sed 's/[[:space:]]*$//')" fi SVC_EXEC="$exec_args" @@ -4405,6 +4694,15 @@ cmd_update() { failed=$((failed + 1)) continue fi + # U2:批内同样处理中断切换(deploy 可跨越 blocked)。 + if ! svc_deploy_journal_resolve deploy; then + hist_event update fail "$(read_pid)" "from=$from to=$tov(中断切换处置失败)" + c_err "[$name] 更新失败(deploy.journal 处置),其余服务继续" + svc_unlock + CLI_FROM="" + failed=$((failed + 1)) + continue + fi if agent_do_deploy; then hist_event update ok "$(read_pid)" "from=$from to=$tov" c_ok "[$name] 已更新:$from → $tov" @@ -4479,6 +4777,11 @@ EOF c_warn "[$n] 生命周期锁被占用(deploy/手动操作进行中),本轮跳过" skipped=$((skipped + 1)); continue fi + # U2:拉起前处置中断的切换(switched → 回退上一完整版本再按期望态启动)。 + if ! svc_deploy_journal_resolve recover; then + c_warn "[$n] 存在阻断的切换记录,本轮跳过(见 deploy.journal)" + failed=$((failed + 1)); rc=1; svc_unlock; continue + fi c_info "[$n] recover:autostart=1 且进程已死,重新拉起(复用持久化配置)" if do_start; then hist_event recover ok "$(read_pid)" "autostart revival" @@ -4700,6 +5003,11 @@ main() { case "$action" in start|restart|deploy|init) svc_manifest_migrate || exit 1 ;; esac + # U2:入口处理中断的切换(deploy.journal)。blocked 拒绝除 deploy 外的动作 + #(显式换制品即解除路径);start/restart 触发回退判定。 + case "$action" in + start|restart|deploy) svc_deploy_journal_resolve "$action" || exit 1 ;; + esac case "$action" in deploy) case "$SVC_KIND" in diff --git a/tests/test_svc.py b/tests/test_svc.py index 9aefb369..f97553f6 100644 --- a/tests/test_svc.py +++ b/tests/test_svc.py @@ -796,12 +796,14 @@ class SvcDbRecoveryTests(unittest.TestCase): env = self.rdm_env(remote) result = self.svc("rdm1", "deploy", "--from", "rdm", "--port", "18841", env=env) d = self.runtime("rdm1") - # 组合二进制复制改名落位、插件目录空集、工作台静态资产落 htdocs/。 - self.assertTrue((d / "bin" / "agent-system").exists()) - self.assertTrue((d / "bin" / "agent-system").stat().st_mode & 0o111) + # U2:组合产物入不可变 release,经 current 指针解析;插件空集、工作台随 release。 + active = d / "current" + self.assertTrue(active.is_symlink(), "current 指针须就位") + self.assertTrue((active / "bin" / "agent-system").exists()) + self.assertTrue((active / "bin" / "agent-system").stat().st_mode & 0o111) self.assertEqual(self.kv("rdm1", "kind"), "agent") self.assertTrue(self.kv("rdm1", "source").startswith("rdm@")) - self.assertEqual(list((d / "plugins").iterdir()), []) + self.assertEqual(list((active / "plugins").iterdir()), []) self.assertIn("rdm workbench", (d / "htdocs" / "index.html").read_text()) self.assertIn("args:", self.serve_record()) # 健康通过即登记启动参数 # 伴生 static 自动部署并探活 200。 diff --git a/tests/test_svc_u2.py b/tests/test_svc_u2.py new file mode 100644 index 00000000..4a52b6a8 --- /dev/null +++ b/tests/test_svc_u2.py @@ -0,0 +1,228 @@ +"""svc.sh U2 回归:不可变版本目录(releases/)+ current 原子切换 + deploy.journal ++ 失败回退(2026-10-12 统一部署方案 U2)。 + +覆盖:release 布局与 current 解析(pidcheck 经未解析路径跨版本稳定)、启动失败 +回退上一完整版本并恢复期望态、构建失败旧服务保持运行、中断切换经 start/recover +入口回退、schema 前进阻断回退(不触碰业务库)、legacy 布局兼容。夹具复用 +tests/test_svc_identity.py(IdentityTests:manifest 治理 + 源仓轨道 + noid 桩)。 +""" +import importlib.util +import json +import os +import shutil +import subprocess +import unittest +from pathlib import Path + +SOURCE = Path(__file__).resolve().parents[1] + +_spec = importlib.util.spec_from_file_location("tsr", SOURCE / "tests" / "test_svc_recover.py") +tsr = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(tsr) + +_spec2 = importlib.util.spec_from_file_location("tsi", SOURCE / "tests" / "test_svc_identity.py") +tsi = importlib.util.module_from_spec(_spec2) +_spec2.loader.exec_module(tsi) + + +class U2ReleaseTests(tsi.IdentityTests): + """manifest 治理 + identity 桩 + dev 轨道源仓(make_src_repo 等)。""" + + def setUp(self): + super().setUp() + # --from local 需要根为 git 仓(local@HEAD 作源标签);recover 夹具不初始化。 + self.git_ok(self.root, "init", "--initial-branch=main", ".") + self.git_ok(self.root, "add", ".") + self.git_ok(self.root, "commit", "-m", "u2 fixture") + + # ---- 夹具 ---- + + def cur_release(self, name): + c = self.runtime(name) / "current" + return c.resolve().name if c.is_symlink() else None + + def active_bin(self, name): + return self.runtime(name) / "current" / "bin" / "agent-system" + + def cmdline_of(self, pid): + return Path("/proc/%d/cmdline" % pid).read_bytes().replace(b"\0", b" ").decode() + + def journal(self, name): + f = self.runtime(name) / "deploy.journal" + return json.loads(f.read_text()) if f.exists() else None + + def die_stub(self): + """identity 可用、serve 即退的桩(模拟新制品启动失败)。""" + p = self.root / "stub-agent-system-die" + if not p.exists(): + stub = tsr.STUB + head, sep, tail = stub.partition(" serve)") + assert sep, "STUB 缺 serve 分支锚点" + _, sep2, tail2 = tail.partition(' wait "$child"\n ;;') + assert sep2, "STUB 缺 serve 分支结尾锚点" + p.write_text(head + ' serve)\n shift\n' + + ' echo "die-stub: serve exits immediately" >&2\n' + + " exit 1 ;;" + tail2) + p.chmod(0o755) + return p + + def set_makefile(self, stub): + (self.root / "Makefile").write_text( + "build:\n\tmkdir -p bin\n\tcp %s bin/agent-system\n" % stub) + + def bump_schema(self, name, version): + """把库 schema_migrations 推进到 version(桩库无该表则建;模拟新制品 + 已迁移数据库)。""" + import sqlite3 + db = self.runtime(name) / "data" / "agent-system.db" + con = sqlite3.connect(str(db)) + con.execute( + "CREATE TABLE IF NOT EXISTS schema_migrations" + "(id INTEGER PRIMARY KEY, name TEXT, applied_at TEXT)") + for i in (1, 2, 3, version): + con.execute( + "INSERT OR IGNORE INTO schema_migrations(id,name,applied_at)" + " VALUES(?, 'u2-test', '2026-10-12T00:00:00Z')", (i,)) + con.commit() + con.close() + + # ---- T1:release 布局 + 启动失败回退 + 期望态恢复 ---- + + def test_U2_layout_switch_and_rollback_on_start_failure(self): + self.make_src_repo() # 预克隆 srccache(本用例走 --from local,仅保环境一致) + self.seed_agent("u2a", 9461) + self.start_ok("u2a") # legacy 运行中(期望态=running) + self.assertTrue(self.running("u2a")) + + # v1 部署:legacy 领养 → current 切换 → 经 current 启动 + self.set_makefile(self.stub) + result = self.svc("u2a", "deploy", "--from", "local") + self.assertIn("已切换 release", result.stdout) + self.assertIn("领养 legacy", result.stdout) + cur1 = self.cur_release("u2a") + self.assertTrue(cur1 and cur1.startswith("local-"), cur1) + self.assertTrue(self.active_bin("u2a").exists()) + releases = sorted((self.runtime("u2a") / "releases").iterdir()) + self.assertEqual(len(releases), 2, "legacy + v1 两个 release") + pid = self.pid_of("u2a") + self.assertIn("current/bin/agent-system", self.cmdline_of(pid), + "进程 cmdline 须走 current 未解析路径(pidcheck 跨版本稳定)") + self.assertIn("current/bin", self.kv("u2a", "pidcheck") or "") + v1_bin = self.active_bin("u2a").read_bytes() + self.assertEqual(v1_bin, Path(self.stub).read_bytes(), "v1 制品 = 可用桩") + self.assertIsNone(self.journal("u2a"), "成功后 journal 清理") + + # v2 部署:serve 即退 → 回退 v1 并恢复运行 + self.set_makefile(self.die_stub()) + result = self.svc("u2a", "deploy", "--from", "local", check=False) + self.assertNotEqual(result.returncode, 0, result.stdout) + self.assertIn("已回退上一版本", result.stdout) + self.assertTrue(self.running("u2a"), "回退须按原期望态恢复运行") + pid2 = self.pid_of("u2a") + self.assertNotEqual(pid2, pid) + self.assertIn("current/bin/agent-system", self.cmdline_of(pid2)) + self.assertEqual(self.active_bin("u2a").read_bytes(), v1_bin, "回退后制品=v1") + hist = (self.runtime("u2a") / "history.jsonl").read_text() + self.assertIn('"event":"deploy"', hist) + self.assertIn("rollback", hist) + self.assertIsNone(self.journal("u2a")) + self.svc("u2a", "stop") + + # ---- T2:构建失败 → 旧服务保持运行、current 不动 ---- + + def test_U2_build_failure_keeps_old_running(self): + self.seed_agent("u2b", 9462) + self.set_makefile(self.stub) + self.svc("u2b", "deploy", "--from", "local") + cur1 = self.cur_release("u2b") + pid = self.pid_of("u2b") + (self.root / "Makefile").write_text("build:\n\texit 1\n") + result = self.svc("u2b", "deploy", "--from", "local", check=False) + self.assertNotEqual(result.returncode, 0) + self.assertIn("构建失败", result.stdout) + self.assertTrue(self.running("u2b"), "构建失败旧服务必须保持运行") + self.assertEqual(self.pid_of("u2b"), pid) + self.assertEqual(self.cur_release("u2b"), cur1, "current 指针不动") + self.svc("u2b", "stop") + + # ---- T3:中断切换(journal switched)→ start 入口回退并恢复 ---- + + def test_U2_journal_interrupt_resolved_on_start(self): + self.seed_agent("u2c", 9463) + self.set_makefile(self.stub) + self.svc("u2c", "deploy", "--from", "local") + v1 = self.cur_release("u2c") + self.svc("u2c", "stop") + # 伪造中断于「指针已切」:current → 假 release,journal switched + fake = self.runtime("u2c") / "releases" / "fake-interrupt" + (fake / "bin").mkdir(parents=True) + shutil.copy2(self.die_stub(), fake / "bin" / "agent-system") + (self.runtime("u2c") / "current").unlink() + (self.runtime("u2c") / "current").symlink_to("releases/fake-interrupt") + (self.runtime("u2c") / "deploy.journal").write_text(json.dumps({ + "phase": "switched", "new": "fake-interrupt", "prev": v1, + "desired": "running", "ts": "2026-10-12T00:00:00Z"})) + + result = self.svc("u2c", "start") + self.assertIn("中断的切换", result.stdout) + self.assertIn("已回退上一版本", result.stdout) + self.assertTrue(self.running("u2c")) + self.assertEqual(self.cur_release("u2c"), v1) + self.assertIsNone(self.journal("u2c")) + self.svc("u2c", "stop") + + # ---- T4:schema 前进 → 回退阻断,不触碰业务库 ---- + + def test_U2_schema_advance_blocks_rollback(self): + self.seed_agent("u2d", 9464) + self.set_makefile(self.stub) + self.svc("u2d", "deploy", "--from", "local") + v1 = self.cur_release("u2d") + self.svc("u2d", "stop") + # v1 提交时点 schema=3;库被「新制品」推进到 5 + rf = self.runtime("u2d") / "releases" / v1 / "release.json" + data = json.loads(rf.read_text()) + data["schema_at_commit"] = "3" + rf.write_text(json.dumps(data)) + self.bump_schema("u2d", 5) + fake = self.runtime("u2d") / "releases" / "fake-advance" + (fake / "bin").mkdir(parents=True) + shutil.copy2(self.die_stub(), fake / "bin" / "agent-system") + (self.runtime("u2d") / "current").unlink() + (self.runtime("u2d") / "current").symlink_to("releases/fake-advance") + (self.runtime("u2d") / "deploy.journal").write_text(json.dumps({ + "phase": "switched", "new": "fake-advance", "prev": v1, + "desired": "running", "ts": "2026-10-12T00:00:00Z"})) + + result = self.svc("u2d", "start", check=False) + self.assertNotEqual(result.returncode, 0, result.stdout) + self.assertIn("禁止自动回退", result.stdout) + self.assertFalse(self.running("u2d"), "阻断后保持停止") + j = self.journal("u2d") + self.assertEqual(j["phase"], "blocked") + # 业务库不被回写:schema 仍是 5 + import sqlite3 + con = sqlite3.connect(str(self.runtime("u2d") / "data" / "agent-system.db")) + self.assertEqual( + con.execute("select max(id) from schema_migrations").fetchone()[0], 5) + con.close() + # blocked 下普通动作被拒;deploy 是解除路径(此处仅验证拒绝语义) + result = self.svc("u2d", "restart", check=False) + self.assertNotEqual(result.returncode, 0) + self.assertIn("切换被阻断", result.stdout) + + # ---- T5:legacy 布局未迁移 → 裸 start/restart 仍走原路径(兼容) ---- + + def test_U2_legacy_layout_bare_lifecycle_still_works(self): + self.seed_agent("u2e", 9465) + self.start_ok("u2e") + self.assertFalse((self.runtime("u2e") / "current").exists()) + self.assertTrue((self.runtime("u2e") / "bin" / "agent-system").exists()) + self.svc("u2e", "stop") + self.start_ok("u2e") + self.assertTrue(self.running("u2e")) + self.svc("u2e", "stop") + + +if __name__ == "__main__": + unittest.main()