sillyspec 3.18.1 → 3.18.3
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/.claude/skills/sillyspec-brainstorm/SKILL.md +24 -23
- package/.claude/skills/sillyspec-execute/SKILL.md +8 -1
- package/docs/brainstorm-plan-contract.md +64 -0
- package/docs/plan-execute-contract.md +123 -0
- package/docs/revision-mode.md +115 -0
- package/docs/sillyspec/file-lifecycle.md +13 -4
- package/docs/workflow-contract-regression.md +106 -0
- package/package.json +1 -1
- package/packages/dashboard/dist/assets/{index-DpLHK4jv.js → index-Bq_Z2hne.js} +568 -568
- package/packages/dashboard/dist/assets/{index-BcM2J-hv.css → index-O2W5RV4z.css} +1 -1
- package/packages/dashboard/dist/index.html +16 -16
- package/packages/dashboard/src/components/PipelineStage.vue +22 -2
- package/packages/dashboard/src/components/PipelineView.vue +10 -2
- package/packages/dashboard/src/components/StageBadge.vue +17 -3
- package/packages/dashboard/src/components/StepCard.vue +7 -2
- package/src/change-risk-profile.js +167 -0
- package/src/db.js +6 -0
- package/src/index.js +17 -1
- package/src/knowledge-match.js +130 -0
- package/src/progress.js +464 -11
- package/src/run.js +269 -29
- package/src/scan-postcheck.js +34 -2
- package/src/stage-contract.js +90 -5
- package/src/stages/brainstorm.js +23 -0
- package/src/stages/execute.js +122 -16
- package/src/stages/plan.js +82 -0
- package/src/stages/scan.js +40 -0
- package/src/stages/verify.js +38 -2
- package/test/brainstorm-plan-contract.test.mjs +273 -0
- package/test/knowledge-match.test.mjs +231 -0
- package/test/plan-execute-contract.test.mjs +330 -0
- package/test/platform-failure-samples.test.mjs +4 -0
- package/test/revision-v1.test.mjs +1145 -0
- package/test/scan-knowledge.test.mjs +175 -0
- package/test/scan-postcheck.test.mjs +3 -0
- package/test/spec-dir.test.mjs +8 -3
- package/test/stage-definitions.test.mjs +1 -1
|
@@ -0,0 +1,1145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Revision v1 测试
|
|
3
|
+
*
|
|
4
|
+
* 覆盖 10 个核心 case:
|
|
5
|
+
* 1. completed 阶段直接 run → 拒绝
|
|
6
|
+
* 2. completed + --reopen 无 --from-step → 拒绝
|
|
7
|
+
* 3. --reopen --from-step index → 正确标记步骤
|
|
8
|
+
* 4. --reopen --from-step name → 正确标记步骤
|
|
9
|
+
* 5. --from-step 不存在 → fail-fast
|
|
10
|
+
* 6. --from-step 后续步骤 stale
|
|
11
|
+
* 7. 下游 stage cascade stale
|
|
12
|
+
* 8. stale 下游直接 run → 拒绝
|
|
13
|
+
* 9. waiting/pending 阶段 --reopen 无 from-step → 允许继续
|
|
14
|
+
* 10. reopen 不改动产物文件
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { mkdtempSync, mkdirSync, writeFileSync, existsSync, readFileSync, rmSync } from 'fs'
|
|
18
|
+
import { join } from 'path'
|
|
19
|
+
import { tmpdir } from 'os'
|
|
20
|
+
|
|
21
|
+
let failed = 0
|
|
22
|
+
const failures = []
|
|
23
|
+
|
|
24
|
+
function assert(condition, msg) {
|
|
25
|
+
if (!condition) {
|
|
26
|
+
failed++
|
|
27
|
+
failures.push(msg)
|
|
28
|
+
console.log(` ❌ FAIL: ${msg}`)
|
|
29
|
+
} else {
|
|
30
|
+
console.log(` ✅ PASS: ${msg}`)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── 辅助:创建临时项目 ──
|
|
35
|
+
function createTempProject() {
|
|
36
|
+
const cwd = mkdtempSync(join(tmpdir(), 'sillyspec-rev-test-'))
|
|
37
|
+
const specDir = join(cwd, '.sillyspec')
|
|
38
|
+
mkdirSync(join(specDir, '.runtime'), { recursive: true })
|
|
39
|
+
mkdirSync(join(specDir, 'changes'), { recursive: true })
|
|
40
|
+
return { cwd, specDir }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── 辅助:初始化 DB + change ──
|
|
44
|
+
async function setupProgress(cwd, changeName = 'test-change') {
|
|
45
|
+
const { ProgressManager } = await import('../src/progress.js')
|
|
46
|
+
const pm = new ProgressManager()
|
|
47
|
+
await pm.init(cwd)
|
|
48
|
+
await pm.initChange(cwd, changeName)
|
|
49
|
+
return pm
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── 辅助:标记阶段 completed + 填充步骤 ──
|
|
53
|
+
async function markStageCompleted(pm, cwd, changeName, stageName, stepNames) {
|
|
54
|
+
const data = await pm.read(cwd, changeName)
|
|
55
|
+
const now = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
56
|
+
data.stages[stageName] = {
|
|
57
|
+
status: 'completed',
|
|
58
|
+
startedAt: now,
|
|
59
|
+
completedAt: now,
|
|
60
|
+
steps: stepNames.map(name => ({ name, status: 'completed', completedAt: now })),
|
|
61
|
+
}
|
|
62
|
+
data.currentStage = stageName
|
|
63
|
+
await pm._write(cwd, data, changeName)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log('=== Revision v1 测试 ===\n')
|
|
67
|
+
|
|
68
|
+
// ─────────────────────────────────────────
|
|
69
|
+
// Case 1: completed 阶段直接 run → 拒绝
|
|
70
|
+
// ─────────────────────────────────────────
|
|
71
|
+
console.log('--- Case 1: completed 阶段直接 run 拒绝 ---')
|
|
72
|
+
{
|
|
73
|
+
const { cwd } = createTempProject()
|
|
74
|
+
const changeName = 'rev-test-1'
|
|
75
|
+
const pm = await setupProgress(cwd, changeName)
|
|
76
|
+
await markStageCompleted(pm, cwd, changeName, 'brainstorm', ['需求澄清', '方案发散', '方案选择'])
|
|
77
|
+
|
|
78
|
+
// 模拟 run 命令的规则 1 检查
|
|
79
|
+
const data = await pm.read(cwd, changeName)
|
|
80
|
+
const stageStatus = data.stages['brainstorm']?.status
|
|
81
|
+
const isReopen = false
|
|
82
|
+
|
|
83
|
+
const shouldReject = stageStatus === 'completed' && !isReopen
|
|
84
|
+
assert(shouldReject, 'completed 阶段直接 run 应被拒绝')
|
|
85
|
+
|
|
86
|
+
// 验证 reopen 能解决
|
|
87
|
+
const reopenResult = await pm.reopenStage(cwd, 'brainstorm', { fromStep: 2, changeName })
|
|
88
|
+
assert(reopenResult.ok === true, '--reopen 应该能打开 completed 阶段')
|
|
89
|
+
}
|
|
90
|
+
rmSync(mkdtempSync(join(tmpdir(), 'sillyspec-rev-test-')), { recursive: true }) // noop cleanup placeholder
|
|
91
|
+
|
|
92
|
+
// ─────────────────────────────────────────
|
|
93
|
+
// Case 2: completed + --reopen 无 --from-step → 拒绝
|
|
94
|
+
// ─────────────────────────────────────────
|
|
95
|
+
console.log('\n--- Case 2: completed + --reopen 无 --from-step 拒绝 ---')
|
|
96
|
+
{
|
|
97
|
+
const { cwd } = createTempProject()
|
|
98
|
+
const changeName = 'rev-test-2'
|
|
99
|
+
const pm = await setupProgress(cwd, changeName)
|
|
100
|
+
await markStageCompleted(pm, cwd, changeName, 'brainstorm', ['需求澄清', '方案发散', '方案选择'])
|
|
101
|
+
|
|
102
|
+
// reopen 不带 fromStep,且所有步骤 completed → 应该失败
|
|
103
|
+
const result = await pm.reopenStage(cwd, 'brainstorm', { changeName })
|
|
104
|
+
assert(!result.ok, '所有步骤 completed 且不带 fromStep 时应拒绝')
|
|
105
|
+
assert(result.error && result.error.includes('--from-step'), '错误提示应包含 --from-step')
|
|
106
|
+
}
|
|
107
|
+
rmSync(mkdtempSync(join(tmpdir(), 'sillyspec-rev-test-')), { recursive: true })
|
|
108
|
+
|
|
109
|
+
// ─────────────────────────────────────────
|
|
110
|
+
// Case 3: --reopen --from-step index → 正确标记步骤
|
|
111
|
+
// ─────────────────────────────────────────
|
|
112
|
+
console.log('\n--- Case 3: --reopen --from-step index 正确标记步骤 ---')
|
|
113
|
+
{
|
|
114
|
+
const { cwd } = createTempProject()
|
|
115
|
+
const changeName = 'rev-test-3'
|
|
116
|
+
const pm = await setupProgress(cwd, changeName)
|
|
117
|
+
await markStageCompleted(pm, cwd, changeName, 'brainstorm', ['需求澄清', '方案发散', '方案选择', '设计整理'])
|
|
118
|
+
|
|
119
|
+
const result = await pm.reopenStage(cwd, 'brainstorm', { fromStep: 3, changeName })
|
|
120
|
+
assert(result.ok, 'reopen with fromStep=3 应成功')
|
|
121
|
+
assert(result.revision === 1, 'revision 应为 1')
|
|
122
|
+
|
|
123
|
+
const data = await pm.read(cwd, changeName)
|
|
124
|
+
const steps = data.stages['brainstorm'].steps
|
|
125
|
+
assert(steps[0].status === 'completed', 'step 1 (index 0) 应保持 completed')
|
|
126
|
+
assert(steps[1].status === 'completed', 'step 2 (index 1) 应保持 completed')
|
|
127
|
+
assert(steps[2].status === 'pending', 'step 3 (index 2) 应为 pending')
|
|
128
|
+
assert(steps[3].status === 'stale', 'step 4 (index 3) 应为 stale')
|
|
129
|
+
assert(data.stages['brainstorm'].status === 'revising', 'stage 状态应为 revising')
|
|
130
|
+
}
|
|
131
|
+
rmSync(mkdtempSync(join(tmpdir(), 'sillyspec-rev-test-')), { recursive: true })
|
|
132
|
+
|
|
133
|
+
// ─────────────────────────────────────────
|
|
134
|
+
// Case 4: --reopen --from-step name → 正确标记步骤
|
|
135
|
+
// ─────────────────────────────────────────
|
|
136
|
+
console.log('\n--- Case 4: --reopen --from-step name 正确标记步骤 ---')
|
|
137
|
+
{
|
|
138
|
+
const { cwd } = createTempProject()
|
|
139
|
+
const changeName = 'rev-test-4'
|
|
140
|
+
const pm = await setupProgress(cwd, changeName)
|
|
141
|
+
await markStageCompleted(pm, cwd, changeName, 'plan', ['复杂度分类', '状态检查', '上下文加载', 'Wave 分组'])
|
|
142
|
+
|
|
143
|
+
const result = await pm.reopenStage(cwd, 'plan', { fromStep: '上下文加载', changeName })
|
|
144
|
+
assert(result.ok, 'reopen with fromStep="上下文加载" 应成功')
|
|
145
|
+
assert(result.fromStep === '上下文加载', 'fromStep 应为 "上下文加载"')
|
|
146
|
+
|
|
147
|
+
const data = await pm.read(cwd, changeName)
|
|
148
|
+
const steps = data.stages['plan'].steps
|
|
149
|
+
assert(steps[0].status === 'completed', 'step 1 应保持 completed')
|
|
150
|
+
assert(steps[1].status === 'completed', 'step 2 应保持 completed')
|
|
151
|
+
assert(steps[2].status === 'pending', 'step 3 (上下文加载) 应为 pending')
|
|
152
|
+
assert(steps[3].status === 'stale', 'step 4 应为 stale')
|
|
153
|
+
}
|
|
154
|
+
rmSync(mkdtempSync(join(tmpdir(), 'sillyspec-rev-test-')), { recursive: true })
|
|
155
|
+
|
|
156
|
+
// ─────────────────────────────────────────
|
|
157
|
+
// Case 5: --from-step 不存在 → fail-fast
|
|
158
|
+
// ─────────────────────────────────────────
|
|
159
|
+
console.log('\n--- Case 5: --from-step 不存在 fail-fast ---')
|
|
160
|
+
{
|
|
161
|
+
const { cwd } = createTempProject()
|
|
162
|
+
const changeName = 'rev-test-5'
|
|
163
|
+
const pm = await setupProgress(cwd, changeName)
|
|
164
|
+
await markStageCompleted(pm, cwd, changeName, 'brainstorm', ['需求澄清', '方案发散'])
|
|
165
|
+
|
|
166
|
+
// 名称不存在
|
|
167
|
+
const result1 = await pm.reopenStage(cwd, 'brainstorm', { fromStep: '不存在的步骤', changeName })
|
|
168
|
+
assert(!result1.ok, '不存在的步骤名应失败')
|
|
169
|
+
assert(result1.error && result1.error.includes('步骤不存在'), '错误应包含"步骤不存在"')
|
|
170
|
+
|
|
171
|
+
// 序号超出范围
|
|
172
|
+
const result2 = await pm.reopenStage(cwd, 'brainstorm', { fromStep: 99, changeName })
|
|
173
|
+
assert(!result2.ok, '超出范围的序号应失败')
|
|
174
|
+
assert(result2.error && result2.error.includes('超出范围'), '错误应包含"超出范围"')
|
|
175
|
+
}
|
|
176
|
+
rmSync(mkdtempSync(join(tmpdir(), 'sillyspec-rev-test-')), { recursive: true })
|
|
177
|
+
|
|
178
|
+
// ─────────────────────────────────────────
|
|
179
|
+
// Case 6: --from-step 后续步骤 stale
|
|
180
|
+
// ─────────────────────────────────────────
|
|
181
|
+
console.log('\n--- Case 6: --from-step 后续步骤 stale ---')
|
|
182
|
+
{
|
|
183
|
+
const { cwd } = createTempProject()
|
|
184
|
+
const changeName = 'rev-test-6'
|
|
185
|
+
const pm = await setupProgress(cwd, changeName)
|
|
186
|
+
await markStageCompleted(pm, cwd, changeName, 'brainstorm', ['s1', 's2', 's3', 's4', 's5'])
|
|
187
|
+
|
|
188
|
+
const result = await pm.reopenStage(cwd, 'brainstorm', { fromStep: 2, changeName })
|
|
189
|
+
assert(result.ok, 'reopen fromStep=2 应成功')
|
|
190
|
+
|
|
191
|
+
const data = await pm.read(cwd, changeName)
|
|
192
|
+
const steps = data.stages['brainstorm'].steps
|
|
193
|
+
assert(steps[0].status === 'completed', 's1 保持 completed')
|
|
194
|
+
assert(steps[1].status === 'pending', 's2 变为 pending')
|
|
195
|
+
assert(steps[2].status === 'stale', 's3 变为 stale')
|
|
196
|
+
assert(steps[3].status === 'stale', 's4 变为 stale')
|
|
197
|
+
assert(steps[4].status === 'stale', 's5 变为 stale')
|
|
198
|
+
}
|
|
199
|
+
rmSync(mkdtempSync(join(tmpdir(), 'sillyspec-rev-test-')), { recursive: true })
|
|
200
|
+
|
|
201
|
+
// ─────────────────────────────────────────
|
|
202
|
+
// Case 7: 下游 stage cascade stale
|
|
203
|
+
// ─────────────────────────────────────────
|
|
204
|
+
console.log('\n--- Case 7: 下游 stage cascade stale ---')
|
|
205
|
+
{
|
|
206
|
+
const { cwd } = createTempProject()
|
|
207
|
+
const changeName = 'rev-test-7'
|
|
208
|
+
const pm = await setupProgress(cwd, changeName)
|
|
209
|
+
// brain + plan + execute 都 completed
|
|
210
|
+
await markStageCompleted(pm, cwd, changeName, 'brainstorm', ['s1', 's2', 's3'])
|
|
211
|
+
await markStageCompleted(pm, cwd, changeName, 'plan', ['p1', 'p2'])
|
|
212
|
+
await markStageCompleted(pm, cwd, changeName, 'execute', ['e1', 'e2'])
|
|
213
|
+
await markStageCompleted(pm, cwd, changeName, 'verify', ['v1', 'v2'])
|
|
214
|
+
|
|
215
|
+
// reopen brainstorm from step 2
|
|
216
|
+
const result = await pm.reopenStage(cwd, 'brainstorm', { fromStep: 2, changeName })
|
|
217
|
+
assert(result.ok, 'reopen brainstorm fromStep=2 应成功')
|
|
218
|
+
|
|
219
|
+
const data = await pm.read(cwd, changeName)
|
|
220
|
+
assert(data.stages['brainstorm'].status === 'revising', 'brainstorm 应为 revising')
|
|
221
|
+
assert(data.stages['plan'].status === 'stale', 'plan 应为 stale')
|
|
222
|
+
assert(data.stages['execute'].status === 'stale', 'execute 应为 stale')
|
|
223
|
+
assert(data.stages['verify'].status === 'stale', 'verify 应为 stale')
|
|
224
|
+
assert(data.stages['archive'].status !== 'stale' || data.stages['archive'].status === 'pending', 'archive 保持 pending(未执行过的不会变 stale)')
|
|
225
|
+
|
|
226
|
+
// 检查 staleReason
|
|
227
|
+
const planStaleReason = data.stages['plan'].staleReason
|
|
228
|
+
assert(planStaleReason && planStaleReason.includes('brainstorm'), 'plan staleReason 应包含 brainstorm')
|
|
229
|
+
|
|
230
|
+
// 验证 scan 不在 cascade 中(scan 是 brainstorm 的上游)
|
|
231
|
+
assert(data.stages['scan'].status !== 'stale', 'scan 不应被 cascade(它是 brainstorm 的上游,不是下游)')
|
|
232
|
+
}
|
|
233
|
+
rmSync(mkdtempSync(join(tmpdir(), 'sillyspec-rev-test-')), { recursive: true })
|
|
234
|
+
|
|
235
|
+
// ─────────────────────────────────────────
|
|
236
|
+
// Case 8: stale 下游直接 run → 拒绝
|
|
237
|
+
// ─────────────────────────────────────────
|
|
238
|
+
console.log('\n--- Case 8: stale 下游直接 run 拒绝 ---')
|
|
239
|
+
{
|
|
240
|
+
const { cwd } = createTempProject()
|
|
241
|
+
const changeName = 'rev-test-8'
|
|
242
|
+
const pm = await setupProgress(cwd, changeName)
|
|
243
|
+
await markStageCompleted(pm, cwd, changeName, 'brainstorm', ['s1', 's2'])
|
|
244
|
+
await markStageCompleted(pm, cwd, changeName, 'plan', ['p1', 'p2'])
|
|
245
|
+
|
|
246
|
+
// reopen brainstorm → plan becomes stale
|
|
247
|
+
await pm.reopenStage(cwd, 'brainstorm', { fromStep: 1, changeName })
|
|
248
|
+
|
|
249
|
+
const data = await pm.read(cwd, changeName)
|
|
250
|
+
const planStatus = data.stages['plan'].status
|
|
251
|
+
assert(planStatus === 'stale', 'plan 应为 stale')
|
|
252
|
+
|
|
253
|
+
// 模拟 run 命令的规则 5 检查
|
|
254
|
+
const isReopen = false
|
|
255
|
+
const shouldReject = planStatus === 'stale' && !isReopen
|
|
256
|
+
assert(shouldReject, 'stale 阶段直接 run 应被拒绝')
|
|
257
|
+
|
|
258
|
+
// reopen plan from step 1 应该可以
|
|
259
|
+
const reopenResult = await pm.reopenStage(cwd, 'plan', { fromStep: 1, changeName })
|
|
260
|
+
assert(reopenResult.ok === true, 'stale 阶段用 --reopen --from-step 1 应该可以')
|
|
261
|
+
}
|
|
262
|
+
rmSync(mkdtempSync(join(tmpdir(), 'sillyspec-rev-test-')), { recursive: true })
|
|
263
|
+
|
|
264
|
+
// ─────────────────────────────────────────
|
|
265
|
+
// Case 9: waiting/pending 阶段 --reopen 无 from-step → 允许继续
|
|
266
|
+
// ─────────────────────────────────────────
|
|
267
|
+
console.log('\n--- Case 9: waiting/pending 阶段 --reopen 无 from-step 允许继续 ---')
|
|
268
|
+
{
|
|
269
|
+
const { cwd } = createTempProject()
|
|
270
|
+
const changeName = 'rev-test-9'
|
|
271
|
+
const pm = await setupProgress(cwd, changeName)
|
|
272
|
+
|
|
273
|
+
// 手动构造一个有 waiting 步骤的 brainstorm
|
|
274
|
+
const now = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
275
|
+
const data = await pm.read(cwd, changeName)
|
|
276
|
+
data.stages['brainstorm'] = {
|
|
277
|
+
status: 'in-progress',
|
|
278
|
+
startedAt: now,
|
|
279
|
+
completedAt: null,
|
|
280
|
+
steps: [
|
|
281
|
+
{ name: '需求澄清', status: 'completed', completedAt: now },
|
|
282
|
+
{ name: '方案发散', status: 'waiting', waitReason: '需要用户确认' },
|
|
283
|
+
{ name: '方案选择', status: 'pending' },
|
|
284
|
+
],
|
|
285
|
+
}
|
|
286
|
+
data.currentStage = 'brainstorm'
|
|
287
|
+
await pm._write(cwd, data, changeName)
|
|
288
|
+
|
|
289
|
+
// reopen 不带 fromStep,应该能找到 waiting 步骤并继续
|
|
290
|
+
const result = await pm.reopenStage(cwd, 'brainstorm', { changeName })
|
|
291
|
+
assert(result.ok, '有 waiting 步骤时 --reopen 不带 fromStep 应成功')
|
|
292
|
+
assert(result.fromStep === '方案发散', '应从 waiting 步骤继续')
|
|
293
|
+
}
|
|
294
|
+
rmSync(mkdtempSync(join(tmpdir(), 'sillyspec-rev-test-')), { recursive: true })
|
|
295
|
+
|
|
296
|
+
// ─────────────────────────────────────────
|
|
297
|
+
// Case 10: reopen 不改动产物文件
|
|
298
|
+
// ─────────────────────────────────────────
|
|
299
|
+
console.log('\n--- Case 10: reopen 不改动产物文件 ---')
|
|
300
|
+
{
|
|
301
|
+
const { cwd, specDir } = createTempProject()
|
|
302
|
+
const changeName = 'rev-test-10'
|
|
303
|
+
const pm = await setupProgress(cwd, changeName)
|
|
304
|
+
await markStageCompleted(pm, cwd, changeName, 'brainstorm', ['s1', 's2', 's3'])
|
|
305
|
+
|
|
306
|
+
// 创建产物文件
|
|
307
|
+
const changeDir = join(specDir, 'changes', changeName)
|
|
308
|
+
mkdirSync(changeDir, { recursive: true })
|
|
309
|
+
const designPath = join(changeDir, 'design.md')
|
|
310
|
+
const designContent = '# Original Design\n\nOriginal content'
|
|
311
|
+
writeFileSync(designPath, designContent)
|
|
312
|
+
|
|
313
|
+
// reopen
|
|
314
|
+
const result = await pm.reopenStage(cwd, 'brainstorm', { fromStep: 2, changeName })
|
|
315
|
+
assert(result.ok, 'reopen 应成功')
|
|
316
|
+
|
|
317
|
+
// 验证文件未被改动
|
|
318
|
+
assert(existsSync(designPath), 'design.md 应仍然存在')
|
|
319
|
+
const afterContent = readFileSync(designPath, 'utf8')
|
|
320
|
+
assert(afterContent === designContent, 'design.md 内容应未被改动')
|
|
321
|
+
}
|
|
322
|
+
rmSync(mkdtempSync(join(tmpdir(), 'sillyspec-rev-test-')), { recursive: true })
|
|
323
|
+
|
|
324
|
+
// ─────────────────────────────────────────
|
|
325
|
+
// Bonus: 多次 reopen revision 递增
|
|
326
|
+
// ─────────────────────────────────────────
|
|
327
|
+
console.log('\n--- Bonus: 多次 reopen revision 递增 ---')
|
|
328
|
+
{
|
|
329
|
+
const { cwd } = createTempProject()
|
|
330
|
+
const changeName = 'rev-test-bonus'
|
|
331
|
+
const pm = await setupProgress(cwd, changeName)
|
|
332
|
+
await markStageCompleted(pm, cwd, changeName, 'brainstorm', ['s1', 's2', 's3'])
|
|
333
|
+
|
|
334
|
+
const r1 = await pm.reopenStage(cwd, 'brainstorm', { fromStep: 2, changeName })
|
|
335
|
+
assert(r1.ok && r1.revision === 1, '第一次 reopen revision=1')
|
|
336
|
+
|
|
337
|
+
// 完成 step 2
|
|
338
|
+
const data = await pm.read(cwd, changeName)
|
|
339
|
+
data.stages['brainstorm'].steps[1].status = 'completed'
|
|
340
|
+
data.stages['brainstorm'].steps[1].completedAt = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
341
|
+
data.stages['brainstorm'].steps[2].status = 'completed'
|
|
342
|
+
data.stages['brainstorm'].steps[2].completedAt = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
343
|
+
data.stages['brainstorm'].status = 'completed'
|
|
344
|
+
data.stages['brainstorm'].completedAt = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
345
|
+
await pm._write(cwd, data, changeName)
|
|
346
|
+
|
|
347
|
+
// 第二次 reopen
|
|
348
|
+
const r2 = await pm.reopenStage(cwd, 'brainstorm', { fromStep: 3, changeName })
|
|
349
|
+
assert(r2.ok && r2.revision === 2, '第二次 reopen revision=2')
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ─────────────────────────────────────────
|
|
353
|
+
// Bonus: scan reopen → brainstorm cascade
|
|
354
|
+
// ─────────────────────────────────────────
|
|
355
|
+
console.log('\n--- Bonus: scan reopen 应 cascade 到 brainstorm ---')
|
|
356
|
+
{
|
|
357
|
+
const { cwd } = createTempProject()
|
|
358
|
+
const changeName = 'rev-test-scan'
|
|
359
|
+
const pm = await setupProgress(cwd, changeName)
|
|
360
|
+
await markStageCompleted(pm, cwd, changeName, 'scan', ['s1', 's2'])
|
|
361
|
+
await markStageCompleted(pm, cwd, changeName, 'brainstorm', ['b1', 'b2'])
|
|
362
|
+
|
|
363
|
+
const result = await pm.reopenStage(cwd, 'scan', { fromStep: 2, changeName })
|
|
364
|
+
assert(result.ok, 'reopen scan 应成功')
|
|
365
|
+
|
|
366
|
+
const data = await pm.read(cwd, changeName)
|
|
367
|
+
assert(data.stages['scan'].status === 'revising', 'scan 应为 revising')
|
|
368
|
+
assert(data.stages['brainstorm'].status === 'stale', 'brainstorm 应被 cascade 为 stale')
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ─────────────────────────────────────────
|
|
372
|
+
// TODO Fix 1: stale 阶段 --status 不拦截
|
|
373
|
+
// ─────────────────────────────────────────
|
|
374
|
+
console.log('\n--- TODO Fix 1: stale 阶段 --status 放行 ---')
|
|
375
|
+
{
|
|
376
|
+
const { cwd } = createTempProject()
|
|
377
|
+
const changeName = 'rev-todo-1'
|
|
378
|
+
const pm = await setupProgress(cwd, changeName)
|
|
379
|
+
await markStageCompleted(pm, cwd, changeName, 'brainstorm', ['s1', 's2'])
|
|
380
|
+
await markStageCompleted(pm, cwd, changeName, 'plan', ['p1', 'p2'])
|
|
381
|
+
|
|
382
|
+
// reopen brainstorm → plan cascade stale
|
|
383
|
+
await pm.reopenStage(cwd, 'brainstorm', { fromStep: 1, changeName })
|
|
384
|
+
|
|
385
|
+
const data = await pm.read(cwd, changeName)
|
|
386
|
+
assert(data.stages['plan'].status === 'stale', 'plan 应为 stale')
|
|
387
|
+
|
|
388
|
+
// 模拟 run.js 的拦截逻辑:stale + --status 应放行
|
|
389
|
+
const isStatus = true
|
|
390
|
+
const isReopen = false
|
|
391
|
+
const isReset = false
|
|
392
|
+
const stageStatus = data.stages['plan'].status
|
|
393
|
+
const shouldBlock = stageStatus === 'stale' && !isReopen && !isStatus && !isReset
|
|
394
|
+
assert(!shouldBlock, 'stale + --status 不应被拦截')
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ─────────────────────────────────────────
|
|
398
|
+
// TODO Fix 2: stale 阶段 --reset 不拦截
|
|
399
|
+
// ─────────────────────────────────────────
|
|
400
|
+
console.log('\n--- TODO Fix 2: stale 阶段 --reset 放行 ---')
|
|
401
|
+
{
|
|
402
|
+
const { cwd } = createTempProject()
|
|
403
|
+
const changeName = 'rev-todo-2'
|
|
404
|
+
const pm = await setupProgress(cwd, changeName)
|
|
405
|
+
await markStageCompleted(pm, cwd, changeName, 'brainstorm', ['s1', 's2'])
|
|
406
|
+
await markStageCompleted(pm, cwd, changeName, 'plan', ['p1', 'p2'])
|
|
407
|
+
|
|
408
|
+
await pm.reopenStage(cwd, 'brainstorm', { fromStep: 1, changeName })
|
|
409
|
+
|
|
410
|
+
const data = await pm.read(cwd, changeName)
|
|
411
|
+
const isReset = true
|
|
412
|
+
const isStatus = false
|
|
413
|
+
const isReopen = false
|
|
414
|
+
const stageStatus = data.stages['plan'].status
|
|
415
|
+
const shouldBlock = stageStatus === 'stale' && !isReopen && !isStatus && !isReset
|
|
416
|
+
assert(!shouldBlock, 'stale + --reset 不应被拦截')
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// ─────────────────────────────────────────
|
|
420
|
+
// TODO Fix 3: stale empty steps + --reopen --from-step 1 自动初始化
|
|
421
|
+
// ─────────────────────────────────────────
|
|
422
|
+
console.log('\n--- TODO Fix 3: stale empty steps + reopen from-step 1 ---')
|
|
423
|
+
{
|
|
424
|
+
const { cwd } = createTempProject()
|
|
425
|
+
const changeName = 'rev-todo-3'
|
|
426
|
+
const pm = await setupProgress(cwd, changeName)
|
|
427
|
+
|
|
428
|
+
// brainstorm completed with steps
|
|
429
|
+
await markStageCompleted(pm, cwd, changeName, 'brainstorm', ['s1', 's2'])
|
|
430
|
+
|
|
431
|
+
// plan marked completed but WITHOUT steps (模拟 cascade stale 后 steps 为空)
|
|
432
|
+
const data = await pm.read(cwd, changeName)
|
|
433
|
+
data.stages['plan'] = {
|
|
434
|
+
status: 'completed',
|
|
435
|
+
startedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
|
|
436
|
+
completedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
|
|
437
|
+
steps: [], // 空 steps
|
|
438
|
+
}
|
|
439
|
+
data.currentStage = 'brainstorm'
|
|
440
|
+
await pm._write(cwd, data, changeName)
|
|
441
|
+
|
|
442
|
+
// reopen brainstorm → plan cascade stale
|
|
443
|
+
await pm.reopenStage(cwd, 'brainstorm', { fromStep: 1, changeName })
|
|
444
|
+
|
|
445
|
+
// plan 现在是 stale,steps 为空
|
|
446
|
+
const data2 = await pm.read(cwd, changeName)
|
|
447
|
+
assert(data2.stages['plan'].status === 'stale', 'plan 应为 stale')
|
|
448
|
+
assert(data2.stages['plan'].steps.length === 0, 'plan steps 应为空')
|
|
449
|
+
|
|
450
|
+
// 模拟 run.js 的 ensureStageSteps:手动注入 steps
|
|
451
|
+
// 因为 reopenStage 需要 steps 来 resolve from-step
|
|
452
|
+
// 这里测试逻辑:先 ensure steps,再 reopen
|
|
453
|
+
const data3 = await pm.read(cwd, changeName)
|
|
454
|
+
data3.stages['plan'].steps = [
|
|
455
|
+
{ name: '复杂度分类', status: 'pending' },
|
|
456
|
+
{ name: '状态检查', status: 'pending' },
|
|
457
|
+
]
|
|
458
|
+
await pm._write(cwd, data3, changeName)
|
|
459
|
+
|
|
460
|
+
// 现在 reopen plan from step 1
|
|
461
|
+
const result = await pm.reopenStage(cwd, 'plan', { fromStep: 1, changeName })
|
|
462
|
+
assert(result.ok, 'stale empty steps 初始化后 reopen 应成功')
|
|
463
|
+
assert(result.fromStep === '复杂度分类', 'fromStep 应为第一个步骤')
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ─────────────────────────────────────────
|
|
467
|
+
// TODO Fix 4: stale empty steps + --reopen 无 from-step 仍 fail-fast
|
|
468
|
+
// ─────────────────────────────────────────
|
|
469
|
+
console.log('\n--- TODO Fix 4: stale empty steps + reopen 无 from-step fail-fast ---')
|
|
470
|
+
{
|
|
471
|
+
const { cwd } = createTempProject()
|
|
472
|
+
const changeName = 'rev-todo-4'
|
|
473
|
+
const pm = await setupProgress(cwd, changeName)
|
|
474
|
+
|
|
475
|
+
await markStageCompleted(pm, cwd, changeName, 'brainstorm', ['s1', 's2'])
|
|
476
|
+
|
|
477
|
+
// plan stale with empty steps
|
|
478
|
+
const data = await pm.read(cwd, changeName)
|
|
479
|
+
data.stages['plan'] = {
|
|
480
|
+
status: 'stale',
|
|
481
|
+
startedAt: null,
|
|
482
|
+
completedAt: null,
|
|
483
|
+
steps: [],
|
|
484
|
+
staleReason: 'upstream brainstorm revised',
|
|
485
|
+
}
|
|
486
|
+
await pm._write(cwd, data, changeName)
|
|
487
|
+
|
|
488
|
+
// reopen without fromStep → should fail because all steps are... well there are none
|
|
489
|
+
const result = await pm.reopenStage(cwd, 'plan', { changeName })
|
|
490
|
+
assert(!result.ok, 'stale empty steps 无 fromStep 应失败')
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ─────────────────────────────────────────
|
|
494
|
+
// v1.1: progress 展示 revising + stale 信息
|
|
495
|
+
// ─────────────────────────────────────────
|
|
496
|
+
console.log('\n--- v1.1: progress 展示 revising 信息 ---')
|
|
497
|
+
{
|
|
498
|
+
const { cwd } = createTempProject()
|
|
499
|
+
const changeName = 'rev-11-1'
|
|
500
|
+
const pm = await setupProgress(cwd, changeName)
|
|
501
|
+
await markStageCompleted(pm, cwd, changeName, 'brainstorm', ['s1', 's2', 's3'])
|
|
502
|
+
await markStageCompleted(pm, cwd, changeName, 'plan', ['p1', 'p2'])
|
|
503
|
+
|
|
504
|
+
await pm.reopenStage(cwd, 'brainstorm', { fromStep: 2, changeName })
|
|
505
|
+
|
|
506
|
+
const data = await pm.read(cwd, changeName)
|
|
507
|
+
// 验证 revision 信息存在
|
|
508
|
+
assert(data.stages['brainstorm'].revision === 1, 'brainstorm revision 应为 1')
|
|
509
|
+
assert(!!data.stages['brainstorm'].reopenedFromStep, 'brainstorm 应有 reopenedFromStep')
|
|
510
|
+
assert(data.stages['plan'].status === 'stale', 'plan 应为 stale')
|
|
511
|
+
assert(!!data.stages['plan'].staleReason, 'plan 应有 staleReason')
|
|
512
|
+
|
|
513
|
+
// 验证 _getNextSuggestion 返回正确建议
|
|
514
|
+
const suggestion = pm._getNextSuggestion(data)
|
|
515
|
+
assert(suggestion !== null, '应有 suggestion')
|
|
516
|
+
assert(suggestion.text.includes('brainstorm') || suggestion.text.includes('需求探索'), 'suggestion 应提到 brainstorm 修订中')
|
|
517
|
+
assert(suggestion.command === 'sillyspec run brainstorm', 'suggestion command 应为继续 brainstorm')
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// ─────────────────────────────────────────
|
|
521
|
+
// v1.1: _getNextSuggestion 返回 stale 建议
|
|
522
|
+
// ─────────────────────────────────────────
|
|
523
|
+
console.log('\n--- v1.1: suggestion 返回 stale 阶段建议 ---')
|
|
524
|
+
{
|
|
525
|
+
const { cwd } = createTempProject()
|
|
526
|
+
const changeName = 'rev-11-2'
|
|
527
|
+
const pm = await setupProgress(cwd, changeName)
|
|
528
|
+
await markStageCompleted(pm, cwd, changeName, 'brainstorm', ['s1', 's2'])
|
|
529
|
+
await markStageCompleted(pm, cwd, changeName, 'plan', ['p1', 'p2'])
|
|
530
|
+
|
|
531
|
+
// reopen brainstorm → plan stale
|
|
532
|
+
await pm.reopenStage(cwd, 'brainstorm', { fromStep: 1, changeName })
|
|
533
|
+
|
|
534
|
+
// 手动完成 brainstorm 修订(不再 revising)
|
|
535
|
+
const data = await pm.read(cwd, changeName)
|
|
536
|
+
data.stages['brainstorm'].status = 'completed'
|
|
537
|
+
data.stages['brainstorm'].completedAt = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
538
|
+
await pm._write(cwd, data, changeName)
|
|
539
|
+
|
|
540
|
+
// 现在 brainstorm completed, plan stale
|
|
541
|
+
const data2 = await pm.read(cwd, changeName)
|
|
542
|
+
const suggestion = pm._getNextSuggestion(data2)
|
|
543
|
+
assert(suggestion !== null, '应有 suggestion')
|
|
544
|
+
assert(suggestion.text.includes('plan') || suggestion.text.includes('实现计划'), 'suggestion 应提到 plan')
|
|
545
|
+
assert(suggestion.command.includes('--reopen --from-step 1'), 'suggestion 应建议 reopen plan')
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// ─────────────────────────────────────────
|
|
549
|
+
// v1.1: checkConsistency 发现 completed + stale steps
|
|
550
|
+
// ─────────────────────────────────────────
|
|
551
|
+
console.log('\n--- v1.1: checkConsistency 发现 completed stage 有 stale steps ---')
|
|
552
|
+
{
|
|
553
|
+
const { cwd } = createTempProject()
|
|
554
|
+
const changeName = 'rev-11-3'
|
|
555
|
+
const pm = await setupProgress(cwd, changeName)
|
|
556
|
+
|
|
557
|
+
// 手动构造异常状态:stage completed 但 step stale
|
|
558
|
+
const now = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
559
|
+
const data = await pm.read(cwd, changeName)
|
|
560
|
+
data.stages['brainstorm'] = {
|
|
561
|
+
status: 'completed',
|
|
562
|
+
startedAt: now, completedAt: now,
|
|
563
|
+
steps: [
|
|
564
|
+
{ name: 's1', status: 'completed', completedAt: now },
|
|
565
|
+
{ name: 's2', status: 'stale' }, // 异常
|
|
566
|
+
],
|
|
567
|
+
}
|
|
568
|
+
await pm._write(cwd, data, changeName)
|
|
569
|
+
|
|
570
|
+
const result = await pm.checkConsistency(cwd, changeName)
|
|
571
|
+
assert(!result.ok, '应检测到问题')
|
|
572
|
+
assert(result.issues.length > 0, '应有 issue')
|
|
573
|
+
assert(result.issues.some(i => i.includes('stale') && i.includes('completed')), '应有 stale step + completed stage 问题')
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// ─────────────────────────────────────────
|
|
577
|
+
// v1.1: checkConsistency 发现 stale stage 缺 staleReason
|
|
578
|
+
// ─────────────────────────────────────────
|
|
579
|
+
console.log('\n--- v1.1: checkConsistency 发现 stale 缺 staleReason ---')
|
|
580
|
+
{
|
|
581
|
+
const { cwd } = createTempProject()
|
|
582
|
+
const changeName = 'rev-11-4'
|
|
583
|
+
const pm = await setupProgress(cwd, changeName)
|
|
584
|
+
|
|
585
|
+
const now = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
586
|
+
const data = await pm.read(cwd, changeName)
|
|
587
|
+
data.stages['plan'] = {
|
|
588
|
+
status: 'stale',
|
|
589
|
+
steps: [],
|
|
590
|
+
staleReason: null, // 缺失
|
|
591
|
+
}
|
|
592
|
+
await pm._write(cwd, data, changeName)
|
|
593
|
+
|
|
594
|
+
const result = await pm.checkConsistency(cwd, changeName)
|
|
595
|
+
assert(result.warnings.some(w => w.includes('plan') && w.includes('staleReason')), '应警告 plan 缺 staleReason')
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// ─────────────────────────────────────────
|
|
599
|
+
// v1.1: checkConsistency 发现上游 stale 但下游 completed
|
|
600
|
+
// ─────────────────────────────────────────
|
|
601
|
+
console.log('\n--- v1.1: checkConsistency 发现上游 stale 下游 completed ---')
|
|
602
|
+
{
|
|
603
|
+
const { cwd } = createTempProject()
|
|
604
|
+
const changeName = 'rev-11-5'
|
|
605
|
+
const pm = await setupProgress(cwd, changeName)
|
|
606
|
+
|
|
607
|
+
const now = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
608
|
+
const data = await pm.read(cwd, changeName)
|
|
609
|
+
data.stages['brainstorm'] = {
|
|
610
|
+
status: 'stale', staleReason: 'test', steps: [],
|
|
611
|
+
}
|
|
612
|
+
data.stages['plan'] = {
|
|
613
|
+
status: 'completed', startedAt: now, completedAt: now,
|
|
614
|
+
steps: [{ name: 'p1', status: 'completed', completedAt: now }],
|
|
615
|
+
}
|
|
616
|
+
await pm._write(cwd, data, changeName)
|
|
617
|
+
|
|
618
|
+
const result = await pm.checkConsistency(cwd, changeName)
|
|
619
|
+
assert(!result.ok, '应检测到问题')
|
|
620
|
+
assert(result.issues.some(i => i.includes('plan') && i.includes('brainstorm') && i.includes('stale')), '应检测到 plan completed 但 brainstorm stale')
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// ─────────────────────────────────────────
|
|
624
|
+
// v1.1: checkConsistency 正常状态无问题
|
|
625
|
+
// ─────────────────────────────────────────
|
|
626
|
+
console.log('\n--- v1.1: checkConsistency 正常状态无问题 ---')
|
|
627
|
+
{
|
|
628
|
+
const { cwd } = createTempProject()
|
|
629
|
+
const changeName = 'rev-11-6'
|
|
630
|
+
const pm = await setupProgress(cwd, changeName)
|
|
631
|
+
await markStageCompleted(pm, cwd, changeName, 'brainstorm', ['s1', 's2'])
|
|
632
|
+
|
|
633
|
+
const result = await pm.checkConsistency(cwd, changeName)
|
|
634
|
+
assert(result.ok, '正常状态应无问题')
|
|
635
|
+
assert(result.issues.length === 0, '不应有 issues')
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// ─────────────────────────────────────────
|
|
639
|
+
// v1.2: repair dry-run 不修改 DB
|
|
640
|
+
// ─────────────────────────────────────────
|
|
641
|
+
console.log('\n--- v1.2: repair dry-run 不修改 DB ---')
|
|
642
|
+
{
|
|
643
|
+
const { cwd } = createTempProject()
|
|
644
|
+
const changeName = 'rev-12-1'
|
|
645
|
+
const pm = await setupProgress(cwd, changeName)
|
|
646
|
+
|
|
647
|
+
// 构造 stale 缺 staleReason
|
|
648
|
+
const now = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
649
|
+
const data = await pm.read(cwd, changeName)
|
|
650
|
+
data.stages['plan'] = { status: 'stale', steps: [], staleReason: null }
|
|
651
|
+
await pm._write(cwd, data, changeName)
|
|
652
|
+
|
|
653
|
+
// dry-run
|
|
654
|
+
const result = await pm.repairConsistency(cwd, { apply: false, changeName })
|
|
655
|
+
assert(result.fixable.length > 0, '应有可修复项')
|
|
656
|
+
assert(result.applied.length === 0, 'dry-run 不应执行修复')
|
|
657
|
+
|
|
658
|
+
// 验证 DB 未变
|
|
659
|
+
const after = await pm.read(cwd, changeName)
|
|
660
|
+
assert(!after.stages['plan'].staleReason, 'dry-run 后 staleReason 仍应为空')
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// ─────────────────────────────────────────
|
|
664
|
+
// v1.2: repair --apply 修复 stale_reason
|
|
665
|
+
// ─────────────────────────────────────────
|
|
666
|
+
console.log('\n--- v1.2: repair --apply 修复 stale_reason ---')
|
|
667
|
+
{
|
|
668
|
+
const { cwd } = createTempProject()
|
|
669
|
+
const changeName = 'rev-12-2'
|
|
670
|
+
const pm = await setupProgress(cwd, changeName)
|
|
671
|
+
|
|
672
|
+
const data = await pm.read(cwd, changeName)
|
|
673
|
+
data.stages['plan'] = { status: 'stale', steps: [], staleReason: null }
|
|
674
|
+
await pm._write(cwd, data, changeName)
|
|
675
|
+
|
|
676
|
+
const result = await pm.repairConsistency(cwd, { apply: true, changeName })
|
|
677
|
+
assert(result.applied.length > 0, '应有已修复项')
|
|
678
|
+
assert(result.applied.some(a => a.action === 'set_stale_reason'), '应有 set_stale_reason 修复')
|
|
679
|
+
|
|
680
|
+
const after = await pm.read(cwd, changeName)
|
|
681
|
+
assert(!!after.stages['plan'].staleReason, '修复后 plan 应有 staleReason')
|
|
682
|
+
assert(after.stages['plan'].staleReason.includes('upstream'), 'staleReason 应包含 upstream')
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// ─────────────────────────────────────────
|
|
686
|
+
// v1.2: repair --apply cascade 下游 completed -> stale
|
|
687
|
+
// ─────────────────────────────────────────
|
|
688
|
+
console.log('\n--- v1.2: repair --apply cascade 下游 completed -> stale ---')
|
|
689
|
+
{
|
|
690
|
+
const { cwd } = createTempProject()
|
|
691
|
+
const changeName = 'rev-12-3'
|
|
692
|
+
const pm = await setupProgress(cwd, changeName)
|
|
693
|
+
|
|
694
|
+
const now2 = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
695
|
+
const data = await pm.read(cwd, changeName)
|
|
696
|
+
data.stages['brainstorm'] = {
|
|
697
|
+
status: 'stale', staleReason: 'test', steps: [],
|
|
698
|
+
}
|
|
699
|
+
data.stages['plan'] = {
|
|
700
|
+
status: 'completed', startedAt: now2, completedAt: now2,
|
|
701
|
+
steps: [{ name: 'p1', status: 'completed', completedAt: now2 }],
|
|
702
|
+
}
|
|
703
|
+
await pm._write(cwd, data, changeName)
|
|
704
|
+
|
|
705
|
+
const result = await pm.repairConsistency(cwd, { apply: true, changeName })
|
|
706
|
+
assert(result.applied.some(a => a.action === 'cascade_stale' && a.stage === 'plan'), '应有 plan cascade_stale 修复')
|
|
707
|
+
|
|
708
|
+
const after = await pm.read(cwd, changeName)
|
|
709
|
+
assert(after.stages['plan'].status === 'stale', '修复后 plan 应为 stale')
|
|
710
|
+
assert(!!after.stages['plan'].staleReason, 'plan 应有 staleReason')
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// ─────────────────────────────────────────
|
|
714
|
+
// v1.2: repair --apply 补 revising reopened_at
|
|
715
|
+
// ─────────────────────────────────────────
|
|
716
|
+
console.log('\n--- v1.2: repair --apply 补 revising reopened_at ---')
|
|
717
|
+
{
|
|
718
|
+
const { cwd } = createTempProject()
|
|
719
|
+
const changeName = 'rev-12-4'
|
|
720
|
+
const pm = await setupProgress(cwd, changeName)
|
|
721
|
+
|
|
722
|
+
const data = await pm.read(cwd, changeName)
|
|
723
|
+
data.stages['brainstorm'] = {
|
|
724
|
+
status: 'revising', revision: 1, reopenedFromStep: '1: s1',
|
|
725
|
+
reopenedAt: null, steps: [{ name: 's1', status: 'pending' }],
|
|
726
|
+
}
|
|
727
|
+
await pm._write(cwd, data, changeName)
|
|
728
|
+
|
|
729
|
+
const result = await pm.repairConsistency(cwd, { apply: true, changeName })
|
|
730
|
+
assert(result.applied.some(a => a.action === 'set_reopened_at'), '应有 set_reopened_at 修复')
|
|
731
|
+
|
|
732
|
+
const after = await pm.read(cwd, changeName)
|
|
733
|
+
assert(!!after.stages['brainstorm'].reopenedAt, '修复后应有 reopenedAt')
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// ─────────────────────────────────────────
|
|
737
|
+
// v1.2: completed + stale steps 只报告 manual,不修改
|
|
738
|
+
// ─────────────────────────────────────────
|
|
739
|
+
console.log('\n--- v1.2: completed + stale steps 只报告不修复 ---')
|
|
740
|
+
{
|
|
741
|
+
const { cwd } = createTempProject()
|
|
742
|
+
const changeName = 'rev-12-5'
|
|
743
|
+
const pm = await setupProgress(cwd, changeName)
|
|
744
|
+
|
|
745
|
+
const now3 = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
746
|
+
const data = await pm.read(cwd, changeName)
|
|
747
|
+
data.stages['brainstorm'] = {
|
|
748
|
+
status: 'completed', startedAt: now3, completedAt: now3,
|
|
749
|
+
steps: [
|
|
750
|
+
{ name: 's1', status: 'completed', completedAt: now3 },
|
|
751
|
+
{ name: 's2', status: 'stale' }, // 异常
|
|
752
|
+
],
|
|
753
|
+
}
|
|
754
|
+
await pm._write(cwd, data, changeName)
|
|
755
|
+
|
|
756
|
+
const result = await pm.repairConsistency(cwd, { apply: true, changeName })
|
|
757
|
+
assert(result.manual.length > 0, '应有 manual 项')
|
|
758
|
+
assert(result.manual.some(m => m.includes('stale') && m.includes('completed')), '应有 stale step + completed manual')
|
|
759
|
+
assert(result.applied.length === 0, '不应有自动修复')
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// ─────────────────────────────────────────
|
|
763
|
+
// v1.2: 正常状态 repair 无操作
|
|
764
|
+
// ─────────────────────────────────────────
|
|
765
|
+
console.log('\n--- v1.2: 正常状态 repair 无操作 ---')
|
|
766
|
+
{
|
|
767
|
+
const { cwd } = createTempProject()
|
|
768
|
+
const changeName = 'rev-12-6'
|
|
769
|
+
const pm = await setupProgress(cwd, changeName)
|
|
770
|
+
await markStageCompleted(pm, cwd, changeName, 'brainstorm', ['s1', 's2'])
|
|
771
|
+
|
|
772
|
+
const result = await pm.repairConsistency(cwd, { apply: true, changeName })
|
|
773
|
+
assert(result.fixable.length === 0, '不应有可修复项')
|
|
774
|
+
assert(result.manual.length === 0, '不应有 manual 项')
|
|
775
|
+
assert(result.applied.length === 0, '不应执行任何修复')
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// ─────────────────────────────────────────
|
|
779
|
+
// Execute Stale Safety: plan stale 时 execute 被拒绝
|
|
780
|
+
// ─────────────────────────────────────────
|
|
781
|
+
console.log('\n--- Execute Safety: plan stale → execute stale → run 拒绝 ---')
|
|
782
|
+
{
|
|
783
|
+
const { cwd } = createTempProject()
|
|
784
|
+
const changeName = 'exec-safety-1'
|
|
785
|
+
const pm = await setupProgress(cwd, changeName)
|
|
786
|
+
await markStageCompleted(pm, cwd, changeName, 'brainstorm', ['s1', 's2'])
|
|
787
|
+
await markStageCompleted(pm, cwd, changeName, 'plan', ['p1', 'p2'])
|
|
788
|
+
await markStageCompleted(pm, cwd, changeName, 'execute', ['e1', 'e2'])
|
|
789
|
+
|
|
790
|
+
// reopen plan → execute cascade stale
|
|
791
|
+
await pm.reopenStage(cwd, 'plan', { fromStep: 1, changeName })
|
|
792
|
+
|
|
793
|
+
const data = await pm.read(cwd, changeName)
|
|
794
|
+
assert(data.stages['execute'].status === 'stale', 'execute 应为 stale')
|
|
795
|
+
|
|
796
|
+
// 模拟 run.js 规则 5:stale 直接 run 被拒
|
|
797
|
+
const isReopen = false
|
|
798
|
+
const isStatus = false
|
|
799
|
+
const isReset = false
|
|
800
|
+
const shouldBlock = data.stages['execute'].status === 'stale' && !isReopen && !isStatus && !isReset
|
|
801
|
+
assert(shouldBlock, 'execute stale 直接 run 应被拒绝')
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// ─────────────────────────────────────────
|
|
805
|
+
// Execute Stale Safety: execute --status 放行
|
|
806
|
+
// ─────────────────────────────────────────
|
|
807
|
+
console.log('\n--- Execute Safety: execute stale + --status 放行 ---')
|
|
808
|
+
{
|
|
809
|
+
const { cwd } = createTempProject()
|
|
810
|
+
const changeName = 'exec-safety-2'
|
|
811
|
+
const pm = await setupProgress(cwd, changeName)
|
|
812
|
+
await markStageCompleted(pm, cwd, changeName, 'brainstorm', ['s1'])
|
|
813
|
+
await markStageCompleted(pm, cwd, changeName, 'plan', ['p1'])
|
|
814
|
+
await markStageCompleted(pm, cwd, changeName, 'execute', ['e1'])
|
|
815
|
+
|
|
816
|
+
await pm.reopenStage(cwd, 'plan', { fromStep: 1, changeName })
|
|
817
|
+
|
|
818
|
+
const data = await pm.read(cwd, changeName)
|
|
819
|
+
// execute --status 应放行
|
|
820
|
+
const isStatus = true
|
|
821
|
+
const shouldBlock = data.stages['execute'].status === 'stale' && !isStatus
|
|
822
|
+
assert(!shouldBlock, 'execute stale + --status 不应被拦截')
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// ─────────────────────────────────────────
|
|
826
|
+
// Execute Stale Safety: execute reopen 后 steps 被重置
|
|
827
|
+
// ─────────────────────────────────────────
|
|
828
|
+
console.log('\n--- Execute Safety: execute reopen 后旧 steps 不保留 ---')
|
|
829
|
+
{
|
|
830
|
+
const { cwd } = createTempProject()
|
|
831
|
+
const changeName = 'exec-safety-3'
|
|
832
|
+
const pm = await setupProgress(cwd, changeName)
|
|
833
|
+
|
|
834
|
+
// 模拟 execute 之前有 3 个 wave steps
|
|
835
|
+
const now = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
836
|
+
const data = await pm.read(cwd, changeName)
|
|
837
|
+
data.stages['execute'] = {
|
|
838
|
+
status: 'stale',
|
|
839
|
+
staleReason: 'upstream plan revised',
|
|
840
|
+
startedAt: now, completedAt: null,
|
|
841
|
+
steps: [
|
|
842
|
+
{ name: 'Wave 1 执行', status: 'completed', completedAt: now },
|
|
843
|
+
{ name: 'Wave 2 执行', status: 'completed', completedAt: now },
|
|
844
|
+
{ name: 'Wave 3 执行', status: 'completed', completedAt: now },
|
|
845
|
+
{ name: '验收检查', status: 'completed', completedAt: now },
|
|
846
|
+
],
|
|
847
|
+
}
|
|
848
|
+
await pm._write(cwd, data, changeName)
|
|
849
|
+
|
|
850
|
+
// reopen execute from step 1
|
|
851
|
+
const result = await pm.reopenStage(cwd, 'execute', { fromStep: 1, changeName })
|
|
852
|
+
assert(result.ok, 'execute reopen 应成功')
|
|
853
|
+
|
|
854
|
+
const after = await pm.read(cwd, changeName)
|
|
855
|
+
assert(after.stages['execute'].status === 'revising', 'execute 应为 revising')
|
|
856
|
+
|
|
857
|
+
// 所有步骤应被重置(step 1 pending, 后续 stale)
|
|
858
|
+
const steps = after.stages['execute'].steps
|
|
859
|
+
assert(steps[0].status === 'pending', 'step 1 应为 pending')
|
|
860
|
+
for (let i = 1; i < steps.length; i++) {
|
|
861
|
+
assert(steps[i].status === 'stale', `step ${i+1} 应为 stale,实际 ${steps[i].status}`)
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// 旧 completed 状态不保留
|
|
865
|
+
assert(!steps.some(s => s.status === 'completed'), '不应有 completed steps(旧状态已清除)')
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// ─────────────────────────────────────────
|
|
869
|
+
// Execute Stale Safety: revision 递增后 reopen 正确
|
|
870
|
+
// ─────────────────────────────────────────
|
|
871
|
+
console.log('\n--- Execute Safety: execute revision 递增 ---')
|
|
872
|
+
{
|
|
873
|
+
const { cwd } = createTempProject()
|
|
874
|
+
const changeName = 'exec-safety-4'
|
|
875
|
+
const pm = await setupProgress(cwd, changeName)
|
|
876
|
+
await markStageCompleted(pm, cwd, changeName, 'brainstorm', ['s1'])
|
|
877
|
+
await markStageCompleted(pm, cwd, changeName, 'plan', ['p1'])
|
|
878
|
+
await markStageCompleted(pm, cwd, changeName, 'execute', ['e1', 'e2'])
|
|
879
|
+
|
|
880
|
+
// reopen execute
|
|
881
|
+
const r1 = await pm.reopenStage(cwd, 'execute', { fromStep: 1, changeName })
|
|
882
|
+
assert(r1.ok && r1.revision === 1, '第一次 reopen revision=1')
|
|
883
|
+
|
|
884
|
+
// 标记完成
|
|
885
|
+
const data = await pm.read(cwd, changeName)
|
|
886
|
+
data.stages['execute'].steps.forEach(s => { s.status = 'completed'; s.completedAt = new Date().toLocaleString('zh-CN', { hour12: false }) })
|
|
887
|
+
data.stages['execute'].status = 'completed'
|
|
888
|
+
data.stages['execute'].completedAt = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
889
|
+
await pm._write(cwd, data, changeName)
|
|
890
|
+
|
|
891
|
+
// 第二次 reopen
|
|
892
|
+
const r2 = await pm.reopenStage(cwd, 'execute', { fromStep: 1, changeName })
|
|
893
|
+
assert(r2.ok && r2.revision === 2, '第二次 reopen revision=2')
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// ─────────────────────────────────────────
|
|
897
|
+
// Execute Stale Safety: checkConsistency 检测 execute stale
|
|
898
|
+
// ─────────────────────────────────────────
|
|
899
|
+
console.log('\n--- Execute Safety: checkConsistency 检测 execute 旧状态 ---')
|
|
900
|
+
{
|
|
901
|
+
const { cwd } = createTempProject()
|
|
902
|
+
const changeName = 'exec-safety-5'
|
|
903
|
+
const pm = await setupProgress(cwd, changeName)
|
|
904
|
+
|
|
905
|
+
// 构造异常:plan stale 但 execute completed
|
|
906
|
+
const now2 = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
907
|
+
const data = await pm.read(cwd, changeName)
|
|
908
|
+
data.stages['plan'] = {
|
|
909
|
+
status: 'stale', staleReason: 'test', steps: [],
|
|
910
|
+
}
|
|
911
|
+
data.stages['execute'] = {
|
|
912
|
+
status: 'completed', startedAt: now2, completedAt: now2,
|
|
913
|
+
steps: [{ name: 'e1', status: 'completed', completedAt: now2 }],
|
|
914
|
+
}
|
|
915
|
+
await pm._write(cwd, data, changeName)
|
|
916
|
+
|
|
917
|
+
const result = await pm.checkConsistency(cwd, changeName)
|
|
918
|
+
assert(!result.ok, '应检测到问题')
|
|
919
|
+
assert(result.issues.some(i => i.includes('execute') && i.includes('plan') && i.includes('stale')), '应检测到 execute completed 但 plan stale')
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// ─────────────────────────────────────────
|
|
923
|
+
// Execute Stale Safety: repair 修复 execute cascade
|
|
924
|
+
// ─────────────────────────────────────────
|
|
925
|
+
console.log('\n--- Execute Safety: repair cascade execute stale ---')
|
|
926
|
+
{
|
|
927
|
+
const { cwd } = createTempProject()
|
|
928
|
+
const changeName = 'exec-safety-6'
|
|
929
|
+
const pm = await setupProgress(cwd, changeName)
|
|
930
|
+
|
|
931
|
+
const now3 = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
932
|
+
const data = await pm.read(cwd, changeName)
|
|
933
|
+
data.stages['plan'] = {
|
|
934
|
+
status: 'stale', staleReason: 'brainstorm revised', steps: [],
|
|
935
|
+
}
|
|
936
|
+
data.stages['execute'] = {
|
|
937
|
+
status: 'completed', startedAt: now3, completedAt: now3,
|
|
938
|
+
steps: [{ name: 'e1', status: 'completed', completedAt: now3 }],
|
|
939
|
+
}
|
|
940
|
+
await pm._write(cwd, data, changeName)
|
|
941
|
+
|
|
942
|
+
const result = await pm.repairConsistency(cwd, { apply: true, changeName })
|
|
943
|
+
assert(result.applied.some(a => a.action === 'cascade_stale' && a.stage === 'execute'), '应有 execute cascade_stale 修复')
|
|
944
|
+
|
|
945
|
+
const after = await pm.read(cwd, changeName)
|
|
946
|
+
assert(after.stages['execute'].status === 'stale', 'execute 应被修复为 stale')
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// ─────────────────────────────────────────
|
|
950
|
+
// Verify/Archive Safety: execute stale → verify stale → run 拒绝
|
|
951
|
+
// ─────────────────────────────────────────
|
|
952
|
+
console.log('\n--- Verify/Archive Safety: execute stale → verify/archive 被拒 ---')
|
|
953
|
+
{
|
|
954
|
+
const { cwd } = createTempProject()
|
|
955
|
+
const changeName = 'va-safety-1'
|
|
956
|
+
const pm = await setupProgress(cwd, changeName)
|
|
957
|
+
await markStageCompleted(pm, cwd, changeName, 'brainstorm', ['s1'])
|
|
958
|
+
await markStageCompleted(pm, cwd, changeName, 'plan', ['p1'])
|
|
959
|
+
await markStageCompleted(pm, cwd, changeName, 'execute', ['e1'])
|
|
960
|
+
await markStageCompleted(pm, cwd, changeName, 'verify', ['v1', 'v2'])
|
|
961
|
+
await markStageCompleted(pm, cwd, changeName, 'archive', ['a1', 'a2'])
|
|
962
|
+
|
|
963
|
+
// reopen plan → execute/verify/archive cascade stale
|
|
964
|
+
await pm.reopenStage(cwd, 'plan', { fromStep: 1, changeName })
|
|
965
|
+
|
|
966
|
+
const data = await pm.read(cwd, changeName)
|
|
967
|
+
assert(data.stages['verify'].status === 'stale', 'verify 应为 stale')
|
|
968
|
+
assert(data.stages['archive'].status === 'stale', 'archive 应为 stale')
|
|
969
|
+
|
|
970
|
+
// run verify/archive 被拒
|
|
971
|
+
const isReopen = false, isStatus = false, isReset = false
|
|
972
|
+
assert(data.stages['verify'].status === 'stale' && !isReopen && !isStatus && !isReset, 'verify stale 直接 run 应被拒')
|
|
973
|
+
assert(data.stages['archive'].status === 'stale' && !isReopen && !isStatus && !isReset, 'archive stale 直接 run 应被拒')
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// ─────────────────────────────────────────
|
|
977
|
+
// Verify/Archive Safety: verify stale + --status 放行
|
|
978
|
+
// ─────────────────────────────────────────
|
|
979
|
+
console.log('\n--- Verify/Archive Safety: verify stale + --status 放行 ---')
|
|
980
|
+
{
|
|
981
|
+
const { cwd } = createTempProject()
|
|
982
|
+
const changeName = 'va-safety-2'
|
|
983
|
+
const pm = await setupProgress(cwd, changeName)
|
|
984
|
+
await markStageCompleted(pm, cwd, changeName, 'brainstorm', ['s1'])
|
|
985
|
+
await markStageCompleted(pm, cwd, changeName, 'plan', ['p1'])
|
|
986
|
+
await markStageCompleted(pm, cwd, changeName, 'execute', ['e1'])
|
|
987
|
+
await markStageCompleted(pm, cwd, changeName, 'verify', ['v1'])
|
|
988
|
+
|
|
989
|
+
await pm.reopenStage(cwd, 'execute', { fromStep: 1, changeName })
|
|
990
|
+
|
|
991
|
+
const data = await pm.read(cwd, changeName)
|
|
992
|
+
assert(data.stages['verify'].status === 'stale', 'verify 应为 stale')
|
|
993
|
+
// --status 放行
|
|
994
|
+
const isStatus = true
|
|
995
|
+
assert(!(data.stages['verify'].status === 'stale' && !isStatus), 'verify stale + --status 不应被拦截')
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// ─────────────────────────────────────────
|
|
999
|
+
// Verify/Archive Safety: verify --reopen 清除旧 completed steps
|
|
1000
|
+
// ─────────────────────────────────────────
|
|
1001
|
+
console.log('\n--- Verify/Archive Safety: verify reopen 清除旧 steps ---')
|
|
1002
|
+
{
|
|
1003
|
+
const { cwd } = createTempProject()
|
|
1004
|
+
const changeName = 'va-safety-3'
|
|
1005
|
+
const pm = await setupProgress(cwd, changeName)
|
|
1006
|
+
await markStageCompleted(pm, cwd, changeName, 'verify', ['v1', 'v2', 'v3'])
|
|
1007
|
+
|
|
1008
|
+
const result = await pm.reopenStage(cwd, 'verify', { fromStep: 1, changeName })
|
|
1009
|
+
assert(result.ok, 'verify reopen 应成功')
|
|
1010
|
+
|
|
1011
|
+
const after = await pm.read(cwd, changeName)
|
|
1012
|
+
const steps = after.stages['verify'].steps
|
|
1013
|
+
assert(after.stages['verify'].status === 'revising', 'verify 应为 revising')
|
|
1014
|
+
assert(steps[0].status === 'pending', 'step 1 应为 pending')
|
|
1015
|
+
assert(steps[1].status === 'stale', 'step 2 应为 stale')
|
|
1016
|
+
assert(steps[2].status === 'stale', 'step 3 应为 stale')
|
|
1017
|
+
assert(!steps.some(s => s.status === 'completed'), '不应保留 completed steps')
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// ─────────────────────────────────────────
|
|
1021
|
+
// Verify/Archive Safety: archive --reopen 清除旧 completed steps
|
|
1022
|
+
// ─────────────────────────────────────────
|
|
1023
|
+
console.log('\n--- Verify/Archive Safety: archive reopen 清除旧 steps ---')
|
|
1024
|
+
{
|
|
1025
|
+
const { cwd } = createTempProject()
|
|
1026
|
+
const changeName = 'va-safety-4'
|
|
1027
|
+
const pm = await setupProgress(cwd, changeName)
|
|
1028
|
+
await markStageCompleted(pm, cwd, changeName, 'archive', ['a1', 'a2'])
|
|
1029
|
+
|
|
1030
|
+
const result = await pm.reopenStage(cwd, 'archive', { fromStep: 1, changeName })
|
|
1031
|
+
assert(result.ok, 'archive reopen 应成功')
|
|
1032
|
+
|
|
1033
|
+
const after = await pm.read(cwd, changeName)
|
|
1034
|
+
const steps = after.stages['archive'].steps
|
|
1035
|
+
assert(after.stages['archive'].status === 'revising', 'archive 应为 revising')
|
|
1036
|
+
assert(steps[0].status === 'pending', 'step 1 应为 pending')
|
|
1037
|
+
assert(steps[1].status === 'stale', 'step 2 应为 stale')
|
|
1038
|
+
assert(!steps.some(s => s.status === 'completed'), '不应保留 completed steps')
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// ─────────────────────────────────────────
|
|
1042
|
+
// Verify/Archive Safety: checkConsistency 检测 execute stale + verify/archive completed
|
|
1043
|
+
// ─────────────────────────────────────────
|
|
1044
|
+
console.log('\n--- Verify/Archive Safety: check 检测 execute stale 下游 ---')
|
|
1045
|
+
{
|
|
1046
|
+
const { cwd } = createTempProject()
|
|
1047
|
+
const changeName = 'va-safety-5'
|
|
1048
|
+
const pm = await setupProgress(cwd, changeName)
|
|
1049
|
+
|
|
1050
|
+
const now = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
1051
|
+
const data = await pm.read(cwd, changeName)
|
|
1052
|
+
data.stages['execute'] = { status: 'stale', staleReason: 'plan revised', steps: [] }
|
|
1053
|
+
data.stages['verify'] = { status: 'completed', startedAt: now, completedAt: now,
|
|
1054
|
+
steps: [{ name: 'v1', status: 'completed', completedAt: now }] }
|
|
1055
|
+
data.stages['archive'] = { status: 'completed', startedAt: now, completedAt: now,
|
|
1056
|
+
steps: [{ name: 'a1', status: 'completed', completedAt: now }] }
|
|
1057
|
+
await pm._write(cwd, data, changeName)
|
|
1058
|
+
|
|
1059
|
+
const result = await pm.checkConsistency(cwd, changeName)
|
|
1060
|
+
assert(!result.ok, '应检测到问题')
|
|
1061
|
+
assert(result.issues.some(i => i.includes('verify') && i.includes('execute') && i.includes('stale')), '应检测 verify 假完成')
|
|
1062
|
+
assert(result.issues.some(i => i.includes('archive') && i.includes('execute') && i.includes('stale')), '应检测 archive 假完成')
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// ─────────────────────────────────────────
|
|
1066
|
+
// Verify/Archive Safety: checkConsistency 检测 verify stale + archive completed
|
|
1067
|
+
// ─────────────────────────────────────────
|
|
1068
|
+
console.log('\n--- Verify/Archive Safety: check 检测 verify stale 下游 archive ---')
|
|
1069
|
+
{
|
|
1070
|
+
const { cwd } = createTempProject()
|
|
1071
|
+
const changeName = 'va-safety-6'
|
|
1072
|
+
const pm = await setupProgress(cwd, changeName)
|
|
1073
|
+
|
|
1074
|
+
const now = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
1075
|
+
const data = await pm.read(cwd, changeName)
|
|
1076
|
+
data.stages['verify'] = { status: 'stale', staleReason: 'execute revised', steps: [] }
|
|
1077
|
+
data.stages['archive'] = { status: 'completed', startedAt: now, completedAt: now,
|
|
1078
|
+
steps: [{ name: 'a1', status: 'completed', completedAt: now }] }
|
|
1079
|
+
await pm._write(cwd, data, changeName)
|
|
1080
|
+
|
|
1081
|
+
const result = await pm.checkConsistency(cwd, changeName)
|
|
1082
|
+
assert(!result.ok, '应检测到问题')
|
|
1083
|
+
assert(result.issues.some(i => i.includes('archive') && i.includes('verify') && i.includes('stale')), '应检测 archive 假完成(verify stale)')
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// ─────────────────────────────────────────
|
|
1087
|
+
// Verify/Archive Safety: repair cascade execute → verify + archive
|
|
1088
|
+
// ─────────────────────────────────────────
|
|
1089
|
+
console.log('\n--- Verify/Archive Safety: repair cascade execute → verify + archive ---')
|
|
1090
|
+
{
|
|
1091
|
+
const { cwd } = createTempProject()
|
|
1092
|
+
const changeName = 'va-safety-7'
|
|
1093
|
+
const pm = await setupProgress(cwd, changeName)
|
|
1094
|
+
|
|
1095
|
+
const now = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
1096
|
+
const data = await pm.read(cwd, changeName)
|
|
1097
|
+
data.stages['execute'] = { status: 'stale', staleReason: 'plan revised', steps: [] }
|
|
1098
|
+
data.stages['verify'] = { status: 'completed', startedAt: now, completedAt: now,
|
|
1099
|
+
steps: [{ name: 'v1', status: 'completed', completedAt: now }] }
|
|
1100
|
+
data.stages['archive'] = { status: 'completed', startedAt: now, completedAt: now,
|
|
1101
|
+
steps: [{ name: 'a1', status: 'completed', completedAt: now }] }
|
|
1102
|
+
await pm._write(cwd, data, changeName)
|
|
1103
|
+
|
|
1104
|
+
const result = await pm.repairConsistency(cwd, { apply: true, changeName })
|
|
1105
|
+
assert(result.applied.some(a => a.action === 'cascade_stale' && a.stage === 'verify'), '应修复 verify')
|
|
1106
|
+
assert(result.applied.some(a => a.action === 'cascade_stale' && a.stage === 'archive'), '应修复 archive')
|
|
1107
|
+
|
|
1108
|
+
const after = await pm.read(cwd, changeName)
|
|
1109
|
+
assert(after.stages['verify'].status === 'stale', 'verify 应为 stale')
|
|
1110
|
+
assert(after.stages['archive'].status === 'stale', 'archive 应为 stale')
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// ─────────────────────────────────────────
|
|
1114
|
+
// Verify/Archive Safety: repair cascade verify → archive
|
|
1115
|
+
// ─────────────────────────────────────────
|
|
1116
|
+
console.log('\n--- Verify/Archive Safety: repair cascade verify → archive ---')
|
|
1117
|
+
{
|
|
1118
|
+
const { cwd } = createTempProject()
|
|
1119
|
+
const changeName = 'va-safety-8'
|
|
1120
|
+
const pm = await setupProgress(cwd, changeName)
|
|
1121
|
+
|
|
1122
|
+
const now = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
1123
|
+
const data = await pm.read(cwd, changeName)
|
|
1124
|
+
data.stages['verify'] = { status: 'stale', staleReason: 'execute revised', steps: [] }
|
|
1125
|
+
data.stages['archive'] = { status: 'completed', startedAt: now, completedAt: now,
|
|
1126
|
+
steps: [{ name: 'a1', status: 'completed', completedAt: now }] }
|
|
1127
|
+
await pm._write(cwd, data, changeName)
|
|
1128
|
+
|
|
1129
|
+
const result = await pm.repairConsistency(cwd, { apply: true, changeName })
|
|
1130
|
+
assert(result.applied.some(a => a.action === 'cascade_stale' && a.stage === 'archive'), '应修复 archive')
|
|
1131
|
+
|
|
1132
|
+
const after = await pm.read(cwd, changeName)
|
|
1133
|
+
assert(after.stages['archive'].status === 'stale', 'archive 应为 stale')
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// ── 结果 ──
|
|
1137
|
+
console.log(`\n${'='.repeat(50)}`)
|
|
1138
|
+
console.log(`✅ 通过: ${12 - failed} ❌ 失败: ${failed}`)
|
|
1139
|
+
if (failures.length > 0) {
|
|
1140
|
+
console.log(`失败项:`)
|
|
1141
|
+
failures.forEach(f => console.log(` - ${f}`))
|
|
1142
|
+
}
|
|
1143
|
+
console.log(`${'='.repeat(50)}`)
|
|
1144
|
+
|
|
1145
|
+
if (failed > 0) process.exit(1)
|