@haposoft/cafekit 0.16.0-rc.3 → 0.16.0-rc.4
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/bin/__tests__/codex-hooks.test.js +444 -14
- package/bin/__tests__/codex-native.test.js +228 -7
- package/bin/__tests__/develop-contract.test.js +615 -185
- package/bin/__tests__/installer-safety.test.js +39 -0
- package/bin/__tests__/opencode-spec-gate.test.js +24 -16
- package/bin/__tests__/package-inventory.test.js +687 -13
- package/bin/__tests__/specs-v2-execution-closeout.test.js +421 -0
- package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +601 -0
- package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +952 -0
- package/bin/__tests__/test-runtime-dependency-closure.cjs +182 -0
- package/bin/__tests__/validator-grounding.test.js +1914 -221
- package/bin/lib/managed-writer.js +12 -3
- package/bin/phases/codex-runtime.js +17 -1
- package/package.json +1 -1
- package/src/claude/CLAUDE.md +6 -0
- package/src/claude/agents/code-auditor.md +20 -0
- package/src/claude/agents/spec-maker.md +143 -99
- package/src/claude/hooks/__tests__/completion-authority.test.js +291 -58
- package/src/claude/hooks/__tests__/privacy-block.test.js +172 -0
- package/src/claude/hooks/__tests__/semantic-review-authority.test.js +257 -0
- package/src/claude/hooks/__tests__/spec-gate.test.js +115 -40
- package/src/claude/hooks/completion-authority-check.cjs +37 -288
- package/src/claude/hooks/completion-authority-state.cjs +10 -8
- package/src/claude/hooks/completion-authority.cjs +13 -7
- package/src/claude/hooks/docs-sync.cjs +5 -2
- package/src/claude/hooks/lib/privacy-command-analysis.cjs +393 -0
- package/src/claude/hooks/lib/runtime-path-safety.cjs +119 -0
- package/src/claude/hooks/privacy-block.cjs +77 -30
- package/src/claude/hooks/semantic-review-authority.cjs +152 -0
- package/src/claude/hooks/spec-gate.cjs +47 -117
- package/src/claude/hooks/spec-state.cjs +8 -2
- package/src/claude/migration-manifest.json +9 -1
- package/src/claude/rules/state-sync.md +52 -21
- package/src/claude/rules/workflow.md +17 -9
- package/src/claude/scripts/spec-final-state.cjs +244 -0
- package/src/claude/scripts/spec-ground.cjs +535 -173
- package/src/claude/scripts/spec-readiness.cjs +118 -0
- package/src/claude/scripts/spec-receipt.cjs +162 -0
- package/src/claude/scripts/spec-resolver.cjs +67 -16
- package/src/claude/scripts/spec-scaffold.cjs +1354 -139
- package/src/claude/scripts/spec-semantic-model.cjs +396 -0
- package/src/claude/scripts/validate-spec-output.cjs +2782 -142
- package/src/claude/scripts/workflow-policy.cjs +501 -183
- package/src/claude/settings/settings.json +4 -0
- package/src/claude/skills/code-review/SKILL.md +12 -3
- package/src/claude/skills/code-review/references/spec-compliance-review.md +10 -2
- package/src/claude/skills/code-review/references/verification-gate.md +13 -0
- package/src/claude/skills/develop/SKILL.md +47 -60
- package/src/claude/skills/develop/references/parallel-waves.md +9 -6
- package/src/claude/skills/develop/references/quality-gate.md +19 -12
- package/src/claude/skills/specs/SKILL.md +183 -177
- package/src/claude/skills/specs/references/ask-user-question-gates.md +27 -18
- package/src/claude/skills/specs/references/cross-spec-dependency.md +8 -4
- package/src/claude/skills/specs/references/research-strategy.md +4 -1
- package/src/claude/skills/specs/references/review.md +200 -260
- package/src/claude/skills/specs/references/scope-inquiry.md +18 -5
- package/src/claude/skills/specs/rules/design-discovery-full.md +33 -81
- package/src/claude/skills/specs/rules/design-discovery-light.md +21 -42
- package/src/claude/skills/specs/rules/design-principles.md +136 -180
- package/src/claude/skills/specs/rules/design-review.md +42 -97
- package/src/claude/skills/specs/rules/ears-format.md +42 -50
- package/src/claude/skills/specs/rules/phase-decision-matrix.md +42 -41
- package/src/claude/skills/specs/rules/task-scoring-rubric.md +2 -1
- package/src/claude/skills/specs/rules/tasks-generation.md +189 -300
- package/src/claude/skills/specs/templates/design.md +49 -263
- package/src/claude/skills/specs/templates/requirements.md +26 -36
- package/src/claude/skills/specs/templates/research.md +9 -98
- package/src/claude/skills/specs/templates/spec-state.json +23 -85
- package/src/claude/skills/specs/templates/task.md +30 -102
- package/src/claude/skills/sync/SKILL.md +33 -10
- package/src/claude/skills/sync/references/sync-protocols.md +31 -13
- package/src/claude/skills/test/SKILL.md +17 -7
- package/src/codex/AGENTS.md +6 -0
- package/src/codex/hooks/completion-authority-check.cjs +43 -240
- package/src/codex/hooks/completion-authority-state.cjs +10 -8
- package/src/codex/hooks/completion-authority.cjs +13 -7
- package/src/codex/hooks/lib/hook-context.cjs +16 -2
- package/src/codex/hooks/lib/privacy-command-analysis.cjs +3 -0
- package/src/codex/hooks/lib/privacy-state.cjs +38 -16
- package/src/codex/hooks/lib/runtime-path-safety.cjs +3 -0
- package/src/codex/hooks/lib/spec-receipt.cjs +72 -134
- package/src/codex/hooks/lib/spec-utils.cjs +56 -5
- package/src/codex/hooks/privacy-approval.cjs +1 -1
- package/src/codex/hooks/privacy-block.cjs +67 -11
- package/src/codex/hooks/semantic-review-authority.cjs +130 -0
- package/src/codex/hooks/spec-gate.cjs +49 -34
- package/src/codex/hooks/spec-state.cjs +10 -3
- package/src/codex/hooks/state.cjs +2 -0
- package/src/codex/hooks.json +6 -1
- package/src/codex/rules/state-sync.md +47 -27
- package/src/common/AGENTS.md +1 -1
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const { spawnSync } = require('node:child_process');
|
|
8
|
+
const test = require('node:test');
|
|
9
|
+
|
|
10
|
+
const PACKAGE_ROOT = path.resolve(__dirname, '../..');
|
|
11
|
+
const POLICY = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/workflow-policy.cjs'));
|
|
12
|
+
const SCAFFOLD = path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-scaffold.cjs');
|
|
13
|
+
const TEMPLATE = path.join(PACKAGE_ROOT, 'src/claude/skills/specs/templates/spec-state.json');
|
|
14
|
+
const CANONICAL_POLICY_FIELDS = [
|
|
15
|
+
'version', 'planning_depth', 'assurance_level', 'classified_minimum', 'risks',
|
|
16
|
+
];
|
|
17
|
+
const CANONICAL_SPEC_FIELDS_WITH_TASKS = [
|
|
18
|
+
'schema_version', 'feature_name', 'created_at', 'updated_at', 'language', 'status',
|
|
19
|
+
'scope_lock', 'authoring', 'coordination', 'validation', 'semantic_model',
|
|
20
|
+
'ready_for_implementation', 'workflow_policy', 'task_files', 'task_registry',
|
|
21
|
+
].sort();
|
|
22
|
+
|
|
23
|
+
function run(root, args, env = {}) {
|
|
24
|
+
return spawnSync(process.execPath, [SCAFFOLD, ...args], {
|
|
25
|
+
cwd: root, encoding: 'utf8', env: { ...process.env, ...env },
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function output(result) { return `${result.stdout}\n${result.stderr}`; }
|
|
30
|
+
|
|
31
|
+
function readSpec(root, feature) {
|
|
32
|
+
return JSON.parse(fs.readFileSync(path.join(root, 'specs', feature, 'spec.json'), 'utf8'));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function ownershipBoundary() {
|
|
36
|
+
return JSON.stringify([{
|
|
37
|
+
id: 'B-OWN', type: 'ownership', tasks: ['R1-01', 'R1-02'],
|
|
38
|
+
write_sets: { 'R1-01': ['src/one.js'], 'R1-02': ['src/two.js'] },
|
|
39
|
+
}]);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readTree(root) {
|
|
43
|
+
const result = {};
|
|
44
|
+
function visit(directory) {
|
|
45
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
46
|
+
const absolute = path.join(directory, entry.name);
|
|
47
|
+
const relative = path.relative(root, absolute);
|
|
48
|
+
if (entry.isDirectory()) visit(absolute);
|
|
49
|
+
else result[relative] = fs.readFileSync(absolute).toString('base64');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
visit(root);
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function runMatchingTasksOnly(root, feature, env = {}) {
|
|
57
|
+
return run(root, [
|
|
58
|
+
feature, '--tasks-only', '--tasks', 'R1-01-one,R1-02-two',
|
|
59
|
+
'--boundaries', ownershipBoundary(),
|
|
60
|
+
], env);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function transactionDirs(root) {
|
|
64
|
+
return fs.readdirSync(root).filter((entry) => entry.startsWith('.cafekit-scaffold-tx-'));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
test('policy 2.1 persists exactly selected axes, classified minimum, and risks', () => {
|
|
68
|
+
const snapshot = POLICY.canonicalWorkflowPolicySnapshot({
|
|
69
|
+
planning_depth: 'Compact', assurance_level: 'Routine',
|
|
70
|
+
});
|
|
71
|
+
assert.deepEqual(Object.keys(snapshot), CANONICAL_POLICY_FIELDS);
|
|
72
|
+
assert.equal(snapshot.version, '2.1');
|
|
73
|
+
const view = POLICY.readWorkflowPolicySnapshot({ workflow_policy: snapshot });
|
|
74
|
+
assert.equal(view.lane, 'Standard');
|
|
75
|
+
assert.equal(view.artifact_profile, 'bounded');
|
|
76
|
+
assert.deepEqual(view.proof_obligations, ['needsExecutionProof']);
|
|
77
|
+
assert.deepEqual(POLICY.validateWorkflowPolicySnapshot(snapshot), { valid: true, errors: [] });
|
|
78
|
+
const legacyCanonical = { ...snapshot, override_receipt: null };
|
|
79
|
+
assert.equal(POLICY.validateWorkflowPolicySnapshot(legacyCanonical).valid, false);
|
|
80
|
+
assert.deepEqual(
|
|
81
|
+
POLICY.canonicalWorkflowPolicySnapshot({ workflow_policy: legacyCanonical }),
|
|
82
|
+
snapshot,
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('legacy policy is losslessly readable while authoring persistence migrates to 2.1', () => {
|
|
87
|
+
const legacy = {
|
|
88
|
+
version: '2', planning_depth: 'Compact', automatic_planning_depth: 'Compact',
|
|
89
|
+
assurance_level: 'Routine', automatic_assurance_level: 'Routine', lane: 'Standard',
|
|
90
|
+
automatic_lane: 'Standard', risks: [], artifact_profile: 'bounded',
|
|
91
|
+
planning_obligations: ['needsRequirements', 'needsDesign'], proof_obligations: ['needsExecutionProof'],
|
|
92
|
+
actor_needs: [{ capability: 'execution-proof', independence: 'same-session' }], override_receipt: null,
|
|
93
|
+
};
|
|
94
|
+
const view = POLICY.readWorkflowPolicySnapshot({ workflow_policy: legacy });
|
|
95
|
+
assert.equal(view.version, '2');
|
|
96
|
+
assert.equal(view.lane, 'Standard');
|
|
97
|
+
const migrated = POLICY.persistWorkflowPolicySnapshot({ workflow_policy: legacy }).workflow_policy;
|
|
98
|
+
assert.equal(migrated.version, '2.1');
|
|
99
|
+
assert.equal(migrated.planning_depth, 'Compact');
|
|
100
|
+
assert.equal(Object.hasOwn(migrated, 'lane'), false);
|
|
101
|
+
assert.equal(Object.hasOwn(migrated, 'override_receipt'), false);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('canonical state template is schema 2.1 and approval-free', () => {
|
|
105
|
+
const template = JSON.parse(fs.readFileSync(TEMPLATE, 'utf8'));
|
|
106
|
+
assert.equal(template.schema_version, '2.1');
|
|
107
|
+
assert.equal(template.workflow_policy.version, '2.1');
|
|
108
|
+
assert.equal(Object.hasOwn(template, 'approvals'), false);
|
|
109
|
+
assert.deepEqual(template.authoring, {
|
|
110
|
+
requirements: 'draft', design: 'draft', research: 'absent', tasks: 'absent',
|
|
111
|
+
});
|
|
112
|
+
assert.deepEqual(template.coordination, { boundaries: [] });
|
|
113
|
+
assert.equal(template.semantic_model, null);
|
|
114
|
+
assert.deepEqual(Object.keys(template.workflow_policy), CANONICAL_POLICY_FIELDS);
|
|
115
|
+
assert.equal(Object.hasOwn(template, 'override_receipt'), false);
|
|
116
|
+
assert.doesNotMatch(
|
|
117
|
+
fs.readFileSync(SCAFFOLD, 'utf8'),
|
|
118
|
+
/persist only workflow_policy\.override_receipt/,
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('new scaffold emits taskless Compact 2.1 without Routine ceremony', () => {
|
|
123
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-sk21-taskless-'));
|
|
124
|
+
try {
|
|
125
|
+
const result = run(root, ['compact']);
|
|
126
|
+
assert.equal(result.status, 0, result.stderr);
|
|
127
|
+
const spec = readSpec(root, 'compact');
|
|
128
|
+
assert.equal(spec.schema_version, '2.1');
|
|
129
|
+
assert.equal(spec.workflow_policy.version, '2.1');
|
|
130
|
+
assert.deepEqual(spec.coordination, { boundaries: [] });
|
|
131
|
+
assert.equal(spec.authoring.tasks, 'absent');
|
|
132
|
+
assert.equal(Object.hasOwn(spec, 'task_files'), false);
|
|
133
|
+
assert.equal(Object.hasOwn(spec, 'task_registry'), false);
|
|
134
|
+
assert.equal(Object.hasOwn(spec, 'research'), false);
|
|
135
|
+
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('research materializes only from a concrete uncertainty and stays minimal', () => {
|
|
139
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-sk21-research-'));
|
|
140
|
+
try {
|
|
141
|
+
for (const [feature, args] of [
|
|
142
|
+
['missing', ['--research']],
|
|
143
|
+
['placeholder', ['--research', '--uncertainty', 'TBD']],
|
|
144
|
+
['orphan', ['--uncertainty', 'Which persistence boundary is authoritative?']],
|
|
145
|
+
]) {
|
|
146
|
+
const result = run(root, [feature, ...args]);
|
|
147
|
+
assert.equal(result.status, 2, output(result));
|
|
148
|
+
assert.equal(fs.existsSync(path.join(root, 'specs', feature)), false);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const uncertainty = 'Which persisted boundary owns retry idempotency?';
|
|
152
|
+
const result = run(root, ['grounded', '--research', '--uncertainty', uncertainty]);
|
|
153
|
+
assert.equal(result.status, 0, output(result));
|
|
154
|
+
const spec = readSpec(root, 'grounded');
|
|
155
|
+
assert.equal(spec.research, 'research.md');
|
|
156
|
+
assert.equal(spec.authoring.research, 'draft');
|
|
157
|
+
const research = fs.readFileSync(path.join(root, 'specs/grounded/research.md'), 'utf8');
|
|
158
|
+
assert.deepEqual(
|
|
159
|
+
[...research.matchAll(/^## (.+)$/gm)].map((match) => match[1]),
|
|
160
|
+
['Uncertainty', 'Evidence Summary', 'Decision', 'Remaining Gaps'],
|
|
161
|
+
);
|
|
162
|
+
assert.match(research, new RegExp(uncertainty.replace(/[?]/g, '\\?')));
|
|
163
|
+
assert.doesNotMatch(research, /\{\{[^}]+\}\}/);
|
|
164
|
+
assert.doesNotMatch(research, /Finding 1|Option A|example\.com|Architecture Pattern Evaluation/);
|
|
165
|
+
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('scaffold generates typed topology and canonical task projection', () => {
|
|
169
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-sk21-owned-'));
|
|
170
|
+
try {
|
|
171
|
+
const result = run(root, [
|
|
172
|
+
'owned', '--tasks', 'R1-01-one,R1-02-two', '--boundaries', ownershipBoundary(),
|
|
173
|
+
]);
|
|
174
|
+
assert.equal(result.status, 0, result.stderr);
|
|
175
|
+
const spec = readSpec(root, 'owned');
|
|
176
|
+
assert.deepEqual(spec.coordination.boundaries, JSON.parse(ownershipBoundary()));
|
|
177
|
+
assert.equal(spec.authoring.tasks, 'draft');
|
|
178
|
+
for (const taskPath of spec.task_files) {
|
|
179
|
+
const body = fs.readFileSync(path.join(root, 'specs', 'owned', taskPath), 'utf8');
|
|
180
|
+
for (const heading of ['Outcome', 'Scope', 'Anchors and Ownership', 'Changes', 'Acceptance', 'Dependencies', 'Verification Plan']) {
|
|
181
|
+
assert.match(body, new RegExp(`^## ${heading}$`, 'm'));
|
|
182
|
+
}
|
|
183
|
+
assert.match(body, /\| ID \| Type \| Target \| Role \| Access \| Action \|/);
|
|
184
|
+
assert.doesNotMatch(body, /^## Related Files$/m);
|
|
185
|
+
}
|
|
186
|
+
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('scaffold rejects marker-only evidence, single-task parallel, and parent-child contention', () => {
|
|
190
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-sk21-negative-'));
|
|
191
|
+
try {
|
|
192
|
+
const marker = run(root, ['marker', '--tasks', 'R1-01-one', '--task-triggers', 'separate_proof']);
|
|
193
|
+
assert.equal(marker.status, 2);
|
|
194
|
+
assert.match(`${marker.stdout}\n${marker.stderr}`, /only topology authority|typed coordination/);
|
|
195
|
+
|
|
196
|
+
const single = run(root, [
|
|
197
|
+
'single', '--tasks', 'R1-01-one', '--boundaries',
|
|
198
|
+
JSON.stringify([{ id: 'B-P', type: 'parallel', tasks: ['R1-01'], resources: { 'R1-01': ['src/one.js'] } }]),
|
|
199
|
+
]);
|
|
200
|
+
assert.equal(single.status, 2);
|
|
201
|
+
assert.match(`${single.stdout}\n${single.stderr}`, /at least 2 task/);
|
|
202
|
+
|
|
203
|
+
const contention = run(root, [
|
|
204
|
+
'contention', '--tasks', 'R1-01-one,R1-02-two', '--boundaries',
|
|
205
|
+
JSON.stringify([{ id: 'B-P', type: 'parallel', tasks: ['R1-01', 'R1-02'], resources: {
|
|
206
|
+
'R1-01': ['src/domain'], 'R1-02': ['src/domain/model.js'],
|
|
207
|
+
} }]),
|
|
208
|
+
]);
|
|
209
|
+
assert.equal(contention.status, 2);
|
|
210
|
+
assert.match(`${contention.stdout}\n${contention.stderr}`, /resource contention/);
|
|
211
|
+
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('scaffold rejects incomplete dependency, transition, and proof boundaries', () => {
|
|
215
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-sk21-boundaries-'));
|
|
216
|
+
try {
|
|
217
|
+
const cases = [
|
|
218
|
+
['dependency', { id: 'B-D', type: 'dependency', producer: 'R1-01', consumer: 'R1-02' }, /fields must be exactly/],
|
|
219
|
+
['transition', { id: 'B-T', type: 'transition', design_ref: 'T1', owner: 'R1-01', consumers: ['R1-02'], precondition: 'ready state', postcondition: 'done state', failure: 'failed state' }, /fields must be exactly/],
|
|
220
|
+
['proof', { id: 'B-V', type: 'proof', subject: 'R1-01', verifier: 'R1-01', verification_ref: 'V1', artifact_anchor: 'A-R1-01-02' }, /duplicate task ids|subject and verifier must differ/],
|
|
221
|
+
];
|
|
222
|
+
for (const [feature, boundary, expected] of cases) {
|
|
223
|
+
const result = run(root, [
|
|
224
|
+
feature, '--tasks', 'R1-01-one,R1-02-two', '--boundaries', JSON.stringify([boundary]),
|
|
225
|
+
]);
|
|
226
|
+
assert.equal(result.status, 2);
|
|
227
|
+
assert.match(`${result.stdout}\n${result.stderr}`, expected);
|
|
228
|
+
}
|
|
229
|
+
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('scaffold transaction rolls back injected writes exactly', () => {
|
|
233
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-sk21-rollback-'));
|
|
234
|
+
try {
|
|
235
|
+
const result = run(root, ['rollback'], { CAFEKIT_SCAFFOLD_FAIL_AFTER_WRITES: '1' });
|
|
236
|
+
assert.equal(result.status, 2);
|
|
237
|
+
assert.match(`${result.stdout}\n${result.stderr}`, /transaction rolled back/);
|
|
238
|
+
assert.equal(fs.existsSync(path.join(root, 'specs', 'rollback')), false);
|
|
239
|
+
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('durable transaction recovery covers every fresh and replacement rename boundary', { skip: process.platform === 'win32' }, () => {
|
|
243
|
+
const matrices = [
|
|
244
|
+
{ name: 'fresh', boundaries: 3, prepare() {}, invoke(root, feature, env) { return run(root, [feature], env); } },
|
|
245
|
+
{
|
|
246
|
+
name: 'replacement', boundaries: 2,
|
|
247
|
+
prepare(root, feature) {
|
|
248
|
+
const initial = run(root, [
|
|
249
|
+
feature, '--tasks', 'R1-01-one,R1-02-two', '--boundaries', ownershipBoundary(),
|
|
250
|
+
]);
|
|
251
|
+
assert.equal(initial.status, 0, output(initial));
|
|
252
|
+
const specPath = path.join(root, 'specs', feature, 'spec.json');
|
|
253
|
+
const spec = readSpec(root, feature);
|
|
254
|
+
spec.schema_version = '2.0';
|
|
255
|
+
fs.writeFileSync(specPath, `${JSON.stringify(spec, null, 2)}\n`);
|
|
256
|
+
},
|
|
257
|
+
invoke(root, feature, env) { return runMatchingTasksOnly(root, feature, env); },
|
|
258
|
+
},
|
|
259
|
+
];
|
|
260
|
+
|
|
261
|
+
for (const matrix of matrices) {
|
|
262
|
+
for (let boundary = 1; boundary <= matrix.boundaries; boundary += 1) {
|
|
263
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), `cafekit-sk21-kill-${matrix.name}-`));
|
|
264
|
+
const feature = `${matrix.name}-${boundary}`;
|
|
265
|
+
try {
|
|
266
|
+
matrix.prepare(root, feature);
|
|
267
|
+
const killed = matrix.invoke(root, feature, {
|
|
268
|
+
CAFEKIT_SCAFFOLD_KILL_AFTER_RENAMES: String(boundary),
|
|
269
|
+
});
|
|
270
|
+
assert.equal(killed.status, null, `${matrix.name}:${boundary}: ${output(killed)}`);
|
|
271
|
+
assert.equal(killed.signal, 'SIGKILL', `${matrix.name}:${boundary}`);
|
|
272
|
+
assert.equal(transactionDirs(root).length, 1, `${matrix.name}:${boundary} missing journal`);
|
|
273
|
+
|
|
274
|
+
const recovered = matrix.invoke(root, feature, {});
|
|
275
|
+
assert.equal(recovered.status, 0, `${matrix.name}:${boundary}: ${output(recovered)}`);
|
|
276
|
+
assert.equal(transactionDirs(root).length, 0, `${matrix.name}:${boundary} leaked journal`);
|
|
277
|
+
assert.equal(readSpec(root, feature).schema_version, '2.1');
|
|
278
|
+
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test('startup recovery preserves a target whose transaction ownership cannot be proven', { skip: process.platform === 'win32' }, () => {
|
|
284
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-sk21-unowned-'));
|
|
285
|
+
try {
|
|
286
|
+
const killed = run(root, ['unowned'], { CAFEKIT_SCAFFOLD_KILL_AFTER_RENAMES: '1' });
|
|
287
|
+
assert.equal(killed.status, null, output(killed));
|
|
288
|
+
assert.equal(killed.signal, 'SIGKILL');
|
|
289
|
+
const target = path.join(root, 'specs/unowned/spec.json');
|
|
290
|
+
const externalBytes = Buffer.from('{"external":"concurrent writer"}\n');
|
|
291
|
+
fs.writeFileSync(target, externalBytes);
|
|
292
|
+
|
|
293
|
+
const recovery = run(root, ['unowned']);
|
|
294
|
+
assert.equal(recovery.status, 2, output(recovery));
|
|
295
|
+
assert.match(output(recovery), /refuses to delete unowned target spec\.json/);
|
|
296
|
+
assert.deepEqual(fs.readFileSync(target), externalBytes);
|
|
297
|
+
assert.equal(transactionDirs(root).length, 1, 'journal must remain for explicit resolution');
|
|
298
|
+
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test('typed boundary mutation preserves legacy graph counterexamples atomically', () => {
|
|
302
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-sk21-graph-mutations-'));
|
|
303
|
+
try {
|
|
304
|
+
const tasks = 'R1-01-owner,R1-02-consumer';
|
|
305
|
+
const cases = [
|
|
306
|
+
['unknown-task', [{ id: 'B-D', type: 'dependency', producer: 'R1-99', consumer: 'R1-02', deliverable: 'artifacts/value.json' }], /unknown task R1-99/],
|
|
307
|
+
['canonical-path-alias', [{ id: 'B-D', type: 'dependency', producer: 'tasks/task-R1-01-owner.md', consumer: 'R1-02', deliverable: 'artifacts/value.json' }], /unknown task tasks\/task-R1-01-owner\.md/],
|
|
308
|
+
['duplicate-boundary-id', [
|
|
309
|
+
{ id: 'B-DUP', type: 'dependency', producer: 'R1-01', consumer: 'R1-02', deliverable: 'artifacts/a.json' },
|
|
310
|
+
{ id: 'B-DUP', type: 'dependency', producer: 'R1-01', consumer: 'R1-02', deliverable: 'artifacts/b.json' },
|
|
311
|
+
], /duplicate id B-DUP/],
|
|
312
|
+
['dependency-cycle', [
|
|
313
|
+
{ id: 'B-D1', type: 'dependency', producer: 'R1-01', consumer: 'R1-02', deliverable: 'artifacts/a.json' },
|
|
314
|
+
{ id: 'B-D2', type: 'dependency', producer: 'R1-02', consumer: 'R1-01', deliverable: 'artifacts/b.json' },
|
|
315
|
+
], /dependency cycle/],
|
|
316
|
+
['malformed-fields', [{ id: 'B-D', type: 'dependency', producer: 'R1-01', consumer: 'R1-02' }], /fields must be exactly/],
|
|
317
|
+
];
|
|
318
|
+
for (const [feature, boundaries, expected] of cases) {
|
|
319
|
+
const before = fs.existsSync(path.join(root, 'specs'))
|
|
320
|
+
? fs.readdirSync(path.join(root, 'specs')).sort() : [];
|
|
321
|
+
const result = run(root, [feature, '--tasks', tasks, '--boundaries', JSON.stringify(boundaries)]);
|
|
322
|
+
assert.equal(result.status, 2, output(result));
|
|
323
|
+
assert.match(output(result), expected);
|
|
324
|
+
assert.equal(fs.existsSync(path.join(root, 'specs', feature)), false, `${feature} wrote before validation`);
|
|
325
|
+
assert.deepEqual(fs.existsSync(path.join(root, 'specs')) ? fs.readdirSync(path.join(root, 'specs')).sort() : [], before);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const identity = run(root, [
|
|
329
|
+
'duplicate-task-identity', '--tasks', 'R1-01-owner,R1-01-alias', '--boundaries', '[]',
|
|
330
|
+
]);
|
|
331
|
+
assert.equal(identity.status, 2);
|
|
332
|
+
assert.match(output(identity), /duplicate task id "R1-01"/);
|
|
333
|
+
assert.equal(fs.existsSync(path.join(root, 'specs', 'duplicate-task-identity')), false);
|
|
334
|
+
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test('tasks-only normalization is table-driven, conservative, and idempotent', () => {
|
|
338
|
+
const cases = [
|
|
339
|
+
{
|
|
340
|
+
name: 'partial authoring and legacy schema',
|
|
341
|
+
mutate(spec) {
|
|
342
|
+
spec.schema_version = '2.0';
|
|
343
|
+
spec.override_receipt = null;
|
|
344
|
+
spec.workflow_policy.override_receipt = null;
|
|
345
|
+
delete spec.feature_name;
|
|
346
|
+
spec.authoring = { requirements: 'validated' };
|
|
347
|
+
spec.ready_for_implementation = true;
|
|
348
|
+
spec.validation = {
|
|
349
|
+
status: 'completed',
|
|
350
|
+
semantic_review: {
|
|
351
|
+
status: 'completed', semantic_digest: 'legacy',
|
|
352
|
+
reviewed_criteria: ['R1.1'], counterexamples: ['legacy review'],
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
},
|
|
356
|
+
verify(spec) {
|
|
357
|
+
assert.equal(spec.feature_name, 'partial-authoring-and-legacy-schema');
|
|
358
|
+
assert.deepEqual(Object.keys(spec).sort(), CANONICAL_SPEC_FIELDS_WITH_TASKS);
|
|
359
|
+
assert.equal(Object.hasOwn(spec, 'override_receipt'), false);
|
|
360
|
+
assert.deepEqual(Object.keys(spec.scope_lock).sort(), [
|
|
361
|
+
'expansion_policy', 'in_scope', 'out_of_scope', 'source',
|
|
362
|
+
]);
|
|
363
|
+
assert.equal(Object.hasOwn(spec.task_registry['tasks/task-R1-01-one.md'], 'legacy_owner'), false);
|
|
364
|
+
assert.deepEqual(Object.keys(spec.workflow_policy), CANONICAL_POLICY_FIELDS);
|
|
365
|
+
assert.deepEqual(spec.authoring, {
|
|
366
|
+
requirements: 'validated', design: 'draft', research: 'absent', tasks: 'draft',
|
|
367
|
+
});
|
|
368
|
+
assert.equal(spec.ready_for_implementation, false);
|
|
369
|
+
assert.deepEqual(spec.validation, {
|
|
370
|
+
status: 'not-run',
|
|
371
|
+
semantic_review: {
|
|
372
|
+
status: 'not-run', semantic_digest: null, reviewed_criteria: [], counterexamples: [],
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
name: 'legacy research pointer and valid done lifecycle',
|
|
379
|
+
research: true,
|
|
380
|
+
mutate(spec) {
|
|
381
|
+
spec.schema_version = '2.0';
|
|
382
|
+
spec.research = { path: 'research.md' };
|
|
383
|
+
delete spec.authoring.research;
|
|
384
|
+
const task = spec.task_registry['tasks/task-R1-01-one.md'];
|
|
385
|
+
Object.assign(task, {
|
|
386
|
+
status: 'done', blocker: null,
|
|
387
|
+
started_at: '2026-08-13T01:00:00.000Z',
|
|
388
|
+
completed_at: '2026-08-13T02:00:00.000Z',
|
|
389
|
+
last_updated_at: '2026-08-13T03:00:00.000Z',
|
|
390
|
+
});
|
|
391
|
+
},
|
|
392
|
+
verify(spec) {
|
|
393
|
+
assert.equal(spec.research, 'research.md');
|
|
394
|
+
assert.equal(spec.authoring.research, 'draft');
|
|
395
|
+
assert.equal(spec.task_registry['tasks/task-R1-01-one.md'].status, 'done');
|
|
396
|
+
assert.equal(
|
|
397
|
+
spec.task_registry['tasks/task-R1-01-one.md'].completed_at,
|
|
398
|
+
'2026-08-13T02:00:00.000Z',
|
|
399
|
+
);
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
name: 'legacy nested inert receipt read migration',
|
|
404
|
+
mutate(spec) {
|
|
405
|
+
spec.schema_version = '2.0';
|
|
406
|
+
spec.workflow_policy.override_receipt = null;
|
|
407
|
+
},
|
|
408
|
+
verify(spec) {
|
|
409
|
+
assert.equal(Object.hasOwn(spec, 'override_receipt'), false);
|
|
410
|
+
assert.deepEqual(Object.keys(spec.workflow_policy), CANONICAL_POLICY_FIELDS);
|
|
411
|
+
assert.equal(spec.ready_for_implementation, false);
|
|
412
|
+
assert.equal(spec.validation.status, 'not-run');
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
name: 'already canonical matching bundle',
|
|
417
|
+
expectStable: true,
|
|
418
|
+
mutate() {},
|
|
419
|
+
verify(spec) {
|
|
420
|
+
assert.equal(spec.schema_version, '2.1');
|
|
421
|
+
assert.equal(spec.authoring.tasks, 'draft');
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
];
|
|
425
|
+
|
|
426
|
+
for (const scenario of cases) {
|
|
427
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-sk21-normalize-'));
|
|
428
|
+
try {
|
|
429
|
+
const initial = run(root, [
|
|
430
|
+
scenario.name.replaceAll(' ', '-'), '--tasks', 'R1-01-one,R1-02-two',
|
|
431
|
+
'--boundaries', ownershipBoundary(), ...(scenario.research ? [
|
|
432
|
+
'--research', '--uncertainty', 'Which evidence resolves the legacy research decision?',
|
|
433
|
+
] : []),
|
|
434
|
+
]);
|
|
435
|
+
assert.equal(initial.status, 0, output(initial));
|
|
436
|
+
const feature = scenario.name.replaceAll(' ', '-');
|
|
437
|
+
const specPath = path.join(root, 'specs', feature, 'spec.json');
|
|
438
|
+
const spec = readSpec(root, feature);
|
|
439
|
+
scenario.mutate(spec);
|
|
440
|
+
fs.writeFileSync(specPath, `${JSON.stringify(spec, null, 2)}\n`);
|
|
441
|
+
const before = fs.readFileSync(specPath, 'utf8');
|
|
442
|
+
const result = runMatchingTasksOnly(root, feature);
|
|
443
|
+
assert.equal(result.status, 0, `${scenario.name}: ${output(result)}`);
|
|
444
|
+
const after = fs.readFileSync(specPath, 'utf8');
|
|
445
|
+
if (scenario.expectStable) assert.equal(after, before, `${scenario.name} rewrote canonical state`);
|
|
446
|
+
scenario.verify(JSON.parse(after));
|
|
447
|
+
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
test('canonical 2.1 input rejects unknown semantics at every authored object boundary', () => {
|
|
452
|
+
const mutations = [
|
|
453
|
+
['top', (spec) => { spec.dead_field = true; }],
|
|
454
|
+
['scope', (spec) => { spec.scope_lock.dead_field = true; }],
|
|
455
|
+
['authoring', (spec) => { spec.authoring.approved = true; }],
|
|
456
|
+
['coordination', (spec) => { spec.coordination.task_triggers = ['marker']; }],
|
|
457
|
+
['boundary', (spec) => { spec.coordination.boundaries[0].narrative_owner = 'self-claim'; }],
|
|
458
|
+
['validation', (spec) => { spec.validation.audit_status = 'PASS'; }],
|
|
459
|
+
['semantic-review', (spec) => { spec.validation.semantic_review.reviewer_claim = 'self'; }],
|
|
460
|
+
['counterexample', (spec) => {
|
|
461
|
+
spec.validation.semantic_review.counterexamples.push({
|
|
462
|
+
criterion: 'R1.1', case_kind: 'failure', scenario: 'request fails',
|
|
463
|
+
expected: 'failure remains observable', decision_refs: ['D1'],
|
|
464
|
+
verification_ref: 'V1', hidden_claim: 'trusted',
|
|
465
|
+
});
|
|
466
|
+
}],
|
|
467
|
+
['registry', (spec) => { spec.task_registry['tasks/task-R1-01-one.md'].owner_note = 'narrative'; }],
|
|
468
|
+
];
|
|
469
|
+
for (const [name, mutate] of mutations) {
|
|
470
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-sk21-closed-world-'));
|
|
471
|
+
try {
|
|
472
|
+
const feature = `unknown-${name}`;
|
|
473
|
+
const initial = run(root, [
|
|
474
|
+
feature, '--tasks', 'R1-01-one,R1-02-two', '--boundaries', ownershipBoundary(),
|
|
475
|
+
]);
|
|
476
|
+
assert.equal(initial.status, 0, output(initial));
|
|
477
|
+
const specPath = path.join(root, 'specs', feature, 'spec.json');
|
|
478
|
+
const spec = readSpec(root, feature);
|
|
479
|
+
mutate(spec);
|
|
480
|
+
fs.writeFileSync(specPath, `${JSON.stringify(spec, null, 2)}\n`);
|
|
481
|
+
const before = readTree(path.dirname(specPath));
|
|
482
|
+
const result = runMatchingTasksOnly(root, feature);
|
|
483
|
+
assert.equal(result.status, 2, `${name}: ${output(result)}`);
|
|
484
|
+
assert.match(output(result), /closed-world; unknown=\[[^\]]+\]/);
|
|
485
|
+
assert.deepEqual(readTree(path.dirname(specPath)), before, `${name} mutated on rejection`);
|
|
486
|
+
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
test('tasks-only migration rejects invalid evidence without any write', () => {
|
|
491
|
+
const cases = [
|
|
492
|
+
{
|
|
493
|
+
name: 'missing requirements',
|
|
494
|
+
mutate(root, feature) { fs.unlinkSync(path.join(root, 'specs', feature, 'requirements.md')); },
|
|
495
|
+
expected: /requirements\.md|physical requirements/,
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
name: 'forged done lifecycle',
|
|
499
|
+
mutate(root, feature, spec) {
|
|
500
|
+
spec.task_registry['tasks/task-R1-01-one.md'].status = 'done';
|
|
501
|
+
fs.writeFileSync(path.join(root, 'specs', feature, 'spec.json'), `${JSON.stringify(spec, null, 2)}\n`);
|
|
502
|
+
},
|
|
503
|
+
expected: /done lifecycle is inconsistent/,
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
name: 'research pointer without artifact',
|
|
507
|
+
mutate(root, feature, spec) {
|
|
508
|
+
spec.research = 'research.md';
|
|
509
|
+
spec.authoring.research = 'validated';
|
|
510
|
+
fs.writeFileSync(path.join(root, 'specs', feature, 'spec.json'), `${JSON.stringify(spec, null, 2)}\n`);
|
|
511
|
+
},
|
|
512
|
+
expected: /without a physical research\.md/,
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
name: 'canonical top level inert receipt',
|
|
516
|
+
mutate(root, feature, spec) {
|
|
517
|
+
spec.override_receipt = null;
|
|
518
|
+
fs.writeFileSync(path.join(root, 'specs', feature, 'spec.json'), `${JSON.stringify(spec, null, 2)}\n`);
|
|
519
|
+
},
|
|
520
|
+
expected: /closed-world.*override_receipt/,
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
name: 'ambiguous top level inert receipt',
|
|
524
|
+
mutate(root, feature, spec) {
|
|
525
|
+
delete spec.schema_version;
|
|
526
|
+
spec.override_receipt = null;
|
|
527
|
+
fs.writeFileSync(path.join(root, 'specs', feature, 'spec.json'), `${JSON.stringify(spec, null, 2)}\n`);
|
|
528
|
+
},
|
|
529
|
+
expected: /only readable as inert null during schema 2\.0 migration; omit the field/,
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
name: 'legacy top level non inert receipt',
|
|
533
|
+
mutate(root, feature, spec) {
|
|
534
|
+
spec.schema_version = '2.0';
|
|
535
|
+
spec.override_receipt = { issuer: 'unsupported' };
|
|
536
|
+
fs.writeFileSync(path.join(root, 'specs', feature, 'spec.json'), `${JSON.stringify(spec, null, 2)}\n`);
|
|
537
|
+
},
|
|
538
|
+
expected: /top-level override_receipt is unsupported; omit the field/,
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
name: 'nested non inert receipt',
|
|
542
|
+
mutate(root, feature, spec) {
|
|
543
|
+
spec.workflow_policy.override_receipt = 'forged';
|
|
544
|
+
fs.writeFileSync(path.join(root, 'specs', feature, 'spec.json'), `${JSON.stringify(spec, null, 2)}\n`);
|
|
545
|
+
},
|
|
546
|
+
expected: /workflow_policy\.override_receipt is unsupported; omit the field/,
|
|
547
|
+
},
|
|
548
|
+
...['', 42, 'different-feature'].map((featureName) => ({
|
|
549
|
+
name: `conflicting feature identity ${String(featureName) || 'empty'}`,
|
|
550
|
+
mutate(root, feature, spec) {
|
|
551
|
+
spec.feature_name = featureName;
|
|
552
|
+
fs.writeFileSync(path.join(root, 'specs', feature, 'spec.json'), `${JSON.stringify(spec, null, 2)}\n`);
|
|
553
|
+
},
|
|
554
|
+
expected: /feature_name conflicts with the scaffold feature identity/,
|
|
555
|
+
})),
|
|
556
|
+
];
|
|
557
|
+
|
|
558
|
+
for (const scenario of cases) {
|
|
559
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-sk21-atomic-'));
|
|
560
|
+
try {
|
|
561
|
+
const feature = scenario.name.replaceAll(' ', '-');
|
|
562
|
+
const initial = run(root, [
|
|
563
|
+
feature, '--tasks', 'R1-01-one,R1-02-two', '--boundaries', ownershipBoundary(),
|
|
564
|
+
]);
|
|
565
|
+
assert.equal(initial.status, 0, output(initial));
|
|
566
|
+
scenario.mutate(root, feature, readSpec(root, feature));
|
|
567
|
+
const featureDir = path.join(root, 'specs', feature);
|
|
568
|
+
const before = readTree(featureDir);
|
|
569
|
+
const result = runMatchingTasksOnly(root, feature);
|
|
570
|
+
assert.equal(result.status, 2, `${scenario.name}: ${output(result)}`);
|
|
571
|
+
assert.match(output(result), scenario.expected);
|
|
572
|
+
assert.deepEqual(readTree(featureDir), before, `${scenario.name} mutated the bundle on rejection`);
|
|
573
|
+
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
test('schema 2.0 migration rejects unknown authority fields instead of projection-dropping them', () => {
|
|
578
|
+
const mutations = [
|
|
579
|
+
['top-level', (spec) => { spec.dead_authority = true; }],
|
|
580
|
+
['scope-lock', (spec) => { spec.scope_lock.dead_authority = true; }],
|
|
581
|
+
['workflow-policy', (spec) => { spec.workflow_policy.dead_authority = true; }],
|
|
582
|
+
['registry', (spec) => { spec.task_registry['tasks/task-R1-01-one.md'].dead_authority = true; }],
|
|
583
|
+
];
|
|
584
|
+
for (const [name, mutate] of mutations) {
|
|
585
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-sk20-unknown-'));
|
|
586
|
+
try {
|
|
587
|
+
const feature = `legacy-unknown-${name}`;
|
|
588
|
+
assert.equal(run(root, [feature, '--tasks', 'R1-01-one,R1-02-two', '--boundaries', ownershipBoundary()]).status, 0);
|
|
589
|
+
const specPath = path.join(root, 'specs', feature, 'spec.json');
|
|
590
|
+
const spec = readSpec(root, feature);
|
|
591
|
+
spec.schema_version = '2.0';
|
|
592
|
+
mutate(spec);
|
|
593
|
+
fs.writeFileSync(specPath, `${JSON.stringify(spec, null, 2)}\n`);
|
|
594
|
+
const before = readTree(path.dirname(specPath));
|
|
595
|
+
const result = runMatchingTasksOnly(root, feature);
|
|
596
|
+
assert.equal(result.status, 2, `${name}: ${output(result)}`);
|
|
597
|
+
assert.match(output(result), /unsupported legacy authority field/);
|
|
598
|
+
assert.deepEqual(readTree(path.dirname(specPath)), before);
|
|
599
|
+
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
600
|
+
}
|
|
601
|
+
});
|