Merge authoritative main into the task branch

Why: include the current integration baseline before verification.

Evidence: fetched origin/main=4f84c2e1630cd0311643529fae1c3a99f1dd5d64.

Impact: integrate upstream changes without rewriting task history.

Refs: task-sync
This commit is contained in:
2026-10-14 06:13:02 +00:00
12 changed files with 1894 additions and 36 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+63
View File
@@ -0,0 +1,63 @@
# U7 账户事务、审计与只读打开
本规格落实账户方案 r2 的 U7(R13–R15、KTD11–14)。HTTP 响应形状不变。`scripts/svc.sh` 与登录页不在本增量。
## 1. 事务
存储层提供一个事务助手。业务函数在该事务内完成写入,成功则同事务追加审计,失败则整笔回滚。
严格建号使用 INSERT。owner 已存在时返回 `ErrConflict`,不读取后再 UPDATE,因此并发不能覆盖先提交的账户。
以下每条是一次提交:
- 开户:用户行、激活 Key(无初始密码时)、成功审计。
- `applyPasswordChange`:写密码哈希、更新 must_change、作废该 owner 未使用的登录 Key、按路径处理会话、成功审计。覆盖四条调用:主动改密、强制设密、管理员或 CLI 重置、恢复设密。
- `--revoke-all-credentials`:吊销长期 API Key、删除会话、作废未使用登录 Key、签发新 Key、成功审计,一次提交。
失败路径在回滚之后另记一条 `outcome=rejected` 的审计。该条不包含密码、哈希或明文 Key。
## 2. bootstrap
`bootstrap-admin` 在事务内复核,不能只看用户表在事务外为空。
- SQLite:`BEGIN IMMEDIATE`。
- PostgreSQL:`SELECT ... FROM agent_system_identity FOR UPDATE`。没有身份行则拒绝,本命令不补建身份。
复核条件:`users` 为空,且下列业务表都为空:`projects`、`conversations`、`messages`、`runs`、`api_keys`、`sessions`、`login_keys`。任一表有行则拒绝。通过后,管理员、激活 Key 与成功审计同事务提交。
## 3. 审计
迁移在 `audit_events` 增加列:`entry`、`instance_id`、`operation_id`、`outcome`。文本,默认空串。既有行保持可读。版本号取本分支当前最大版本的下一个整数。若 U8 已占用该号,task-sync 时顺延,不改已发布的迁移正文。
成功记录与变更同事务。`actor`:HTTP 取会话 owner;CLI 取操作系统 uid 的十进制字符串。`login_keys.instance_id` 在签发时写入调用方给出的实例标识,缺省空串。
## 4. 账户运维打开
`user`、`apikey`、`identity verify` 只打开既有库:文件或库不存在则失败,不创建父目录,不执行迁移。`user`、`apikey` 的 schema 版本与本二进制不一致时拒绝,错误文本提示使用 `migrate` 或先启动一次服务。`identity verify` 只校验既有实例身份并报告 schema 版本,不以版本不一致阻断启动前身份核验。
设置了 `AGENT_SYSTEM_INSTANCE_ID` 或 `--instance-id` 时,与库内身份行严格比对,不一致则拒绝。未设置时不补建身份。
## 5. 凭据输入输出
- DSN:`AGENT_SYSTEM_DSN`,或模式 0600 的 `--dsn-file`。
- 密码:`--password-stdin`。
- 明文 Key 或生成的密码写入 `--out-file`。创建使用独占创建(已存在则失败),权限 0600。
- stderr 不输出凭据明文;可打印 operation id、凭据 id,以及必要的输出路径、弃用警告和错误提示。
`--dsn` 与 `--password` 仍可用,并在 stderr 打印弃用警告,以兼容现有 `scripts/svc.sh` 与 `scripts/e2e.sh`。移除留给账户 U5。
`agent-system capabilities --json` 打印本二进制能力,不打开数据库,不创建目录。
## 6. HTTP
`internal/server/accounts.go` 的建号、改密、重置走同一事务助手和审计。响应 JSON 字段保持现状。
## 7. 测试
`internal/store/account_lifecycle_test.go` 固定在 SQLite 上运行;`account_security_integration_test.go` 使用 `openTestStore` 在 SQLite 和设有 `AGENT_PG_TEST_DSN` 的 PostgreSQL 上分别运行。`account_serialization_pg_test.go` 与 `account_two_process_pg_test.go` 覆盖真实 PostgreSQL 交叠和跨进程入场;未配置 DSN 时 PostgreSQL 专项 SKIP,不能记为已验证。Dev CI 的 `make test-backend-pg` 是父仓 PostgreSQL 组合的 GATE。
- T7-1 / T7-4:两个进程同时用同一 owner 开户,只有一个成功。并发 bootstrap,以及 bootstrap 与普通开户竞争,同样只有一个成功。
- T7-2:在提交中途注入失败,读到的只能是完整旧态或完整新态。
- T7-3:改密、重置、恢复设密之后,旧的未使用登录 Key 不能再兑换。长期 API Key 仅在 `--revoke-all-credentials` 时撤销。
- T7-5:关闭再打开后原密码仍能校验。真实进程收到 SIGTERM 或 SIGKILL 后同样能校验。两个版本的真实升级记 NOT_RUN。
- 账户 CLI 对既有库不执行迁移;缺文件时不建目录。既有登录 Key 与认证测试保持通过。
+5
View File
@@ -550,6 +550,11 @@ scripts/svc.sh help <主题> # 主题详解:model deploy lifecycle accounts db
# accounts:实例绑定账户 CLI(create/list/reset-password/issue-login-key/revoke-login-key/bootstrap-admin)
```
`agent-system` 的账户命令仍接受 `--dsn` 和 `--password`,并在 stderr 打印弃用警告。
新调用使用 `AGENT_SYSTEM_DSN` 或模式 0600 的 `--dsn-file`,密码使用 `--password-stdin`,
明文写入独占创建的 `--out-file`。`agent-system capabilities --json` 不打开数据库。
`scripts/svc.sh` 仍传 `--dsn` / `--password`,切换留给账户 U5。
## 7. 命令参考:数据库
### 7.1 db create(创建实例)
+14 -15
View File
@@ -1,16 +1,16 @@
# 最终验证与合入调度(候选条文 r1)
# 最终验证与合入调度(r1,manual 受控试行)
状态:approved(r1 条文作为实施计划基线);执行模式:未切换;日期:2026-10-13(Asia/Shanghai);规范采纳负责人:weichen。
状态:approved(r1 条文作为实施计划基线);执行模式:manual 受控试行;更新日期:2026-10-14(Asia/Shanghai);规范采纳负责人:weichen。实际启用时间、对象和窗口以调度记录 #518 为准,候选文件不代替当前平台准入。
r1 条文来源提交为 913b214f2826a50b3dab65f79d9ebf83e84f2519;Documents 实施与测试计划 r1@ba4044d57e38d0887dd425d052abf26b20a28bf4 已于 2026-10-13 获用户「批准」。阶段结论与执行进展按 [迭代交付](delivery.md) 记录在任务主记录,候选固定后的结论保存在关联 PR 与源码外收据中;规范及计划批准不等于模式已启用。
现行规则仍以根 AGENTS.md、[迭代交付](delivery.md)、[开发门禁](governance.md) 和已采纳 gitlink 为准。
本分支已实施 CLI 同步/恢复及源码外证据基础,自检依据见任务主记录及固定后记录;通用条文和实施计划已经各自 PR 合入,本候选组合固定实际合入版本。线上队列、CI 分流与平台保护尚未切换,不承诺自动调度能力已就位。
本分支已实施 CLI 同步/恢复及源码外证据基础,自检依据见任务主记录及固定后记录;通用条文和实施计划已经各自 PR 合入,本候选组合固定实际合入版本。用户已于 2026-10-14 明确授权当前智能体接管 #489/#488/#486,完成平台验证、试行、实际合入与清理。平台所有者操作已实际回读,隔离正反例及独立重放已完成;本轮使用单一智能体协调的 manual 窗口,原 CI 触发和全部必需作业保持,未建设自动调度。
## 通用规范与项目职责
通用正文的唯一候选来源是 Development-Guidelines 的 `standards/merge-queue.md`,
配套模板为该仓 `templates/merge-queue.md`,候选内容版本 `0.1.0-draft.11`。
本仓仅细化目标、入口、阶段、预算和采纳条件,不复制通用调度正文。
通用候选已经 [Guidelines PR #5](https://git-home.u1680832.nyat.app:30319/weichen/Development-Guidelines/pulls/5) 合入为 `2f5b19872be2a4597838a4131ad75d1be089ee5e`;本候选 `deps/development-guidelines` 已物化该固定版本及上述文件。Dev 组合验收、平台正负例、全部正常写入者覆盖及模式切换条件满足前,本文件保持 approved/pending-activation,不标 active。
通用候选已经 [Guidelines PR #5](https://git-home.u1680832.nyat.app:30319/weichen/Development-Guidelines/pulls/5) 合入为 `2f5b19872be2a4597838a4131ad75d1be089ee5e`;本候选 `deps/development-guidelines` 已物化该固定版本及上述文件。Dev 已经 #519/#536 固定采纳并通过其精确 HEAD 的全部必需 CI。平台机制与手动写者覆盖的实际依据见 #518;受控试行的当前候选仍须独立组合测试、当前 HEAD CI 与平台准入,不把已采纳规范标成自动 enforced。
Dev 目标身份使用实际托管实例、仓库 ID 与 `refs/heads/main`;其他组件仓各有自己的目标队列。
跨仓组合按任务中的依赖关系先交付组件,再固定父仓组合;保留其他任务已经合入的 gitlink 能力。
@@ -21,7 +21,7 @@ Dev 目标身份使用实际托管实例、仓库 ID 与 `refs/heads/main`;其
采纳后的首版使用每个目标一份 Gitea 调度 Issue,由一个明确协调者维护,标为 manual。
任务卡引用 Issue 条目及尝试标识,原任务仍是授权、需求和阶段的状态权威。
共享记录已建立:[Dev main 调度记录 #518](https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/issues/518),包含实际托管实例、仓库 ID 和目标引用。用户已指定当前交付智能体负责后续人工协调,weichen 保留阶段评审和平台保护操作;具体任期与接管约束以该记录为准。
记录仍为准备中,不开放领取,不自动向其他任务分配工作;角色分工不代替启用条件。
本轮仅协调已获授权的三个真实任务,入队/阻塞/当前窗口及时维护于 #518。普通、闭合、机器人与紧急通道都必须经智能体和同一目标窗口;owner-only 合入账号与无自动合入 workflow/活跃 webhook 的当场清单另留 SELF 收据。用户不亲手合入;API 不具备区分人工与智能体共用账号的身份隔离,协调能力保持 manual。
必须记录目标、条目、执行者、源 HEAD、依赖、顺序、尝试、最终 base/head、组件清单、
检查定义及相关配置、实际 CI 链接/身份、Review 对象、真实合入结果和暂停/恢复原因。
@@ -34,15 +34,14 @@ Dev 目标身份使用实际托管实例、仓库 ID 与 `refs/heads/main`;其
合入 SHA 和清理结果在实际发生后回填,不能提前将未来合入或清理标为已完成。
闭合 PR 进入同一队列;可在授权范围内批量闭合已合入任务,保留每个真实主 PR 和结果。
## 拟替换的启动与同步要求
## 启动与同步要求
正式采纳时须一起修订根 AGENTS.md、`policy/governance.md` 与 `scripts/task.sh` 的对应入口;
不能仅新增本文就宣布以下能力有效。
根 AGENTS.md、`policy/governance.md` 与 `scripts/task.sh` 的联合入口已随 #519 合入。启用状态仍须核对 #518;新任务严格 READY,既有任务只在明确选择 local 时允许已知落后,正式交付仍真实同步。
| 场景 | 拟采用动作 | 当前实现边界 |
|---|---|---|
| 新任务启动 | 保留唯一 precheck 收据与 `task-start`,从最新权威 main 建独立工作区 | 现有命令保留 |
| 恢复落后任务的局部工作 | 核对现场、授权和落后状态,记录待最终同步;允许已授权局部编辑 | 已实现显式 local/LOCAL_READY 及 task-resume;默认仍严格,流程待采纳启用 |
| 恢复落后任务的局部工作 | 核对现场、授权和落后状态,记录待最终同步;允许已授权局部编辑 | 已实现显式 local/LOCAL_READY 及 task-resume;默认仍严格,本轮在授权试行范围启用 |
| 其他任务合入、排队等待 | 更新落后/依赖提示,不自动同步各分支或触发完整 CI | 无自动调度能力,不能宣称已接线 |
| 领取最终交付窗口 | 成功 fetch,运行 `make task-sync`,实际合并最新 main、处置冲突、物化固定组件 | 使用真实同步及祖先检查,保留脏状态/远端失败拒绝 |
| 窗口内再次到达验收、审核、合入节点 | 再查最新目标和候选;未变只核验,不创建元数据提交 | 已实现同基线不提交,保留组件核对和完整最终 SHA |
@@ -56,7 +55,7 @@ SYNC_REQUIRED 不整体降级为 READY;local 与 delivery 用独立结果和
仍产生可复核的实际输出。任务真正需要纳入新目标时保留 merge 与对应对象记录。
所有候选身份使用必要记录提交完成后的最终 HEAD,不能把记录提交前的 SHA 当作最终被测对象。
## 拟调整的交付阶段
## 交付阶段与窗口
人工评审和独立 Review 要求不变;提前审阅可以并行,最终有效性核验和受影响重放在窗口内完成。
入队与领取不构成人工批准,也不授权跳过缺失阶段。
@@ -75,7 +74,7 @@ SYNC_REQUIRED 不整体降级为 READY;local 与 delivery 用独立结果和
## CI 资源与检查
本草案只定义后续分配要求;`.gitea/workflows/ci.yaml` 当前行为保持原样。
首版由协调者安排固定候选的顺序;`.gitea/workflows/ci.yaml` 当前行为保持原样。
后续将局部反馈与最终准入的触发和资源安排区分,不通过跳过必需作业返回绿色来节省成本。
当前 workflow 中的 `test`、`test-pg`、`svc-realcombo` 等作业须按最终已批准集合核验;
平台状态上下文的完整名称、运行身份和被测 SHA 从当次实际运行核对,不从历史文档直接照抄。
@@ -86,7 +85,7 @@ SYNC_REQUIRED 不整体降级为 READY;local 与 delivery 用独立结果和
- 最大无进展时间 20 分钟,单次窗口预算 90 分钟;有效进展、延长及原因记入尝试。
- 已知基础设施失败最多额外重试一次;代码失败直接退出整改,不自动重复跑完整 CI。
- 等待队列每 30 分钟复查一次;最终准入优先,但不无限延后其他目标、main 验证或发布检查。
- 全局 runner 并发预算在试行前读取实际容量后登记;未核验容量时按现有资源安排,不凭建议数值调整平台。
- 当场读取 home-runner 配置 capacity=12、timeout=3h(SELF);试行每个目标最多一个最终候选,候选内三作业按现有 runner 安排。配置容量不证明抢占/优先级或可用空槽,不调整全局平台。
预算是试行参数,长期值由真实耗时和等待情况复盘决定。
容量不足时先等待 runner,再领取目标窗口;不得持有另一个仓库窗口去等待组件窗口。
@@ -106,7 +105,7 @@ SYNC_REQUIRED 不整体降级为 READY;local 与 delivery 用独立结果和
需要调整平台保护时,由 Agent 准备具体配置、回退和隔离验证命令,用户按根 AGENTS.md 双钥匙要求确认并亲手执行。
至少验证合法候选可合入,以及缺检查、失败检查、陈旧候选和无权限推进被拒。
人工首版验证旧执行者确认退出后交接及未知结果对账;自动接管启用前另须验证实际合入入口拒绝旧代次。
当前不运行这些平台试验,也不影响其他进行中任务。
本轮隔离机制实测已完成:缺检查/受控失败、陈旧绿色候选、匿名请求及已认证绿色候选直推被拒,合法固定候选由智能体合入;原始 SELF 收据与独立 REPLAY 身份见 #518。夹具 CI 只验证机制,不替代 Dev 的完整业务 CI;所有认证角色、自动互斥和旧代次 fencing 未验证,也不宣称具备。
## 采纳顺序与停止条件
@@ -131,7 +130,7 @@ SYNC_REQUIRED 不整体降级为 READY;local 与 delivery 用独立结果和
| MQ5 | 冲突、代码失败、等待人工决定超时释放窗口,不阻塞无依赖任务 | NOT_RUN(后续试行) |
| MQ6 | 紧急插队有依据,受影响候选失效;跨仓依赖和组件能力不回退 | NOT_RUN(后续试行) |
| MQ7 | 请求超时、协调者退出先对账;不能仅因租约过期让旧新执行者同时合入 | NOT_RUN(后续恢复验证) |
| MQ8 | 平台拒绝未满足准入者,合法固定候选可合入;manual 能力如实登记,自动接管另验证旧代次拒绝 | NOT_RUN(所有者操作后验证) |
| MQ8 | 平台拒绝未满足准入者,合法固定候选可合入;manual 能力如实登记,自动接管另验证旧代次拒绝 | 隔离机制已实测并独立重放;Dev 精确 CI 另核验。manual,所有角色及自动 fencing NOT_RUN |
| MQ9 | 文档链接、入口、版本、单一来源、状态与模板字段一致 | 本轮执行;原始输出见任务卡 SELF 收据 |
这些验收 ID 用于实施与验收追溯;本分支已完成可独立实施的 CLI/证据基础、规则与模板准备及临时真实 Git 场景自检。固定规范采纳、实际模式切换、平台正负例和真实 CI/三任务试行仍待各自门禁,未执行项保持 NOT_RUN;当前进展与原始证据统一见任务卡。
这些验收 ID 用于实施与验收追溯;本分支已完成可独立实施的 CLI/证据基础、规则与模板准备及临时真实 Git 场景自检。固定规范已采纳,平台机制实测已独立重放,当前执行 manual 受控试行;真实三任务及复盘仍待完成,未执行项保持 NOT_RUN。动态进展以 #518 和各任务/PR 为准,原始命令由工具收据保存。
+1 -1
View File
@@ -16,7 +16,7 @@
| A7 证据 | 本地 SELF 收据;外部来源独立核验 | 首次捕获、完整脱敏输出/摘要、对象记录、原样导入 | `tests/test_evidence.py`;CI/审阅链接另核 |
| A8 阶段评审与迭代 | manual(人工/Agent 执行);前序人工决策,智能体 Review/合入;模板和提示不构成批准身份校验或全流程阻断 | policy/delivery.md;任务卡分别记录阶段结论、Review、冲突、当前 HEAD 的 CI 和合入;post-merge 默认仅提示 | `tests/test_task.py` 核对模板及钩子不自动写入;智能体核对 Review、冲突及匹配 HEAD 的 CI/平台准入;人工批准范围仍须核验 |
| A9 长期方向与后续任务 | manual;模板不校验规划有效性或自动调度任务 | policy/delivery.md;PROJECT.md 提供规划入口,任务卡关联规划/里程碑并记录下一步建议 | `tests/test_task.py` 核对生成字段与未授权默认值;人工核对当前规划、进展、重复任务和建议依据 |
| A10 交付窗口 | manual(待启用);无自动互斥或接管 | policy/merge-queue.md;Issue #518 与协调者已登记,本候选固定实际合入规范版本;Dev 组合验收、平台正负例及全部写入者覆盖待完成 | tests/test_merge_queue_trial.py 临时 Git 场景 SELF;平台/真实 CI 三任务试行 NOT_RUN |
| A10 交付窗口 | manual 受控试行;无自动互斥或接管 | policy/merge-queue.md;Issue #518 与协调者已登记,本候选固定实际合入规范版本;用户授权本轮三个任务并指定单一协调者;已实测平台机制及当前写者清单,当前候选组合与最终准入另核验 | tests/test_merge_queue_trial.py 临时 Git 场景 SELF;平台机制独立 REPLAY 已完成(范围见 Issue518);真实三任务及复盘待完成 |
| A11 固定后证据 | manual 选择源码外位置;工具捕获绝对未用路径/原始输出;无 OS 冻结 | policy/governance.md;RECEIPT 捕获,后续授权闭合再原样导入 | `tests/test_evidence.py` 固定对象、权限、覆盖拒绝、原样导入与幂等 |
## 失败处理
+46 -16
View File
@@ -16,6 +16,7 @@
# RDM_REPO RDM 仓路径(默认 <本仓上级>/Agent-System-RDM)
# E2E_API_PORT/E2E_UI_PORT 服务端口(默认 18791/4173)
# E2E_OWNER/E2E_PASSWORD 登录账号(默认 e2e-auditor/E2eAudit#2026)
# E2E_PASSWORD2 首次登录后的设密结果(默认 E2eAudit#2026b;种子命令用它)
# E2E_PROJECT_MAIN/E2E_PROJECT_ISOLATED 项目名(默认 审计演示项目/隔离检查项目)
# E2E_OUT 工件目录(默认 .tmp/e2e/<ts>;.gitignore 已覆盖 .tmp)
# 依赖:go、node/npm、curl;首次运行会 npx playwright install chromium。
@@ -32,6 +33,7 @@ API_PORT=${E2E_API_PORT:-18791}
UI_PORT=${E2E_UI_PORT:-4173}
OWNER=${E2E_OWNER:-e2e-auditor}
PASSWORD=${E2E_PASSWORD:-E2eAudit#2026}
PASSWORD2=${E2E_PASSWORD2:-E2eAudit#2026b}
PROJ_MAIN=${E2E_PROJECT_MAIN:-审计演示项目}
PROJ_ISO=${E2E_PROJECT_ISOLATED:-隔离检查项目}
API_BASE="http://127.0.0.1:$API_PORT"
@@ -46,7 +48,8 @@ die() { printf '\033[31m[e2e][FAIL]\033[0m %s\n' "$*" >&2; exit 1; }
command -v go >/dev/null || die "go not found"
command -v node >/dev/null || die "node not found"
command -v curl >/dev/null || die "curl not found"
[ -d "$RDM_REPO/.git" ] || die "RDM repo not found at $RDM_REPO (set RDM_REPO)"
[ "$(git -C "$RDM_REPO" rev-parse --is-inside-work-tree 2>/dev/null)" = true ] \
|| die "RDM repo not found at $RDM_REPO (set RDM_REPO)"
for p in "$API_PORT" "$UI_PORT"; do
if timeout 2 bash -c "echo > /dev/tcp/127.0.0.1/$p" 2>/dev/null; then
@@ -73,12 +76,12 @@ PIN_SHA_WANT=$(git -C "$RDM_REPO" ls-tree "$RDM_SHA" pins/agent-system-backend |
mkdir -p "$RDM_WT/pins"
tar -C "$RDM_REPO/pins" --exclude=.git -cf - agent-system-backend | tar -C "$RDM_WT/pins" -xf -
cleanup() {
[ -n "${API_PID:-}" ] && kill "$API_PID" 2>/dev/null || true
# npx→node 的进程树用单 PID 杀不净:按命令行匹配兜底(本脚本专用端口)。
[ -n "${UI_PID:-}" ] && kill "$UI_PID" 2>/dev/null || true
pkill -f "vite preview --port $UI_PORT" 2>/dev/null || true
pkill -f "agent-system-rdm -data.*e2e" 2>/dev/null || true
pkill -f "agent-system serve .*$API_PORT" 2>/dev/null || true
[ -n "${API_PID:-}" ] && { kill "$API_PID" 2>/dev/null || true; wait "$API_PID" 2>/dev/null || true; }
# Preview runs directly as one owned Vite process. Verify its command before
# signaling: a recycled PID must never stop another worktree's process.
if [ -n "${UI_PID:-}" ] && ps -p "$UI_PID" -o args= 2>/dev/null | rg -Fq -- "vite preview --port $UI_PORT"; then
kill "$UI_PID" 2>/dev/null || true
fi
}
trap cleanup EXIT
(cd "$RDM_WT" && go build -o "$OUT/agent-system-rdm" .) || die "rdm build failed"
@@ -122,9 +125,25 @@ stop_serve() {
[ -n "${API_PID:-}" ] && { kill "$API_PID" 2>/dev/null || true; wait "$API_PID" 2>/dev/null || true; API_PID=""; }
}
login() {
curl -sf -c "$JAR" -X POST "$API_BASE/api/auth/login" \
local i
for i in $(seq 1 20); do
if curl -sf -c "$JAR" -X POST "$API_BASE/api/auth/login" \
-H 'Content-Type: application/json' \
-d "{\"owner\":\"$OWNER\",\"password\":\"$PASSWORD\"}" >/dev/null; then
return 0
fi
sleep 0.25
done
die "login failed"
}
# CLI 建号留下 must_change_password。会话在改密前不能调用业务命令。
# 强制设密会作废当前会话,所以设密后用新密码再登录。
finish_password_gate() {
curl -sf -b "$JAR" -c "$JAR" -X POST "$API_BASE/api/auth/password" \
-H 'Content-Type: application/json' \
-d "{\"owner\":\"$OWNER\",\"password\":\"$PASSWORD\"}" >/dev/null || die "login failed"
-d "{\"next\":\"$PASSWORD2\"}" >/dev/null || die "password setup failed"
PASSWORD=$PASSWORD2
login
}
cmd() { # cmd <id> <params-json> → payload(失败即死,种子步骤不容忍部分成功)
@@ -139,11 +158,19 @@ cmd() { # cmd <id> <params-json> → payload(失败即死,种子步骤不容
start_serve
JAR="$OUT/cookies.txt"
login
finish_password_gate
log "seed: create projects"
PID_MAIN=$(cmd rdm.project.create "{\"name\":\"$PROJ_MAIN\"}" | python3 -c 'import json,sys; print(json.load(sys.stdin)["projectId"])')
PID_ISO=$(cmd rdm.project.create "{\"name\":\"$PROJ_ISO\"}" | python3 -c 'import json,sys; print(json.load(sys.stdin)["projectId"])')
log "seed: projects main=$PID_MAIN iso=$PID_ISO"
# I1 requires a current business grant even for a project creator or system
# administrator. Both projects are ordinary test fixtures in this private data root.
log "seed: grant explicit audit.read for the fixture owner"
cmd rdm.perm.cap '{"action":"audit.read","allow":true}' >/dev/null
cmd rdm.perm.set "{\"projectId\":\"$PID_MAIN\",\"member\":\"$OWNER\",\"action\":\"audit.read\",\"allow\":true}" >/dev/null
cmd rdm.perm.set "{\"projectId\":\"$PID_ISO\",\"member\":\"$OWNER\",\"action\":\"audit.read\",\"allow\":true}" >/dev/null
log "seed: import requirement ledger into $PID_MAIN"
cmd rdm.reqs.import "{\"projectId\":\"$PID_MAIN\",\"items\":$(
python3 - <<'PY'
@@ -196,11 +223,12 @@ if [ "$SEED_ONLY" = 1 ]; then
fi
log "start vite preview at http://127.0.0.1:$UI_PORT"
(cd "$DEV_ROOT/modules/frontend" && \
PLAYWRIGHT_API_TARGET="$API_BASE" npx vite preview --port "$UI_PORT" --strictPort \
>"$OUT/preview.log" 2>&1 &
echo $! > "$OUT/preview.pid")
UI_PID=$(cat "$OUT/preview.pid")
(
cd "$DEV_ROOT/modules/frontend"
export PLAYWRIGHT_API_TARGET="$API_BASE"
exec ./node_modules/.bin/vite preview --port "$UI_PORT" --strictPort
) >"$OUT/preview.log" 2>&1 &
UI_PID=$!
for i in $(seq 1 40); do
curl -sf -o /dev/null "http://127.0.0.1:$UI_PORT/" && break
sleep 0.25
@@ -212,10 +240,12 @@ log "playwright (install chromium if missing)"
log "run specs"
cd "$DEV_ROOT/modules/frontend"
export PLAYWRIGHT_BASE_URL="http://127.0.0.1:$UI_PORT" E2E_OWNER="$OWNER" E2E_PASSWORD="$PASSWORD" \
export PLAYWRIGHT_BASE_URL="http://127.0.0.1:$UI_PORT" E2E_OWNER="$OWNER" \
E2E_PASSWORD="${E2E_PASSWORD:-E2eAudit#2026}" E2E_PASSWORD2="$PASSWORD2" \
E2E_PROJECT_MAIN="$PROJ_MAIN" E2E_PROJECT_ISOLATED="$PROJ_ISO"
set +e
npx playwright test --reporter=list >"$OUT/playwright.log" 2>&1
# 本脚本的种子只覆盖审计工作区。u7 现场验收用例依赖另一套入口,不在这里跑。
npx playwright test e2e/rdm-audit.playwright.ts --reporter=list >"$OUT/playwright.log" 2>&1
PW_EXIT=$?
set -e
cat "$OUT/playwright.log"
+4 -2
View File
@@ -57,7 +57,9 @@ cmd_precheck() {
pb=$(card_of_branch "$pc"); [ -n "$pb" ] || continue
pst=$(card_status "$pc"); case "$pst" in 已完成*) continue ;; esac
card_pending "${pc##*/}" && continue
if git log --merges --format='%s' "origin/${TARGET_BRANCH}" 2>/dev/null | grep -qF " from $pb into ${TARGET_BRANCH}"; then
# Consume the complete producer output: grep -q exits on the first match,
# making git log die with SIGPIPE (141 under pipefail) on long histories.
if git log --merges --format='%s' "origin/${TARGET_BRANCH}" 2>/dev/null | grep -F " from $pb into ${TARGET_BRANCH}" >/dev/null; then
open_cards="${open_cards} ${pc##*/}"
fi
done
@@ -379,7 +381,7 @@ card_of_branch() { sed -n 's/^- 工作区:.*分支:\([A-Za-z0-9._/-]\+\).*/\
card_status() { sed -n 's/^.*;状态:\([^;]*\).*/\1/p' "$1" | head -1; }
card_merge() { # $1=branch → "sha|subject"(该分支合入 origin/<target> 的合并提交)
git log --merges --format='%H|%s' "origin/${TARGET_BRANCH}" 2>/dev/null \
| grep -F " from $1 into ${TARGET_BRANCH}" | head -1
| grep -F " from $1 into ${TARGET_BRANCH}" | sed -n '1p'
}
card_pending() { # $1=卡片文件名——闭合是否已在某 origin/feature/card-close-* 分支待合入
local cb
+29
View File
@@ -758,6 +758,35 @@ class TaskLifecycleTests(unittest.TestCase):
self.git(publisher, "push", "origin", "main")
return self.git(publisher, "rev-parse", "HEAD").stdout.strip(), card.read_text()
def test_long_merge_history_keeps_merged_card_visible_and_closable(self):
# A recent match makes grep -q/head exit before git log has finished
# writing a large history. With pipefail, its SIGPIPE used to hide the
# card from precheck and leave task-close with nothing to archive.
base = self.git(self.repo, "rev-parse", "HEAD").stdout.strip()
tree = self.git(self.repo, "rev-parse", "HEAD^{tree}").stdout.strip()
side = self.git(self.repo, "commit-tree", tree, "-p", base,
"-m", "Fixture side commit").stdout.strip()
tip = base
for i in range(220):
tip = self.git(self.repo, "commit-tree", tree, "-p", tip, "-p", side,
"-m", f"Older fixture merge {i} " + "x" * 512).stdout.strip()
self.git(self.repo, "update-ref", "refs/heads/main", tip)
self.git(self.repo, "push", "origin", "main")
actual_merge, _ = self.publish_merged_task()
self.git(self.repo, "fetch", "origin")
self.git(self.repo, "merge", "--ff-only", "origin/main")
precheck = self.task(self.repo, "precheck")
self.assertIn("[cards] 已合入未闭合任务卡:feature-merged.md", precheck.stdout)
closed = self.task(self.repo, "close")
self.assertIn("[closed] 1 张已合入卡片", closed.stdout)
refs = self.git(self.repo, "for-each-ref", "--format=%(refname:short)",
"refs/heads/feature/card-close-*").stdout.splitlines()
self.assertEqual(len(refs), 1)
archived = self.git(self.repo, "show", f"{refs[0]}:.task/archive/feature-merged.md").stdout
self.assertIn("状态:已完成(回填)", archived)
self.assertIn(actual_merge, archived)
def assert_post_merge_only_reminds_owner(self, operation):
expected_head, expected_card = self.publish_merged_task()
self.git(self.repo, "fetch", "origin")