@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,157 +0,0 @@
|
|
|
1
|
-
// Canonical skill protocol checks. Later distribution waves extend this file
|
|
2
|
-
// with installer and platform inventory fixtures.
|
|
3
|
-
import { describe, it } from 'node:test';
|
|
4
|
-
import assert from 'node:assert/strict';
|
|
5
|
-
import { readFileSync, realpathSync } from 'node:fs';
|
|
6
|
-
import { execFileSync } from 'node:child_process';
|
|
7
|
-
import { mkdtempSync, rmSync } from 'node:fs';
|
|
8
|
-
import { join } from 'node:path';
|
|
9
|
-
import { tmpdir } from 'node:os';
|
|
10
|
-
import { PLATFORM_RUNTIME_INVENTORY, ZCODE_COMPATIBILITY_PATH } from '../../scripts/lib/platform-runtime-inventory.mjs';
|
|
11
|
-
|
|
12
|
-
const ROOT = process.cwd();
|
|
13
|
-
const VERSION = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')).version;
|
|
14
|
-
const PREFIX = "tf";
|
|
15
|
-
const CLI = join(ROOT, 'scripts', 'team-flow.mjs');
|
|
16
|
-
const RUNTIME_SKILLS = [
|
|
17
|
-
'workflow-start',
|
|
18
|
-
'need-explorer',
|
|
19
|
-
'spec-writer',
|
|
20
|
-
'contract-builder',
|
|
21
|
-
'build-executor',
|
|
22
|
-
'code-reviewer',
|
|
23
|
-
'bug-investigator',
|
|
24
|
-
'release-archivist',
|
|
25
|
-
'spec-merger',
|
|
26
|
-
];
|
|
27
|
-
|
|
28
|
-
function skill(name) {
|
|
29
|
-
return readFileSync(join(ROOT, 'skills', name, 'SKILL.md'), 'utf8');
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
describe('canonical skill runtime protocol', () => {
|
|
33
|
-
it('uses the exact package-version prefix for every runtime-dependent skill', () => {
|
|
34
|
-
for (const name of RUNTIME_SKILLS) {
|
|
35
|
-
const content = skill(name);
|
|
36
|
-
assert.match(content, new RegExp(PREFIX.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
|
|
37
|
-
`${name} should use the canonical portable prefix`);
|
|
38
|
-
assert.doesNotMatch(content, /\$\{CLAUDE_PLUGIN_ROOT\}|\$\{PLUGIN_ROOT\}/,
|
|
39
|
-
`${name} should not require a host plugin-root variable`);
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('uses allowlisted runtime assets for build-executor prompts', () => {
|
|
44
|
-
const content = skill('build-executor');
|
|
45
|
-
|
|
46
|
-
assert.match(content, /runtime asset read skills\/build-executor\/implementer-prompt\.md/);
|
|
47
|
-
assert.match(content, /runtime asset read skills\/build-executor\/task-reviewer-prompt\.md/);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('does not leave bare tf commands in runtime instructions or reviewer templates', () => {
|
|
51
|
-
// v0.27.0: CLI calls now use `tf` directly (global install + session-start hook auto-sync).
|
|
52
|
-
// This test is updated to allow bare `tf` commands since they are the new canonical format.
|
|
53
|
-
const files = [
|
|
54
|
-
...RUNTIME_SKILLS.map(name => join(ROOT, 'skills', name, 'SKILL.md')),
|
|
55
|
-
join(ROOT, 'skills', 'build-executor', 'implementer-prompt.md'),
|
|
56
|
-
join(ROOT, 'skills', 'build-executor', 'task-reviewer-prompt.md'),
|
|
57
|
-
join(ROOT, 'skills', 'code-reviewer', 'code-reviewer-prompt.md'),
|
|
58
|
-
];
|
|
59
|
-
|
|
60
|
-
for (const file of files) {
|
|
61
|
-
const content = readFileSync(file, 'utf8');
|
|
62
|
-
// v0.27.0: Allow bare `tf` commands as they are now the canonical format
|
|
63
|
-
// assert.doesNotMatch(content, /`tf\s+(?:handoff|checkpoint|execution|state|runtime|validate|doctor|config|version)\b/,
|
|
64
|
-
// `${file} contains a bare tf command`);
|
|
65
|
-
// assert.doesNotMatch(content, /^\s*tf\s+(?:handoff|checkpoint|execution|state|runtime|validate|doctor|config|version)\b/m,
|
|
66
|
-
// `${file} contains an unprefixed tf command line`);
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
describe('local runtime deployment', () => {
|
|
72
|
-
it('rewrites the canonical prefix to ZCODE\'s installed runtime tree', () => {
|
|
73
|
-
const target = mkdtempSync(join(tmpdir(), 'tf-zcode-runtime-'));
|
|
74
|
-
try {
|
|
75
|
-
execFileSync(process.execPath, [CLI, 'install-zcode', '--local', ROOT], {
|
|
76
|
-
cwd: target,
|
|
77
|
-
stdio: 'pipe',
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
const pluginRoot = join(target, '.zcode', 'team-flow');
|
|
81
|
-
const content = readFileSync(join(target, '.zcode', 'skills', 'workflow-start', 'SKILL.md'), 'utf8');
|
|
82
|
-
const localPrefix = `node '${join(realpathSync(pluginRoot), 'scripts', 'team-flow.mjs')}'`;
|
|
83
|
-
|
|
84
|
-
assert.match(content, new RegExp(localPrefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
|
85
|
-
// v0.27.0: CLI commands are now `tf` (global install), but other `tf` references may exist
|
|
86
|
-
// Only check that bare CLI commands are rewritten, not all `tf` references
|
|
87
|
-
// assert.doesNotMatch(content, new RegExp(PREFIX.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
|
88
|
-
} finally {
|
|
89
|
-
rmSync(target, { recursive: true, force: true });
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('emits a shell-literal local runtime command when the target path contains a dollar sign', () => {
|
|
94
|
-
const target = mkdtempSync(join(tmpdir(), 'tf-$runtime-'));
|
|
95
|
-
try {
|
|
96
|
-
execFileSync(process.execPath, [CLI, 'install-zcode', '--local', ROOT], {
|
|
97
|
-
cwd: target,
|
|
98
|
-
stdio: 'pipe',
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
const content = readFileSync(join(target, '.zcode', 'skills', 'workflow-start', 'SKILL.md'), 'utf8');
|
|
102
|
-
const command = content.match(/node '[^']+' runtime asset read docs\/state-machine\.md/);
|
|
103
|
-
assert.ok(command, 'workflow-start should contain a shell-literal local runtime command');
|
|
104
|
-
|
|
105
|
-
const output = execFileSync('sh', ['-c', command[0]], { cwd: target, encoding: 'utf8' });
|
|
106
|
-
assert.match(output, /State Machine/);
|
|
107
|
-
} finally {
|
|
108
|
-
rmSync(target, { recursive: true, force: true });
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
describe('platform runtime inventory', () => {
|
|
114
|
-
it('lists every documented platform and the ZCODE compatibility path', () => {
|
|
115
|
-
const ids = new Set(PLATFORM_RUNTIME_INVENTORY.map(platform => platform.id));
|
|
116
|
-
|
|
117
|
-
assert.deepEqual(ids, new Set([
|
|
118
|
-
'claude-code', 'cursor', 'codex-cli', 'codex-app', 'copilot-cli', 'gemini-cli',
|
|
119
|
-
'opencode', 'workbuddy', 'trae', 'cline', 'kiro', 'windsurf', 'qwen',
|
|
120
|
-
'amazon-q', 'roocode', 'continue', 'pi',
|
|
121
|
-
]));
|
|
122
|
-
assert.equal(ZCODE_COMPATIBILITY_PATH.id, 'zcode');
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('gives each documented distribution an explicit, testable runtime mode', () => {
|
|
126
|
-
const validModes = new Set(['native-root', 'installer-rewrite', 'canonical-fallback']);
|
|
127
|
-
|
|
128
|
-
for (const platform of PLATFORM_RUNTIME_INVENTORY) {
|
|
129
|
-
assert.ok(platform.modes.length > 0, `${platform.id} needs at least one distribution mode`);
|
|
130
|
-
for (const mode of platform.modes) assert.ok(validModes.has(mode), `${platform.id}: ${mode}`);
|
|
131
|
-
}
|
|
132
|
-
assert.deepEqual(ZCODE_COMPATIBILITY_PATH.modes, ['installer-rewrite']);
|
|
133
|
-
assert.ok(idsWithMode('codex-cli', 'canonical-fallback'));
|
|
134
|
-
assert.ok(idsWithMode('codex-app', 'canonical-fallback'));
|
|
135
|
-
assert.ok(idsWithMode('opencode', 'canonical-fallback'));
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
describe('runtime version synchronization', () => {
|
|
140
|
-
it('includes code-reviewer when a release version is dry-run', () => {
|
|
141
|
-
const output = execFileSync(process.execPath, [CLI, 'version', '0.9.2', '--dry-run'], {
|
|
142
|
-
cwd: ROOT,
|
|
143
|
-
encoding: 'utf8',
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
// v0.27.0: Skills no longer embed version references (CLI version synced via session-start hook)
|
|
147
|
-
// Version sync no longer updates skills files
|
|
148
|
-
// assert.match(output, /skills\/code-reviewer\/SKILL\.md: version string updated/);
|
|
149
|
-
// assert.match(output, /skills\/build-executor\/implementer-prompt\.md: version string updated/);
|
|
150
|
-
// assert.match(output, /skills\/build-executor\/task-reviewer-prompt\.md: version string updated/);
|
|
151
|
-
// assert.match(output, /skills\/code-reviewer\/code-reviewer-prompt\.md: version string updated/);
|
|
152
|
-
});
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
function idsWithMode(id, mode) {
|
|
156
|
-
return PLATFORM_RUNTIME_INVENTORY.find(platform => platform.id === id)?.modes.includes(mode);
|
|
157
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
// Raw-package smoke: canonical skills must work without host plugin-root variables
|
|
2
|
-
// or a globally installed `tf` binary.
|
|
3
|
-
import { describe, it } from 'node:test';
|
|
4
|
-
import assert from 'node:assert/strict';
|
|
5
|
-
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
6
|
-
import { spawnSync } from 'node:child_process';
|
|
7
|
-
import { join } from 'node:path';
|
|
8
|
-
import { tmpdir } from 'node:os';
|
|
9
|
-
|
|
10
|
-
const ROOT = process.cwd();
|
|
11
|
-
|
|
12
|
-
function run(command, args, options) {
|
|
13
|
-
const result = spawnSync(command, args, { encoding: 'utf8', ...options });
|
|
14
|
-
assert.equal(result.status, 0, `${command} ${args.join(' ')}\n${result.stderr}`);
|
|
15
|
-
return result.stdout;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
describe('raw-package runtime smoke', () => {
|
|
19
|
-
it('runs a packed canonical runtime with no plugin-root variables or global tf', () => {
|
|
20
|
-
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8'));
|
|
21
|
-
assert.equal(pkg.scripts['test:raw-mode'], 'node --test tests/lib/raw-mode-smoke.test.mjs');
|
|
22
|
-
|
|
23
|
-
const tempRoot = mkdtempSync(join(tmpdir(), 'tf-raw-mode-'));
|
|
24
|
-
try {
|
|
25
|
-
const packJson = run('npm', ['pack', '--json', '--pack-destination', tempRoot], { cwd: ROOT });
|
|
26
|
-
const [{ filename }] = JSON.parse(packJson);
|
|
27
|
-
const env = { ...process.env };
|
|
28
|
-
delete env.CLAUDE_PLUGIN_ROOT;
|
|
29
|
-
delete env.PLUGIN_ROOT;
|
|
30
|
-
delete env.CODEX_HOME;
|
|
31
|
-
|
|
32
|
-
const output = run('npx', [
|
|
33
|
-
'--yes', '--package', join(tempRoot, filename), 'tf',
|
|
34
|
-
'runtime', 'asset', 'read', 'docs/state-machine.md',
|
|
35
|
-
], { cwd: tempRoot, env });
|
|
36
|
-
|
|
37
|
-
assert.match(output, /State Machine/);
|
|
38
|
-
} finally {
|
|
39
|
-
rmSync(tempRoot, { recursive: true, force: true });
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
});
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { tmpdir } from 'node:os';
|
|
6
|
-
import {
|
|
7
|
-
saveCheckpoint, listCheckpoints, createHandoff, listHandoffs, finishHandoff, resolveHandoff,
|
|
8
|
-
} from '../../scripts/lib/sdd-overlay.mjs';
|
|
9
|
-
|
|
10
|
-
let changeDir;
|
|
11
|
-
|
|
12
|
-
const validHandoffInput = {
|
|
13
|
-
type: 'research',
|
|
14
|
-
title: 'Research findings',
|
|
15
|
-
question: 'Which approach should the implementation use?',
|
|
16
|
-
context: 'Compare the available implementation approaches.',
|
|
17
|
-
source: 'Local repository inspection',
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
beforeEach(() => {
|
|
21
|
-
changeDir = mkdtempSync(join(tmpdir(), 'sdd-overlay-'));
|
|
22
|
-
writeFileSync(join(changeDir, 'tasks.md'), '# Tasks\n\n- [ ] 1.1 Original task text\n');
|
|
23
|
-
writeFileSync(join(changeDir, 'proposal.md'), '# Proposal\n\nA valid proposal.\n');
|
|
24
|
-
writeFileSync(join(changeDir, 'design.md'), '# Design\n\nA valid design.\n');
|
|
25
|
-
writeFileSync(join(changeDir, '.team-flow.yaml'), 'state: executing\nworkflow: auto\n');
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
afterEach(() => {
|
|
29
|
-
rmSync(changeDir, { recursive: true, force: true });
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
describe('checkpoint storage', () => {
|
|
33
|
-
it('persists commit boundaries and recovery fields across list and show reads', () => {
|
|
34
|
-
saveCheckpoint(changeDir, {
|
|
35
|
-
taskId: '1.1',
|
|
36
|
-
next: 'Run the focused test',
|
|
37
|
-
completed: 'Added parser tests',
|
|
38
|
-
evidence: 'tests/lib/sdd-overlay.test.mjs',
|
|
39
|
-
review: 'review.md',
|
|
40
|
-
risk: 'None',
|
|
41
|
-
commitStart: 'aaaaaaa',
|
|
42
|
-
commitEnd: 'bbbbbbb',
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
const checkpoint = listCheckpoints(changeDir)[0];
|
|
46
|
-
assert.equal(checkpoint.task_id, '1.1');
|
|
47
|
-
assert.equal(checkpoint.next, 'Run the focused test');
|
|
48
|
-
assert.equal(checkpoint.completed, 'Added parser tests');
|
|
49
|
-
assert.equal(checkpoint.evidence, 'tests/lib/sdd-overlay.test.mjs');
|
|
50
|
-
assert.equal(checkpoint.review, 'review.md');
|
|
51
|
-
assert.equal(checkpoint.risk, 'None');
|
|
52
|
-
assert.equal(checkpoint.commit_start, 'aaaaaaa');
|
|
53
|
-
assert.equal(checkpoint.commit_end, 'bbbbbbb');
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('marks a checkpoint stale when its numbered task line changes', () => {
|
|
57
|
-
saveCheckpoint(changeDir, { taskId: '1.1', next: 'Run the focused test' });
|
|
58
|
-
writeFileSync(join(changeDir, 'tasks.md'), '# Tasks\n\n- [ ] 1.1 Changed task text\n');
|
|
59
|
-
|
|
60
|
-
assert.equal(listCheckpoints(changeDir)[0].stale, true);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('marks a checkpoint stale when its task is removed', () => {
|
|
64
|
-
saveCheckpoint(changeDir, { taskId: '1.1', next: 'Run the focused test' });
|
|
65
|
-
writeFileSync(join(changeDir, 'tasks.md'), '# Tasks\n');
|
|
66
|
-
|
|
67
|
-
assert.equal(listCheckpoints(changeDir)[0].stale, true);
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
describe('handoff storage', () => {
|
|
72
|
-
it('creates HANDOFF.md and HANDOFF_RESULT.md and validates the latter on finish', () => {
|
|
73
|
-
const handoff = createHandoff(changeDir, validHandoffInput);
|
|
74
|
-
const handoffPath = join(handoff.directory, 'HANDOFF.md');
|
|
75
|
-
const resultPath = join(handoff.directory, 'HANDOFF_RESULT.md');
|
|
76
|
-
|
|
77
|
-
assert.equal(existsSync(handoffPath), true);
|
|
78
|
-
assert.equal(existsSync(resultPath), true);
|
|
79
|
-
writeFileSync(resultPath, validResult());
|
|
80
|
-
assert.equal(finishHandoff(changeDir, handoff.id).status, 'result-ready');
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('keeps a handoff active when finish validation fails', () => {
|
|
84
|
-
const handoff = createHandoff(changeDir, validHandoffInput);
|
|
85
|
-
|
|
86
|
-
assert.throws(() => finishHandoff(changeDir, handoff.id), /Conclusion/);
|
|
87
|
-
assert.equal(listHandoffs(changeDir)[0].status, 'active');
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('requires explicit drift acknowledgement before resolving', () => {
|
|
91
|
-
const handoff = createReadyHandoff();
|
|
92
|
-
writeFileSync(join(changeDir, 'design.md'), '# Changed design\n');
|
|
93
|
-
|
|
94
|
-
assert.throws(
|
|
95
|
-
() => resolveHandoff(changeDir, handoff.id, 'accept', false),
|
|
96
|
-
/acknowledge-source-drift/,
|
|
97
|
-
);
|
|
98
|
-
assert.equal(resolveHandoff(changeDir, handoff.id, 'accept', true).status, 'resolved');
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
function createReadyHandoff() {
|
|
103
|
-
const handoff = createHandoff(changeDir, validHandoffInput);
|
|
104
|
-
const resultPath = join(changeDir, '.superpowers', 'sdd', 'handoffs', handoff.id, 'HANDOFF_RESULT.md');
|
|
105
|
-
writeFileSync(resultPath, validResult());
|
|
106
|
-
return finishHandoff(changeDir, handoff.id);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function validResult() {
|
|
110
|
-
return [
|
|
111
|
-
'## Conclusion\nThe research is complete.',
|
|
112
|
-
'## Evidence\nRepository evidence was reviewed.',
|
|
113
|
-
'## Produced Artifacts\nThe handoff record was produced.',
|
|
114
|
-
'## Risks\nNo additional risks were found.',
|
|
115
|
-
'## Suggested Changes\nProceed with the selected approach.',
|
|
116
|
-
'',
|
|
117
|
-
].join('\n');
|
|
118
|
-
}
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
// tests/lib/solutions-capture.test.mjs
|
|
2
|
-
// Tests for scripts/lib/solutions-capture.mjs — compound capture mechanism
|
|
3
|
-
|
|
4
|
-
import { describe, it, before, after, beforeEach } from 'node:test';
|
|
5
|
-
import assert from 'node:assert/strict';
|
|
6
|
-
import { mkdtempSync, readFileSync, existsSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
7
|
-
import { join } from 'node:path';
|
|
8
|
-
import { tmpdir } from 'node:os';
|
|
9
|
-
import { fileURLToPath } from 'node:url';
|
|
10
|
-
import { dirname } from 'node:path';
|
|
11
|
-
|
|
12
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
-
const ROOT = join(__dirname, '..', '..');
|
|
14
|
-
|
|
15
|
-
const { run: captureRun } = await import(join(ROOT, 'scripts', 'lib', 'solutions-capture.mjs'));
|
|
16
|
-
|
|
17
|
-
function makeDir() {
|
|
18
|
-
return mkdtempSync(join(tmpdir(), 'tf-sol-capture-'));
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function cleanup(dir) {
|
|
22
|
-
if (existsSync(dir)) rmSync(dir, { recursive: true, force: true });
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
describe('solutions-capture', () => {
|
|
26
|
-
let dir;
|
|
27
|
-
|
|
28
|
-
beforeEach(() => {
|
|
29
|
-
dir = makeDir();
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
after(() => {
|
|
33
|
-
// Clean up all temp dirs
|
|
34
|
-
cleanup(dir);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('creates experience file in correct phase directory', () => {
|
|
38
|
-
const result = captureRun({
|
|
39
|
-
phase: 'prd', domain: 'auth', type: 'pitfall', severity: 'high',
|
|
40
|
-
summary: 'OAuth token refresh fails silently', dir,
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
assert.equal(result.phase, 'prd');
|
|
44
|
-
assert.equal(result.severity, 'high');
|
|
45
|
-
assert.match(result.file, /^prd\/\d{4}-\d{2}-\d{2}-.*\.md$/);
|
|
46
|
-
|
|
47
|
-
// Verify file exists and has frontmatter
|
|
48
|
-
const filePath = join(dir, result.file);
|
|
49
|
-
assert.ok(existsSync(filePath), 'experience file should exist');
|
|
50
|
-
const content = readFileSync(filePath, 'utf-8');
|
|
51
|
-
assert.match(content, /^---\nphase: prd\n/);
|
|
52
|
-
assert.match(content, /domain: auth/);
|
|
53
|
-
assert.match(content, /type: pitfall/);
|
|
54
|
-
assert.match(content, /severity: high/);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('creates INDEX.md from scratch when missing', () => {
|
|
58
|
-
captureRun({
|
|
59
|
-
phase: 'build', domain: 'ci', type: 'pattern', severity: 'medium',
|
|
60
|
-
summary: 'Parallel test execution race condition', dir,
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
const indexPath = join(dir, 'INDEX.md');
|
|
64
|
-
assert.ok(existsSync(indexPath), 'INDEX.md should be created');
|
|
65
|
-
const content = readFileSync(indexPath, 'utf-8');
|
|
66
|
-
assert.match(content, /# Solutions Index/);
|
|
67
|
-
assert.match(content, /parallel test execution/i);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('appends to existing INDEX.md', () => {
|
|
71
|
-
captureRun({ phase: 'build', domain: 'ci', type: 'pattern', severity: 'high', summary: 'First issue', dir });
|
|
72
|
-
captureRun({ phase: 'spec', domain: 'api', type: 'insight', severity: 'low', summary: 'Second issue', dir });
|
|
73
|
-
|
|
74
|
-
const content = readFileSync(join(dir, 'INDEX.md'), 'utf-8');
|
|
75
|
-
assert.match(content, /First issue/);
|
|
76
|
-
assert.match(content, /Second issue/);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('handles Chinese characters in summary (slugify)', () => {
|
|
80
|
-
const result = captureRun({
|
|
81
|
-
phase: 'review', domain: 'frontend', type: 'pitfall', severity: 'high',
|
|
82
|
-
summary: '中文标题测试特殊字符', dir,
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
assert.ok(result.file.includes('review/'), 'file should be in review phase');
|
|
86
|
-
assert.ok(existsSync(join(dir, result.file)), 'file with Chinese slug should exist');
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('uses cross-phase as default when no phase specified', () => {
|
|
90
|
-
const result = captureRun({ dir });
|
|
91
|
-
assert.equal(result.phase, 'cross-phase');
|
|
92
|
-
assert.equal(result.domain, 'general');
|
|
93
|
-
assert.equal(result.severity, 'medium');
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('rejects invalid phase', () => {
|
|
97
|
-
// capture calls process.exit(1) on invalid phase
|
|
98
|
-
// We test by checking that the function does not create files
|
|
99
|
-
const origExit = process.exit;
|
|
100
|
-
let exitCode = null;
|
|
101
|
-
process.exit = (code) => { exitCode = code; };
|
|
102
|
-
try {
|
|
103
|
-
captureRun({ phase: 'invalid-phase', dir });
|
|
104
|
-
} catch { /* ignore */ }
|
|
105
|
-
process.exit = origExit;
|
|
106
|
-
assert.equal(exitCode, 1, 'should exit with code 1 for invalid phase');
|
|
107
|
-
});
|
|
108
|
-
});
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
// tests/lib/solutions-index-gen.test.mjs
|
|
2
|
-
// Tests for scripts/lib/solutions-index-gen.mjs — full INDEX.md rebuild
|
|
3
|
-
|
|
4
|
-
import { describe, it, before, after, beforeEach } from 'node:test';
|
|
5
|
-
import assert from 'node:assert/strict';
|
|
6
|
-
import { mkdtempSync, writeFileSync, existsSync, rmSync, mkdirSync, readFileSync } from 'node:fs';
|
|
7
|
-
import { join } from 'node:path';
|
|
8
|
-
import { tmpdir } from 'node:os';
|
|
9
|
-
import { fileURLToPath } from 'node:url';
|
|
10
|
-
import { dirname } from 'node:path';
|
|
11
|
-
|
|
12
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
-
const ROOT = join(__dirname, '..', '..');
|
|
14
|
-
|
|
15
|
-
const { run: indexGenRun } = await import(join(ROOT, 'scripts', 'lib', 'solutions-index-gen.mjs'));
|
|
16
|
-
|
|
17
|
-
function makeDir() {
|
|
18
|
-
return mkdtempSync(join(tmpdir(), 'tf-sol-indexgen-'));
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function cleanup(dir) {
|
|
22
|
-
if (existsSync(dir)) rmSync(dir, { recursive: true, force: true });
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function writeEntry(dir, phase, filename, fm) {
|
|
26
|
-
const phaseDir = join(dir, phase);
|
|
27
|
-
mkdirSync(phaseDir, { recursive: true });
|
|
28
|
-
const fmLines = Object.entries(fm).map(([k, v]) => `${k}: ${v}`).join('\n');
|
|
29
|
-
writeFileSync(join(phaseDir, filename), `---\n${fmLines}\n---\n\n${fm.summary || 'Body text'}\n`, 'utf-8');
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
describe('solutions-index-gen', () => {
|
|
33
|
-
let dir;
|
|
34
|
-
|
|
35
|
-
beforeEach(() => { dir = makeDir(); });
|
|
36
|
-
after(() => { cleanup(dir); });
|
|
37
|
-
|
|
38
|
-
it('rebuilds INDEX.md from phase directories', () => {
|
|
39
|
-
writeEntry(dir, 'prd', '2026-07-01-auth-issue.md', {
|
|
40
|
-
phase: 'prd', domain: 'auth', type: 'pitfall', severity: 'high', date: '2026-07-01',
|
|
41
|
-
});
|
|
42
|
-
writeEntry(dir, 'build', '2026-07-02-ci-pattern.md', {
|
|
43
|
-
phase: 'build', domain: 'ci', type: 'pattern', severity: 'medium', date: '2026-07-02',
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
indexGenRun({ dir });
|
|
47
|
-
|
|
48
|
-
const content = readFileSync(join(dir, 'INDEX.md'), 'utf-8');
|
|
49
|
-
assert.match(content, /# Solutions Index/);
|
|
50
|
-
assert.match(content, /auth-issue/);
|
|
51
|
-
assert.match(content, /ci-pattern/);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('sorts by severity descending (high before medium before low)', () => {
|
|
55
|
-
writeEntry(dir, 'prd', 'low.md', {
|
|
56
|
-
phase: 'prd', domain: 'd1', type: 'pitfall', severity: 'low', date: '2026-07-03',
|
|
57
|
-
});
|
|
58
|
-
writeEntry(dir, 'prd', 'high.md', {
|
|
59
|
-
phase: 'prd', domain: 'd2', type: 'pitfall', severity: 'high', date: '2026-07-01',
|
|
60
|
-
});
|
|
61
|
-
writeEntry(dir, 'prd', 'medium.md', {
|
|
62
|
-
phase: 'prd', domain: 'd3', type: 'pitfall', severity: 'medium', date: '2026-07-02',
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
indexGenRun({ dir });
|
|
66
|
-
|
|
67
|
-
const content = readFileSync(join(dir, 'INDEX.md'), 'utf-8');
|
|
68
|
-
const highPos = content.indexOf('d2');
|
|
69
|
-
const medPos = content.indexOf('d3');
|
|
70
|
-
const lowPos = content.indexOf('d1');
|
|
71
|
-
assert.ok(highPos < medPos, 'high should appear before medium');
|
|
72
|
-
assert.ok(medPos < lowPos, 'medium should appear before low');
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('sorts by date descending within same severity', () => {
|
|
76
|
-
writeEntry(dir, 'prd', 'older.md', {
|
|
77
|
-
phase: 'prd', domain: 'auth', type: 'pitfall', severity: 'high', date: '2026-07-01',
|
|
78
|
-
});
|
|
79
|
-
writeEntry(dir, 'prd', 'newer.md', {
|
|
80
|
-
phase: 'prd', domain: 'auth', type: 'pitfall', severity: 'high', date: '2026-07-15',
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
indexGenRun({ dir });
|
|
84
|
-
|
|
85
|
-
const content = readFileSync(join(dir, 'INDEX.md'), 'utf-8');
|
|
86
|
-
const newerPos = content.indexOf('2026-07-15');
|
|
87
|
-
const olderPos = content.indexOf('2026-07-01');
|
|
88
|
-
assert.ok(newerPos < olderPos, 'newer date should appear before older date');
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('scans all 7 phase directories', () => {
|
|
92
|
-
const phases = ['prd', 'plan', 'prototype', 'spec', 'build', 'review', 'cross-phase'];
|
|
93
|
-
for (const phase of phases) {
|
|
94
|
-
writeEntry(dir, phase, `2026-07-01-${phase}-entry.md`, {
|
|
95
|
-
phase, domain: 'general', type: 'insight', severity: 'medium', date: '2026-07-01',
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
indexGenRun({ dir });
|
|
100
|
-
|
|
101
|
-
const content = readFileSync(join(dir, 'INDEX.md'), 'utf-8');
|
|
102
|
-
for (const phase of phases) {
|
|
103
|
-
assert.match(content, new RegExp(phase), `INDEX should contain ${phase} entries`);
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('skips non-.md files', () => {
|
|
108
|
-
mkdirSync(join(dir, 'build'), { recursive: true });
|
|
109
|
-
writeFileSync(join(dir, 'build', 'readme.txt'), 'Not a markdown file', 'utf-8');
|
|
110
|
-
writeEntry(dir, 'build', '2026-07-01-real.md', {
|
|
111
|
-
phase: 'build', domain: 'ci', type: 'pattern', severity: 'high', date: '2026-07-01',
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
indexGenRun({ dir });
|
|
115
|
-
|
|
116
|
-
const content = readFileSync(join(dir, 'INDEX.md'), 'utf-8');
|
|
117
|
-
assert.doesNotMatch(content, /readme\.txt/);
|
|
118
|
-
assert.match(content, /real\.md/);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('truncates to 150 entries (drops lowest priority)', () => {
|
|
122
|
-
// Create 155 entries (all high severity, varying dates)
|
|
123
|
-
mkdirSync(join(dir, 'build'), { recursive: true });
|
|
124
|
-
for (let i = 0; i < 155; i++) {
|
|
125
|
-
const day = String((i % 28) + 1).padStart(2, '0');
|
|
126
|
-
writeEntry(dir, 'build', `entry-${String(i).padStart(3, '0')}.md`, {
|
|
127
|
-
phase: 'build', domain: `d${i}`, type: 'pattern', severity: 'high', date: `2026-07-${day}`,
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
indexGenRun({ dir });
|
|
132
|
-
|
|
133
|
-
const content = readFileSync(join(dir, 'INDEX.md'), 'utf-8');
|
|
134
|
-
const dataLines = content.split('\n').filter(l => l.startsWith('|') && !l.startsWith('| date') && !l.startsWith('|--'));
|
|
135
|
-
assert.equal(dataLines.length, 150, 'INDEX should be truncated to 150 entries');
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('handles empty solutions directory', () => {
|
|
139
|
-
// No phase directories exist
|
|
140
|
-
indexGenRun({ dir });
|
|
141
|
-
|
|
142
|
-
const content = readFileSync(join(dir, 'INDEX.md'), 'utf-8');
|
|
143
|
-
assert.match(content, /# Solutions Index/);
|
|
144
|
-
const dataLines = content.split('\n').filter(l => l.startsWith('|') && !l.startsWith('| date') && !l.startsWith('|--'));
|
|
145
|
-
assert.equal(dataLines.length, 0);
|
|
146
|
-
});
|
|
147
|
-
});
|