agentic-flow-kit 0.9.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -93
- package/modules/hosts/claude/commands/wf-build.md +3 -3
- package/modules/hosts/claude/commands/wf-design.md +3 -3
- package/modules/hosts/claude/commands/wf-new-task.md +14 -2
- package/modules/hosts/claude/commands/wf-pipeline-run.md +35 -0
- package/modules/hosts/claude/commands/wf-plan.md +3 -3
- package/modules/hosts/claude/commands/wf-sync-hosts.md +1 -1
- package/modules/hosts/claude/commands/wf-test.md +1 -1
- package/modules/hosts/codex/commands/wf-build.md +3 -3
- package/modules/hosts/codex/commands/wf-design.md +3 -3
- package/modules/hosts/codex/commands/wf-new-task.md +14 -2
- package/modules/hosts/codex/commands/wf-pipeline-run.md +35 -0
- package/modules/hosts/codex/commands/wf-plan.md +3 -3
- package/modules/hosts/codex/commands/wf-sync-hosts.md +1 -1
- package/modules/hosts/codex/commands/wf-test.md +1 -1
- package/modules/hosts/cursor/commands/wf-build.md +3 -3
- package/modules/hosts/cursor/commands/wf-design.md +3 -3
- package/modules/hosts/cursor/commands/wf-new-task.md +14 -2
- package/modules/hosts/cursor/commands/wf-pipeline-run.md +35 -0
- package/modules/hosts/cursor/commands/wf-plan.md +3 -3
- package/modules/hosts/cursor/commands/wf-sync-hosts.md +1 -1
- package/modules/hosts/cursor/commands/wf-test.md +1 -1
- package/modules/hosts/opencode/commands/wf-build.md +3 -3
- package/modules/hosts/opencode/commands/wf-design.md +3 -3
- package/modules/hosts/opencode/commands/wf-new-task.md +14 -2
- package/modules/hosts/opencode/commands/wf-pipeline-run.md +35 -0
- package/modules/hosts/opencode/commands/wf-plan.md +3 -3
- package/modules/hosts/opencode/commands/wf-sync-hosts.md +1 -1
- package/modules/hosts/opencode/commands/wf-test.md +1 -1
- package/modules/hosts/trae/commands/wf-build.md +3 -3
- package/modules/hosts/trae/commands/wf-design.md +3 -3
- package/modules/hosts/trae/commands/wf-new-task.md +14 -2
- package/modules/hosts/trae/commands/wf-pipeline-run.md +36 -0
- package/modules/hosts/trae/commands/wf-plan.md +3 -3
- package/modules/hosts/trae/commands/wf-sync-hosts.md +1 -1
- package/modules/hosts/trae/commands/wf-test.md +1 -1
- package/modules/hosts/trae/rules/project-workflow.md +1 -1
- package/package.json +45 -45
- package/templates/_agents/commands/build.md +2 -2
- package/templates/_agents/commands/design.md +1 -1
- package/templates/_agents/commands/new-task.md +1 -0
- package/templates/_agents/commands/pipeline-run.md +43 -0
- package/templates/_agents/commands/plan.md +3 -3
- package/templates/_agents/commands/sync-hosts.md +1 -1
- package/templates/_agents/scripts/check-lane-surface.mjs +20 -7
- package/templates/_agents/scripts/check-lane-surface.test.mjs +24 -2
- package/templates/_agents/scripts/check-ledger-invariant.mjs +135 -0
- package/templates/_agents/scripts/check-ledger-invariant.test.mjs +161 -0
- package/templates/_agents/scripts/check-loop.mjs +70 -18
- package/templates/_agents/scripts/check-loop.test.mjs +155 -8
- package/templates/_agents/scripts/confirm-doc.mjs +19 -7
- package/templates/_agents/scripts/confirm-doc.test.mjs +31 -0
- package/templates/_agents/scripts/fill-intent.mjs +1 -1
- package/templates/_agents/scripts/fill-plan.test.mjs +4 -3
- package/templates/_agents/scripts/gate-checklist.mjs +1 -0
- package/templates/_agents/scripts/gen-workflow-metrics.mjs +75 -5
- package/templates/_agents/scripts/gen-workflow-metrics.test.mjs +104 -2
- package/templates/_agents/scripts/pipeline-run.mjs +999 -0
- package/templates/_agents/scripts/pipeline-run.test.mjs +537 -0
- package/templates/_agents/scripts/policy.mjs +1 -0
- package/templates/_agents/scripts/pre-push.test.mjs +8 -1
- package/templates/_agents/scripts/solidify-task.test.mjs +5 -2
- package/templates/_agents/scripts/stage-gates.mjs +29 -6
- package/templates/_agents/scripts/trust-mode.test.mjs +110 -0
- package/templates/_agents/scripts-test-exempt.txt +10 -0
- package/templates/_githooks/pre-commit +12 -0
- package/templates/workflow/README.md +3 -2
- package/templates/workflow/plans/_TEMPLATE.md +1 -1
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// pipeline-run.test.mjs — pipeline-run 状态机测试(2026-10-02 pipeline-run)
|
|
3
|
+
// 方法:临时夹具树 + 假门脚本(fill-*/confirm-doc/verify/kb-search/gen-workflow-index/check-loop)
|
|
4
|
+
// + 假 git(PIPELINE_RUN_GIT_BIN="node <fixture>/fake-git.mjs",行为经 FIXTURE_GIT_STATUS 控制),
|
|
5
|
+
// 驱动真实 pipeline-run.mjs 走全路径;断言 stdout 的 PIPELINE-STOP 标记、run 文件事件流、假 git/台账日志。
|
|
6
|
+
// 运行:node templates/_agents/scripts/pipeline-run.test.mjs(不触真实仓库/真实 git)
|
|
7
|
+
// 注意:夹具路径不含空格(os.tmpdir);假 git 经 env 前缀注入(跨平台不依赖 shell)。
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { spawnSync } from 'node:child_process';
|
|
12
|
+
import assert from 'node:assert/strict';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
|
|
15
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const MAIN = path.join(HERE, 'pipeline-run.mjs');
|
|
17
|
+
|
|
18
|
+
// ── 夹具构建 ────────────────────────────────────────────────────────────
|
|
19
|
+
function buildFixture() {
|
|
20
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'prun-'));
|
|
21
|
+
const bin = path.join(root, '.agents', 'scripts');
|
|
22
|
+
fs.mkdirSync(bin, { recursive: true });
|
|
23
|
+
for (const d of ['intents', 'specs', 'plans', 'incidents']) fs.mkdirSync(path.join(root, 'workflow', d), { recursive: true });
|
|
24
|
+
fs.writeFileSync(path.join(root, '.agents', 'workflow-modules.txt'), '# 词表\npipeline\nwiki\n');
|
|
25
|
+
|
|
26
|
+
const fake = (name, src) => fs.writeFileSync(path.join(bin, name), src);
|
|
27
|
+
|
|
28
|
+
fake('kb-search.mjs', `#!/usr/bin/env node
|
|
29
|
+
console.log('── workflow(1 个文件命中)──');
|
|
30
|
+
console.log('📄 workflow/intents/1900-01-01-demo.md — done · L2 · pipeline · 1 处命中');
|
|
31
|
+
`);
|
|
32
|
+
|
|
33
|
+
const FILL_HEAD = `#!/usr/bin/env node
|
|
34
|
+
import fs from 'node:fs';
|
|
35
|
+
import path from 'node:path';
|
|
36
|
+
const args = process.argv.slice(2);
|
|
37
|
+
const out = args[args.indexOf('--output') + 1];
|
|
38
|
+
const t = args[args.indexOf('--topic') + 1] || 'task';
|
|
39
|
+
const lv = args[args.indexOf('--level') + 1] || 'L2';
|
|
40
|
+
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
41
|
+
`;
|
|
42
|
+
fake('fill-intent.mjs', FILL_HEAD + `const body = '---\\n状态: draft\\n级别: ' + lv + '\\nrisk_level: ' + lv + '\\n日期: 2026-10-02\\n模块: pipeline\\n---\\n# INTENT — ' + t + '\\n\\n## 背景与问题\\n<为什么做>\\n\\n## 历史教训/防复发\\n- 无\\n\\n## 目标\\n- 目标一\\n\\n## 非目标\\n- 不做\\n\\n## 约束\\n- 复用\\n\\n## 验收标准\\n\\n- [ ] 项一(证据:)\\n- [ ] 项二(证据:)\\n\\n## 确认与复核\\n- 待\\n';
|
|
43
|
+
fs.writeFileSync(out, body);
|
|
44
|
+
console.log('✅ intent 草稿:' + out);
|
|
45
|
+
`);
|
|
46
|
+
fake('fill-spec.mjs', FILL_HEAD + `const body = '---\\n状态: draft\\n级别: ' + lv + '\\n日期: 2026-10-02\\n模块: pipeline\\n---\\n# SPEC — ' + t + '\\n\\n## 功能行为\\n行为。\\n\\n## 数据流\\n流。\\n\\n## 系统改动\\n\\n- 改动一\\n\\n## 约束遵守映射\\n- 回应。\\n\\n## 风险评估\\n- 低。\\n\\n## 确认与复核\\n- 复核:待\\n';
|
|
47
|
+
fs.writeFileSync(out, body);
|
|
48
|
+
console.log('✅ spec 草稿:' + out);
|
|
49
|
+
`);
|
|
50
|
+
fake('fill-plan.mjs', FILL_HEAD + `const body = '---\\n状态: draft\\n级别: ' + (lv === 'L0' ? 'L1' : lv) + '\\n模块: pipeline\\n---\\n# PLAN — ' + t + '\\n\\n## 改动方案\\n\\n- src/app.mjs:实现功能\\n\\n## 约束与风险\\n\\n- 风险低\\n\\n## 任务拆解\\n\\n1. 实现\\n\\n## 执行顺序\\n\\n1\\n\\n## 验证计划\\n\\n- 静态门:npm test\\n\\n## 确认与复核\\n- 待\\n';
|
|
51
|
+
fs.writeFileSync(out, body);
|
|
52
|
+
console.log('✅ plan 草稿:' + out);
|
|
53
|
+
`);
|
|
54
|
+
|
|
55
|
+
fake('confirm-doc.mjs', `#!/usr/bin/env node
|
|
56
|
+
import fs from 'node:fs';
|
|
57
|
+
const args = process.argv.slice(2);
|
|
58
|
+
const doc = args.find((x) => !x.startsWith('--'));
|
|
59
|
+
const to = args.includes('--to') ? args[args.indexOf('--to') + 1] : undefined;
|
|
60
|
+
const q = args.includes('--delegated') ? args[args.indexOf('--delegated') + 1] : undefined;
|
|
61
|
+
if (process.env.FIXTURE_CONFIRM_REJECT === '1') { console.error('前置门拒绝(夹具强制)'); process.exit(2); }
|
|
62
|
+
const text = fs.readFileSync(doc, 'utf8');
|
|
63
|
+
const cur = (text.match(/^状态: (.*)$/m) || [])[1];
|
|
64
|
+
const target = to || ({ draft: 'approved', approved: 'done', open: 'fixed', fixed: 'closed' })[cur] || cur;
|
|
65
|
+
fs.writeFileSync(doc, text.replace(/^状态: .*$/m, '状态: ' + target));
|
|
66
|
+
fs.appendFileSync(process.env.PRTEST_LEDGER, JSON.stringify({ doc, to: target, quote: q }) + '\\n');
|
|
67
|
+
console.log('✓ ' + doc + ' ' + cur + ' → ' + target + '(source=chat-delegated)');
|
|
68
|
+
`);
|
|
69
|
+
|
|
70
|
+
fake('verify.mjs', `#!/usr/bin/env node
|
|
71
|
+
import fs from 'node:fs';
|
|
72
|
+
if (fs.existsSync(process.env.PRTEST_VERIFY_FAIL || '/nonexist')) { console.error('verify 失败(夹具):1/2 npm test 未通过'); process.exit(1); }
|
|
73
|
+
console.log('[verify] ✅ 全绿');
|
|
74
|
+
`);
|
|
75
|
+
|
|
76
|
+
fake('gen-workflow-index.mjs', '#!/usr/bin/env node\nconsole.log("index ok");\n');
|
|
77
|
+
fake('check-loop.mjs', '#!/usr/bin/env node\nconsole.log("check-loop ok");\n');
|
|
78
|
+
|
|
79
|
+
fake('fake-git.mjs', `#!/usr/bin/env node
|
|
80
|
+
import fs from 'node:fs';
|
|
81
|
+
const args = process.argv.slice(2);
|
|
82
|
+
fs.appendFileSync(process.env.PRTEST_GITLOG, args.join(' ') + '\\n');
|
|
83
|
+
if (args[0] === 'status' && args[1] === '--porcelain') { process.stdout.write(process.env.FIXTURE_GIT_STATUS || ''); process.exit(0); }
|
|
84
|
+
if (args[0] === 'rev-parse') { console.log('abc1234'); process.exit(0); }
|
|
85
|
+
process.exit(0);
|
|
86
|
+
`);
|
|
87
|
+
|
|
88
|
+
fs.writeFileSync(path.join(root, 'workflow', 'incidents', '_TEMPLATE.md'),
|
|
89
|
+
'---\n状态: open\n日期: 2026-10-02\n模块: pipeline\n---\n# INCIDENT — 模板\n\n## 时间线\n\n<时间线>\n\n## 根因\n\n<根因>\n\n## 复盘三件套\n\n<三件套>\n');
|
|
90
|
+
|
|
91
|
+
const gitLog = path.join(root, 'git.log');
|
|
92
|
+
const ledger = path.join(root, 'confirm-ledger.jsonl');
|
|
93
|
+
fs.writeFileSync(gitLog, '');
|
|
94
|
+
fs.writeFileSync(ledger, '');
|
|
95
|
+
return { root, bin, gitLog, ledger, verifyFailMarker: path.join(root, 'verify-fail-marker') };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── 驱动 ───────────────────────────────────────────────────────────────
|
|
99
|
+
function prun(fixture, args, extraEnv = {}) {
|
|
100
|
+
const env = {
|
|
101
|
+
...process.env,
|
|
102
|
+
PIPELINE_RUN_ROOT: fixture.root,
|
|
103
|
+
PIPELINE_RUN_BIN: fixture.bin,
|
|
104
|
+
PIPELINE_RUN_GIT_BIN: `node ${path.join(fixture.bin, 'fake-git.mjs')}`,
|
|
105
|
+
PRTEST_GITLOG: fixture.gitLog,
|
|
106
|
+
PRTEST_LEDGER: fixture.ledger,
|
|
107
|
+
PRTEST_VERIFY_FAIL: fixture.verifyFailMarker,
|
|
108
|
+
PIPELINE_RUN_TODAY: '2026-10-02', // 固定日期锚:消除测试对系统时钟的依赖(断言路径全部硬编码本日)
|
|
109
|
+
...extraEnv,
|
|
110
|
+
};
|
|
111
|
+
return spawnSync(process.execPath, [MAIN, ...args], { env, encoding: 'utf8', windowsHide: true });
|
|
112
|
+
}
|
|
113
|
+
const out = (p) => p.stdout || '';
|
|
114
|
+
const runsDir = (f) => path.join(f.root, '.agents', 'cache', 'pipeline-runs');
|
|
115
|
+
const runFile = (f, id) => JSON.parse(fs.readFileSync(path.join(runsDir(f), `${id}.json`), 'utf8'));
|
|
116
|
+
const latestId = (f) => fs.readdirSync(runsDir(f)).filter((x) => x.endsWith('.json')).map((x) => x.slice(0, -5)).sort().pop();
|
|
117
|
+
const docP = (f, rel) => path.join(f.root, rel);
|
|
118
|
+
function editDoc(f, rel, fn) { const p = docP(f, rel); fs.writeFileSync(p, fn(fs.readFileSync(p, 'utf8'))); }
|
|
119
|
+
const fillValid = (t) => t.replace(/<为什么做>|<时间线>|<根因>|<三件套>/g, '已填实的正文内容。');
|
|
120
|
+
|
|
121
|
+
let PASS = 0;
|
|
122
|
+
function test(name, fn) {
|
|
123
|
+
try { fn(); console.log(`✅ ${name}`); PASS++; }
|
|
124
|
+
catch (e) { console.error(`❌ ${name}\n${(e && e.stack) || e}`); process.exitCode = 1; }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 通用推进器:L1/L2 到 implement 工单停机(返回 fixture)
|
|
128
|
+
function driveToImplement(f, level, topic) {
|
|
129
|
+
prun(f, ['start', '给 wiki 加主题']);
|
|
130
|
+
prun(f, ['next', '--triage', `kind=require level=${level} module=wiki topic=${topic}`]);
|
|
131
|
+
editDoc(f, `workflow/intents/2026-10-02-${topic}.md`, fillValid);
|
|
132
|
+
prun(f, ['next']);
|
|
133
|
+
prun(f, ['next', '--delegated', 'intent ok']);
|
|
134
|
+
if (level === 'L2') {
|
|
135
|
+
editDoc(f, `workflow/specs/2026-10-02-${topic}.md`, fillValid);
|
|
136
|
+
prun(f, ['next']);
|
|
137
|
+
prun(f, ['next', '--delegated', 'spec ok']);
|
|
138
|
+
}
|
|
139
|
+
editDoc(f, `workflow/plans/2026-10-02-${topic}.md`, fillValid);
|
|
140
|
+
prun(f, ['next']);
|
|
141
|
+
prun(f, ['next', '--delegated', 'plan ok']);
|
|
142
|
+
prun(f, ['next', '--delegated', '改动清单 ok']);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── 用例 ───────────────────────────────────────────────────────────────
|
|
146
|
+
test('start → triage 工单 + run 文件落盘', () => {
|
|
147
|
+
const f = buildFixture();
|
|
148
|
+
const p = prun(f, ['start', '给 wiki 看板加 dark theme']);
|
|
149
|
+
assert.equal(p.status, 0, out(p));
|
|
150
|
+
assert.match(out(p), /PIPELINE-STOP work-order/);
|
|
151
|
+
const run = runFile(f, latestId(f));
|
|
152
|
+
assert.equal(run.stage, 'triage');
|
|
153
|
+
assert.equal(run.events[0].type, 'run-created');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('start 缺需求 → exit 1', () => {
|
|
157
|
+
const f = buildFixture();
|
|
158
|
+
assert.equal(prun(f, ['start']).status, 1);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('triage 回填 → kb+fill 门 → 起草工单(L2)', () => {
|
|
162
|
+
const f = buildFixture();
|
|
163
|
+
prun(f, ['start', '给 wiki 加主题']);
|
|
164
|
+
const p = prun(f, ['next', '--triage', 'kind=require level=L2 module=wiki topic=demo']);
|
|
165
|
+
assert.equal(p.status, 0, out(p));
|
|
166
|
+
assert.match(out(p), /PIPELINE-STOP work-order/);
|
|
167
|
+
const run = runFile(f, latestId(f));
|
|
168
|
+
assert.equal(run.docs.entry, 'workflow/intents/2026-10-02-demo.md');
|
|
169
|
+
assert.ok(fs.existsSync(docP(f, run.docs.entry)), 'fill-intent 应已生成草稿');
|
|
170
|
+
const gates = run.events.filter((e) => e.type === 'gate');
|
|
171
|
+
assert.ok(gates.length >= 2, 'kb-search + fill-intent 两道门');
|
|
172
|
+
assert.ok(gates.every((e) => e.exit === 0));
|
|
173
|
+
assert.ok(run.workOrder.context.kbHits.length > 0, '查重命中附进工单');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('占位符残留 → gate-fail exit 2;填实 → 等确认', () => {
|
|
177
|
+
const f = buildFixture();
|
|
178
|
+
prun(f, ['start', '给 wiki 加主题']);
|
|
179
|
+
prun(f, ['next', '--triage', 'kind=require level=L2 module=wiki topic=demo']);
|
|
180
|
+
const p1 = prun(f, ['next']);
|
|
181
|
+
assert.equal(p1.status, 2);
|
|
182
|
+
assert.match(out(p1), /占位符/);
|
|
183
|
+
assert.match(out(p1), /PIPELINE-STOP gate-fail/);
|
|
184
|
+
editDoc(f, 'workflow/intents/2026-10-02-demo.md', (t) => fillValid(t) + '\n用法备注:回跑 `next --delegated "<原话>"`(命令语法占位在 code span 内,不属模板占位符——复核 P1-3 回归)\n\n```\nnext --delegated <原话>\nnext --files <清单>\n```\n(上行围栏块内裸尖括号命令示例——复核 P2-B 回归,整块剔除不误拦)\n');
|
|
185
|
+
const p2 = prun(f, ['next']);
|
|
186
|
+
assert.equal(p2.status, 0, out(p2));
|
|
187
|
+
assert.match(out(p2), /PIPELINE-STOP await-confirm [\w-]+ workflow\/intents\/2026-10-02-demo\.md/);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('确认门:无原话重停;有原话过 confirm-doc;幂等不重跑 fill', () => {
|
|
191
|
+
const f = buildFixture();
|
|
192
|
+
prun(f, ['start', '给 wiki 加主题']);
|
|
193
|
+
prun(f, ['next', '--triage', 'kind=require level=L2 module=wiki topic=demo']);
|
|
194
|
+
editDoc(f, 'workflow/intents/2026-10-02-demo.md', fillValid);
|
|
195
|
+
prun(f, ['next']);
|
|
196
|
+
const p = prun(f, ['next']); // 无 --delegated:重停
|
|
197
|
+
assert.equal(p.status, 0);
|
|
198
|
+
assert.match(out(p), /PIPELINE-STOP await-confirm/);
|
|
199
|
+
const p2 = prun(f, ['next', '--delegated', '可以,按这个来']);
|
|
200
|
+
assert.equal(p2.status, 0, out(p2));
|
|
201
|
+
const run = runFile(f, latestId(f));
|
|
202
|
+
assert.equal(run.stage, 'spec-draft');
|
|
203
|
+
assert.equal(run.events.filter((e) => e.type === 'gate' && e.cmd.includes('fill-intent')).length, 1, 'next 重入不重复 fill');
|
|
204
|
+
const conf = run.events.filter((e) => e.type === 'confirm');
|
|
205
|
+
assert.equal(conf.length, 1);
|
|
206
|
+
assert.equal(conf[0].source, 'chat-delegated');
|
|
207
|
+
assert.equal(conf[0].quote, '可以,按这个来');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('L2 全程到 done:spec→plan→改动清单→实现→复核→勾验→关单×3→双批提交', () => {
|
|
211
|
+
const f = buildFixture();
|
|
212
|
+
driveToImplement(f, 'L2', 'demo');
|
|
213
|
+
const specRel = 'workflow/specs/2026-10-02-demo.md';
|
|
214
|
+
const intentRel = 'workflow/intents/2026-10-02-demo.md';
|
|
215
|
+
// 批次一提交已在假 git 日志(plan 确认时)
|
|
216
|
+
assert.match(fs.readFileSync(f.gitLog, 'utf8'), /commit -m docs\(workflow\): demo 三件套 approved/);
|
|
217
|
+
// 实现(git 状态=授权文件)→ L2 → 复核工单
|
|
218
|
+
const pImp = prun(f, ['next', '--files', 'src/app.mjs'], { FIXTURE_GIT_STATUS: ' M src/app.mjs' });
|
|
219
|
+
assert.equal(pImp.status, 0, out(pImp));
|
|
220
|
+
assert.match(out(pImp), /PIPELINE-STOP work-order/);
|
|
221
|
+
editDoc(f, specRel, (t) => t.replace('- 复核:待', '- 复核:无 P0/P1 问题'));
|
|
222
|
+
const pRev = prun(f, ['next'], { FIXTURE_GIT_STATUS: ' M src/app.mjs' });
|
|
223
|
+
assert.match(out(pRev), /PIPELINE-STOP work-order/); // closeout 工单
|
|
224
|
+
editDoc(f, intentRel, (t) => t.replace('- [ ] 项一(证据:)', '- [x] 项一(证据:abc1234)').replace('- [ ] 项二(证据:)', '- [x] 项二(证据:t1)'));
|
|
225
|
+
prun(f, ['next'], { FIXTURE_GIT_STATUS: ' M src/app.mjs' }); // done-confirm 停机
|
|
226
|
+
prun(f, ['next', '--delegated', '关单 intent'], { FIXTURE_GIT_STATUS: ' M src/app.mjs' });
|
|
227
|
+
prun(f, ['next', '--delegated', '关单 spec'], { FIXTURE_GIT_STATUS: ' M src/app.mjs' });
|
|
228
|
+
const pDone = prun(f, ['next', '--delegated', '关单 plan'], { FIXTURE_GIT_STATUS: ' M src/app.mjs' });
|
|
229
|
+
assert.equal(pDone.status, 0, out(pDone));
|
|
230
|
+
assert.match(out(pDone), /PIPELINE-STOP done/);
|
|
231
|
+
const run = runFile(f, latestId(f));
|
|
232
|
+
assert.equal(run.stopType, 'done');
|
|
233
|
+
assert.equal(run.events.filter((e) => e.type === 'commit').length, 3, '批次一 + 代码提交 + 批次二');
|
|
234
|
+
assert.match(fs.readFileSync(f.gitLog, 'utf8'), /commit -m feat\(wiki\): /, 'verify 绿后落代码提交(沙箱演练实证缺口)');
|
|
235
|
+
assert.equal(fs.readFileSync(docP(f, run.docs.entry), 'utf8').match(/^状态: (.*)$/m)[1], 'done');
|
|
236
|
+
assert.equal(fs.readFileSync(docP(f, run.docs.spec), 'utf8').match(/^状态: (.*)$/m)[1], 'done');
|
|
237
|
+
const ledger = fs.readFileSync(f.ledger, 'utf8').trim().split('\n').map((l) => JSON.parse(l));
|
|
238
|
+
assert.equal(ledger.length, 6, '3×approved + 3×done');
|
|
239
|
+
assert.ok(ledger.every((l) => typeof l.quote === 'string' && l.quote), '逐份原话');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('改动越权 → gate-fail 偏离即停', () => {
|
|
243
|
+
const f = buildFixture();
|
|
244
|
+
driveToImplement(f, 'L1', 'ov1');
|
|
245
|
+
const p = prun(f, ['next', '--files', 'src/app.mjs'], { FIXTURE_GIT_STATUS: ' M src/app.mjs\n?? sneaky.mjs' });
|
|
246
|
+
assert.equal(p.status, 2);
|
|
247
|
+
assert.match(out(p), /超出回填清单/);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('运行态副产物不误拦(confirmations/INDEX/cache 排除)——沙箱演练回归', () => {
|
|
251
|
+
const f = buildFixture();
|
|
252
|
+
driveToImplement(f, 'L1', 'rt1');
|
|
253
|
+
const p = prun(f, ['next', '--files', 'src/app.mjs'], { FIXTURE_GIT_STATUS: ' M .agents/confirmations.jsonl\n M workflow/INDEX.md\n?? .agents/cache/pipeline-runs/x.json\n?? src/app.mjs' });
|
|
254
|
+
assert.equal(p.status, 0, out(p));
|
|
255
|
+
assert.match(out(p), /PIPELINE-STOP work-order/); // 不被运行态误拦,推进至下一停机点
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test('implement 无 --files 重入 → 重发工单不误报', () => {
|
|
259
|
+
const f = buildFixture();
|
|
260
|
+
driveToImplement(f, 'L1', 'rf1');
|
|
261
|
+
const p = prun(f, ['next']);
|
|
262
|
+
assert.equal(p.status, 0);
|
|
263
|
+
assert.match(out(p), /PIPELINE-STOP work-order/);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('verify 失败 → 修复环 1/3(stderr 附工单)→ 修复后通过', () => {
|
|
267
|
+
const f = buildFixture();
|
|
268
|
+
fs.writeFileSync(f.verifyFailMarker, 'fail');
|
|
269
|
+
driveToImplement(f, 'L1', 'f1');
|
|
270
|
+
const p1 = prun(f, ['next', '--files', 'src/app.mjs'], { FIXTURE_GIT_STATUS: ' M src/app.mjs' });
|
|
271
|
+
assert.equal(p1.status, 0);
|
|
272
|
+
assert.match(out(p1), /修复环 1\/3/);
|
|
273
|
+
assert.match(out(p1), /verify 失败(夹具)/);
|
|
274
|
+
fs.rmSync(f.verifyFailMarker);
|
|
275
|
+
const p2 = prun(f, ['next', '--files', 'src/app.mjs'], { FIXTURE_GIT_STATUS: ' M src/app.mjs' });
|
|
276
|
+
assert.equal(p2.status, 0, out(p2));
|
|
277
|
+
assert.match(out(p2), /PIPELINE-STOP work-order/); // L1 → closeout 工单
|
|
278
|
+
assert.equal(runFile(f, latestId(f)).fixLoop.count, 0, '验证绿后 count 复位(1/3 已在上方标题断言)');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test('修复环 cap 3 → 超限 gate-fail exit 2', () => {
|
|
282
|
+
const f = buildFixture();
|
|
283
|
+
fs.writeFileSync(f.verifyFailMarker, 'fail');
|
|
284
|
+
driveToImplement(f, 'L1', 'f2');
|
|
285
|
+
prun(f, ['next', '--files', 'src/app.mjs'], { FIXTURE_GIT_STATUS: ' M src/app.mjs' });
|
|
286
|
+
prun(f, ['next', '--files', 'src/app.mjs'], { FIXTURE_GIT_STATUS: ' M src/app.mjs' });
|
|
287
|
+
const p = prun(f, ['next', '--files', 'src/app.mjs'], { FIXTURE_GIT_STATUS: ' M src/app.mjs' });
|
|
288
|
+
assert.equal(p.status, 2);
|
|
289
|
+
assert.match(out(p), /修复环超限/);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test('L0 路径:直改 → 提交 → done', () => {
|
|
293
|
+
const f = buildFixture();
|
|
294
|
+
prun(f, ['start', '改一处文案']);
|
|
295
|
+
prun(f, ['next', '--triage', 'kind=require level=L0 module=wiki topic=copy1']);
|
|
296
|
+
const pw = prun(f, ['next']); // 无 --files:重发工单
|
|
297
|
+
assert.match(out(pw), /PIPELINE-STOP work-order/);
|
|
298
|
+
const p = prun(f, ['next', '--files', 'README.md'], { FIXTURE_GIT_STATUS: ' M README.md' });
|
|
299
|
+
assert.equal(p.status, 0, out(p));
|
|
300
|
+
assert.match(out(p), /PIPELINE-STOP done/);
|
|
301
|
+
assert.match(fs.readFileSync(f.gitLog, 'utf8'), /commit -m docs\(wiki\): 改一处文案/);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test('verify-only 短路径直达 done', () => {
|
|
305
|
+
const f = buildFixture();
|
|
306
|
+
prun(f, ['start', '跑下 verify']);
|
|
307
|
+
const p = prun(f, ['next', '--triage', 'kind=verify level=L0 module=pipeline topic=v1']);
|
|
308
|
+
assert.equal(p.status, 0, out(p));
|
|
309
|
+
assert.match(out(p), /PIPELINE-STOP done/);
|
|
310
|
+
assert.ok(runFile(f, latestId(f)).events.some((e) => e.type === 'gate' && e.cmd.includes('verify.mjs') && e.exit === 0));
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test('review-only:工单 → --done 收尾可达(复核 P1-1 回归)', () => {
|
|
314
|
+
const f = buildFixture();
|
|
315
|
+
prun(f, ['start', '评审一下']);
|
|
316
|
+
const pw = prun(f, ['next', '--triage', 'kind=review level=L0 module=pipeline topic=rv1']);
|
|
317
|
+
assert.match(out(pw), /PIPELINE-STOP work-order/);
|
|
318
|
+
const pd = prun(f, ['next', '--done']);
|
|
319
|
+
assert.equal(pd.status, 0, out(pd));
|
|
320
|
+
assert.match(out(pd), /PIPELINE-STOP done/);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// deploy 夹具:预置终态/非终态三件套
|
|
324
|
+
function seedDeployDocs(f, { intentState, planState, specState, topic = 'dep1' }) {
|
|
325
|
+
const w = (rel, fm) => fs.writeFileSync(path.join(f.root, rel), `---\n${fm}\n---\n# DOC — ${topic}\n\n## 正文\n\n内容。\n`);
|
|
326
|
+
w(`workflow/intents/2026-10-02-${topic}.md`, `状态: ${intentState}\n级别: L1\n日期: 2026-10-02\n模块: pipeline`);
|
|
327
|
+
w(`workflow/plans/2026-10-02-${topic}.md`, `状态: ${planState}\n级别: L1\n模块: pipeline`);
|
|
328
|
+
if (specState) w(`workflow/specs/2026-10-02-${topic}.md`, `状态: ${specState}\n级别: L2\n日期: 2026-10-02\n模块: pipeline`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
test('deploy 路径:三件 done → prep 工单 → 机器校验 → 授权 → done(复核 P1-2 补实现)', () => {
|
|
332
|
+
const f = buildFixture();
|
|
333
|
+
seedDeployDocs(f, { intentState: 'done', planState: 'done' });
|
|
334
|
+
prun(f, ['start', '可以上了']);
|
|
335
|
+
const pw = prun(f, ['next', '--triage', 'kind=deploy level=L0 module=pipeline topic=dep1']);
|
|
336
|
+
assert.equal(pw.status, 0, out(pw));
|
|
337
|
+
assert.match(out(pw), /deploy-prep/);
|
|
338
|
+
assert.match(out(pw), /PIPELINE-STOP work-order/);
|
|
339
|
+
const pv = prun(f, ['next']);
|
|
340
|
+
assert.equal(pv.status, 0, out(pv));
|
|
341
|
+
assert.match(out(pv), /await-confirm/); // 三件校验过 → 停授权点
|
|
342
|
+
const pd = prun(f, ['next', '--delegated', '上']);
|
|
343
|
+
assert.equal(pd.status, 0, out(pd));
|
|
344
|
+
assert.match(out(pd), /PIPELINE-STOP done/);
|
|
345
|
+
const run = runFile(f, latestId(f));
|
|
346
|
+
assert.equal(run.deployTarget.plan, 'workflow/plans/2026-10-02-dep1.md');
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test('deploy 三件缺口 → gate-fail 列缺口 exit 2(P1-2)', () => {
|
|
350
|
+
const f = buildFixture();
|
|
351
|
+
seedDeployDocs(f, { intentState: 'done', planState: 'approved' }); // plan 未关单
|
|
352
|
+
prun(f, ['start', '可以上了']);
|
|
353
|
+
prun(f, ['next', '--triage', 'kind=deploy level=L0 module=pipeline topic=dep1']);
|
|
354
|
+
const p = prun(f, ['next']);
|
|
355
|
+
assert.equal(p.status, 2);
|
|
356
|
+
assert.match(out(p), /须 done/);
|
|
357
|
+
assert.match(out(p), /2026-10-02-dep1\.md(approved/);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test('deploy 边界:incident closed 但 plan 缺失 → gate-fail(复核 P2-C 回归)', () => {
|
|
361
|
+
const f = buildFixture();
|
|
362
|
+
fs.writeFileSync(path.join(f.root, 'workflow', 'incidents', '2026-10-02-incd.md'),
|
|
363
|
+
'---\n状态: closed\n发现: 2026-10-02\n模块: pipeline\n---\n# INCIDENT — incd\n\n## 时间线\n\n- x\n\n## 根因\n\nx\n\n## 复盘三件套\n\nx\n');
|
|
364
|
+
prun(f, ['start', '可以上了']);
|
|
365
|
+
prun(f, ['next', '--triage', 'kind=deploy level=L0 module=pipeline topic=incd']);
|
|
366
|
+
const p = prun(f, ['next']);
|
|
367
|
+
assert.equal(p.status, 2);
|
|
368
|
+
assert.match(out(p), /plans\/2026-10-02-incd\.md(缺失——须 done)/);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test('deploy 边界:topic 子串不再误匹配(复核 P2-C 收紧回归)', () => {
|
|
372
|
+
const f = buildFixture();
|
|
373
|
+
seedDeployDocs(f, { intentState: 'done', planState: 'done', topic: 'dep1' });
|
|
374
|
+
prun(f, ['start', '可以上了']);
|
|
375
|
+
const p = prun(f, ['next', '--triage', 'kind=deploy level=L0 module=pipeline topic=dep']); // 旧版 includes 会误命中 dep1
|
|
376
|
+
assert.equal(p.status, 2);
|
|
377
|
+
assert.match(out(p), /找不到可上线的对象/);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// P2-4 A′:提交命中 managed 面的处理(装户降级 WARN / 包源自愈 sync)
|
|
381
|
+
test('P2-4 A′:命中台账件且无 flow-kit → 装户降级 WARN + 台账随提交(复核回归)', () => {
|
|
382
|
+
const f = buildFixture();
|
|
383
|
+
fs.mkdirSync(path.join(f.root, '.agents', 'notes'), { recursive: true });
|
|
384
|
+
fs.writeFileSync(path.join(f.root, '.agents', 'notes', 'runtime-env.md'), 'notes\n');
|
|
385
|
+
fs.writeFileSync(path.join(f.root, '.agents', 'kit.json'), JSON.stringify({ managed: [{ rel: '.agents/notes/runtime-env.md', sha256: 'x' }] }));
|
|
386
|
+
prun(f, ['start', '改一处文案']);
|
|
387
|
+
prun(f, ['next', '--triage', 'kind=require level=L0 module=wiki topic=mng1']);
|
|
388
|
+
editDoc(f, '.agents/notes/runtime-env.md', (t) => t + '\n- 演练追加一行\n');
|
|
389
|
+
const p = prun(f, ['next', '--files', '.agents/notes/runtime-env.md'], { FIXTURE_GIT_STATUS: ' M .agents/notes/runtime-env.md' });
|
|
390
|
+
assert.equal(p.status, 0, out(p));
|
|
391
|
+
assert.match(out(p), /⚠️ 触及 managed 面/);
|
|
392
|
+
const addLine = fs.readFileSync(f.gitLog, 'utf8').split('\n').find((l) => l.startsWith('add '));
|
|
393
|
+
assert.equal(addLine, 'add .agents/notes/runtime-env.md', '装户降级只 WARN:add 面不含 kit.json(复核 P2-D 断言改实)');
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test('P2-4 A′:包源环境自动 sync——flow-kit 假件被调 + 产物入提交(复核回归)', () => {
|
|
397
|
+
const f = buildFixture();
|
|
398
|
+
fs.mkdirSync(path.join(f.root, '.agents', 'notes'), { recursive: true });
|
|
399
|
+
fs.writeFileSync(path.join(f.root, '.agents', 'notes', 'runtime-env.md'), 'notes\n');
|
|
400
|
+
fs.writeFileSync(path.join(f.root, '.agents', 'kit.json'), JSON.stringify({ managed: [{ rel: '.agents/notes/runtime-env.md', sha256: 'x' }] }));
|
|
401
|
+
fs.mkdirSync(path.join(f.root, 'bin'), { recursive: true });
|
|
402
|
+
const syncLog = path.join(f.root, 'flowkit-sync.log');
|
|
403
|
+
fs.writeFileSync(path.join(f.root, 'bin', 'flow-kit.mjs'), `#!/usr/bin/env node
|
|
404
|
+
import fs from 'node:fs';
|
|
405
|
+
fs.appendFileSync(${JSON.stringify(syncLog)}, 'sync-called\\n');
|
|
406
|
+
const kp = ${JSON.stringify(path.join(f.root, '.agents', 'kit.json'))};
|
|
407
|
+
const kit = JSON.parse(fs.readFileSync(kp, 'utf8'));
|
|
408
|
+
kit.managed[0].sha256 = 'refreshed';
|
|
409
|
+
fs.writeFileSync(kp, JSON.stringify(kit));
|
|
410
|
+
`);
|
|
411
|
+
prun(f, ['start', '改一处文案']);
|
|
412
|
+
prun(f, ['next', '--triage', 'kind=require level=L0 module=wiki topic=mng2']);
|
|
413
|
+
editDoc(f, '.agents/notes/runtime-env.md', (t) => t + '\n- 演练追加一行\n');
|
|
414
|
+
const p = prun(f, ['next', '--files', '.agents/notes/runtime-env.md'], { FIXTURE_GIT_STATUS: ' M .agents/notes/runtime-env.md' });
|
|
415
|
+
assert.equal(p.status, 0, out(p));
|
|
416
|
+
assert.match(fs.readFileSync(syncLog, 'utf8'), /sync-called/);
|
|
417
|
+
const run = runFile(f, latestId(f));
|
|
418
|
+
assert.ok(run.events.some((e) => e.type === 'gate' && e.cmd.includes('flow-kit.mjs sync') && e.exit === 0));
|
|
419
|
+
assert.ok(run.events.some((e) => e.type === 'managed-sync'));
|
|
420
|
+
assert.match(fs.readFileSync(f.gitLog, 'utf8'), /kit\.json/); // 台账(已被假 sync 刷新)随提交
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test('P2-4 A′:未命中台账面 → 不触发 sync/告警', () => {
|
|
424
|
+
const f = buildFixture();
|
|
425
|
+
fs.writeFileSync(path.join(f.root, '.agents', 'kit.json'), JSON.stringify({ managed: [{ rel: '.agents/notes/runtime-env.md', sha256: 'x' }] }));
|
|
426
|
+
prun(f, ['start', '改一处文案']);
|
|
427
|
+
prun(f, ['next', '--triage', 'kind=require level=L0 module=wiki topic=mng3']);
|
|
428
|
+
const p = prun(f, ['next', '--files', 'README.md'], { FIXTURE_GIT_STATUS: ' M README.md' });
|
|
429
|
+
assert.equal(p.status, 0, out(p));
|
|
430
|
+
assert.doesNotMatch(out(p), /managed 面/);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test('rename 行 old+new 双收录防逃逸(复核 P2-2 回归)', () => {
|
|
434
|
+
const f = buildFixture();
|
|
435
|
+
driveToImplement(f, 'L1', 'rn1');
|
|
436
|
+
const p = prun(f, ['next', '--files', 'src/app.mjs'], { FIXTURE_GIT_STATUS: 'R old.mjs -> src/app.mjs' });
|
|
437
|
+
assert.equal(p.status, 2);
|
|
438
|
+
assert.match(out(p), /old\.mjs/); // 旧路径删除计入改动面 → 越权拦截
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test('布尔旗标不吞参:status --json --run <id>(复核 P2-5 回归)', () => {
|
|
442
|
+
const f = buildFixture();
|
|
443
|
+
prun(f, ['start', '给 wiki 加主题']);
|
|
444
|
+
prun(f, ['next', '--triage', 'kind=require level=L2 module=wiki topic=demo']); // 建第二个可辨 run 前,确保 latest 唯一
|
|
445
|
+
const id = latestId(f);
|
|
446
|
+
const p = prun(f, ['status', '--json', '--run', id]);
|
|
447
|
+
assert.equal(p.status, 0);
|
|
448
|
+
assert.match(out(p), new RegExp('"runId": "' + id + '"')); // --run 生效(未被 --json 吞掉)
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
test('空「证据:」不过勾验(复核 P2-6 回归)', () => {
|
|
452
|
+
const f = buildFixture();
|
|
453
|
+
driveToImplement(f, 'L1', 'ev1');
|
|
454
|
+
prun(f, ['next', '--files', 'src/app.mjs'], { FIXTURE_GIT_STATUS: ' M src/app.mjs' }); // verify 绿 → closeout WO
|
|
455
|
+
editDoc(f, 'workflow/intents/2026-10-02-ev1.md', (t) => t.replace(/- \[ \] 项一(证据:)/, '- [x] 项一(证据:)').replace(/- \[ \] 项二(证据:)/, '- [x] 项二(证据:t1)'));
|
|
456
|
+
const p = prun(f, ['next'], { FIXTURE_GIT_STATUS: ' M src/app.mjs' });
|
|
457
|
+
assert.equal(p.status, 0);
|
|
458
|
+
assert.match(out(p), /PIPELINE-STOP work-order/); // 空证据 → 重发 closeout 工单,不进关单
|
|
459
|
+
assert.match(out(p), /(closeout)勾验收补证据/);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
test('incident 路径:模板复制→两跳 fixed/closed→plan 不可免', () => {
|
|
463
|
+
const f = buildFixture();
|
|
464
|
+
prun(f, ['start', '线上出事了']);
|
|
465
|
+
prun(f, ['next', '--triage', 'kind=fix level=L1 module=pipeline topic=inc9']);
|
|
466
|
+
assert.ok(fs.existsSync(docP(f, 'workflow/incidents/2026-10-02-inc9.md')), '模板已复制');
|
|
467
|
+
editDoc(f, 'workflow/incidents/2026-10-02-inc9.md', fillValid);
|
|
468
|
+
prun(f, ['next']); // 事故草稿确认(对话级)
|
|
469
|
+
prun(f, ['next', '--delegated', '事故草稿通过']);
|
|
470
|
+
assert.ok(fs.existsSync(docP(f, 'workflow/plans/2026-10-02-inc9.md')), 'plan 不可免');
|
|
471
|
+
editDoc(f, 'workflow/plans/2026-10-02-inc9.md', fillValid);
|
|
472
|
+
prun(f, ['next']);
|
|
473
|
+
prun(f, ['next', '--delegated', 'plan ok']);
|
|
474
|
+
prun(f, ['next', '--delegated', '改动清单 ok']);
|
|
475
|
+
prun(f, ['next', '--files', 'src/hotfix.mjs'], { FIXTURE_GIT_STATUS: ' M src/hotfix.mjs' }); // verify 绿 → fixed 确认停机
|
|
476
|
+
prun(f, ['next', '--delegated', 'fixed 确认'], { FIXTURE_GIT_STATUS: ' M src/hotfix.mjs' }); // open→fixed
|
|
477
|
+
prun(f, ['next', '--delegated', 'closed 确认'], { FIXTURE_GIT_STATUS: ' M src/hotfix.mjs' }); // fixed→closed
|
|
478
|
+
const p = prun(f, ['next', '--delegated', 'plan 关单'], { FIXTURE_GIT_STATUS: ' M src/hotfix.mjs' }); // plan done → 终态
|
|
479
|
+
assert.equal(p.status, 0, out(p));
|
|
480
|
+
assert.match(out(p), /PIPELINE-STOP done/);
|
|
481
|
+
assert.equal(fs.readFileSync(docP(f, 'workflow/incidents/2026-10-02-inc9.md'), 'utf8').match(/^状态: (.*)$/m)[1], 'closed');
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test('confirm-doc exit 2 → gate-fail 透传不绕(真实退出码入事件流)', () => {
|
|
485
|
+
const f = buildFixture();
|
|
486
|
+
prun(f, ['start', '给 wiki 加主题']);
|
|
487
|
+
prun(f, ['next', '--triage', 'kind=require level=L1 module=wiki topic=rj']);
|
|
488
|
+
editDoc(f, 'workflow/intents/2026-10-02-rj.md', fillValid);
|
|
489
|
+
prun(f, ['next']);
|
|
490
|
+
const p = prun(f, ['next', '--delegated', 'ok'], { FIXTURE_CONFIRM_REJECT: '1' });
|
|
491
|
+
assert.equal(p.status, 2);
|
|
492
|
+
assert.match(out(p), /exit 2/);
|
|
493
|
+
const last = runFile(f, latestId(f)).events.filter((e) => e.type === 'gate').pop();
|
|
494
|
+
assert.equal(last.exit, 2);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
test('工单/事件流 schema 键集(契约防漂移)', () => {
|
|
498
|
+
const f = buildFixture();
|
|
499
|
+
prun(f, ['start', '给 wiki 加主题']);
|
|
500
|
+
prun(f, ['next', '--triage', 'kind=require level=L2 module=wiki topic=sm']);
|
|
501
|
+
const run = runFile(f, latestId(f));
|
|
502
|
+
for (const k of ['id', 'kind', 'title', 'goal', 'instructions', 'context', 'constraints', 'acceptance']) {
|
|
503
|
+
assert.ok(k in run.workOrder, `workOrder 缺键 ${k}`);
|
|
504
|
+
}
|
|
505
|
+
for (const k of ['kbHits', 'stderrTail', 'relatedDocs', 'docPath']) assert.ok(k in run.workOrder.context);
|
|
506
|
+
const gate = run.events.find((e) => e.type === 'gate');
|
|
507
|
+
for (const k of ['t', 'type', 'cmd', 'exit', 'ms']) assert.ok(k in gate, `gate 事件缺键 ${k}`);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
test('abort:approved 文档走 --to 显式放弃并提交', () => {
|
|
511
|
+
const f = buildFixture();
|
|
512
|
+
prun(f, ['start', '给 wiki 加主题']);
|
|
513
|
+
prun(f, ['next', '--triage', 'kind=require level=L1 module=wiki topic=ab1']);
|
|
514
|
+
editDoc(f, 'workflow/intents/2026-10-02-ab1.md', fillValid);
|
|
515
|
+
prun(f, ['next']);
|
|
516
|
+
prun(f, ['next', '--delegated', 'approved 了']);
|
|
517
|
+
const p = prun(f, ['abort', '--run', latestId(f), '--to', 'cancelled', '--delegated', '不做这个了']);
|
|
518
|
+
assert.equal(p.status, 0, out(p));
|
|
519
|
+
assert.match(out(p), /PIPELINE-STOP aborted/);
|
|
520
|
+
assert.equal(fs.readFileSync(docP(f, 'workflow/intents/2026-10-02-ab1.md'), 'utf8').match(/^状态: (.*)$/m)[1], 'cancelled');
|
|
521
|
+
assert.match(fs.readFileSync(f.gitLog, 'utf8'), /docs\(workflow\): ab1 放弃/);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
test('status --adopt:run 文件丢失后按文档重建', () => {
|
|
525
|
+
const f = buildFixture();
|
|
526
|
+
prun(f, ['start', '给 wiki 加主题']);
|
|
527
|
+
prun(f, ['next', '--triage', 'kind=require level=L1 module=wiki topic=wiki']);
|
|
528
|
+
const id = latestId(f);
|
|
529
|
+
fs.rmSync(path.join(runsDir(f), `${id}.json`));
|
|
530
|
+
const p = prun(f, ['status', '--run', id, '--adopt']); // --adopt 须置于参数尾(parseArgs 盲取下一参为值)
|
|
531
|
+
assert.equal(p.status, 0, out(p));
|
|
532
|
+
assert.match(out(p), /已重建/);
|
|
533
|
+
assert.equal(runFile(f, id).stage, 'adopted');
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
console.log(`\npipeline-run.test:${PASS} 组断言全过${process.exitCode ? '(有失败)' : ',绿'}`);
|
|
537
|
+
process.exit(process.exitCode || 0);
|
|
@@ -23,6 +23,7 @@ export const POLICIES = {
|
|
|
23
23
|
bindingTs: '2026-09-28',
|
|
24
24
|
check17UnclosedAfter: '0.8.0',
|
|
25
25
|
stageGateSince: '2026-09-29', // 检查 19 逐阶段审计的生效日(2026-09-30 stage-gate-machine);锚 = 台账 ts 的 UTC 日期(复核 P2-3:本批 ts 2026-09-29T17:33Z 落在受审范围,顺序合规故静默);v1 无此键 → 该检查整体跳过
|
|
26
|
+
riskLevelSince: '2026-10-01', // intent risk_level 双字段协议生效日(2026-10-01 hybrid-governance 落地 fill-intent 双写;2026-10-02 caliber-convergence 方案 C 引入消费)——此日前建档的 intent 无该字段不判 suspect;v1 无此键 → 双字段协议视为从未生效(单字段判)
|
|
26
27
|
},
|
|
27
28
|
};
|
|
28
29
|
|
|
@@ -12,7 +12,12 @@ import { spawnSync } from 'node:child_process';
|
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
13
|
|
|
14
14
|
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
-
|
|
15
|
+
// 钩子双布局探测:templates 布局(../../_githooks)与装户布局(../../.githooks)——本测试随 sync
|
|
16
|
+
// 落到 .agents/scripts 后 ../../_githooks 不存在(装户钩子在仓库根 .githooks/),2026-10-02 ci-red-batch
|
|
17
|
+
const PRE_PUSH = [
|
|
18
|
+
path.join(SCRIPT_DIR, '..', '..', '_githooks', 'pre-push'),
|
|
19
|
+
path.join(SCRIPT_DIR, '..', '..', '.githooks', 'pre-push'),
|
|
20
|
+
].find((p) => fs.existsSync(p));
|
|
16
21
|
|
|
17
22
|
let pass = 0;
|
|
18
23
|
let fail = 0;
|
|
@@ -31,6 +36,8 @@ const setup = () => {
|
|
|
31
36
|
fs.mkdirSync(path.join(root, '.agents', 'scripts'), { recursive: true });
|
|
32
37
|
// 挂载真实 pre-push(装户同款),stub 替换 check-loop 记录 argv
|
|
33
38
|
fs.copyFileSync(PRE_PUSH, path.join(root, '.githooks', 'pre-push'));
|
|
39
|
+
// Linux:git 静默跳过不可执行钩子(推送照常成功、桩无记录)——挂载后必须设执行位;桩经 sh 调用无须 +x
|
|
40
|
+
fs.chmodSync(path.join(root, '.githooks', 'pre-push'), 0o755);
|
|
34
41
|
fs.writeFileSync(path.join(root, '.agents', 'scripts', 'check-loop.sh'), '#!/bin/sh\necho "$@" >> .stub-args\nexit 0\n');
|
|
35
42
|
fs.writeFileSync(path.join(root, 'a.txt'), 'a\n');
|
|
36
43
|
git(root, ['init', '-q']);
|
|
@@ -50,6 +50,9 @@ const ledgerLines = (root) => {
|
|
|
50
50
|
const p = path.join(root, '.agents', 'confirmations.jsonl');
|
|
51
51
|
return fs.existsSync(p) ? fs.readFileSync(p, 'utf8').split(/\n/).filter(Boolean) : [];
|
|
52
52
|
};
|
|
53
|
+
// 迁移目标名含当天日期前缀(solidify 以 toISOString 生成)——动态取值防跨天脆断
|
|
54
|
+
const TODAY = new Date().toISOString().slice(0, 10);
|
|
55
|
+
const migratedPath = (root) => path.join(root, 'workflow', 'intents', `${TODAY}-demo-task.md`);
|
|
53
56
|
|
|
54
57
|
// T1:默认只迁移不落账
|
|
55
58
|
{
|
|
@@ -58,7 +61,7 @@ const ledgerLines = (root) => {
|
|
|
58
61
|
const r = run(d, ['--topic', 'demo', '--root', d]);
|
|
59
62
|
check('T1 默认迁移成功 exit 0', r.code === 0, r.out);
|
|
60
63
|
check('T1 草稿已迁移到 intents 且日期前缀正确',
|
|
61
|
-
fs.existsSync(
|
|
64
|
+
fs.existsSync(migratedPath(d)), '');
|
|
62
65
|
check('T1 零台账新增(无确认来源绝不落账)', ledgerLines(d).length === 0, JSON.stringify(ledgerLines(d)));
|
|
63
66
|
check('T1 输出含确认指引', r.out.includes('确认指引') && r.out.includes('--delegated'), r.out);
|
|
64
67
|
}
|
|
@@ -73,7 +76,7 @@ const ledgerLines = (root) => {
|
|
|
73
76
|
check('T2 quote 原样(脚本不编造话术)',
|
|
74
77
|
lines[0].includes('"quote":"固化了可以"') && lines[0].includes('chat-delegated'), lines[0]);
|
|
75
78
|
check('T2 目标文档状态已 approved',
|
|
76
|
-
fs.readFileSync(
|
|
79
|
+
fs.readFileSync(migratedPath(d), 'utf8').includes('状态: approved'), '');
|
|
77
80
|
}
|
|
78
81
|
// T3:--auto 在 Strict(缺 trust-mode.json)下被拒 → 失败计数并 exit 1
|
|
79
82
|
{
|