@xulthekl/team-flow 0.31.0 → 0.32.2
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/always/phase-guard.md +1 -1
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -2
- package/.cursor-plugin/marketplace.json +2 -2
- package/.cursor-plugin/plugin.json +2 -2
- package/.github/plugin/marketplace.json +2 -2
- package/AGENTS.md +3 -3
- package/CHANGELOG.md +69 -0
- package/GEMINI.md +1 -1
- package/HANDOFF.md +1 -1
- package/INSTALL.md +1 -1
- package/README.md +2 -2
- package/agents/contract-builder.md +6 -1
- package/docs/README_en.md +1 -1
- package/docs/solutions/INDEX.md +2 -0
- package/docs/solutions/cross-phase/2026-08-01-no-summary.md +17 -0
- package/docs/solutions/cross-phase/2026-08-03-no-summary.md +17 -0
- package/gemini-extension.json +2 -2
- package/hooks/pre-tool-use-guard +43 -2
- package/hooks/session-start +2 -2
- package/llms.txt +1 -1
- package/package.json +6 -2
- package/plugin.json +1 -1
- package/scripts/guard/checks/test-gate-exemptions.mjs +53 -0
- package/scripts/guard/checks/test-matrix-complete.mjs +54 -30
- package/scripts/guard/checks/test-matrix-ready.mjs +60 -0
- package/scripts/guard/checks/tests-passing.mjs +112 -24
- package/scripts/guard/guard.mjs +11 -2
- package/scripts/lib/cmd-doctor.mjs +43 -1
- package/scripts/lib/cmd-execution.mjs +24 -1
- package/scripts/lib/cmd-state.mjs +28 -5
- package/scripts/lib/cmd-validate.mjs +42 -0
- package/scripts/lib/cmd-version.mjs +26 -21
- package/scripts/lib/execution-plan.mjs +23 -0
- package/scripts/lib/state-loader.mjs +16 -2
- package/scripts/lib/test-record.mjs +212 -0
- package/scripts/team-flow.mjs +3 -0
- package/skills/contract-builder/SKILL.md +9 -10
- package/skills/release-archivist/SKILL.md +16 -9
- package/skills/session-handoff/SKILL.md +1 -0
- package/skills/workflow-start/SKILL.md +5 -3
- package/skills/workflow-start/references/routing-rules.md +10 -6
- package/tests/e2e.test.mjs +0 -266
- package/tests/lib/cmd-audit.test.mjs +0 -202
- package/tests/lib/cmd-checkpoint.test.mjs +0 -98
- package/tests/lib/cmd-config.test.mjs +0 -101
- package/tests/lib/cmd-doctor.test.mjs +0 -450
- package/tests/lib/cmd-execution.test.mjs +0 -647
- package/tests/lib/cmd-handoff.test.mjs +0 -137
- package/tests/lib/cmd-inject.test.mjs +0 -218
- package/tests/lib/cmd-install-workbuddy.test.mjs +0 -113
- package/tests/lib/cmd-install-zcode.test.mjs +0 -42
- package/tests/lib/cmd-isolate.test.mjs +0 -68
- package/tests/lib/cmd-list.test.mjs +0 -127
- package/tests/lib/cmd-runtime.test.mjs +0 -83
- package/tests/lib/cmd-state-missing.test.mjs +0 -65
- package/tests/lib/cmd-state.test.mjs +0 -365
- package/tests/lib/cmd-sync-paths.test.mjs +0 -84
- package/tests/lib/cmd-validate-paths.test.mjs +0 -90
- package/tests/lib/config-loader.test.mjs +0 -175
- package/tests/lib/ensure-branch.test.mjs +0 -110
- package/tests/lib/execution-control-plane.test.mjs +0 -168
- package/tests/lib/execution-plan.test.mjs +0 -474
- package/tests/lib/guard-compound-captured.test.mjs +0 -92
- package/tests/lib/guard-specs-merged.test.mjs +0 -110
- package/tests/lib/guard-tests-passing.test.mjs +0 -114
- package/tests/lib/guard-transitions.test.mjs +0 -262
- package/tests/lib/guard.test.mjs +0 -613
- package/tests/lib/hash.test.mjs +0 -138
- package/tests/lib/infer-workflow.test.mjs +0 -161
- package/tests/lib/install-git-hooks.test.mjs +0 -20
- package/tests/lib/marketplace-release-docs.test.mjs +0 -31
- package/tests/lib/minimality-discipline.test.mjs +0 -37
- package/tests/lib/model-profiles-docs.test.mjs +0 -33
- package/tests/lib/node20-compatibility.test.mjs +0 -73
- package/tests/lib/node20-test-entry.test.mjs +0 -17
- package/tests/lib/platform-runtime-distribution.test.mjs +0 -157
- package/tests/lib/raw-mode-smoke.test.mjs +0 -42
- package/tests/lib/sdd-overlay.test.mjs +0 -118
- package/tests/lib/solutions-capture.test.mjs +0 -108
- package/tests/lib/solutions-index-gen.test.mjs +0 -147
- package/tests/lib/solutions-inject.test.mjs +0 -115
- package/tests/lib/solutions-promote.test.mjs +0 -200
- package/tests/lib/spec-paths.test.mjs +0 -80
- package/tests/lib/state-loader.test.mjs +0 -251
- package/tests/lib/token-baseline.test.mjs +0 -72
- package/tests/lib/token-rules.test.mjs +0 -94
- package/tests/lib/verify-marketplace-release.test.mjs +0 -128
- package/tests/tsconfig.json +0 -12
|
@@ -1,647 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, it } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { execFileSync } from 'node:child_process';
|
|
4
|
-
import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
5
|
-
import { join } from 'node:path';
|
|
6
|
-
import { tmpdir } from 'node:os';
|
|
7
|
-
|
|
8
|
-
const CLI = join(process.cwd(), 'scripts/team-flow.mjs');
|
|
9
|
-
let changeDir;
|
|
10
|
-
let gitRefs;
|
|
11
|
-
|
|
12
|
-
function runSsf(args, cwd = process.cwd(), { confirmPlan = true, acknowledgePlan = true, prepareRecommendation = true } = {}) {
|
|
13
|
-
const isPlan = args[0] === 'execution' && ['plan', 'revise'].includes(args[1]);
|
|
14
|
-
let effectiveArgs = args;
|
|
15
|
-
if (confirmPlan && isPlan && !effectiveArgs.includes('--confirm')) effectiveArgs = [...effectiveArgs, '--confirm'];
|
|
16
|
-
if (confirmPlan && acknowledgePlan && isPlan && requiresAcknowledgement(effectiveArgs) && !effectiveArgs.includes('--acknowledge-recommendation')) {
|
|
17
|
-
effectiveArgs = [...effectiveArgs, '--acknowledge-recommendation'];
|
|
18
|
-
}
|
|
19
|
-
if (prepareRecommendation && isPlan) {
|
|
20
|
-
const changePath = effectiveArgs[2];
|
|
21
|
-
const waves = effectiveArgs.flatMap((value, index) => value === '--wave' ? ['--wave', effectiveArgs[index + 1]] : []).filter(Boolean);
|
|
22
|
-
try {
|
|
23
|
-
execFileSync(process.execPath, [CLI, 'execution', 'recommend', changePath, ...waves], {
|
|
24
|
-
cwd,
|
|
25
|
-
encoding: 'utf8',
|
|
26
|
-
stdio: ['ignore', 'ignore', 'pipe'],
|
|
27
|
-
});
|
|
28
|
-
} catch {
|
|
29
|
-
// Let the requested command report malformed arguments through the usual test helper.
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
try {
|
|
33
|
-
const stdout = execFileSync(process.execPath, [CLI, ...effectiveArgs], {
|
|
34
|
-
cwd,
|
|
35
|
-
encoding: 'utf8',
|
|
36
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
37
|
-
});
|
|
38
|
-
return { exitCode: 0, stdout, stderr: '', json: tryJson(stdout) };
|
|
39
|
-
} catch (error) {
|
|
40
|
-
return {
|
|
41
|
-
exitCode: error.status ?? 1,
|
|
42
|
-
stdout: error.stdout?.toString() ?? '',
|
|
43
|
-
stderr: error.stderr?.toString() ?? '',
|
|
44
|
-
json: tryJson(error.stdout?.toString() ?? ''),
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function requiresAcknowledgement(args) {
|
|
50
|
-
const mode = args[args.indexOf('--mode') + 1];
|
|
51
|
-
const waves = args.flatMap((value, index) => value === '--wave' ? [args[index + 1]] : []).filter(Boolean);
|
|
52
|
-
const hasParallelWave = waves.some(wave => wave.split(':')[1] === 'parallel');
|
|
53
|
-
const plannedTaskCount = waves.reduce((count, wave) => count + (wave.split(':')[2]?.split(',').filter(Boolean).length || 0), 0);
|
|
54
|
-
const isSddRecommendation = hasParallelWave || waves.length > 1 || plannedTaskCount > 3;
|
|
55
|
-
const recommendedMode = isSddRecommendation ? 'sdd' : plannedTaskCount === 1 ? 'inline' : 'batch-inline';
|
|
56
|
-
return mode !== recommendedMode;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function runGit(directory, args) {
|
|
60
|
-
return execFileSync('git', args, { cwd: directory, encoding: 'utf8' }).trim();
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function tryJson(text) {
|
|
64
|
-
try { return JSON.parse(text); } catch { return null; }
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function writeChangeDirectory(directory, workflow = 'full', revision = null) {
|
|
68
|
-
writeFileSync(join(directory, 'proposal.md'), '## Why\nEnough context to create a controlled execution plan.\n## What Changes\n- Guard execution.\n');
|
|
69
|
-
writeFileSync(join(directory, 'design.md'), '# Design\n');
|
|
70
|
-
writeFileSync(join(directory, 'tasks.md'), '# Tasks\n\n- [ ] 1.1 First task\n- [ ] 1.2 Second task\n');
|
|
71
|
-
writeFileSync(join(directory, 'execution-contract.md'), '# Execution Contract\n');
|
|
72
|
-
mkdirSync(join(directory, 'specs', 'execution'), { recursive: true });
|
|
73
|
-
writeFileSync(join(directory, 'specs', 'execution', 'spec.md'), '## ADDED Requirements\n### Requirement: Guarded execution\nThe system SHALL guard execution.\n#### Scenario: Create plan\n- **WHEN** a plan is created\n- **THEN** it is persisted.\n');
|
|
74
|
-
writeFileSync(join(directory, '.team-flow.yaml'), [
|
|
75
|
-
'state: approved-for-build',
|
|
76
|
-
`workflow: ${workflow}`,
|
|
77
|
-
revision === null ? null : `revision: ${revision}`,
|
|
78
|
-
'',
|
|
79
|
-
].filter(line => line !== null).join('\n'));
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function writeReviewReport(name, content = 'Review completed without blocking findings.\n') {
|
|
83
|
-
const reportsDir = join(changeDir, '.superpowers', 'sdd', 'reviews');
|
|
84
|
-
mkdirSync(reportsDir, { recursive: true });
|
|
85
|
-
const reportPath = join(reportsDir, name);
|
|
86
|
-
writeFileSync(reportPath, content);
|
|
87
|
-
return reportPath;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function initializeGitRepository(directory) {
|
|
91
|
-
runGit(directory, ['init', '--quiet']);
|
|
92
|
-
runGit(directory, ['config', 'user.email', 'tests@example.invalid']);
|
|
93
|
-
runGit(directory, ['config', 'user.name', 'Execution Test']);
|
|
94
|
-
runGit(directory, ['add', '--all']);
|
|
95
|
-
runGit(directory, ['commit', '--quiet', '--message', 'initial execution change']);
|
|
96
|
-
const base = runGit(directory, ['rev-parse', 'HEAD']);
|
|
97
|
-
|
|
98
|
-
writeFileSync(join(directory, 'git-range-marker.txt'), 'second commit\n');
|
|
99
|
-
runGit(directory, ['add', 'git-range-marker.txt']);
|
|
100
|
-
runGit(directory, ['commit', '--quiet', '--message', 'second execution change']);
|
|
101
|
-
const head = runGit(directory, ['rev-parse', 'HEAD']);
|
|
102
|
-
const divergent = runGit(directory, ['commit-tree', `${head}^{tree}`, '-m', 'independent execution change']);
|
|
103
|
-
return { base, head, divergent };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
beforeEach(() => {
|
|
107
|
-
changeDir = mkdtempSync(join(tmpdir(), 'tf-execution-cmd-'));
|
|
108
|
-
writeChangeDirectory(changeDir);
|
|
109
|
-
gitRefs = initializeGitRepository(changeDir);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
afterEach(() => {
|
|
113
|
-
rmSync(changeDir, { recursive: true, force: true });
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
describe('tf execution', () => {
|
|
117
|
-
it('records DP-4 and state summary after a user-confirmed recommended SDD plan', () => {
|
|
118
|
-
const result = runSsf(['execution', 'plan', changeDir, '--mode', 'sdd',
|
|
119
|
-
'--reason', 'full workflow default', '--wave', 'wave-1:parallel:1.1,1.2', '--json']);
|
|
120
|
-
|
|
121
|
-
assert.equal(result.exitCode, 0, result.stderr);
|
|
122
|
-
assert.equal(result.json.plan.mode, 'sdd');
|
|
123
|
-
assert.equal(result.json.plan.revision, 1);
|
|
124
|
-
assert.equal(runSsf(['state', 'get', changeDir, 'execution_mode', '--json']).json.value, 'sdd');
|
|
125
|
-
assert.match(runSsf(['state', 'get', changeDir, 'execution_plan_hash', '--json']).json.value, /^sha256:/);
|
|
126
|
-
assert.equal(runSsf(['state', 'get', changeDir, 'execution_plan_revision', '--json']).json.value, 1);
|
|
127
|
-
assert.match(runSsf(['state', 'get', changeDir, 'dp_4_result', '--json']).json.value, /plan revision 1/);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('lists applicable execution modes and recommends one from the change evidence', () => {
|
|
131
|
-
const result = runSsf(['execution', 'recommend', changeDir, '--json']);
|
|
132
|
-
|
|
133
|
-
assert.equal(result.exitCode, 0, result.stderr);
|
|
134
|
-
assert.deepEqual(result.json.recommendation.available_modes, ['inline', 'batch-inline', 'sdd']);
|
|
135
|
-
assert.equal(result.json.recommendation.recommendation.mode, 'batch-inline');
|
|
136
|
-
assert.equal(result.json.recommendation.facts.documented_task_count, 2);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('requires a current persisted recommendation before a plan can be confirmed', () => {
|
|
140
|
-
const result = runSsf(['execution', 'plan', changeDir, '--mode', 'sdd', '--confirm',
|
|
141
|
-
'--reason', 'independent implementation', '--wave', 'wave-1:parallel:1.1,1.2', '--json'], process.cwd(), {
|
|
142
|
-
prepareRecommendation: false,
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
assert.notEqual(result.exitCode, 0);
|
|
146
|
-
assert.match(result.stderr, /recommend/i);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it('rejects a mode that is not available for the current workflow', () => {
|
|
150
|
-
const workflow = runSsf(['state', 'set', changeDir, 'workflow', 'tweak']);
|
|
151
|
-
assert.equal(workflow.exitCode, 0, workflow.stderr);
|
|
152
|
-
const recommended = runSsf(['execution', 'recommend', changeDir,
|
|
153
|
-
'--wave', 'wave-1:serial:1.1']);
|
|
154
|
-
assert.equal(recommended.exitCode, 0, recommended.stderr);
|
|
155
|
-
|
|
156
|
-
const result = runSsf(['execution', 'plan', changeDir, '--mode', 'sdd', '--confirm',
|
|
157
|
-
'--acknowledge-recommendation', '--reason', 'operator wants delegated review',
|
|
158
|
-
'--wave', 'wave-1:serial:1.1'], process.cwd(), { prepareRecommendation: false });
|
|
159
|
-
|
|
160
|
-
assert.notEqual(result.exitCode, 0);
|
|
161
|
-
assert.match(result.stderr, /available|mode/i);
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it('rejects a plan when the saved recommendation was for different waves', () => {
|
|
165
|
-
const recommended = runSsf(['execution', 'recommend', changeDir,
|
|
166
|
-
'--wave', 'wave-1:serial:1.1,1.2']);
|
|
167
|
-
assert.equal(recommended.exitCode, 0, recommended.stderr);
|
|
168
|
-
|
|
169
|
-
const result = runSsf(['execution', 'plan', changeDir, '--mode', 'sdd', '--confirm',
|
|
170
|
-
'--reason', 'independent implementation', '--wave', 'wave-1:parallel:1.1,1.2', '--json'], process.cwd(), {
|
|
171
|
-
prepareRecommendation: false,
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
assert.notEqual(result.exitCode, 0);
|
|
175
|
-
assert.match(result.stderr, /recommend/i);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it('requires a user confirmation before recording any execution plan', () => {
|
|
179
|
-
const result = runSsf(['execution', 'plan', changeDir, '--mode', 'sdd',
|
|
180
|
-
'--reason', 'independent implementation', '--wave', 'wave-1:parallel:1.1,1.2', '--json'], process.cwd(), { confirmPlan: false });
|
|
181
|
-
|
|
182
|
-
assert.notEqual(result.exitCode, 0);
|
|
183
|
-
assert.match(result.stderr, /confirm/i);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it('records an acknowledged non-recommended selection instead of an override', () => {
|
|
187
|
-
const result = runSsf(['execution', 'plan', changeDir, '--mode', 'inline', '--confirm',
|
|
188
|
-
'--acknowledge-recommendation', '--reason', 'operator will keep this focused',
|
|
189
|
-
'--wave', 'wave-1:serial:1.1,1.2', '--json']);
|
|
190
|
-
|
|
191
|
-
assert.equal(result.exitCode, 0, result.stderr);
|
|
192
|
-
assert.equal(result.json.plan.mode, 'inline');
|
|
193
|
-
assert.equal(result.json.plan.source, 'user-confirmed');
|
|
194
|
-
assert.equal(result.json.plan.selection.followed_recommendation, false);
|
|
195
|
-
assert.equal(result.json.plan.selection.acknowledged_non_recommendation, true);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it('requires acknowledgement for a non-recommended selection', () => {
|
|
199
|
-
const result = runSsf(['execution', 'plan', changeDir, '--mode', 'batch-inline',
|
|
200
|
-
'--reason', 'operator wants a batch', '--wave', 'wave-1:serial:1.1', '--json'], process.cwd(), { acknowledgePlan: false });
|
|
201
|
-
|
|
202
|
-
assert.notEqual(result.exitCode, 0);
|
|
203
|
-
assert.match(result.stderr, /acknowledge/i);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it('rejects multiline and control-character reasons before mutating the plan or state', () => {
|
|
207
|
-
const statePath = join(changeDir, '.team-flow.yaml');
|
|
208
|
-
const planPath = join(changeDir, '.superpowers', 'sdd', 'execution-plan.json');
|
|
209
|
-
const originalState = readFileSync(statePath, 'utf8');
|
|
210
|
-
|
|
211
|
-
for (const reason of ['approved\nexecution_mode: inline', 'approved\u0001inline']) {
|
|
212
|
-
const result = runSsf(['execution', 'plan', changeDir, '--mode', 'sdd', '--reason', reason,
|
|
213
|
-
'--wave', 'wave-1:parallel:1.1,1.2', '--json']);
|
|
214
|
-
|
|
215
|
-
assert.notEqual(result.exitCode, 0);
|
|
216
|
-
assert.match(result.stderr, /reason.*control|reason.*line/i);
|
|
217
|
-
assert.equal(readFileSync(statePath, 'utf8'), originalState);
|
|
218
|
-
assert.equal(existsSync(planPath), false);
|
|
219
|
-
}
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it('shows the persisted execution plan', () => {
|
|
223
|
-
runSsf(['execution', 'plan', changeDir, '--mode', 'sdd', '--reason', 'full workflow default',
|
|
224
|
-
'--wave', 'wave-1:parallel:1.1,1.2']);
|
|
225
|
-
|
|
226
|
-
const result = runSsf(['execution', 'show', changeDir, '--json']);
|
|
227
|
-
|
|
228
|
-
assert.equal(result.exitCode, 0, result.stderr);
|
|
229
|
-
assert.equal(result.json.plan.mode, 'sdd');
|
|
230
|
-
assert.equal(result.json.valid, true);
|
|
231
|
-
assert.equal(result.json.current, true);
|
|
232
|
-
assert.deepEqual(result.json.waves, [{
|
|
233
|
-
id: 'wave-1',
|
|
234
|
-
strategy: 'parallel',
|
|
235
|
-
tasks: ['1.1', '1.2'],
|
|
236
|
-
depends_on: [],
|
|
237
|
-
eligible: true,
|
|
238
|
-
retryable: false,
|
|
239
|
-
receipt: null,
|
|
240
|
-
blockers: [],
|
|
241
|
-
}]);
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
it('keeps an overlay-relative review report current across working directories', () => {
|
|
245
|
-
const planned = runSsf(['execution', 'plan', changeDir, '--mode', 'sdd', '--reason', 'full workflow default',
|
|
246
|
-
'--wave', 'wave-1:serial:1.1']);
|
|
247
|
-
assert.equal(planned.exitCode, 0, planned.stderr);
|
|
248
|
-
writeReviewReport('wave-1.md');
|
|
249
|
-
const reviewCwd = mkdtempSync(join(tmpdir(), 'tf-review-cwd-'));
|
|
250
|
-
const showCwd = mkdtempSync(join(tmpdir(), 'tf-show-cwd-'));
|
|
251
|
-
|
|
252
|
-
try {
|
|
253
|
-
const reviewed = runSsf(['execution', 'review', changeDir, '--wave', 'wave-1',
|
|
254
|
-
'--base', gitRefs.base, '--head', gitRefs.head,
|
|
255
|
-
'--report', '.superpowers/sdd/reviews/wave-1.md', '--verdict', 'pass'], reviewCwd);
|
|
256
|
-
assert.equal(reviewed.exitCode, 0, reviewed.stderr);
|
|
257
|
-
|
|
258
|
-
const shown = runSsf(['execution', 'show', changeDir, '--json'], showCwd);
|
|
259
|
-
assert.equal(shown.exitCode, 0, shown.stderr);
|
|
260
|
-
assert.equal(shown.json.current, true);
|
|
261
|
-
assert.equal(shown.json.waves[0].receipt.status, 'pass');
|
|
262
|
-
} finally {
|
|
263
|
-
rmSync(reviewCwd, { recursive: true, force: true });
|
|
264
|
-
rmSync(showCwd, { recursive: true, force: true });
|
|
265
|
-
}
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
it('rejects review reports outside the change overlay', () => {
|
|
269
|
-
const planned = runSsf(['execution', 'plan', changeDir, '--mode', 'sdd', '--reason', 'full workflow default',
|
|
270
|
-
'--wave', 'wave-1:serial:1.1']);
|
|
271
|
-
assert.equal(planned.exitCode, 0, planned.stderr);
|
|
272
|
-
const outsideReport = join(changeDir, 'reports', 'wave-1.md');
|
|
273
|
-
mkdirSync(join(changeDir, 'reports'), { recursive: true });
|
|
274
|
-
writeFileSync(outsideReport, 'Review completed without blocking findings.\n');
|
|
275
|
-
|
|
276
|
-
const reviewed = runSsf(['execution', 'review', changeDir, '--wave', 'wave-1',
|
|
277
|
-
'--base', gitRefs.base, '--head', gitRefs.head, '--report', outsideReport, '--verdict', 'pass']);
|
|
278
|
-
|
|
279
|
-
assert.notEqual(reviewed.exitCode, 0);
|
|
280
|
-
assert.match(reviewed.stderr, /overlay|review/i);
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
it('rejects a report reached through a nested review-directory symlink', () => {
|
|
284
|
-
const planned = runSsf(['execution', 'plan', changeDir, '--mode', 'sdd', '--reason', 'full workflow default',
|
|
285
|
-
'--wave', 'wave-1:serial:1.1']);
|
|
286
|
-
assert.equal(planned.exitCode, 0, planned.stderr);
|
|
287
|
-
const outsideDir = join(changeDir, 'reports');
|
|
288
|
-
mkdirSync(outsideDir, { recursive: true });
|
|
289
|
-
writeFileSync(join(outsideDir, 'escaped.md'), 'Review completed without blocking findings.\n');
|
|
290
|
-
const reviewsDir = join(changeDir, '.superpowers', 'sdd', 'reviews');
|
|
291
|
-
mkdirSync(reviewsDir, { recursive: true });
|
|
292
|
-
symlinkSync(outsideDir, join(reviewsDir, 'linked'), 'dir');
|
|
293
|
-
|
|
294
|
-
const reviewed = runSsf(['execution', 'review', changeDir, '--wave', 'wave-1',
|
|
295
|
-
'--base', gitRefs.base, '--head', gitRefs.head,
|
|
296
|
-
'--report', '.superpowers/sdd/reviews/linked/escaped.md', '--verdict', 'pass']);
|
|
297
|
-
|
|
298
|
-
assert.notEqual(reviewed.exitCode, 0);
|
|
299
|
-
assert.match(reviewed.stderr, /overlay|review/i);
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
it('rejects a report when the reviews overlay root is a symlink', () => {
|
|
303
|
-
const planned = runSsf(['execution', 'plan', changeDir, '--mode', 'sdd', '--reason', 'full workflow default',
|
|
304
|
-
'--wave', 'wave-1:serial:1.1']);
|
|
305
|
-
assert.equal(planned.exitCode, 0, planned.stderr);
|
|
306
|
-
const outsideReviewsDir = mkdtempSync(join(tmpdir(), 'tf-external-reviews-'));
|
|
307
|
-
const reviewsDir = join(changeDir, '.superpowers', 'sdd', 'reviews');
|
|
308
|
-
|
|
309
|
-
try {
|
|
310
|
-
writeFileSync(join(outsideReviewsDir, 'wave-1.md'), 'Review completed without blocking findings.\n');
|
|
311
|
-
symlinkSync(outsideReviewsDir, reviewsDir, 'dir');
|
|
312
|
-
|
|
313
|
-
const reviewed = runSsf(['execution', 'review', changeDir, '--wave', 'wave-1',
|
|
314
|
-
'--base', gitRefs.base, '--head', gitRefs.head,
|
|
315
|
-
'--report', '.superpowers/sdd/reviews/wave-1.md', '--verdict', 'pass']);
|
|
316
|
-
|
|
317
|
-
assert.notEqual(reviewed.exitCode, 0);
|
|
318
|
-
assert.match(reviewed.stderr, /overlay|review|symbolic/i);
|
|
319
|
-
} finally {
|
|
320
|
-
rmSync(outsideReviewsDir, { recursive: true, force: true });
|
|
321
|
-
}
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
it('rejects a receipt range containing a nonexistent Git commit', () => {
|
|
325
|
-
const planned = runSsf(['execution', 'plan', changeDir, '--mode', 'sdd', '--reason', 'full workflow default',
|
|
326
|
-
'--wave', 'wave-1:serial:1.1']);
|
|
327
|
-
assert.equal(planned.exitCode, 0, planned.stderr);
|
|
328
|
-
const forgedCommit = '0000000000000000000000000000000000000001';
|
|
329
|
-
|
|
330
|
-
const reviewed = runSsf(['execution', 'review', changeDir, '--wave', 'wave-1',
|
|
331
|
-
'--base', forgedCommit, '--head', gitRefs.head, '--report', writeReviewReport('wave-1.md'), '--verdict', 'pass']);
|
|
332
|
-
|
|
333
|
-
assert.notEqual(reviewed.exitCode, 0);
|
|
334
|
-
assert.match(reviewed.stderr, /base|commit|Git/i);
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
it('rejects a receipt range whose base is not an ancestor of head', () => {
|
|
338
|
-
const planned = runSsf(['execution', 'plan', changeDir, '--mode', 'sdd', '--reason', 'full workflow default',
|
|
339
|
-
'--wave', 'wave-1:serial:1.1']);
|
|
340
|
-
assert.equal(planned.exitCode, 0, planned.stderr);
|
|
341
|
-
|
|
342
|
-
const reviewed = runSsf(['execution', 'review', changeDir, '--wave', 'wave-1',
|
|
343
|
-
'--base', gitRefs.head, '--head', gitRefs.divergent,
|
|
344
|
-
'--report', writeReviewReport('wave-1.md'), '--verdict', 'pass']);
|
|
345
|
-
|
|
346
|
-
assert.notEqual(reviewed.exitCode, 0);
|
|
347
|
-
assert.match(reviewed.stderr, /ancestor|range|base/i);
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
it('treats a persisted pass receipt with a forged Git base as unusable', () => {
|
|
351
|
-
const planned = runSsf(['execution', 'plan', changeDir, '--mode', 'sdd', '--reason', 'full workflow default',
|
|
352
|
-
'--wave', 'wave-1:serial:1.1', '--wave', 'wave-2:serial:1.2:wave-1']);
|
|
353
|
-
assert.equal(planned.exitCode, 0, planned.stderr);
|
|
354
|
-
const reviewed = runSsf(['execution', 'review', changeDir, '--wave', 'wave-1',
|
|
355
|
-
'--base', gitRefs.base, '--head', gitRefs.head, '--report', writeReviewReport('wave-1.md'), '--verdict', 'pass']);
|
|
356
|
-
assert.equal(reviewed.exitCode, 0, reviewed.stderr);
|
|
357
|
-
|
|
358
|
-
const receiptPath = join(changeDir, '.superpowers', 'sdd', 'reviews', Buffer.from('wave-1', 'utf8').toString('base64url') + '.json');
|
|
359
|
-
const receipt = JSON.parse(readFileSync(receiptPath, 'utf8'));
|
|
360
|
-
receipt.base = '0000000000000000000000000000000000000001';
|
|
361
|
-
writeFileSync(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`);
|
|
362
|
-
|
|
363
|
-
const shown = runSsf(['execution', 'show', changeDir, '--json']);
|
|
364
|
-
assert.equal(shown.exitCode, 0, shown.stderr);
|
|
365
|
-
assert.equal(shown.json.waves[0].receipt, null);
|
|
366
|
-
assert.deepEqual(shown.json.waves[1].blockers, ['wave-1']);
|
|
367
|
-
assert.equal(shown.json.waves[1].eligible, false);
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
it('treats a persisted pass receipt with a non-ancestral Git range as unusable', () => {
|
|
371
|
-
const planned = runSsf(['execution', 'plan', changeDir, '--mode', 'sdd', '--reason', 'full workflow default',
|
|
372
|
-
'--wave', 'wave-1:serial:1.1', '--wave', 'wave-2:serial:1.2:wave-1']);
|
|
373
|
-
assert.equal(planned.exitCode, 0, planned.stderr);
|
|
374
|
-
const reviewed = runSsf(['execution', 'review', changeDir, '--wave', 'wave-1',
|
|
375
|
-
'--base', gitRefs.base, '--head', gitRefs.head, '--report', writeReviewReport('wave-1.md'), '--verdict', 'pass']);
|
|
376
|
-
assert.equal(reviewed.exitCode, 0, reviewed.stderr);
|
|
377
|
-
|
|
378
|
-
const receiptPath = join(changeDir, '.superpowers', 'sdd', 'reviews', Buffer.from('wave-1', 'utf8').toString('base64url') + '.json');
|
|
379
|
-
const receipt = JSON.parse(readFileSync(receiptPath, 'utf8'));
|
|
380
|
-
receipt.base = gitRefs.head;
|
|
381
|
-
receipt.head = gitRefs.divergent;
|
|
382
|
-
writeFileSync(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`);
|
|
383
|
-
|
|
384
|
-
const shown = runSsf(['execution', 'show', changeDir, '--json']);
|
|
385
|
-
assert.equal(shown.exitCode, 0, shown.stderr);
|
|
386
|
-
assert.equal(shown.json.waves[0].receipt, null);
|
|
387
|
-
assert.deepEqual(shown.json.waves[1].blockers, ['wave-1']);
|
|
388
|
-
assert.equal(shown.json.waves[1].eligible, false);
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
it('does not show a pass receipt after its report evidence is deleted', () => {
|
|
392
|
-
runSsf(['execution', 'plan', changeDir, '--mode', 'sdd', '--reason', 'full workflow default',
|
|
393
|
-
'--wave', 'wave-1:serial:1.1']);
|
|
394
|
-
const reportPath = writeReviewReport('wave-1.md');
|
|
395
|
-
const reviewed = runSsf(['execution', 'review', changeDir, '--wave', 'wave-1',
|
|
396
|
-
'--base', gitRefs.base, '--head', gitRefs.head, '--report', reportPath, '--verdict', 'pass']);
|
|
397
|
-
assert.equal(reviewed.exitCode, 0, reviewed.stderr);
|
|
398
|
-
|
|
399
|
-
rmSync(reportPath);
|
|
400
|
-
|
|
401
|
-
const shown = runSsf(['execution', 'show', changeDir, '--json']);
|
|
402
|
-
assert.equal(shown.exitCode, 0, shown.stderr);
|
|
403
|
-
assert.equal(shown.json.waves[0].receipt, null);
|
|
404
|
-
assert.equal(shown.json.waves[0].eligible, true);
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
it('encodes wave dependencies and refuses review of a wave before its dependencies pass', () => {
|
|
408
|
-
const planned = runSsf(['execution', 'plan', changeDir, '--mode', 'sdd',
|
|
409
|
-
'--reason', 'full workflow default',
|
|
410
|
-
'--wave', 'wave-1:parallel:1.1,1.2',
|
|
411
|
-
'--wave', 'wave-2:serial:2.1:wave-1', '--json']);
|
|
412
|
-
assert.equal(planned.exitCode, 0, planned.stderr);
|
|
413
|
-
assert.deepEqual(planned.json.plan.waves[1].depends_on, ['wave-1']);
|
|
414
|
-
|
|
415
|
-
const shown = runSsf(['execution', 'show', changeDir, '--json']);
|
|
416
|
-
assert.equal(shown.exitCode, 0, shown.stderr);
|
|
417
|
-
assert.equal(shown.json.waves[0].eligible, true);
|
|
418
|
-
assert.equal(shown.json.waves[1].eligible, false);
|
|
419
|
-
assert.deepEqual(shown.json.waves[1].blockers, ['wave-1']);
|
|
420
|
-
|
|
421
|
-
const premature = runSsf(['execution', 'review', changeDir, '--wave', 'wave-2',
|
|
422
|
-
'--base', gitRefs.base, '--head', gitRefs.head, '--report', 'reports/wave-2.md', '--verdict', 'pass']);
|
|
423
|
-
assert.notEqual(premature.exitCode, 0);
|
|
424
|
-
assert.match(premature.stderr, /wave-1.*pass|dependencies/i);
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
it('rejects a plan when state mode differs from the frozen plan mode', () => {
|
|
428
|
-
runSsf(['execution', 'plan', changeDir, '--mode', 'sdd', '--reason', 'full workflow default',
|
|
429
|
-
'--wave', 'wave-1:parallel:1.1,1.2']);
|
|
430
|
-
const statePath = join(changeDir, '.team-flow.yaml');
|
|
431
|
-
writeFileSync(statePath, readFileSync(statePath, 'utf8').replace('execution_mode: sdd', 'execution_mode: inline'));
|
|
432
|
-
|
|
433
|
-
const result = runSsf(['execution', 'show', changeDir, '--json']);
|
|
434
|
-
|
|
435
|
-
assert.notEqual(result.exitCode, 0);
|
|
436
|
-
assert.equal(result.json.valid, false);
|
|
437
|
-
assert.ok(result.json.failures.includes('execution plan mode does not match state'));
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
it('increments revision when a batch-inline plan is revised to SDD', () => {
|
|
441
|
-
const initial = runSsf(['execution', 'plan', changeDir, '--mode', 'batch-inline', '--confirm', '--acknowledge-recommendation',
|
|
442
|
-
'--reason', 'operator requested a batch', '--wave', 'wave-1:serial:1.1']);
|
|
443
|
-
assert.equal(initial.exitCode, 0, initial.stderr);
|
|
444
|
-
|
|
445
|
-
const revised = runSsf(['execution', 'revise', changeDir, '--mode', 'sdd',
|
|
446
|
-
'--reason', 'risk requires independent review', '--wave', 'wave-1:parallel:1.1,1.2', '--json']);
|
|
447
|
-
|
|
448
|
-
assert.equal(revised.exitCode, 0, revised.stderr);
|
|
449
|
-
assert.equal(revised.json.plan.revision, 2);
|
|
450
|
-
assert.equal(runSsf(['state', 'get', changeDir, 'execution_plan_revision', '--json']).json.value, 2);
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
it('requires confirmation and acknowledgement when a revision differs from its recommendation', () => {
|
|
454
|
-
const initial = runSsf(['execution', 'plan', changeDir, '--mode', 'sdd',
|
|
455
|
-
'--reason', 'parallel work needs review', '--wave', 'wave-1:parallel:1.1,1.2']);
|
|
456
|
-
assert.equal(initial.exitCode, 0, initial.stderr);
|
|
457
|
-
|
|
458
|
-
const recommended = runSsf(['execution', 'recommend', changeDir,
|
|
459
|
-
'--wave', 'wave-1:serial:1.1']);
|
|
460
|
-
assert.equal(recommended.exitCode, 0, recommended.stderr);
|
|
461
|
-
|
|
462
|
-
const missingConfirm = runSsf(['execution', 'revise', changeDir, '--mode', 'sdd',
|
|
463
|
-
'--reason', 'retain SDD for the revised work', '--wave', 'wave-1:serial:1.1'], process.cwd(), {
|
|
464
|
-
confirmPlan: false,
|
|
465
|
-
prepareRecommendation: false,
|
|
466
|
-
});
|
|
467
|
-
assert.notEqual(missingConfirm.exitCode, 0);
|
|
468
|
-
assert.match(missingConfirm.stderr, /confirm/i);
|
|
469
|
-
|
|
470
|
-
const missingAcknowledgement = runSsf(['execution', 'revise', changeDir, '--mode', 'sdd', '--confirm',
|
|
471
|
-
'--reason', 'retain SDD for the revised work', '--wave', 'wave-1:serial:1.1'], process.cwd(), {
|
|
472
|
-
acknowledgePlan: false,
|
|
473
|
-
prepareRecommendation: false,
|
|
474
|
-
});
|
|
475
|
-
assert.notEqual(missingAcknowledgement.exitCode, 0);
|
|
476
|
-
assert.match(missingAcknowledgement.stderr, /acknowledge/i);
|
|
477
|
-
|
|
478
|
-
const revised = runSsf(['execution', 'revise', changeDir, '--mode', 'sdd', '--confirm',
|
|
479
|
-
'--acknowledge-recommendation', '--reason', 'retain SDD for the revised work',
|
|
480
|
-
'--wave', 'wave-1:serial:1.1', '--json'], process.cwd(), { prepareRecommendation: false });
|
|
481
|
-
assert.equal(revised.exitCode, 0, revised.stderr);
|
|
482
|
-
assert.equal(revised.json.plan.selection.confirmed, true);
|
|
483
|
-
assert.equal(revised.json.plan.selection.followed_recommendation, false);
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
it('requires a fresh recommendation after the prior plan before recording a revision', () => {
|
|
487
|
-
const initial = runSsf(['execution', 'plan', changeDir, '--mode', 'sdd',
|
|
488
|
-
'--reason', 'parallel work needs review', '--wave', 'wave-1:parallel:1.1,1.2']);
|
|
489
|
-
assert.equal(initial.exitCode, 0, initial.stderr);
|
|
490
|
-
|
|
491
|
-
const result = runSsf(['execution', 'revise', changeDir, '--mode', 'sdd', '--confirm',
|
|
492
|
-
'--reason', 'reconfirm the same work as a new revision', '--wave', 'wave-1:parallel:1.1,1.2'], process.cwd(), {
|
|
493
|
-
prepareRecommendation: false,
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
assert.notEqual(result.exitCode, 0);
|
|
497
|
-
assert.match(result.stderr, /recommend/i);
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
it('invalidates receipts from the replaced plan revision', () => {
|
|
501
|
-
const initial = runSsf(['execution', 'plan', changeDir, '--mode', 'batch-inline', '--confirm', '--acknowledge-recommendation',
|
|
502
|
-
'--reason', 'operator requested a batch', '--wave', 'wave-1:serial:1.1']);
|
|
503
|
-
assert.equal(initial.exitCode, 0, initial.stderr);
|
|
504
|
-
const reportPath = writeReviewReport('wave-1.md');
|
|
505
|
-
const reviewed = runSsf(['execution', 'review', changeDir, '--wave', 'wave-1',
|
|
506
|
-
'--base', gitRefs.base, '--head', gitRefs.head, '--report', reportPath, '--verdict', 'pass']);
|
|
507
|
-
assert.equal(reviewed.exitCode, 0, reviewed.stderr);
|
|
508
|
-
|
|
509
|
-
const revised = runSsf(['execution', 'revise', changeDir, '--mode', 'sdd',
|
|
510
|
-
'--reason', 'risk requires independent review', '--wave', 'wave-1:parallel:1.1,1.2', '--json']);
|
|
511
|
-
assert.equal(revised.exitCode, 0, revised.stderr);
|
|
512
|
-
|
|
513
|
-
const shown = runSsf(['execution', 'show', changeDir, '--json']);
|
|
514
|
-
assert.equal(shown.exitCode, 0, shown.stderr);
|
|
515
|
-
assert.equal(shown.json.current, true);
|
|
516
|
-
assert.equal(shown.json.waves[0].receipt, null);
|
|
517
|
-
assert.equal(shown.json.waves[0].eligible, true);
|
|
518
|
-
});
|
|
519
|
-
|
|
520
|
-
it('replans a current SDD plan with a new revision, renewed DP-4 state, and cleared receipts', () => {
|
|
521
|
-
const initial = runSsf(['execution', 'plan', changeDir, '--mode', 'sdd',
|
|
522
|
-
'--reason', 'full workflow default', '--wave', 'wave-1:serial:1.1']);
|
|
523
|
-
assert.equal(initial.exitCode, 0, initial.stderr);
|
|
524
|
-
const reviewed = runSsf(['execution', 'review', changeDir, '--wave', 'wave-1',
|
|
525
|
-
'--base', gitRefs.base, '--head', gitRefs.head, '--report', writeReviewReport('wave-1.md'), '--verdict', 'pass']);
|
|
526
|
-
assert.equal(reviewed.exitCode, 0, reviewed.stderr);
|
|
527
|
-
|
|
528
|
-
const replanned = runSsf(['execution', 'revise', changeDir, '--mode', 'sdd',
|
|
529
|
-
'--reason', 'split independent work into a recovery wave',
|
|
530
|
-
'--wave', 'wave-1:parallel:1.1,1.2', '--json']);
|
|
531
|
-
|
|
532
|
-
assert.equal(replanned.exitCode, 0, replanned.stderr);
|
|
533
|
-
assert.equal(replanned.json.plan.revision, 2);
|
|
534
|
-
assert.equal(runSsf(['state', 'get', changeDir, 'execution_plan_revision', '--json']).json.value, 2);
|
|
535
|
-
assert.match(runSsf(['state', 'get', changeDir, 'dp_4_result', '--json']).json.value, /plan revision 2/);
|
|
536
|
-
const shown = runSsf(['execution', 'show', changeDir, '--json']);
|
|
537
|
-
assert.equal(shown.exitCode, 0, shown.stderr);
|
|
538
|
-
assert.equal(shown.json.current, true);
|
|
539
|
-
assert.equal(shown.json.waves[0].receipt, null);
|
|
540
|
-
});
|
|
541
|
-
|
|
542
|
-
it('recovers a stale SDD plan by revising it to current artifacts and clearing old receipts', () => {
|
|
543
|
-
const initial = runSsf(['execution', 'plan', changeDir, '--mode', 'sdd',
|
|
544
|
-
'--reason', 'full workflow default', '--wave', 'wave-1:serial:1.1']);
|
|
545
|
-
assert.equal(initial.exitCode, 0, initial.stderr);
|
|
546
|
-
const reviewed = runSsf(['execution', 'review', changeDir, '--wave', 'wave-1',
|
|
547
|
-
'--base', gitRefs.base, '--head', gitRefs.head, '--report', writeReviewReport('stale-wave-1.md'), '--verdict', 'pass']);
|
|
548
|
-
assert.equal(reviewed.exitCode, 0, reviewed.stderr);
|
|
549
|
-
writeFileSync(join(changeDir, 'tasks.md'), '# Tasks\n\n- [ ] 1.1 Updated task\n- [ ] 1.2 Recovery task\n');
|
|
550
|
-
|
|
551
|
-
const replanned = runSsf(['execution', 'revise', changeDir, '--mode', 'sdd',
|
|
552
|
-
'--reason', 'refresh the plan after task scope changed',
|
|
553
|
-
'--wave', 'wave-1:parallel:1.1,1.2', '--json']);
|
|
554
|
-
|
|
555
|
-
assert.equal(replanned.exitCode, 0, replanned.stderr);
|
|
556
|
-
assert.equal(replanned.json.plan.revision, 2);
|
|
557
|
-
const shown = runSsf(['execution', 'show', changeDir, '--json']);
|
|
558
|
-
assert.equal(shown.exitCode, 0, shown.stderr);
|
|
559
|
-
assert.equal(shown.json.current, true);
|
|
560
|
-
assert.equal(shown.json.waves[0].receipt, null);
|
|
561
|
-
assert.match(runSsf(['state', 'get', changeDir, 'dp_4_result', '--json']).json.value, /plan revision 2/);
|
|
562
|
-
});
|
|
563
|
-
|
|
564
|
-
it('makes a failed current wave retryable while blocking dependents until its replacement pass receipt', () => {
|
|
565
|
-
const planned = runSsf(['execution', 'plan', changeDir, '--mode', 'sdd',
|
|
566
|
-
'--reason', 'repair reviews before dependent work',
|
|
567
|
-
'--wave', 'wave-1:serial:1.1',
|
|
568
|
-
'--wave', 'wave-2:serial:1.2:wave-1']);
|
|
569
|
-
assert.equal(planned.exitCode, 0, planned.stderr);
|
|
570
|
-
|
|
571
|
-
const failed = runSsf(['execution', 'review', changeDir, '--wave', 'wave-1',
|
|
572
|
-
'--base', gitRefs.base, '--head', gitRefs.head, '--report', writeReviewReport('wave-1-fail.md'), '--verdict', 'fail']);
|
|
573
|
-
assert.equal(failed.exitCode, 0, failed.stderr);
|
|
574
|
-
|
|
575
|
-
let shown = runSsf(['execution', 'show', changeDir, '--json']);
|
|
576
|
-
assert.equal(shown.exitCode, 0, shown.stderr);
|
|
577
|
-
assert.equal(shown.json.waves[0].receipt.status, 'fail');
|
|
578
|
-
assert.equal(shown.json.waves[0].retryable, true);
|
|
579
|
-
assert.equal(shown.json.waves[0].eligible, true);
|
|
580
|
-
assert.equal(shown.json.waves[1].eligible, false);
|
|
581
|
-
assert.deepEqual(shown.json.waves[1].blockers, ['wave-1']);
|
|
582
|
-
|
|
583
|
-
const replacement = runSsf(['execution', 'review', changeDir, '--wave', 'wave-1',
|
|
584
|
-
'--base', gitRefs.base, '--head', gitRefs.head, '--report', writeReviewReport('wave-1-pass.md'), '--verdict', 'pass']);
|
|
585
|
-
assert.equal(replacement.exitCode, 0, replacement.stderr);
|
|
586
|
-
|
|
587
|
-
shown = runSsf(['execution', 'show', changeDir, '--json']);
|
|
588
|
-
assert.equal(shown.exitCode, 0, shown.stderr);
|
|
589
|
-
assert.equal(shown.json.waves[0].receipt.status, 'pass');
|
|
590
|
-
assert.equal(shown.json.waves[0].retryable, false);
|
|
591
|
-
assert.equal(shown.json.waves[0].eligible, false);
|
|
592
|
-
assert.equal(shown.json.waves[1].eligible, true);
|
|
593
|
-
});
|
|
594
|
-
|
|
595
|
-
it('keeps the Task 1 state revision aligned through plan, show, revise, and show', () => {
|
|
596
|
-
writeChangeDirectory(changeDir, 'full', 2);
|
|
597
|
-
const initial = runSsf(['execution', 'plan', changeDir, '--mode', 'batch-inline', '--confirm', '--acknowledge-recommendation',
|
|
598
|
-
'--reason', 'operator requested a batch', '--wave', 'wave-1:serial:1.1', '--json']);
|
|
599
|
-
assert.equal(initial.exitCode, 0, initial.stderr);
|
|
600
|
-
assert.equal(initial.json.plan.revision, 2);
|
|
601
|
-
|
|
602
|
-
const firstShow = runSsf(['execution', 'show', changeDir, '--json']);
|
|
603
|
-
assert.equal(firstShow.exitCode, 0, firstShow.stderr);
|
|
604
|
-
assert.equal(firstShow.json.valid, true);
|
|
605
|
-
assert.equal(runSsf(['state', 'get', changeDir, 'revision', '--json']).json.value, 2);
|
|
606
|
-
|
|
607
|
-
const revised = runSsf(['execution', 'revise', changeDir, '--mode', 'sdd',
|
|
608
|
-
'--reason', 'risk requires independent review', '--wave', 'wave-1:parallel:1.1,1.2', '--json']);
|
|
609
|
-
assert.equal(revised.exitCode, 0, revised.stderr);
|
|
610
|
-
assert.equal(revised.json.plan.revision, 3);
|
|
611
|
-
|
|
612
|
-
const secondShow = runSsf(['execution', 'show', changeDir, '--json']);
|
|
613
|
-
assert.equal(secondShow.exitCode, 0, secondShow.stderr);
|
|
614
|
-
assert.equal(secondShow.json.valid, true);
|
|
615
|
-
assert.equal(runSsf(['state', 'get', changeDir, 'revision', '--json']).json.value, 3);
|
|
616
|
-
});
|
|
617
|
-
|
|
618
|
-
it('rejects an invalid review verdict without writing a receipt', () => {
|
|
619
|
-
runSsf(['execution', 'plan', changeDir, '--mode', 'sdd', '--reason', 'full workflow default',
|
|
620
|
-
'--wave', 'wave-1:parallel:1.1,1.2']);
|
|
621
|
-
|
|
622
|
-
const result = runSsf(['execution', 'review', changeDir, '--wave', 'wave-1',
|
|
623
|
-
'--base', gitRefs.base, '--head', gitRefs.head, '--report', 'reports/wave-1.md', '--verdict', 'maybe', '--json']);
|
|
624
|
-
|
|
625
|
-
assert.notEqual(result.exitCode, 0);
|
|
626
|
-
assert.match(result.stderr, /pass.*fail|verdict/i);
|
|
627
|
-
});
|
|
628
|
-
|
|
629
|
-
it('rejects a review without exactly one wave selector', () => {
|
|
630
|
-
const result = runSsf(['execution', 'review', changeDir, '--base', gitRefs.base,
|
|
631
|
-
'--head', gitRefs.head, '--report', 'reports/wave-1.md', '--verdict', 'pass']);
|
|
632
|
-
|
|
633
|
-
assert.notEqual(result.exitCode, 0);
|
|
634
|
-
assert.match(result.stderr, /--wave is required/);
|
|
635
|
-
});
|
|
636
|
-
|
|
637
|
-
it('rejects malformed waves and SDD plan downgrades', () => {
|
|
638
|
-
const malformed = runSsf(['execution', 'plan', changeDir, '--mode', 'sdd', '--reason', 'bad wave', '--wave', 'missing-parts']);
|
|
639
|
-
assert.notEqual(malformed.exitCode, 0);
|
|
640
|
-
assert.match(malformed.stderr, /wave/i);
|
|
641
|
-
|
|
642
|
-
runSsf(['execution', 'plan', changeDir, '--mode', 'sdd', '--reason', 'full workflow default', '--wave', 'wave-1:serial:1.1']);
|
|
643
|
-
const invalidRevision = runSsf(['execution', 'revise', changeDir, '--mode', 'inline', '--reason', 'downgrade', '--wave', 'wave-1:serial:1.1']);
|
|
644
|
-
assert.notEqual(invalidRevision.exitCode, 0);
|
|
645
|
-
assert.match(invalidRevision.stderr, /sdd|downgrade|upgrade/i);
|
|
646
|
-
});
|
|
647
|
-
});
|