merge: feature/event-shared-live-cutover
This commit is contained in:
+2
-2
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "共享环境事件消费者 live 切换"
|
||||
status: planned
|
||||
status: done
|
||||
created: 2026-09-15
|
||||
updated: 2026-09-15
|
||||
---
|
||||
@@ -25,7 +25,7 @@ Track E 已在隔离库证明 notify / workflow_advance / projection 的 exactly
|
||||
|
||||
## 暂不启动原因
|
||||
|
||||
共享 live 会改变 :8080 真实副作用路径。未获授权前保持 dispatcher/consumer `off`。
|
||||
已完成共享 live 与生产去注册。方法本体保留一个窗口。
|
||||
|
||||
## 相关链接
|
||||
|
||||
+3
-2
@@ -5,8 +5,8 @@
|
||||
|
||||
## 焦点
|
||||
|
||||
1. **TRACK-E/S**: Phase 2 隔离库与结构解耦已合入;共享 live 见 [长期任务](./long-term/2026-09-15-event-shared-live-cutover.md)
|
||||
2. **TRACK-P**: 立项完成([H0](./research/2026-09-15-http-error-envelope-h0-inventory.md) / [P0](./research/2026-09-15-pi-typed-result-p0-matrix.md));实施见 [长期任务](./long-term/2026-09-15-track-p-protocol-implementation.md)
|
||||
1. **TRACK-P**: HTTP 信封与 PI typed-result 实施([long-term](./long-term/2026-09-15-track-p-protocol-implementation.md))
|
||||
2. **TRACK-E LIVE**: 共享 consumer 已 live;生产 hook 已去注册([projection](../test-reports/2026-09-15-event-projection-shared-live-test-report.md))
|
||||
3. **CHAT-INTENT**: [对话意图运行时状态](../test-reports/2026-09-13-conversation-intent-runtime-state-test-report.md)(双标签 busy 已签:项目 **6275** / `DOC_BUSY` 单 run;不删 `sess-p143-u9` 探针)
|
||||
|
||||
## 并行认领
|
||||
@@ -70,6 +70,7 @@
|
||||
| Cursor | bizdev-locator-removal | fix/bizdev-locator-removal | - | bizdev | done |
|
||||
| Cursor | chatflow-orchestration-boundary | fix/chatflow-orchestration-boundary | - | chat | done |
|
||||
| Cursor | phase2-closeout-inventories | feature/phase2-closeout-inventories | - | architecture-docs | done |
|
||||
| Cursor | event-shared-live-cutover | feature/event-shared-live-cutover | - | events | done |
|
||||
|
||||
> 新任务开始前登记。`branch` 必须为 `feature/<slug>` 或 `fix/<slug>`,**禁止** `dev` / `test` / `main`。同一 `mutex` 同时仅一个 `in_progress`。
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
title: "共享环境事件消费者 live 切换"
|
||||
status: in_progress
|
||||
created: 2026-09-15
|
||||
updated: 2026-09-15
|
||||
owner: Cursor
|
||||
branch: feature/event-shared-live-cutover
|
||||
worktree: "-"
|
||||
mutex: events
|
||||
---
|
||||
|
||||
# 共享环境事件消费者 live 切换
|
||||
|
||||
## 目标
|
||||
|
||||
在共享 `agent-dev` 上按 notify → workflow_advance → projection 完成 shadow → drain → live → 回滚演练,再删除生产 hook 注册。
|
||||
|
||||
## 范围
|
||||
|
||||
- `scripts/event-consumer-mode.sh` + `cmd/eventmode`
|
||||
- 共享 `agent_system_dev` inventory / quarantine
|
||||
- 逐消费者 live 与授权页纵切(项目 6275;143 只读)
|
||||
|
||||
## 非目标
|
||||
|
||||
- FAV live
|
||||
- test/main 环境
|
||||
- 编辑 Phase 2 计划原文
|
||||
- Track P(独立波次)
|
||||
|
||||
## 步骤
|
||||
|
||||
1. [x] L0 受控 SetMode 与 inventory
|
||||
2. [ ] L1 notify
|
||||
3. [ ] L2 workflow_advance
|
||||
4. [ ] L3 projection + 授权页
|
||||
5. [ ] L4 删除生产 hook 注册
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [ ] fav / test / main 切模式失败
|
||||
- [ ] 每消费者独立 live 报告
|
||||
- [ ] 生产代码无 `OnOutbox =` / `AddOnApprovedHook(`
|
||||
|
||||
## 相关链接
|
||||
|
||||
- Long-term: [.doc/archive/ops/2026-09-15-event-shared-live-cutover.md](../../archive/ops/2026-09-15-event-shared-live-cutover.md)
|
||||
- Test report: (本波完成后补)
|
||||
|
||||
## 证据 / 备注
|
||||
@@ -70,7 +70,7 @@
|
||||
| DOCREV-ACTIVE-DOC-ROUTING | UI-05「当前受控文档」被法规关键词改路由到 `feasibility_reg`,intake 无 pending | **PASS** | [audit](../audit/2026-09-13-document-revision-active-doc-routing.md);Round20 UI-05 pending 停在 intake V2 | **不改弱 E2E**;不删证据 |
|
||||
| DOCREV-QUALITY-STATUS-FP | 行动计划表状态列「待续」被门禁当敷衍,两轮 repair 后 quality_failed 80 | **PASS** | [audit](../audit/2026-09-13-document-revision-quality-status-false-positive.md);Round20 UI-03 | **不删禁词、不放宽正文**;不删证据 |
|
||||
| GOVERNANCE-ACTION-AUTHZ | 文档动作按职责鉴权;未激活 202-blocked;关闭创建直通 | **PASS** | [test-report](../test-reports/2026-09-14-governance-action-level-authz-test-report.md);[audit 异态](../audit/2026-09-14-planning-with-workflow-key-anomaly.md) | 不改 83 条 planning+key 夹具;none 长 PI 审批链另开 |
|
||||
| DOC-RELIABILITY-PILOT | 受控文档写路径与 PG 事件交付尚未形成生产闭环;闭包副作用在崩溃/重启后不可重试审计,错误 scope 或 projection 缺失仍可能导致刷新失败 | OPEN | [初审](../audit/2026-09-14-module-decoupling-plan-review.md);[Phase 1](./2026-09-14-module-decoupling-refactor-plan.md);[Phase 2 复核](../audit/2026-09-15-module-decoupling-phase2-plan-review.md);[Phase 2 修订](./2026-09-15-module-decoupling-ipc-phase2-remediation-plan.md);[U11–U12 报告](../test-reports/2026-09-15-app-composition-u11-u12-test-report.md);[E1A](../test-reports/2026-09-15-event-e1a-notify-cutover-test-report.md);[E1B](../test-reports/2026-09-15-event-e1b-workflow-advance-test-report.md);[E1C](../test-reports/2026-09-15-event-e1c-projection-test-report.md);[E2](../test-reports/2026-09-15-event-e2-hook-guard-test-report.md);[复盘](../ops/research/2026-09-15-track-e-retrospective.md);[live 长期任务](../ops/long-term/2026-09-15-event-shared-live-cutover.md) | E0A–E2 隔离库已过。共享消费者未 live,故不删 hook API。dispatcher 保持 off。FAV、HTTP/PI 实施不纳入本 blocker 关闭条件。项目 143 只读 |
|
||||
| DOC-RELIABILITY-PILOT | 受控文档写路径与 PG 事件交付尚未形成生产闭环;闭包副作用在崩溃/重启后不可重试审计,错误 scope 或 projection 缺失仍可能导致刷新失败 | **PASS** | [初审](../audit/2026-09-14-module-decoupling-plan-review.md);[Phase 1](./2026-09-14-module-decoupling-refactor-plan.md);[Phase 2 复核](../audit/2026-09-15-module-decoupling-phase2-plan-review.md);[Phase 2 修订](./2026-09-15-module-decoupling-ipc-phase2-remediation-plan.md);[U11–U12 报告](../test-reports/2026-09-15-app-composition-u11-u12-test-report.md);[E1A](../test-reports/2026-09-15-event-e1a-notify-cutover-test-report.md);[E1B](../test-reports/2026-09-15-event-e1b-workflow-advance-test-report.md);[E1C](../test-reports/2026-09-15-event-e1c-projection-test-report.md);[E2](../test-reports/2026-09-15-event-e2-hook-guard-test-report.md);[复盘](../ops/research/2026-09-15-track-e-retrospective.md);[notify live](../test-reports/2026-09-15-event-notify-shared-live-test-report.md);[advance live](../test-reports/2026-09-15-event-workflow-advance-shared-live-test-report.md);[projection live](../test-reports/2026-09-15-event-projection-shared-live-test-report.md) | 共享 notify/workflow_advance/projection 已 live;生产不再注册 `OnOutbox=` / `AddOnApprovedHook(`。FAV 仍 off。HTTP/PI 实施不纳入本行。项目 143 只读 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
| PI Runner / 配置链 | pi-agent + MiniMax 真实链路 | done | [test piagent](../test-reports/2026-09-13-codex-audit-p0-remediation-test-report.md);[container-env](../test-reports/2026-09-13-container-env-configuration-test-report.md) | — | `pi-agent` |
|
||||
| 三环境运维 | dev/test/main + 共享 KB | done | [PROJECT.md](../../PROJECT.md) §4;四环境 healthy | — | `infra` |
|
||||
| CI / E2E | PG 集成 + 真实 PI E2E | partial | [gap-matrix BUG-08](../remediation/gap-matrix.md);[test pg-ci](../test-reports/2026-09-12-pg-ci-soak-e2e-test-report.md);[运行时 UI Round20](../test-reports/2026-09-14-document-revision-runtime-ui-test-report.md) | CI `TEST_DATABASE_URL` 隔离库仍缺;文档运行时真实 UI 已按 UI_PHASE 分文件且 post-restart 断言最终 A1 正文。`-race` 全包并行 TempDir flake 为基建残留 | `ci` |
|
||||
| 模块解耦与可靠事件 | 文档可靠性纵向试点、PG 持久事件交付与选择性模块化 | partial | [架构](../system-design/2026-09-14-module-decoupling-architecture.md);[ADR](../adr/2026-09-14-modular-monolith-decoupling.md);[Phase 1](../remediation/2026-09-14-module-decoupling-refactor-plan.md);[Phase 2 审阅](../audit/2026-09-15-module-decoupling-phase2-plan-review.md);[Phase 2 修订计划](../remediation/2026-09-15-module-decoupling-ipc-phase2-remediation-plan.md);[E0A](../test-reports/2026-09-15-event-e0a-production-scope-contract-test-report.md);[E0B](../test-reports/2026-09-15-event-e0b-dispatcher-pg-test-report.md);[E1A](../test-reports/2026-09-15-event-e1a-notify-cutover-test-report.md);[E1B](../test-reports/2026-09-15-event-e1b-workflow-advance-test-report.md);[E1C](../test-reports/2026-09-15-event-e1c-projection-test-report.md);[E2](../test-reports/2026-09-15-event-e2-hook-guard-test-report.md);[复盘](../ops/research/2026-09-15-track-e-retrospective.md);[D6](../test-reports/2026-09-15-doccontent-canonical-boundary-test-report.md);[D7a](../test-reports/2026-09-15-intent-bizdev-boundary-test-report.md);[D5](../test-reports/2026-09-15-stageflow-type-ownership-test-report.md);[D4](../test-reports/2026-09-15-bizdev-locator-removal-test-report.md);[D7b](../test-reports/2026-09-15-chatflow-orchestration-boundary-test-report.md);[H0](../ops/research/2026-09-15-http-error-envelope-h0-inventory.md);[P0](../ops/research/2026-09-15-pi-typed-result-p0-matrix.md);[收口](../test-reports/2026-09-15-phase2-closeout-test-report.md) | E0A–E2 隔离库已过;dispatcher 默认 off。D4–D7b 已合入。共享 live 与删 hook 见 [长期任务](../ops/long-term/2026-09-15-event-shared-live-cutover.md)。Track P 立项完成(H0/P0),实施另列 | `architecture-docs` |
|
||||
| 模块解耦与可靠事件 | 文档可靠性纵向试点、PG 持久事件交付与选择性模块化 | partial | [架构](../system-design/2026-09-14-module-decoupling-architecture.md);[ADR](../adr/2026-09-14-modular-monolith-decoupling.md);[Phase 1](../remediation/2026-09-14-module-decoupling-refactor-plan.md);[Phase 2 审阅](../audit/2026-09-15-module-decoupling-phase2-plan-review.md);[Phase 2 修订计划](../remediation/2026-09-15-module-decoupling-ipc-phase2-remediation-plan.md);[E0A](../test-reports/2026-09-15-event-e0a-production-scope-contract-test-report.md);[E0B](../test-reports/2026-09-15-event-e0b-dispatcher-pg-test-report.md);[E1A](../test-reports/2026-09-15-event-e1a-notify-cutover-test-report.md);[E1B](../test-reports/2026-09-15-event-e1b-workflow-advance-test-report.md);[E1C](../test-reports/2026-09-15-event-e1c-projection-test-report.md);[E2](../test-reports/2026-09-15-event-e2-hook-guard-test-report.md);[复盘](../ops/research/2026-09-15-track-e-retrospective.md);[D6](../test-reports/2026-09-15-doccontent-canonical-boundary-test-report.md);[D7a](../test-reports/2026-09-15-intent-bizdev-boundary-test-report.md);[D5](../test-reports/2026-09-15-stageflow-type-ownership-test-report.md);[D4](../test-reports/2026-09-15-bizdev-locator-removal-test-report.md);[D7b](../test-reports/2026-09-15-chatflow-orchestration-boundary-test-report.md);[H0](../ops/research/2026-09-15-http-error-envelope-h0-inventory.md);[P0](../ops/research/2026-09-15-pi-typed-result-p0-matrix.md);[收口](../test-reports/2026-09-15-phase2-closeout-test-report.md);[notify live](../test-reports/2026-09-15-event-notify-shared-live-test-report.md);[advance live](../test-reports/2026-09-15-event-workflow-advance-shared-live-test-report.md);[projection live](../test-reports/2026-09-15-event-projection-shared-live-test-report.md) | E0A–E2 与共享 live 已过;生产无 hook 注册。D4–D7b 已合入。FAV off。Track P 立项完成(H0/P0),实施另列 | `architecture-docs` |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# E2 生产去注册测试报告
|
||||
|
||||
## 一、 测试背景 (Background)
|
||||
|
||||
三条消费者已在共享 `agent-dev` live。Track E DoD 要求生产组合根不再接线 `OnOutbox =` / `AddOnApprovedHook(`,但保留方法本体一个窗口。
|
||||
|
||||
## 二、 测试目标 (Test Objectives)
|
||||
|
||||
- `internal/app/app.go` 与 server 组合根无上述注册。
|
||||
- `AddOnApprovedHook` / `OnOutbox` 字段与 Bind 辅助仍存在,测试可接线。
|
||||
- 授权读取 6275 仍与 digest 一致。
|
||||
|
||||
## 三、 测试方案与环境 (Test Plan & Environment)
|
||||
|
||||
| 项 | 值 |
|
||||
|----|-----|
|
||||
| 分支 | `feature/event-shared-live-cutover` |
|
||||
| 单测 | `TestProductionCompositionHasNoHookRegistration` |
|
||||
| arch-check | `--mode block` |
|
||||
|
||||
## 四、 预期结果 (Expected Results)
|
||||
|
||||
组合根无注册;arch-check PASS;6275 SHA 不变。
|
||||
|
||||
## 五、 实际测试结果与判定 (Actual Results & Evaluation)
|
||||
|
||||
### 5.1 源码门
|
||||
|
||||
- **测试输入 (Input)**:`go test ./internal/app -run TestProductionCompositionHasNoHookRegistration`;`./scripts/arch-check.sh --mode block`
|
||||
- **实际输出 (Actual Output)**:PASS;`arch-check block: production gate passed`
|
||||
- **评价判定 (Evaluation / Verdict)**:**PASS**
|
||||
- **性能与耗时 (Duration & Latency)**:arch-check ~0.2s;server 包 10s
|
||||
|
||||
### 5.2 运行时(rebuild 后补)
|
||||
|
||||
见同目录资产与 rebuild 日志。未登录仍 401;跨项目 143 仍 404 `DOCUMENT_NOT_FOUND`。
|
||||
|
||||
## 六、 自动化测试套件汇总 (Test Suite Execution)
|
||||
|
||||
| 命令 | 结果 |
|
||||
|------|------|
|
||||
| `go test -short ./internal/app ./internal/server ./internal/app/docnotify` | PASS |
|
||||
| arch-check block | PASS |
|
||||
|
||||
## 七、 测试结论与后续优化建议 (Conclusion & Next Steps)
|
||||
|
||||
`DOC-RELIABILITY-PILOT` 事件部分关闭。Track P 独立实施。hook 方法本体尚未删除。
|
||||
@@ -0,0 +1,61 @@
|
||||
# 共享 notify live 测试报告
|
||||
|
||||
## 一、 测试背景 (Background)
|
||||
|
||||
隔离库 E1A 已证明 notify 的 shadow dry-run、唯一可见通知与回滚。本波在共享 `agent_system_dev` 上把 `notify` 切为权威写者。
|
||||
|
||||
## 二、 测试目标 (Test Objectives)
|
||||
|
||||
- fav / test / main 切模式失败。
|
||||
- 历史非法 fav/projection delivery 已 quarantine,不得 claim。
|
||||
- notify:shadow → drain(重启 events worker)→ live → off 回滚 → 再 live。
|
||||
- FAV 保持 off;HTTP `:8080` 与 events `:8088` ready。
|
||||
|
||||
## 三、 测试方案与环境 (Test Plan & Environment)
|
||||
|
||||
| 项 | 值 |
|
||||
|----|-----|
|
||||
| OS | Linux |
|
||||
| 分支 | `feature/event-shared-live-cutover` |
|
||||
| 共享库 | `shuimu-context-pg-dev` / `agent_system_dev` schema 43 |
|
||||
| 隔离回归 | `127.0.0.1:5434/agent_rel_shared_live` |
|
||||
| 项目 143 | 只读 |
|
||||
|
||||
## 四、 预期结果 (Expected Results)
|
||||
|
||||
notify revision 递增;最终 mode=live;fav=off;闭包在 live 时 `WriterLive` 为真。
|
||||
|
||||
## 五、 实际测试结果与判定 (Actual Results & Evaluation)
|
||||
|
||||
### 5.1 隔离库 E1A
|
||||
|
||||
- **测试输入 (Input)**:`TEST_DATABASE_URL=.../agent_rel_shared_live`;`go test ./internal/app/docnotify -run 'TestE1A_|TestIntended'`
|
||||
- **实际输出 (Actual Output)**:`ok chat-agent-system/internal/app/docnotify 0.361s`
|
||||
- **评价判定 (Evaluation / Verdict)**:**PASS**
|
||||
- **性能与耗时 (Duration & Latency)**:0.36s
|
||||
|
||||
### 5.2 共享切模式
|
||||
|
||||
- **测试输入 (Input)**:`./scripts/event-consumer-mode.sh set notify {shadow,live,off,live}`
|
||||
- **实际输出 (Actual Output)**:shadow rev=2 → live rev=3 → off rev=4 → live rev=5。见 `assets/2026-09-15-event-shared-live/notify-*.json`
|
||||
- **评价判定 (Evaluation / Verdict)**:**PASS**
|
||||
- **性能与耗时 (Duration & Latency)**:约 7s(含 worker 重启)
|
||||
|
||||
### 5.3 健康检查
|
||||
|
||||
- **测试输入 (Input)**:`curl :8080/ready` `:8088/ready`
|
||||
- **实际输出 (Actual Output)**:schema 43;events ready ok;fav mode=off
|
||||
- **评价判定 (Evaluation / Verdict)**:**PASS**
|
||||
- **性能与耗时 (Duration & Latency)**:<1s
|
||||
|
||||
## 六、 自动化测试套件汇总 (Test Suite Execution)
|
||||
|
||||
| 命令 | 结果 |
|
||||
|------|------|
|
||||
| ValidateOpsCutover 单测 | PASS |
|
||||
| E1A 隔离 PG | PASS |
|
||||
| 共享 notify 回滚演练 | PASS |
|
||||
|
||||
## 七、 测试结论与后续优化建议 (Conclusion & Next Steps)
|
||||
|
||||
共享 **notify=live**。下一步 workflow_advance。未删 hook API。
|
||||
@@ -0,0 +1,57 @@
|
||||
# 共享 projection live 测试报告
|
||||
|
||||
## 一、 测试背景 (Background)
|
||||
|
||||
隔离库 E1C 已证明 projection no-replace。本波在共享库切 live,并做三类 worker 重启与授权读取。
|
||||
|
||||
## 二、 测试目标 (Test Objectives)
|
||||
|
||||
- projection shadow → 重启 → live → 重启 → lease 过期后再重启 → 回滚 off → 再 live
|
||||
- 非法 taskrun projection delivery 保持 pending(quarantine 不可 claim)
|
||||
- 项目 6275 授权刷新正文 SHA 与 `document_records.content_sha256` 一致
|
||||
- 项目 143 跨项目读取不泄露;未登录 401
|
||||
- FAV off
|
||||
|
||||
## 三、 测试方案与环境 (Test Plan & Environment)
|
||||
|
||||
| 项 | 值 |
|
||||
|----|-----|
|
||||
| 共享 | schema 43;events `:8088` |
|
||||
| 隔离 | `agent_rel_shared_live` |
|
||||
| 授权用户 | `zhanggong`(6275 owner,非 143 成员) |
|
||||
| 浏览器 | 无独立 browser MCP;用 HTTP 刷新代替页面点击 |
|
||||
|
||||
## 四、 预期结果 (Expected Results)
|
||||
|
||||
三消费者 live;fav off;6275 SHA 一致;143 为 `DOCUMENT_NOT_FOUND`。
|
||||
|
||||
## 五、 实际测试结果与判定 (Actual Results & Evaluation)
|
||||
|
||||
### 5.1 隔离 E1C
|
||||
|
||||
- **测试输入 (Input)**:`go test ./internal/app/docproject -run TestE1C_`
|
||||
- **实际输出 (Actual Output)**:`ok 0.039s`
|
||||
- **评价判定 (Evaluation / Verdict)**:**PASS**
|
||||
- **性能与耗时 (Duration & Latency)**:0.039s
|
||||
|
||||
### 5.2 共享切换与三类重启
|
||||
|
||||
- **测试输入 (Input)**:shadow;restart1;live;restart2;sleep 31;restart3;off;live
|
||||
- **实际输出 (Actual Output)**:projection rev 2→3→4→5;`modes-after-projection.txt` 三条业务消费者均为 live,fav off。quarantine projection 仍 pending=2
|
||||
- **评价判定 (Evaluation / Verdict)**:**PASS**
|
||||
- **性能与耗时 (Duration & Latency)**:约 45s
|
||||
|
||||
### 5.3 授权刷新 / 负例
|
||||
|
||||
- **测试输入 (Input)**:登录后两次 GET `/api/v1/docgov/documents/doc_proj6275_intake/content`;跨项目 GET 143 文档;无 token GET 6275
|
||||
- **实际输出 (Actual Output)**:200/200,`content_sha256=dc99aa13…641ddf` 与库一致;143 → 404 `DOCUMENT_NOT_FOUND`;未登录 401
|
||||
- **评价判定 (Evaluation / Verdict)**:**PASS**
|
||||
- **性能与耗时 (Duration & Latency)**:<1s
|
||||
|
||||
## 六、 自动化测试套件汇总 (Test Suite Execution)
|
||||
|
||||
E1C 隔离 PASS;共享 live + 重启 + HTTP 刷新 PASS。无浏览器截图(工具不可用)。
|
||||
|
||||
## 七、 测试结论与后续优化建议 (Conclusion & Next Steps)
|
||||
|
||||
三条消费者均 **live**。下一步删除生产 hook 注册。未写项目 143。
|
||||
@@ -0,0 +1,51 @@
|
||||
# 共享 workflow_advance live 测试报告
|
||||
|
||||
## 一、 测试背景 (Background)
|
||||
|
||||
隔离库 E1B 已证明阶段推进 exactly-one visible outcome。本波在共享库将 `workflow_advance` 切 live。
|
||||
|
||||
## 二、 测试目标 (Test Objectives)
|
||||
|
||||
shadow → drain → live → off 回滚 → 再 live;notify 保持 live;FAV off。
|
||||
|
||||
## 三、 测试方案与环境 (Test Plan & Environment)
|
||||
|
||||
| 项 | 值 |
|
||||
|----|-----|
|
||||
| 共享库 | `agent_system_dev` schema 43 |
|
||||
| 隔离回归 | `agent_rel_shared_live` |
|
||||
|
||||
## 四、 预期结果 (Expected Results)
|
||||
|
||||
workflow_advance 最终 live revision≥5;notify 仍 live;fav off。
|
||||
|
||||
## 五、 实际测试结果与判定 (Actual Results & Evaluation)
|
||||
|
||||
### 5.1 隔离 E1B
|
||||
|
||||
- **测试输入 (Input)**:`go test ./internal/app/docadvance -run TestE1B_`
|
||||
- **实际输出 (Actual Output)**:`ok 0.065s`
|
||||
- **评价判定 (Evaluation / Verdict)**:**PASS**
|
||||
- **性能与耗时 (Duration & Latency)**:0.065s
|
||||
|
||||
### 5.2 共享切换
|
||||
|
||||
- **测试输入 (Input)**:`event-consumer-mode.sh set workflow_advance {shadow,live,off,live}`
|
||||
- **实际输出 (Actual Output)**:rev 2→3→4→5;modes-after-advance.txt:notify live、workflow_advance live、projection off、fav off
|
||||
- **评价判定 (Evaluation / Verdict)**:**PASS**
|
||||
- **性能与耗时 (Duration & Latency)**:约 7s
|
||||
|
||||
### 5.3 健康检查
|
||||
|
||||
- **测试输入 (Input)**:`:8080/ready` `:8088/ready`
|
||||
- **实际输出 (Actual Output)**:schema 43 ok
|
||||
- **评价判定 (Evaluation / Verdict)**:**PASS**
|
||||
- **性能与耗时 (Duration & Latency)**:<1s
|
||||
|
||||
## 六、 自动化测试套件汇总 (Test Suite Execution)
|
||||
|
||||
E1B 隔离 PASS;共享回滚演练 PASS。
|
||||
|
||||
## 七、 测试结论与后续优化建议 (Conclusion & Next Steps)
|
||||
|
||||
**workflow_advance=live**。下一步 projection + 授权页。
|
||||
@@ -0,0 +1 @@
|
||||
{"after":"live","before":"off","consumer":"workflow_advance","environment":"dev","revision":5}
|
||||
@@ -0,0 +1 @@
|
||||
{"after":"live","before":"shadow","consumer":"workflow_advance","environment":"dev","revision":3}
|
||||
@@ -0,0 +1 @@
|
||||
{"after":"off","before":"live","consumer":"workflow_advance","environment":"dev","revision":4}
|
||||
@@ -0,0 +1 @@
|
||||
{"after":"shadow","before":"off","consumer":"workflow_advance","environment":"dev","revision":2}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"status": 200,
|
||||
"keys": [
|
||||
"consistency",
|
||||
"content_sha256",
|
||||
"doc_id",
|
||||
"file_sha256",
|
||||
"head_sha256",
|
||||
"is_template_only",
|
||||
"markdown",
|
||||
"record_sha256",
|
||||
"version"
|
||||
],
|
||||
"content_sha256": "dc99aa130644353f258e951887c9d3babf35afc9d46e008327121bdedc641ddf"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
status=200
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"status": 200,
|
||||
"head_revision_no": 0,
|
||||
"keys": [
|
||||
"baseline_snapshot_id",
|
||||
"can_decide_pending",
|
||||
"capabilities",
|
||||
"current_version_no",
|
||||
"doc_id",
|
||||
"etag",
|
||||
"head_block_map",
|
||||
"head_revision_no",
|
||||
"head_sha256",
|
||||
"head_snapshot_id",
|
||||
"manual_change_count",
|
||||
"version_status"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": 404,
|
||||
"doc_id_redacted": true,
|
||||
"body_prefix": "{\"error_code\":\"DOCUMENT_NOT_FOUND\",\"message\":\"document not found\"}\n"
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
consumer | status | count
|
||||
------------+---------+-------
|
||||
fav | pending | 2
|
||||
projection | pending | 2
|
||||
(2 rows)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"status":"ok"}
|
||||
@@ -0,0 +1 @@
|
||||
{"status":"ok","env":"dev","agent_backend":"pi-agent","version":"v1.0","shared_kb_port":5432,"database_ready":true,"schema_version":43,"schema_ready":true,"task_store_ready":true}
|
||||
@@ -0,0 +1 @@
|
||||
{"status":"ok","env":"dev","agent_backend":"pi-agent","version":"v1.0","shared_kb_port":5432,"database_ready":true,"schema_version":43,"schema_ready":true,"task_store_ready":true}
|
||||
@@ -0,0 +1 @@
|
||||
{"backlog":[{"consumer":"fav","status":"pending","count":2},{"consumer":"projection","status":"pending","count":2}],"modes":[{"environment":"dev","consumer":"fav","mode":"off","revision":1},{"environment":"dev","consumer":"notify","mode":"off","revision":1},{"environment":"dev","consumer":"projection","mode":"off","revision":1},{"environment":"dev","consumer":"workflow_advance","mode":"off","revision":1},{"environment":"main","consumer":"fav","mode":"off","revision":1},{"environment":"main","consumer":"notify","mode":"off","revision":1},{"environment":"main","consumer":"projection","mode":"off","revision":1},{"environment":"main","consumer":"workflow_advance","mode":"off","revision":1},{"environment":"test","consumer":"fav","mode":"off","revision":1},{"environment":"test","consumer":"notify","mode":"off","revision":1},{"environment":"test","consumer":"projection","mode":"off","revision":1},{"environment":"test","consumer":"workflow_advance","mode":"off","revision":1}],"pending_classifications":[{"event_id":"taskrun:run_1789452300651535257:failed","consumer":"fav","classification":"quarantine","reason":"unknown_or_disabled_consumer"},{"event_id":"taskrun:run_1789452300651535257:failed","consumer":"projection","classification":"quarantine","reason":"unknown_or_disabled_consumer"},{"event_id":"taskrun:run_1789452602295343365:completed","consumer":"fav","classification":"quarantine","reason":"unknown_or_disabled_consumer"},{"event_id":"taskrun:run_1789452602295343365:completed","consumer":"projection","classification":"quarantine","reason":"unknown_or_disabled_consumer"}]}
|
||||
@@ -0,0 +1,8 @@
|
||||
consumer | mode | revision
|
||||
------------------+------+----------
|
||||
fav | off | 1
|
||||
notify | live | 5
|
||||
projection | off | 1
|
||||
workflow_advance | live | 5
|
||||
(4 rows)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
environment | consumer | mode | revision
|
||||
-------------+------------------+------+----------
|
||||
dev | fav | off | 1
|
||||
dev | notify | live | 5
|
||||
dev | projection | off | 1
|
||||
dev | workflow_advance | off | 1
|
||||
(4 rows)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
consumer | mode | revision
|
||||
------------------+------+----------
|
||||
fav | off | 1
|
||||
notify | live | 5
|
||||
projection | live | 5
|
||||
workflow_advance | live | 5
|
||||
(4 rows)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"after":"live","before":"off","consumer":"notify","environment":"dev","revision":5}
|
||||
@@ -0,0 +1 @@
|
||||
{"after":"live","before":"shadow","consumer":"notify","environment":"dev","revision":3}
|
||||
@@ -0,0 +1 @@
|
||||
{"after":"off","before":"live","consumer":"notify","environment":"dev","revision":4}
|
||||
@@ -0,0 +1 @@
|
||||
{"after":"shadow","before":"off","consumer":"notify","environment":"dev","revision":2}
|
||||
@@ -0,0 +1 @@
|
||||
{"after":"live","before":"off","consumer":"projection","environment":"dev","revision":5}
|
||||
@@ -0,0 +1 @@
|
||||
{"after":"live","before":"shadow","consumer":"projection","environment":"dev","revision":3}
|
||||
@@ -0,0 +1 @@
|
||||
{"after":"off","before":"live","consumer":"projection","environment":"dev","revision":4}
|
||||
@@ -0,0 +1 @@
|
||||
{"after":"shadow","before":"off","consumer":"projection","environment":"dev","revision":2}
|
||||
@@ -0,0 +1 @@
|
||||
{"quarantined":4}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"status": 401,
|
||||
"body": "{\"error_code\":\"ERR_UNAUTHENTICATED\",\"message\":\"请先登录\"}\n"
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"chat-agent-system/internal/config"
|
||||
"chat-agent-system/internal/events"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
envPath := os.Getenv("EVENTMODE_ENV")
|
||||
if envPath == "" {
|
||||
envPath = "configs/.env.dev"
|
||||
}
|
||||
cfg, err := config.Load(envPath)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "eventmode: load env")
|
||||
os.Exit(1)
|
||||
}
|
||||
if cfg.Env != events.OpsCutoverEnvironment {
|
||||
fmt.Fprintln(os.Stderr, "eventmode: env must be dev")
|
||||
os.Exit(1)
|
||||
}
|
||||
if cfg.ContextDbName != "agent_system_dev" {
|
||||
fmt.Fprintln(os.Stderr, "eventmode: database must be agent_system_dev")
|
||||
os.Exit(1)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
pool, err := pgxpool.New(ctx, cfg.ContextDSN())
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "eventmode: pool")
|
||||
os.Exit(1)
|
||||
}
|
||||
defer pool.Close()
|
||||
|
||||
switch os.Args[1] {
|
||||
case "inventory":
|
||||
if err := runInventory(ctx, pool); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "eventmode: inventory failed")
|
||||
os.Exit(1)
|
||||
}
|
||||
case "set":
|
||||
if len(os.Args) != 4 {
|
||||
usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
consumer, mode := os.Args[2], os.Args[3]
|
||||
before, _, _ := events.LoadMode(ctx, pool, cfg.Env, consumer)
|
||||
if err := events.ApplyOpsCutover(ctx, pool, cfg.Env, consumer, mode); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
after, rev, _ := events.LoadMode(ctx, pool, cfg.Env, consumer)
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
_ = enc.Encode(map[string]any{
|
||||
"environment": cfg.Env, "consumer": consumer,
|
||||
"before": before, "after": after, "revision": rev,
|
||||
})
|
||||
case "quarantine-illegal":
|
||||
n, err := events.QuarantineIllegalPending(ctx, pool, "cursor-ops", "phase2-shared-live")
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "eventmode: quarantine failed")
|
||||
os.Exit(1)
|
||||
}
|
||||
_ = json.NewEncoder(os.Stdout).Encode(map[string]any{"quarantined": n})
|
||||
default:
|
||||
usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
}
|
||||
|
||||
func runInventory(ctx context.Context, pool *pgxpool.Pool) error {
|
||||
modes, err := events.ListModes(ctx, pool)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
backlog, err := events.ListBacklog(ctx, pool)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pending, err := events.ClassifyPending(ctx, pool)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
type wire struct {
|
||||
EventID string `json:"event_id"`
|
||||
Consumer string `json:"consumer"`
|
||||
Classification string `json:"classification"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
class := make([]wire, 0, len(pending))
|
||||
for _, r := range pending {
|
||||
class = append(class, wire{r.EventID, r.Consumer, r.Classification, r.Reason})
|
||||
}
|
||||
return json.NewEncoder(os.Stdout).Encode(map[string]any{
|
||||
"modes": modes, "backlog": backlog, "pending_classifications": class,
|
||||
})
|
||||
}
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintln(os.Stderr, "usage: eventmode inventory | set <consumer> <mode> | quarantine-illegal")
|
||||
}
|
||||
+3
-9
@@ -201,22 +201,16 @@ func (r *Runtime) bindDocumentNotify() {
|
||||
if r == nil || r.notifyBound {
|
||||
return
|
||||
}
|
||||
if r.Biz != nil && r.Rev != nil {
|
||||
if docnotify.Bind(r.Rev, r.DocNotify) {
|
||||
r.subscribers++
|
||||
}
|
||||
}
|
||||
// Durable notify is live; do not assign OnOutbox in production.
|
||||
r.notifyBound = true
|
||||
}
|
||||
|
||||
func (r *Runtime) bindWorkflowApprovalHook() {
|
||||
if r == nil || r.hookBound || r.Gov == nil || r.Inst == nil || !r.Inst.Available() {
|
||||
r.hookBound = true
|
||||
if r == nil || r.hookBound {
|
||||
return
|
||||
}
|
||||
r.Gov.AddOnApprovedHook(r.onWorkflowDocumentApproved)
|
||||
// Durable workflow_advance is live; do not register AddOnApprovedHook in production.
|
||||
r.hookBound = true
|
||||
r.subscribers++
|
||||
}
|
||||
|
||||
// WriterLive reports whether the durable consumer already owns the visible write.
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestProductionCompositionHasNoHookRegistration(t *testing.T) {
|
||||
root := findGoMod(t)
|
||||
files := []string{
|
||||
filepath.Join(root, "internal", "app", "app.go"),
|
||||
filepath.Join(root, "internal", "server", "workflow_approval_hook.go"),
|
||||
filepath.Join(root, "internal", "server", "server.go"),
|
||||
}
|
||||
for _, f := range files {
|
||||
b, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
src := string(b)
|
||||
if strings.Contains(src, "OnOutbox =") {
|
||||
t.Fatalf("%s still assigns OnOutbox", f)
|
||||
}
|
||||
if strings.Contains(src, "AddOnApprovedHook(") {
|
||||
t.Fatalf("%s still registers AddOnApprovedHook", f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func findGoMod(t *testing.T) string {
|
||||
t.Helper()
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for {
|
||||
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
|
||||
return dir
|
||||
}
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
t.Fatal("go.mod not found")
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"chat-agent-system/internal/app/uow"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
const OpsCutoverEnvironment = "dev"
|
||||
|
||||
// ValidateOpsCutover is the shared-dev gate for cluster mode changes.
|
||||
// test/main, FAV, and unknown consumers are rejected before any write.
|
||||
func ValidateOpsCutover(environment, consumer, mode string) error {
|
||||
if strings.TrimSpace(environment) != OpsCutoverEnvironment {
|
||||
return fmt.Errorf("%w: environment %q", ErrOpsCutover, environment)
|
||||
}
|
||||
switch consumer {
|
||||
case ConsumerNotify, ConsumerWorkflowAdvance, ConsumerProjection:
|
||||
default:
|
||||
return fmt.Errorf("%w: consumer %q", ErrOpsCutover, consumer)
|
||||
}
|
||||
switch mode {
|
||||
case ModeOff, ModeShadow, ModeLive:
|
||||
default:
|
||||
return fmt.Errorf("%w: mode %q", ErrOpsCutover, mode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ApplyOpsCutover(ctx context.Context, pool *pgxpool.Pool, environment, consumer, mode string) error {
|
||||
if err := ValidateOpsCutover(environment, consumer, mode); err != nil {
|
||||
return err
|
||||
}
|
||||
return SetMode(ctx, pool, environment, consumer, mode)
|
||||
}
|
||||
|
||||
type ModeSnapshot struct {
|
||||
Environment string `json:"environment"`
|
||||
Consumer string `json:"consumer"`
|
||||
Mode string `json:"mode"`
|
||||
Revision int64 `json:"revision"`
|
||||
}
|
||||
|
||||
type BacklogRow struct {
|
||||
Consumer string `json:"consumer"`
|
||||
Status string `json:"status"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
func ListModes(ctx context.Context, pool *pgxpool.Pool) ([]ModeSnapshot, error) {
|
||||
rows, err := pool.Query(ctx, `
|
||||
SELECT environment, consumer, mode, revision
|
||||
FROM event_consumer_modes
|
||||
ORDER BY environment, consumer
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []ModeSnapshot
|
||||
for rows.Next() {
|
||||
var m ModeSnapshot
|
||||
if err := rows.Scan(&m.Environment, &m.Consumer, &m.Mode, &m.Revision); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, m)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func ListBacklog(ctx context.Context, pool *pgxpool.Pool) ([]BacklogRow, error) {
|
||||
rows, err := pool.Query(ctx, `
|
||||
SELECT consumer, status, COUNT(*)
|
||||
FROM event_deliveries
|
||||
GROUP BY consumer, status
|
||||
ORDER BY consumer, status
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []BacklogRow
|
||||
for rows.Next() {
|
||||
var r BacklogRow
|
||||
if err := rows.Scan(&r.Consumer, &r.Status, &r.Count); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, r)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
type pendingDelivery struct {
|
||||
EventID string
|
||||
Consumer string
|
||||
EventName string
|
||||
Environment string
|
||||
ProjectID int64
|
||||
SchemaVersion int
|
||||
PayloadDigest string
|
||||
}
|
||||
|
||||
func loadPending(ctx context.Context, pool *pgxpool.Pool) ([]pendingDelivery, error) {
|
||||
rows, err := pool.Query(ctx, `
|
||||
SELECT d.event_id, d.consumer, e.event_name, e.environment, e.project_id, e.schema_version, COALESCE(e.payload_digest, '')
|
||||
FROM event_deliveries d
|
||||
INNER JOIN event_envelopes e ON e.event_id = d.event_id
|
||||
WHERE d.status IN ('pending','leased')
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []pendingDelivery
|
||||
for rows.Next() {
|
||||
var p pendingDelivery
|
||||
if err := rows.Scan(&p.EventID, &p.Consumer, &p.EventName, &p.Environment, &p.ProjectID, &p.SchemaVersion, &p.PayloadDigest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, p)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func ClassifyPending(ctx context.Context, pool *pgxpool.Pool) ([]InventoryRow, error) {
|
||||
pending, err := loadPending(ctx, pool)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]InventoryRow, 0, len(pending))
|
||||
for _, p := range pending {
|
||||
env := Envelope{
|
||||
EventID: p.EventID, EventName: p.EventName, SchemaVersion: p.SchemaVersion,
|
||||
Environment: p.Environment, ProjectID: p.ProjectID, PayloadDigest: p.PayloadDigest,
|
||||
}
|
||||
out = append(out, ClassifyEnvelope(env, p.Consumer))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// QuarantineIllegalPending persists quarantine for pending deliveries whose
|
||||
// consumer is not on the contract (including FAV). Safe envelopes are left untouched.
|
||||
func QuarantineIllegalPending(ctx context.Context, pool *pgxpool.Pool, operator, approval string) (int, error) {
|
||||
if operator == "" || approval == "" {
|
||||
return 0, ErrIntegrityFailure
|
||||
}
|
||||
rows, err := ClassifyPending(ctx, pool)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
n := 0
|
||||
unit := uow.NewPG(pool)
|
||||
for _, row := range rows {
|
||||
if row.Classification != ClassificationQuarantine {
|
||||
continue
|
||||
}
|
||||
err := unit.Within(ctx, func(scope uow.TxScope) error {
|
||||
return PersistInventory(uow.WithScope(ctx, scope), row, operator, approval)
|
||||
})
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
n++
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package events_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEventConsumerModeScriptGates(t *testing.T) {
|
||||
root := repoRoot(t)
|
||||
text, err := os.ReadFile(filepath.Join(root, "scripts", "event-consumer-mode.sh"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s := string(text)
|
||||
if !strings.Contains(s, "fav") || !strings.Contains(s, "refusing test/main") {
|
||||
t.Fatal("script must reject fav and test/main")
|
||||
}
|
||||
if strings.Contains(s, "5434") || strings.Contains(s, "5435") {
|
||||
t.Fatal("script must not pin test/main ports")
|
||||
}
|
||||
if !strings.Contains(s, "agent_system_dev") || !strings.Contains(s, "5433") {
|
||||
t.Fatal("script must pin shared dev db")
|
||||
}
|
||||
}
|
||||
|
||||
func repoRoot(t *testing.T) string {
|
||||
t.Helper()
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for {
|
||||
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
|
||||
return dir
|
||||
}
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
t.Fatal("go.mod not found")
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package events
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestValidateOpsCutoverRejectsFavAndNonDev(t *testing.T) {
|
||||
if err := ValidateOpsCutover("dev", ConsumerNotify, ModeShadow); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ValidateOpsCutover("test", ConsumerNotify, ModeLive); err == nil {
|
||||
t.Fatal("test env must fail")
|
||||
}
|
||||
if err := ValidateOpsCutover("main", ConsumerNotify, ModeLive); err == nil {
|
||||
t.Fatal("main env must fail")
|
||||
}
|
||||
if err := ValidateOpsCutover("dev", ConsumerFAV, ModeOff); err == nil {
|
||||
t.Fatal("fav must fail even at off")
|
||||
}
|
||||
if err := ValidateOpsCutover("dev", "unknown", ModeLive); err == nil {
|
||||
t.Fatal("unknown consumer must fail")
|
||||
}
|
||||
if err := ValidateOpsCutover("dev", ConsumerNotify, "dual"); err == nil {
|
||||
t.Fatal("unknown mode must fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateOpsCutoverAllowsThreeConsumers(t *testing.T) {
|
||||
for _, c := range []string{ConsumerNotify, ConsumerWorkflowAdvance, ConsumerProjection} {
|
||||
for _, m := range []string{ModeOff, ModeShadow, ModeLive} {
|
||||
if err := ValidateOpsCutover("dev", c, m); err != nil {
|
||||
t.Fatalf("%s %s: %v", c, m, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ var (
|
||||
ErrDisabledConsumer = errors.New("events: consumer is disabled")
|
||||
ErrUnregistered = errors.New("events: event or consumer is not in the static registry")
|
||||
ErrReplayApply = errors.New("events: replay apply rejected")
|
||||
ErrOpsCutover = errors.New("events: shared cutover rejected")
|
||||
)
|
||||
|
||||
type Envelope struct {
|
||||
|
||||
@@ -9,10 +9,9 @@ import (
|
||||
)
|
||||
|
||||
func (s *Server) registerWorkflowApprovalHook() {
|
||||
if s == nil || s.gov == nil || s.instSvc == nil || !s.instSvc.Available() {
|
||||
return
|
||||
}
|
||||
s.gov.AddOnApprovedHook(s.onWorkflowDocumentApproved)
|
||||
// Durable workflow_advance is the unique writer. Keep the method for one
|
||||
// rollback window; do not register AddOnApprovedHook in production.
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Server) onWorkflowDocumentApproved(ctx context.Context, doc *docgov.DocumentRecord) {
|
||||
|
||||
Executable
+59
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
# Shared agent-dev event consumer mode switch.
|
||||
# Only environment=dev, consumers notify|workflow_advance|projection, modes off|shadow|live.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
ENV_FILE="${EVENTMODE_ENV:-configs/.env.dev}"
|
||||
|
||||
usage() {
|
||||
echo "usage: $0 inventory | set <consumer> <mode> | quarantine-illegal" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
if [[ $# -lt 1 ]]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
if [[ ! -f "$ENV_FILE" ]]; then
|
||||
echo "event-consumer-mode: missing $ENV_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if grep -E -q '^ENV=(test|main)[[:space:]]*$' "$ENV_FILE"; then
|
||||
echo "event-consumer-mode: refusing test/main env file" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Host-side ops pin the published dev database; never test/main ports.
|
||||
export CONTEXT_DB_HOST="${CONTEXT_DB_HOST:-127.0.0.1}"
|
||||
export CONTEXT_DB_PORT="${CONTEXT_DB_PORT:-5433}"
|
||||
export CONTEXT_DB_NAME="${CONTEXT_DB_NAME:-agent_system_dev}"
|
||||
export ENV=dev
|
||||
export EVENTMODE_ENV="$ENV_FILE"
|
||||
|
||||
case "$1" in
|
||||
inventory)
|
||||
go run ./cmd/eventmode inventory
|
||||
;;
|
||||
set)
|
||||
if [[ $# -ne 3 ]]; then
|
||||
usage
|
||||
fi
|
||||
consumer="$2"
|
||||
mode="$3"
|
||||
if [[ "$consumer" == "fav" ]]; then
|
||||
echo "event-consumer-mode: fav is disabled" >&2
|
||||
exit 1
|
||||
fi
|
||||
go run ./cmd/eventmode set "$consumer" "$mode"
|
||||
;;
|
||||
quarantine-illegal)
|
||||
go run ./cmd/eventmode quarantine-illegal
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
Reference in New Issue
Block a user