@haposoft/cafekit 0.16.0-rc.1 → 0.16.0-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/__tests__/codex-hooks.test.js +355 -12
- package/bin/__tests__/codex-native.test.js +1 -1
- package/bin/__tests__/develop-contract.test.js +1374 -78
- package/bin/__tests__/installer-safety.test.js +43 -29
- package/bin/__tests__/opencode-spec-gate.test.js +791 -0
- package/bin/__tests__/package-inventory.test.js +157 -0
- package/bin/__tests__/validator-grounding.test.js +707 -2
- package/package.json +1 -1
- package/src/claude/agents/code-auditor.md +8 -1
- package/src/claude/agents/spec-maker.md +107 -200
- package/src/claude/hooks/__tests__/completion-authority.test.js +720 -0
- package/src/claude/hooks/__tests__/spec-gate.test.js +511 -38
- package/src/claude/hooks/completion-authority-check.cjs +307 -0
- package/src/claude/hooks/completion-authority-state.cjs +525 -0
- package/src/claude/hooks/completion-authority.cjs +148 -0
- package/src/claude/hooks/session.cjs +33 -19
- package/src/claude/hooks/spec-gate.cjs +173 -61
- package/src/claude/hooks/spec-state.cjs +104 -45
- package/src/claude/migration-manifest.json +6 -1
- package/src/claude/scripts/provenance.cjs +315 -0
- package/src/claude/scripts/spec-resolver.cjs +450 -0
- package/src/claude/scripts/spec-scaffold.cjs +716 -41
- package/src/claude/scripts/validate-spec-output.cjs +355 -53
- package/src/claude/scripts/workflow-policy.cjs +1529 -162
- package/src/claude/settings/settings.json +8 -0
- package/src/claude/skills/backend-development/references/backend-security.md +52 -0
- package/src/claude/skills/code-review/SKILL.md +85 -66
- package/src/claude/skills/develop/SKILL.md +153 -285
- package/src/claude/skills/develop/references/quality-gate.md +112 -124
- package/src/claude/skills/develop/references/subagent-patterns.md +42 -37
- package/src/claude/skills/specs/SKILL.md +170 -381
- package/src/claude/skills/specs/rules/task-scoring-rubric.md +45 -46
- package/src/claude/skills/specs/rules/tasks-generation.md +49 -20
- package/src/claude/skills/specs/templates/spec-state.json +23 -2
- package/src/claude/skills/specs/templates/task.md +20 -0
- package/src/claude/skills/sync/SKILL.md +15 -8
- package/src/claude/skills/sync/references/sync-protocols.md +4 -4
- package/src/claude/skills/test/SKILL.md +141 -167
- package/src/codex/hooks/completion-authority-check.cjs +260 -0
- package/src/codex/hooks/completion-authority-state.cjs +525 -0
- package/src/codex/hooks/completion-authority.cjs +100 -0
- package/src/codex/hooks/lib/hook-context.cjs +42 -2
- package/src/codex/hooks/lib/spec-receipt.cjs +76 -43
- package/src/codex/hooks/lib/spec-utils.cjs +53 -27
- package/src/codex/hooks/session.cjs +2 -0
- package/src/codex/hooks/spec-gate.cjs +132 -20
- package/src/codex/hooks/spec-state.cjs +56 -10
- package/src/codex/hooks.json +10 -0
- package/src/opencode/AGENTS.md +4 -2
- package/src/opencode/plugins/spec-gate.ts +296 -122
- package/src/opencode/plugins/spec-state.ts +183 -116
|
@@ -0,0 +1,720 @@
|
|
|
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 ROOT = path.resolve(__dirname, '../../..');
|
|
11
|
+
const CLAUDE_HOOK = path.join(__dirname, '..', 'completion-authority.cjs');
|
|
12
|
+
const CODEX_HOOKS = path.join(ROOT, 'codex/hooks');
|
|
13
|
+
const POLICY = require(path.join(ROOT, 'claude/scripts/workflow-policy.cjs'));
|
|
14
|
+
const PROVENANCE = require(path.join(ROOT, 'claude/scripts/provenance.cjs'));
|
|
15
|
+
const FEATURE = 'authority-fixture';
|
|
16
|
+
const TASK = 'tasks/closeout.md';
|
|
17
|
+
const TEST_HOMES = new Map();
|
|
18
|
+
|
|
19
|
+
function homeFor(root) {
|
|
20
|
+
if (!TEST_HOMES.has(root)) {
|
|
21
|
+
const home = path.join(os.tmpdir(), 'cafekit-authority-homes', path.basename(root));
|
|
22
|
+
fs.mkdirSync(home, { recursive: true });
|
|
23
|
+
TEST_HOMES.set(root, home);
|
|
24
|
+
}
|
|
25
|
+
return TEST_HOMES.get(root);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function cleanupFixture(root) {
|
|
29
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
30
|
+
const home = TEST_HOMES.get(root);
|
|
31
|
+
if (home) fs.rmSync(home, { recursive: true, force: true });
|
|
32
|
+
TEST_HOMES.delete(root);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function gitFixture() {
|
|
36
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-authority-'));
|
|
37
|
+
for (const args of [
|
|
38
|
+
['init', '-q'],
|
|
39
|
+
['config', 'user.email', 'cafekit@example.invalid'],
|
|
40
|
+
['config', 'user.name', 'CafeKit Test'],
|
|
41
|
+
['commit', '--allow-empty', '-qm', 'fixture'],
|
|
42
|
+
]) {
|
|
43
|
+
const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' });
|
|
44
|
+
assert.equal(result.status, 0, result.stderr);
|
|
45
|
+
}
|
|
46
|
+
return root;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function context(root, session = 'session-a', featureName = FEATURE) {
|
|
50
|
+
return PROVENANCE.deriveRuntimeContext({
|
|
51
|
+
projectRoot: root,
|
|
52
|
+
specsRoot: path.join(root, 'specs'),
|
|
53
|
+
specFile: path.join(root, 'specs', featureName, 'spec.json'),
|
|
54
|
+
featureName,
|
|
55
|
+
runtimeSession: session,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function prepare(root, { lane = 'Standard', status = 'in_progress', proofs = true, feature = FEATURE } = {}) {
|
|
60
|
+
const featureDir = path.join(root, 'specs', feature);
|
|
61
|
+
fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
|
|
62
|
+
const policyInput = lane === 'Critical'
|
|
63
|
+
? { riskSignals: { auth: true } }
|
|
64
|
+
: lane === 'Direct'
|
|
65
|
+
? { reversible: true, lowRisk: true, isolated: true }
|
|
66
|
+
: { riskSignals: {} };
|
|
67
|
+
const spec = {
|
|
68
|
+
status,
|
|
69
|
+
feature_name: feature,
|
|
70
|
+
workflow_policy: POLICY.workflowPolicySnapshot(policyInput),
|
|
71
|
+
task_registry: { [TASK]: { status: 'done', completed_at: '2026-08-12T00:00:00.000Z' } },
|
|
72
|
+
};
|
|
73
|
+
fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify(spec));
|
|
74
|
+
const runtime = context(root, 'session-a', feature);
|
|
75
|
+
const body = [
|
|
76
|
+
'Verification: PASS',
|
|
77
|
+
'Command: node --test',
|
|
78
|
+
'Exit: 0',
|
|
79
|
+
`Base: ${runtime.base}`,
|
|
80
|
+
`Head: ${runtime.head}`,
|
|
81
|
+
].join('\n');
|
|
82
|
+
fs.writeFileSync(path.join(featureDir, TASK), `# Closeout\n\nStatus: done\n\n## Evidence\n\n${body}\n`);
|
|
83
|
+
if (proofs) {
|
|
84
|
+
spec.proofs = {
|
|
85
|
+
needsInspection: { receipt: body },
|
|
86
|
+
needsResearchGrounding: { receipt: body },
|
|
87
|
+
};
|
|
88
|
+
if (lane === 'Critical') {
|
|
89
|
+
spec.proofs.needsIndependentAudit = {
|
|
90
|
+
schema_version: '1',
|
|
91
|
+
reviewer_session_id: 'reviewer-session',
|
|
92
|
+
implementation_session_id: 'session-a',
|
|
93
|
+
expected_provenance: { base: runtime.base, head: runtime.head },
|
|
94
|
+
evidence: 'independent closeout review passed',
|
|
95
|
+
verdict: 'PASS',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify(spec));
|
|
99
|
+
}
|
|
100
|
+
return { featureDir, specFile: path.join(featureDir, 'spec.json') };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function adapter(kind, root) {
|
|
104
|
+
const home = homeFor(root);
|
|
105
|
+
fs.mkdirSync(home, { recursive: true });
|
|
106
|
+
if (kind === 'claude') {
|
|
107
|
+
return {
|
|
108
|
+
root,
|
|
109
|
+
hook: CLAUDE_HOOK,
|
|
110
|
+
sessionHook: path.join(__dirname, '..', 'session.cjs'),
|
|
111
|
+
state: path.join(home, '.cafekit', 'completion-authority', 'projects'),
|
|
112
|
+
home,
|
|
113
|
+
env: { ...process.env, HOME: home, USERPROFILE: home, PROJECT_ROOT: root },
|
|
114
|
+
run(mode, payload) {
|
|
115
|
+
return spawnSync(process.execPath, [CLAUDE_HOOK, mode], {
|
|
116
|
+
cwd: root,
|
|
117
|
+
env: this.env,
|
|
118
|
+
input: JSON.stringify({ cwd: root, session_id: 'session-a', ...payload }),
|
|
119
|
+
encoding: 'utf8',
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const hooks = path.join(root, '.codex/hooks');
|
|
125
|
+
fs.cpSync(CODEX_HOOKS, hooks, { recursive: true });
|
|
126
|
+
fs.mkdirSync(path.join(root, '.codex/scripts'), { recursive: true });
|
|
127
|
+
for (const file of ['workflow-policy.cjs', 'provenance.cjs', 'spec-resolver.cjs']) {
|
|
128
|
+
fs.copyFileSync(path.join(ROOT, 'claude/scripts', file), path.join(root, '.codex/scripts', file));
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
root,
|
|
132
|
+
hook: path.join(hooks, 'completion-authority.cjs'),
|
|
133
|
+
sessionHook: path.join(hooks, 'session.cjs'),
|
|
134
|
+
state: path.join(home, '.cafekit', 'completion-authority', 'projects'),
|
|
135
|
+
home,
|
|
136
|
+
env: { ...process.env, HOME: home, USERPROFILE: home },
|
|
137
|
+
run(mode, payload) {
|
|
138
|
+
return spawnSync(process.execPath, [path.join(hooks, 'completion-authority.cjs'), mode], {
|
|
139
|
+
cwd: root,
|
|
140
|
+
env: this.env,
|
|
141
|
+
input: JSON.stringify({ cwd: root, session_id: 'session-a', ...payload }),
|
|
142
|
+
encoding: 'utf8',
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function stateFiles(runtime) {
|
|
149
|
+
if (!fs.existsSync(runtime.state)) return [];
|
|
150
|
+
return fs.readdirSync(runtime.state, { withFileTypes: true })
|
|
151
|
+
.filter((entry) => entry.isDirectory())
|
|
152
|
+
.flatMap((entry) => fs.readdirSync(path.join(runtime.state, entry.name)));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function stateRecord(runtime, prefix) {
|
|
156
|
+
const files = stateFiles(runtime).filter((name) => name.startsWith(prefix));
|
|
157
|
+
assert.equal(files.length, 1, `expected exactly one ${prefix} authority record`);
|
|
158
|
+
const projects = fs.readdirSync(runtime.state, { withFileTypes: true }).find((entry) => entry.isDirectory());
|
|
159
|
+
return path.join(runtime.state, projects.name, files[0]);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function sessionStart(runtime, source = 'startup', sessionId = 'session-a') {
|
|
163
|
+
return spawnSync(process.execPath, [runtime.sessionHook], {
|
|
164
|
+
cwd: runtime.root,
|
|
165
|
+
env: runtime.env,
|
|
166
|
+
input: JSON.stringify({ cwd: runtime.root, session_id: sessionId, source, hook_event_name: 'SessionStart' }),
|
|
167
|
+
encoding: 'utf8',
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function stop(runtime, payload = {}) {
|
|
172
|
+
return runtime.run('--stop', { hook_event_name: 'Stop', ...payload });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function approve(runtime, nonce, payload = {}) {
|
|
176
|
+
return runtime.run('--approve', {
|
|
177
|
+
hook_event_name: 'UserPromptSubmit',
|
|
178
|
+
prompt: `APPROVE CAFEKIT COMPLETION ${nonce}`,
|
|
179
|
+
...payload,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function block(result) {
|
|
184
|
+
assert.equal(result.status, 0, result.stderr);
|
|
185
|
+
const output = result.stdout.trim();
|
|
186
|
+
assert.notEqual(output, '', 'expected a controlled block');
|
|
187
|
+
const line = output.split('\n').find((value) => value.includes('"decision"'));
|
|
188
|
+
return JSON.parse(line || output);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function nonceFrom(result) {
|
|
192
|
+
const body = block(result);
|
|
193
|
+
const match = body.reason.match(/APPROVE CAFEKIT COMPLETION ([a-f0-9]{24})/);
|
|
194
|
+
assert.ok(match, body.reason);
|
|
195
|
+
return { body, nonce: match[1] };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
for (const kind of ['claude', 'codex']) {
|
|
199
|
+
test(`${kind}: exact user approval is one-time and runtime-bound`, () => {
|
|
200
|
+
const root = gitFixture();
|
|
201
|
+
try {
|
|
202
|
+
const runtime = adapter(kind, root);
|
|
203
|
+
prepare(root);
|
|
204
|
+
const first = nonceFrom(stop(runtime));
|
|
205
|
+
const forged = runtime.run('--approve', { hook_event_name: 'UserPromptSubmit', user_authorized: true });
|
|
206
|
+
assert.equal(forged.status, 0);
|
|
207
|
+
assert.equal(stateFiles(runtime).filter((name) => name.startsWith('grant-')).length, 0);
|
|
208
|
+
approve(runtime, first.nonce);
|
|
209
|
+
assert.equal(stop(runtime).stdout.trim(), '', 'approved closeout must pass once');
|
|
210
|
+
const replay = approve(runtime, first.nonce);
|
|
211
|
+
assert.equal(replay.status, 0);
|
|
212
|
+
const second = nonceFrom(stop(runtime));
|
|
213
|
+
assert.notEqual(second.nonce, first.nonce, 'replayed nonce must not grant a second closeout');
|
|
214
|
+
} finally {
|
|
215
|
+
cleanupFixture(root);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test(`${kind}: valid Critical closeout succeeds only after exact approval`, () => {
|
|
220
|
+
const root = gitFixture();
|
|
221
|
+
try {
|
|
222
|
+
const runtime = adapter(kind, root);
|
|
223
|
+
prepare(root, { lane: 'Critical' });
|
|
224
|
+
const pending = nonceFrom(stop(runtime));
|
|
225
|
+
assert.equal(stop(runtime).stdout.match(/APPROVE CAFEKIT COMPLETION/g).length, 1);
|
|
226
|
+
approve(runtime, pending.nonce);
|
|
227
|
+
assert.equal(stop(runtime).stdout.trim(), '');
|
|
228
|
+
} finally {
|
|
229
|
+
cleanupFixture(root);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test(`${kind}: wrong nonce/session/target and forged approval fields never grant`, () => {
|
|
234
|
+
const root = gitFixture();
|
|
235
|
+
try {
|
|
236
|
+
const runtime = adapter(kind, root);
|
|
237
|
+
prepare(root);
|
|
238
|
+
const pending = nonceFrom(stop(runtime));
|
|
239
|
+
const replacement = pending.nonce.endsWith('0') ? '1' : '0';
|
|
240
|
+
const wrongNonce = approve(runtime, `${pending.nonce.slice(0, -1)}${replacement}`);
|
|
241
|
+
assert.equal(wrongNonce.status, 0);
|
|
242
|
+
assert.equal(stateFiles(runtime).some((name) => name.startsWith('grant-')), false);
|
|
243
|
+
const wrongEvent = approve(runtime, pending.nonce, { hook_event_name: 'Stop' });
|
|
244
|
+
assert.equal(wrongEvent.status, 0);
|
|
245
|
+
assert.equal(stateFiles(runtime).some((name) => name.startsWith('grant-')), false);
|
|
246
|
+
const wrongSession = approve(runtime, pending.nonce, { session_id: 'other-session' });
|
|
247
|
+
assert.equal(wrongSession.status, 0);
|
|
248
|
+
assert.equal(stateFiles(runtime).some((name) => name.startsWith('grant-')), false);
|
|
249
|
+
const forged = runtime.run('--approve', {
|
|
250
|
+
hook_event_name: 'UserPromptSubmit',
|
|
251
|
+
user_authorized: true,
|
|
252
|
+
userAuthorized: true,
|
|
253
|
+
session: { id: 'session-a', user_authorized: true },
|
|
254
|
+
});
|
|
255
|
+
assert.equal(forged.status, 0);
|
|
256
|
+
assert.equal(stateFiles(runtime).some((name) => name.startsWith('grant-')), false);
|
|
257
|
+
approve(runtime, pending.nonce);
|
|
258
|
+
const wrongStop = stop(runtime, {
|
|
259
|
+
session_id: 'other-session',
|
|
260
|
+
featureName: 'wrong-feature',
|
|
261
|
+
runtime_context: { project_root: '/forged', head: 'forged', context_id: 'forged' },
|
|
262
|
+
user_authorized: true,
|
|
263
|
+
});
|
|
264
|
+
assert.equal(block(wrongStop).decision, 'block');
|
|
265
|
+
} finally {
|
|
266
|
+
cleanupFixture(root);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test(`${kind}: status, policy, registry, and artifact/proof mutation invalidate approval`, () => {
|
|
271
|
+
const root = gitFixture();
|
|
272
|
+
try {
|
|
273
|
+
const runtime = adapter(kind, root);
|
|
274
|
+
const fixture = prepare(root);
|
|
275
|
+
const initial = nonceFrom(stop(runtime));
|
|
276
|
+
approve(runtime, initial.nonce);
|
|
277
|
+
const spec = JSON.parse(fs.readFileSync(fixture.specFile, 'utf8'));
|
|
278
|
+
spec.status = 'completed';
|
|
279
|
+
fs.writeFileSync(fixture.specFile, JSON.stringify(spec));
|
|
280
|
+
const statusMutation = nonceFrom(stop(runtime));
|
|
281
|
+
assert.notEqual(statusMutation.nonce, initial.nonce);
|
|
282
|
+
|
|
283
|
+
approve(runtime, statusMutation.nonce);
|
|
284
|
+
const direct = POLICY.workflowPolicySnapshot({ reversible: true, lowRisk: true, isolated: true });
|
|
285
|
+
spec.workflow_policy = direct;
|
|
286
|
+
fs.writeFileSync(fixture.specFile, JSON.stringify(spec));
|
|
287
|
+
const policyMutation = block(stop(runtime));
|
|
288
|
+
assert.match(policyMutation.reason, /monotonic|downgraded/i);
|
|
289
|
+
spec.task_registry = {};
|
|
290
|
+
fs.writeFileSync(fixture.specFile, JSON.stringify(spec));
|
|
291
|
+
const missingRegistry = block(stop(runtime));
|
|
292
|
+
assert.match(missingRegistry.reason, /monotonic|downgraded/i);
|
|
293
|
+
|
|
294
|
+
prepare(root, { lane: 'Direct' });
|
|
295
|
+
const directRuntime = adapter(kind, root);
|
|
296
|
+
const artifactMutation = block(stop(directRuntime));
|
|
297
|
+
assert.match(artifactMutation.reason, /monotonic|downgraded/i);
|
|
298
|
+
} finally {
|
|
299
|
+
cleanupFixture(root);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test(`${kind}: completed, empty/malformed candidates, Critical fake proofs, and Direct no-spec are fail-closed`, () => {
|
|
304
|
+
const root = gitFixture();
|
|
305
|
+
try {
|
|
306
|
+
const runtime = adapter(kind, root);
|
|
307
|
+
for (const input of ['', 'null', '[]']) {
|
|
308
|
+
const malformed = spawnSync(process.execPath, [runtime.hook, '--stop'], {
|
|
309
|
+
cwd: root,
|
|
310
|
+
env: runtime.env,
|
|
311
|
+
input,
|
|
312
|
+
encoding: 'utf8',
|
|
313
|
+
});
|
|
314
|
+
assert.equal(malformed.status, 0);
|
|
315
|
+
assert.equal(block(malformed).decision, 'block');
|
|
316
|
+
}
|
|
317
|
+
prepare(root, { status: 'completed' });
|
|
318
|
+
assert.match(nonceFrom(stop(runtime)).body.reason, /approval/i);
|
|
319
|
+
|
|
320
|
+
const specFile = path.join(root, 'specs', FEATURE, 'spec.json');
|
|
321
|
+
const spec = JSON.parse(fs.readFileSync(specFile, 'utf8'));
|
|
322
|
+
spec.task_registry = {};
|
|
323
|
+
fs.writeFileSync(specFile, JSON.stringify(spec));
|
|
324
|
+
assert.match(block(stop(runtime)).reason, /task_registry is missing or empty/i);
|
|
325
|
+
|
|
326
|
+
fs.writeFileSync(specFile, '{ malformed');
|
|
327
|
+
assert.equal(block(stop(runtime)).decision, 'block');
|
|
328
|
+
|
|
329
|
+
fs.rmSync(path.join(root, 'specs'), { recursive: true, force: true });
|
|
330
|
+
const disappeared = block(stop(runtime));
|
|
331
|
+
assert.match(disappeared.reason, /disappeared|persisted/i);
|
|
332
|
+
|
|
333
|
+
const noSpecRoot = gitFixture();
|
|
334
|
+
try {
|
|
335
|
+
const noSpec = adapter(kind, noSpecRoot);
|
|
336
|
+
assert.equal(stop(noSpec).stdout.trim(), '', 'true no-spec Direct path remains silent');
|
|
337
|
+
} finally {
|
|
338
|
+
cleanupFixture(noSpecRoot);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
prepare(root, { lane: 'Critical', proofs: false });
|
|
342
|
+
const critical = adapter(kind, root);
|
|
343
|
+
const criticalBlock = block(stop(critical));
|
|
344
|
+
assert.match(criticalBlock.reason, /technical completion proof|needsIndependentAudit/i);
|
|
345
|
+
|
|
346
|
+
fs.mkdirSync(path.join(root, '.claude'), { recursive: true });
|
|
347
|
+
fs.writeFileSync(path.join(root, '.claude', 'runtime.json'), JSON.stringify({ spec: { completion_gate: false } }));
|
|
348
|
+
if (kind === 'codex') {
|
|
349
|
+
fs.rmSync(path.join(root, '.claude'), { recursive: true, force: true });
|
|
350
|
+
fs.mkdirSync(path.join(root, '.codex'), { recursive: true });
|
|
351
|
+
fs.writeFileSync(path.join(root, '.codex', 'runtime.json'), JSON.stringify({ spec: { completion_gate: false } }));
|
|
352
|
+
}
|
|
353
|
+
assert.match(block(stop(critical)).reason, /worker-writable|cannot authorize/i);
|
|
354
|
+
} finally {
|
|
355
|
+
cleanupFixture(root);
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test(`${kind}: copied, unsigned, MAC-invalid, wrong-key, and project-local grants never authorize`, () => {
|
|
360
|
+
const root = gitFixture();
|
|
361
|
+
try {
|
|
362
|
+
const runtime = adapter(kind, root);
|
|
363
|
+
prepare(root);
|
|
364
|
+
const localDir = path.join(root, kind === 'claude' ? '.claude/hooks/.logs/completion-authority' : '.codex/hooks/.logs/completion-authority');
|
|
365
|
+
fs.mkdirSync(localDir, { recursive: true });
|
|
366
|
+
fs.writeFileSync(path.join(localDir, 'grant-deadbeefdeadbeefdeadbeef.json'), JSON.stringify({ kind: 'grant', nonce: 'deadbeefdeadbeefdeadbeef', binding: {} }));
|
|
367
|
+
const pending = nonceFrom(stop(runtime));
|
|
368
|
+
approve(runtime, pending.nonce);
|
|
369
|
+
assert.equal(stop(runtime).stdout.trim(), '', 'project-local legacy grant must be ignored');
|
|
370
|
+
fs.rmSync(localDir, { recursive: true, force: true });
|
|
371
|
+
|
|
372
|
+
const retry = nonceFrom(stop(runtime));
|
|
373
|
+
const pendingFile = stateRecord(runtime, 'pending-');
|
|
374
|
+
const copiedGrant = path.join(path.dirname(pendingFile), `grant-${retry.nonce}.json`);
|
|
375
|
+
fs.copyFileSync(pendingFile, copiedGrant);
|
|
376
|
+
const copiedApproval = approve(runtime, retry.nonce);
|
|
377
|
+
assert.doesNotMatch(copiedApproval.stdout, /accepted/i);
|
|
378
|
+
assert.equal(stateFiles(runtime).some((name) => name.startsWith('grant-')), true);
|
|
379
|
+
fs.rmSync(copiedGrant);
|
|
380
|
+
approve(runtime, retry.nonce);
|
|
381
|
+
assert.equal(stop(runtime).stdout.trim(), '');
|
|
382
|
+
|
|
383
|
+
const clean = gitFixture();
|
|
384
|
+
try {
|
|
385
|
+
const cleanRuntime = adapter(kind, clean);
|
|
386
|
+
prepare(clean);
|
|
387
|
+
const cleanPending = nonceFrom(stop(cleanRuntime));
|
|
388
|
+
const cleanFile = stateRecord(cleanRuntime, 'pending-');
|
|
389
|
+
const record = JSON.parse(fs.readFileSync(cleanFile, 'utf8'));
|
|
390
|
+
record.mac = `${record.mac.slice(0, -1)}${record.mac.endsWith('0') ? '1' : '0'}`;
|
|
391
|
+
fs.writeFileSync(cleanFile, JSON.stringify(record));
|
|
392
|
+
assert.doesNotMatch(approve(cleanRuntime, cleanPending.nonce).stdout, /accepted/i);
|
|
393
|
+
} finally {
|
|
394
|
+
cleanupFixture(clean);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const wrongKeyRoot = gitFixture();
|
|
398
|
+
try {
|
|
399
|
+
const wrongKeyRuntime = adapter(kind, wrongKeyRoot);
|
|
400
|
+
prepare(wrongKeyRoot);
|
|
401
|
+
const wrongKeyPending = nonceFrom(stop(wrongKeyRuntime));
|
|
402
|
+
fs.writeFileSync(path.join(wrongKeyRuntime.home, '.cafekit', 'completion-authority', 'hmac.key'), Buffer.alloc(32, 7));
|
|
403
|
+
assert.doesNotMatch(approve(wrongKeyRuntime, wrongKeyPending.nonce).stdout, /accepted/i);
|
|
404
|
+
} finally {
|
|
405
|
+
cleanupFixture(wrongKeyRoot);
|
|
406
|
+
}
|
|
407
|
+
} finally {
|
|
408
|
+
cleanupFixture(root);
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test(`${kind}: replay and signed-record mutation remain one-use and bound`, () => {
|
|
413
|
+
const root = gitFixture();
|
|
414
|
+
try {
|
|
415
|
+
const runtime = adapter(kind, root);
|
|
416
|
+
prepare(root);
|
|
417
|
+
const pending = nonceFrom(stop(runtime));
|
|
418
|
+
approve(runtime, pending.nonce);
|
|
419
|
+
assert.equal(stop(runtime).stdout.trim(), '');
|
|
420
|
+
assert.doesNotMatch(approve(runtime, pending.nonce).stdout, /accepted/i);
|
|
421
|
+
const next = nonceFrom(stop(runtime));
|
|
422
|
+
const specFile = path.join(root, 'specs', FEATURE, 'spec.json');
|
|
423
|
+
const spec = JSON.parse(fs.readFileSync(specFile, 'utf8'));
|
|
424
|
+
spec.task_registry[TASK].completed_at = '2026-08-13T00:00:00.000Z';
|
|
425
|
+
fs.writeFileSync(specFile, JSON.stringify(spec));
|
|
426
|
+
approve(runtime, next.nonce);
|
|
427
|
+
const mutated = block(stop(runtime));
|
|
428
|
+
assert.match(mutated.reason, /approval|technical|binding|proof/i);
|
|
429
|
+
} finally {
|
|
430
|
+
cleanupFixture(root);
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test(`${kind}: policy baseline is recorded before proof completion, survives SessionStart, and blocks Critical to Direct`, () => {
|
|
435
|
+
const root = gitFixture();
|
|
436
|
+
try {
|
|
437
|
+
const runtime = adapter(kind, root);
|
|
438
|
+
prepare(root, { proofs: false });
|
|
439
|
+
const incomplete = block(stop(runtime));
|
|
440
|
+
assert.match(incomplete.reason, /technical|proof|missing/i);
|
|
441
|
+
assert.equal(stateFiles(runtime).filter((name) => name.startsWith('baseline-')).length, 1);
|
|
442
|
+
|
|
443
|
+
prepare(root);
|
|
444
|
+
const pending = nonceFrom(stop(runtime));
|
|
445
|
+
assert.equal(sessionStart(runtime).status, 0);
|
|
446
|
+
assert.equal(stateFiles(runtime).filter((name) => name.startsWith('baseline-')).length, 1);
|
|
447
|
+
assert.equal(stateFiles(runtime).some((name) => name.startsWith('pending-') || name.startsWith('grant-')), false);
|
|
448
|
+
const freshPending = nonceFrom(stop(runtime));
|
|
449
|
+
assert.notEqual(freshPending.nonce, pending.nonce);
|
|
450
|
+
|
|
451
|
+
approve(runtime, freshPending.nonce);
|
|
452
|
+
assert.equal(stop(runtime).stdout.trim(), '');
|
|
453
|
+
const retry = nonceFrom(stop(runtime));
|
|
454
|
+
approve(runtime, retry.nonce);
|
|
455
|
+
const specFile = path.join(root, 'specs', FEATURE, 'spec.json');
|
|
456
|
+
const spec = JSON.parse(fs.readFileSync(specFile, 'utf8'));
|
|
457
|
+
spec.workflow_policy = POLICY.workflowPolicySnapshot({ reversible: true, lowRisk: true, isolated: true });
|
|
458
|
+
fs.writeFileSync(specFile, JSON.stringify(spec));
|
|
459
|
+
const downgrade = block(stop(runtime));
|
|
460
|
+
assert.match(downgrade.reason, /monotonic|downgraded/i);
|
|
461
|
+
} finally {
|
|
462
|
+
cleanupFixture(root);
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test(`${kind}: Critical feature A survives deletion and blocks Direct feature B`, () => {
|
|
467
|
+
const root = gitFixture();
|
|
468
|
+
try {
|
|
469
|
+
const runtime = adapter(kind, root);
|
|
470
|
+
prepare(root, { feature: 'critical-feature-a', lane: 'Critical' });
|
|
471
|
+
nonceFrom(stop(runtime));
|
|
472
|
+
assert.equal(stateFiles(runtime).filter((name) => name === 'policy-floor.json').length, 1);
|
|
473
|
+
|
|
474
|
+
fs.rmSync(path.join(root, 'specs', 'critical-feature-a'), { recursive: true, force: true });
|
|
475
|
+
prepare(root, { feature: 'direct-feature-b', lane: 'Direct' });
|
|
476
|
+
const blocked = block(stop(runtime));
|
|
477
|
+
assert.match(blocked.reason, /project policy floor|monotonic|downgraded/i);
|
|
478
|
+
} finally {
|
|
479
|
+
cleanupFixture(root);
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test(`${kind}: Critical policy floor survives same-path recreation and a new session`, () => {
|
|
484
|
+
const root = gitFixture();
|
|
485
|
+
try {
|
|
486
|
+
const runtime = adapter(kind, root);
|
|
487
|
+
const featureDir = path.join(root, 'specs', FEATURE);
|
|
488
|
+
prepare(root, { lane: 'Critical' });
|
|
489
|
+
nonceFrom(stop(runtime));
|
|
490
|
+
fs.rmSync(featureDir, { recursive: true, force: true });
|
|
491
|
+
prepare(root, { lane: 'Direct' });
|
|
492
|
+
const recreated = block(stop(runtime));
|
|
493
|
+
assert.match(recreated.reason, /project policy floor|monotonic|downgraded/i);
|
|
494
|
+
} finally {
|
|
495
|
+
cleanupFixture(root);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const sessionRoot = gitFixture();
|
|
499
|
+
try {
|
|
500
|
+
const runtime = adapter(kind, sessionRoot);
|
|
501
|
+
prepare(sessionRoot, { lane: 'Critical' });
|
|
502
|
+
nonceFrom(stop(runtime));
|
|
503
|
+
assert.equal(sessionStart(runtime, 'startup', 'session-b').status, 0);
|
|
504
|
+
assert.equal(stateFiles(runtime).filter((name) => name === 'policy-floor.json').length, 1);
|
|
505
|
+
assert.equal(stateFiles(runtime).some((name) => name.startsWith('pending-') || name.startsWith('grant-')), false);
|
|
506
|
+
prepare(sessionRoot, { lane: 'Direct' });
|
|
507
|
+
const newSession = block(stop(runtime, { session_id: 'session-b' }));
|
|
508
|
+
assert.match(newSession.reason, /project policy floor|monotonic|downgraded/i);
|
|
509
|
+
} finally {
|
|
510
|
+
cleanupFixture(sessionRoot);
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
test(`${kind}: project policy floor prevents risk and proof-obligation removal`, () => {
|
|
515
|
+
const root = gitFixture();
|
|
516
|
+
try {
|
|
517
|
+
const runtime = adapter(kind, root);
|
|
518
|
+
prepare(root, { lane: 'Critical' });
|
|
519
|
+
nonceFrom(stop(runtime));
|
|
520
|
+
const specFile = path.join(root, 'specs', FEATURE, 'spec.json');
|
|
521
|
+
const spec = JSON.parse(fs.readFileSync(specFile, 'utf8'));
|
|
522
|
+
assert.ok(spec.workflow_policy.risks.length > 0);
|
|
523
|
+
assert.ok(spec.workflow_policy.proof_obligations.length > 0);
|
|
524
|
+
const removedObligationIndex = spec.workflow_policy.proof_obligations.length - 1;
|
|
525
|
+
spec.workflow_policy = {
|
|
526
|
+
...spec.workflow_policy,
|
|
527
|
+
risks: spec.workflow_policy.risks.slice(0, -1),
|
|
528
|
+
proof_obligations: spec.workflow_policy.proof_obligations.slice(0, -1),
|
|
529
|
+
actor_needs: spec.workflow_policy.actor_needs.filter((_, index) => index !== removedObligationIndex),
|
|
530
|
+
};
|
|
531
|
+
fs.writeFileSync(specFile, JSON.stringify(spec));
|
|
532
|
+
const blocked = block(stop(runtime));
|
|
533
|
+
assert.match(blocked.reason, /risk .*removed/i);
|
|
534
|
+
assert.match(blocked.reason, /obligation .*removed/i);
|
|
535
|
+
} finally {
|
|
536
|
+
cleanupFixture(root);
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
test(`${kind}: missing or corrupt project policy floor fails closed`, () => {
|
|
541
|
+
const missingRoot = gitFixture();
|
|
542
|
+
try {
|
|
543
|
+
const runtime = adapter(kind, missingRoot);
|
|
544
|
+
prepare(missingRoot);
|
|
545
|
+
nonceFrom(stop(runtime));
|
|
546
|
+
fs.rmSync(stateRecord(runtime, 'policy-floor'), { force: true });
|
|
547
|
+
assert.match(block(stop(runtime)).reason, /floor|authority/i);
|
|
548
|
+
} finally {
|
|
549
|
+
cleanupFixture(missingRoot);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const corruptRoot = gitFixture();
|
|
553
|
+
try {
|
|
554
|
+
const runtime = adapter(kind, corruptRoot);
|
|
555
|
+
prepare(corruptRoot);
|
|
556
|
+
nonceFrom(stop(runtime));
|
|
557
|
+
fs.writeFileSync(stateRecord(runtime, 'policy-floor'), '{ corrupt');
|
|
558
|
+
assert.match(block(stop(runtime)).reason, /floor|malformed|authority/i);
|
|
559
|
+
} finally {
|
|
560
|
+
cleanupFixture(corruptRoot);
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
test(`${kind}: valid policy escalation is allowed, while missing policy is blocked`, () => {
|
|
565
|
+
const root = gitFixture();
|
|
566
|
+
try {
|
|
567
|
+
const runtime = adapter(kind, root);
|
|
568
|
+
prepare(root, { lane: 'Standard' });
|
|
569
|
+
nonceFrom(stop(runtime));
|
|
570
|
+
const standardFloor = JSON.parse(fs.readFileSync(stateRecord(runtime, 'policy-floor'), 'utf8'));
|
|
571
|
+
assert.equal(standardFloor.policy.lane, 'Standard');
|
|
572
|
+
prepare(root, { lane: 'Critical' });
|
|
573
|
+
const escalated = nonceFrom(stop(runtime));
|
|
574
|
+
assert.match(escalated.body.reason, /approval/i);
|
|
575
|
+
const criticalFloor = JSON.parse(fs.readFileSync(stateRecord(runtime, 'policy-floor'), 'utf8'));
|
|
576
|
+
assert.equal(criticalFloor.policy.lane, 'Critical');
|
|
577
|
+
|
|
578
|
+
const missingRoot = gitFixture();
|
|
579
|
+
try {
|
|
580
|
+
const missingRuntime = adapter(kind, missingRoot);
|
|
581
|
+
const fixture = prepare(missingRoot);
|
|
582
|
+
const spec = JSON.parse(fs.readFileSync(fixture.specFile, 'utf8'));
|
|
583
|
+
delete spec.workflow_policy;
|
|
584
|
+
fs.writeFileSync(fixture.specFile, JSON.stringify(spec));
|
|
585
|
+
assert.match(block(stop(missingRuntime)).reason, /workflow_policy.*missing/i);
|
|
586
|
+
} finally {
|
|
587
|
+
cleanupFixture(missingRoot);
|
|
588
|
+
}
|
|
589
|
+
} finally {
|
|
590
|
+
cleanupFixture(root);
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
test(`${kind}: explicit targets cannot hide global persisted specs or mismatch the sole candidate`, () => {
|
|
595
|
+
const root = gitFixture();
|
|
596
|
+
try {
|
|
597
|
+
const runtime = adapter(kind, root);
|
|
598
|
+
prepare(root);
|
|
599
|
+
assert.match(block(stop(runtime, { featureName: 'other-feature' })).reason, /explicit/i);
|
|
600
|
+
const otherDir = path.join(root, 'specs', 'other-feature');
|
|
601
|
+
fs.mkdirSync(otherDir, { recursive: true });
|
|
602
|
+
fs.writeFileSync(path.join(otherDir, 'spec.json'), JSON.stringify({
|
|
603
|
+
feature_name: 'other-feature',
|
|
604
|
+
status: 'in_progress',
|
|
605
|
+
workflow_policy: POLICY.workflowPolicySnapshot({ riskSignals: {} }),
|
|
606
|
+
task_registry: { [TASK]: { status: 'in_progress' } },
|
|
607
|
+
}));
|
|
608
|
+
assert.match(block(stop(runtime, { featureName: FEATURE })).reason, /multiple_persisted|multiple/i);
|
|
609
|
+
} finally {
|
|
610
|
+
cleanupFixture(root);
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
test('Claude hook: CLAUDE_PROJECT_DIR wins over malicious payload.cwd and stale process cwd', () => {
|
|
616
|
+
const target = gitFixture();
|
|
617
|
+
const stale = gitFixture();
|
|
618
|
+
try {
|
|
619
|
+
prepare(target);
|
|
620
|
+
const home = homeFor(target);
|
|
621
|
+
const result = spawnSync(process.execPath, [CLAUDE_HOOK, '--stop'], {
|
|
622
|
+
cwd: stale,
|
|
623
|
+
env: {
|
|
624
|
+
...process.env,
|
|
625
|
+
HOME: home,
|
|
626
|
+
USERPROFILE: home,
|
|
627
|
+
CLAUDE_PROJECT_DIR: target,
|
|
628
|
+
PROJECT_ROOT: stale,
|
|
629
|
+
},
|
|
630
|
+
input: JSON.stringify({
|
|
631
|
+
hook_event_name: 'Stop',
|
|
632
|
+
session_id: 'session-a',
|
|
633
|
+
cwd: stale,
|
|
634
|
+
}),
|
|
635
|
+
encoding: 'utf8',
|
|
636
|
+
});
|
|
637
|
+
assert.match(block(result).reason, /approval/i);
|
|
638
|
+
} finally {
|
|
639
|
+
cleanupFixture(target);
|
|
640
|
+
cleanupFixture(stale);
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
test('installed scaffold observes Critical policy before Stop and source scaffold does not touch authority state', () => {
|
|
645
|
+
const root = gitFixture();
|
|
646
|
+
const sourceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-source-scaffold-'));
|
|
647
|
+
const sourceHome = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-source-home-'));
|
|
648
|
+
const installedHome = homeFor(root);
|
|
649
|
+
try {
|
|
650
|
+
const installedClaude = path.join(root, '.claude');
|
|
651
|
+
fs.cpSync(path.join(ROOT, 'claude/scripts'), path.join(installedClaude, 'scripts'), { recursive: true });
|
|
652
|
+
fs.cpSync(path.join(ROOT, 'claude/skills/specs/templates'), path.join(installedClaude, 'skills/specs/templates'), { recursive: true });
|
|
653
|
+
fs.cpSync(path.join(ROOT, 'claude/hooks'), path.join(installedClaude, 'hooks'), { recursive: true });
|
|
654
|
+
|
|
655
|
+
const installedScaffold = path.join(installedClaude, 'scripts/spec-scaffold.cjs');
|
|
656
|
+
const installedHook = path.join(installedClaude, 'hooks/completion-authority.cjs');
|
|
657
|
+
const installedRuntime = {
|
|
658
|
+
root,
|
|
659
|
+
home: installedHome,
|
|
660
|
+
state: path.join(installedHome, '.cafekit', 'completion-authority', 'projects'),
|
|
661
|
+
run(mode, payload) {
|
|
662
|
+
return spawnSync(process.execPath, [installedHook, mode], {
|
|
663
|
+
cwd: root,
|
|
664
|
+
env: {
|
|
665
|
+
...process.env,
|
|
666
|
+
HOME: installedHome,
|
|
667
|
+
USERPROFILE: installedHome,
|
|
668
|
+
CLAUDE_PROJECT_DIR: root,
|
|
669
|
+
PROJECT_ROOT: root,
|
|
670
|
+
},
|
|
671
|
+
input: JSON.stringify({ cwd: root, session_id: 'session-a', ...payload }),
|
|
672
|
+
encoding: 'utf8',
|
|
673
|
+
});
|
|
674
|
+
},
|
|
675
|
+
};
|
|
676
|
+
const scaffolded = spawnSync(process.execPath, [
|
|
677
|
+
installedScaffold,
|
|
678
|
+
'installed-critical',
|
|
679
|
+
'--tasks',
|
|
680
|
+
'R0-01-authority-baseline',
|
|
681
|
+
'--lane',
|
|
682
|
+
'Critical',
|
|
683
|
+
], {
|
|
684
|
+
cwd: root,
|
|
685
|
+
env: { ...process.env, HOME: installedHome, USERPROFILE: installedHome },
|
|
686
|
+
encoding: 'utf8',
|
|
687
|
+
});
|
|
688
|
+
assert.equal(scaffolded.status, 0, `${scaffolded.stdout}\n${scaffolded.stderr}`);
|
|
689
|
+
|
|
690
|
+
const floor = JSON.parse(fs.readFileSync(stateRecord(installedRuntime, 'policy-floor'), 'utf8'));
|
|
691
|
+
const baseline = JSON.parse(fs.readFileSync(stateRecord(installedRuntime, 'baseline-'), 'utf8'));
|
|
692
|
+
assert.equal(typeof floor.mac, 'string');
|
|
693
|
+
assert.ok(floor.mac.length > 0);
|
|
694
|
+
assert.equal(typeof baseline.mac, 'string');
|
|
695
|
+
assert.ok(baseline.mac.length > 0);
|
|
696
|
+
|
|
697
|
+
fs.rmSync(path.join(root, 'specs', 'installed-critical'), { recursive: true, force: true });
|
|
698
|
+
const disappeared = block(stop(installedRuntime));
|
|
699
|
+
assert.match(disappeared.reason, /disappeared/i);
|
|
700
|
+
|
|
701
|
+
const sourceScaffolded = spawnSync(process.execPath, [
|
|
702
|
+
path.join(ROOT, 'claude/scripts/spec-scaffold.cjs'),
|
|
703
|
+
'source-critical',
|
|
704
|
+
'--tasks',
|
|
705
|
+
'R0-01-source-baseline',
|
|
706
|
+
'--lane',
|
|
707
|
+
'Critical',
|
|
708
|
+
], {
|
|
709
|
+
cwd: sourceRoot,
|
|
710
|
+
env: { ...process.env, HOME: sourceHome, USERPROFILE: sourceHome },
|
|
711
|
+
encoding: 'utf8',
|
|
712
|
+
});
|
|
713
|
+
assert.equal(sourceScaffolded.status, 0, `${sourceScaffolded.stdout}\n${sourceScaffolded.stderr}`);
|
|
714
|
+
assert.equal(fs.existsSync(path.join(sourceHome, '.cafekit', 'completion-authority')), false);
|
|
715
|
+
} finally {
|
|
716
|
+
cleanupFixture(root);
|
|
717
|
+
fs.rmSync(sourceRoot, { recursive: true, force: true });
|
|
718
|
+
fs.rmSync(sourceHome, { recursive: true, force: true });
|
|
719
|
+
}
|
|
720
|
+
});
|