@haposoft/cafekit 0.18.1 → 0.18.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -1
- package/bin/__tests__/adv-skill.test.js +371 -0
- package/bin/__tests__/agent-authority.test.js +38 -0
- package/bin/__tests__/agent-instructions.test.js +54 -0
- package/bin/__tests__/claude-plugin-build.test.js +151 -0
- package/bin/__tests__/claude-settings.test.js +123 -0
- package/bin/__tests__/codex-ask-guard.test.js +123 -0
- package/bin/__tests__/codex-ask-projection.test.js +51 -0
- package/bin/__tests__/codex-hooks-ownership.test.js +80 -1
- package/bin/__tests__/codex-hooks.test.js +812 -12
- package/bin/__tests__/codex-implicit-policy.test.js +160 -0
- package/bin/__tests__/codex-native-hook-boundaries.test.js +136 -0
- package/bin/__tests__/codex-native.test.js +198 -58
- package/bin/__tests__/codex-plugin-build.test.js +230 -0
- package/bin/__tests__/codex-plugin-context.test.js +135 -0
- package/bin/__tests__/codex-plugin-hooks.test.js +220 -0
- package/bin/__tests__/codex-projection-parity.test.js +314 -0
- package/bin/__tests__/codex-quiet-hooks.test.js +170 -0
- package/bin/__tests__/codex-research-projection.test.js +70 -0
- package/bin/__tests__/codex-runtime-layout.test.js +81 -0
- package/bin/__tests__/codex-setup-agents.test.js +216 -0
- package/bin/__tests__/codex-specs-projection.test.js +76 -0
- package/bin/__tests__/develop-contract.test.js +1 -1
- package/bin/__tests__/docs-sync-on-demand.test.js +25 -20
- package/bin/__tests__/inspect-paths.test.js +183 -0
- package/bin/__tests__/installer-safety.test.js +335 -0
- package/bin/__tests__/legacy-hooks-retired.test.js +81 -21
- package/bin/__tests__/migrate-apply.test.js +373 -0
- package/bin/__tests__/migrate-cli.test.js +152 -0
- package/bin/__tests__/migrate-merged.test.js +269 -0
- package/bin/__tests__/migrate-plan.test.js +271 -0
- package/bin/__tests__/omp-bridge.test.js +147 -17
- package/bin/__tests__/omp-hooks.test.js +35 -5
- package/bin/__tests__/orca-session.test.js +7 -0
- package/bin/__tests__/plugin-safety-keys.test.js +121 -0
- package/bin/__tests__/privacy-paths.test.js +501 -0
- package/bin/__tests__/usage-hook-retired.test.js +29 -9
- package/bin/__tests__/vibe-run.test.js +215 -0
- package/bin/__tests__/vibe-skill.test.js +549 -0
- package/bin/install.js +124 -1
- package/bin/lib/backup.js +43 -24
- package/bin/lib/codex-install.js +141 -12
- package/bin/lib/context.js +6 -1
- package/bin/lib/lock.js +98 -18
- package/bin/lib/managed-writer.js +16 -10
- package/bin/lib/migrate-apply.js +202 -0
- package/bin/lib/migrate-merged.js +342 -0
- package/bin/lib/migrate-plan.js +419 -0
- package/bin/lib/plugin-inventory.js +33 -0
- package/bin/phases/claude-runtime.js +93 -25
- package/bin/phases/claude-settings.js +22 -1
- package/bin/phases/codex-hooks.js +134 -33
- package/bin/phases/codex-runtime.js +110 -85
- package/bin/phases/copy-payload.js +50 -4
- package/bin/phases/omp-runtime.js +4 -26
- package/bin/phases/root-config.js +5 -2
- package/package.json +1 -1
- package/src/claude/CLAUDE.md +5 -1
- package/src/claude/agents/advisor.md +55 -0
- package/src/claude/agents/debugger.md +2 -3
- package/src/claude/agents/deployer.md +2 -1
- package/src/claude/agents/docs-keeper.md +12 -0
- package/src/claude/agents/inspector.md +1 -1
- package/src/claude/agents/project-manager.md +2 -2
- package/src/claude/agents/researcher.md +0 -1
- package/src/claude/agents/spec-maker.md +4 -1
- package/src/claude/gitignore +3 -0
- package/src/claude/hooks/__tests__/installed-root.test.js +111 -0
- package/src/claude/hooks/__tests__/plugin-context.test.js +96 -0
- package/src/claude/hooks/__tests__/plugin-options.test.js +141 -0
- package/src/claude/hooks/__tests__/plugin-state-isolation.test.js +125 -0
- package/src/claude/hooks/__tests__/precompact.test.js +213 -11
- package/src/claude/hooks/__tests__/privacy-block.test.js +60 -9
- package/src/claude/hooks/__tests__/session-writeenv.test.js +25 -0
- package/src/claude/hooks/__tests__/setup-statusline.test.js +179 -0
- package/src/claude/hooks/__tests__/spec-gate.test.js +227 -11
- package/src/claude/hooks/__tests__/spec-state-touched.test.js +204 -8
- package/src/claude/hooks/__tests__/state.test.js +207 -7
- package/src/claude/hooks/__tests__/update-check.test.js +88 -0
- package/src/claude/hooks/agent.cjs +11 -7
- package/src/claude/hooks/inspect-block.cjs +20 -66
- package/src/claude/hooks/lib/hook-state-dir.cjs +19 -1
- package/src/claude/hooks/lib/inspect-paths.cjs +304 -0
- package/src/claude/hooks/lib/parser.cjs +1 -1
- package/src/claude/hooks/lib/plugin-options.cjs +47 -0
- package/src/claude/hooks/lib/privacy-command-analysis.cjs +317 -24
- package/src/claude/hooks/lib/privacy-paths.cjs +673 -0
- package/src/claude/hooks/lib/project-root.cjs +15 -0
- package/src/claude/hooks/lib/rules-reservation.cjs +36 -0
- package/src/claude/hooks/lib/spec-touch.cjs +80 -0
- package/src/claude/hooks/lib/update-check.cjs +77 -0
- package/src/claude/hooks/plugin-context.cjs +80 -0
- package/src/claude/hooks/precompact.cjs +8 -15
- package/src/claude/hooks/privacy-block.cjs +25 -206
- package/src/claude/hooks/rules.cjs +10 -29
- package/src/claude/hooks/session.cjs +37 -117
- package/src/claude/hooks/spec-gate.cjs +9 -26
- package/src/claude/hooks/spec-state.cjs +44 -59
- package/src/claude/hooks/state.cjs +101 -52
- package/src/claude/migration-manifest.json +14 -2
- package/src/claude/plugin/commands/setup-statusline.md +12 -0
- package/src/claude/plugin/core.md +71 -0
- package/src/claude/rules/hook-protocols.md +1 -1
- package/src/claude/rules/manage-docs.md +1 -2
- package/src/claude/rules/review-audit-self-decision.md +1 -1
- package/src/claude/rules/skill-workflow-routing.md +1 -1
- package/src/claude/rules/state-sync.md +14 -0
- package/src/claude/rules/workflow.md +1 -1
- package/src/claude/runtime.json +0 -3
- package/src/claude/scripts/generate-skill-catalog.cjs +7 -2
- package/src/claude/scripts/provenance.cjs +54 -16
- package/src/claude/scripts/setup-statusline.cjs +88 -0
- package/src/claude/settings/settings.json +1 -1
- package/src/claude/skills/adv/SKILL.md +139 -0
- package/src/claude/skills/adv/references/optional-modes.md +41 -0
- package/src/claude/skills/adv/references/ultra-mode.md +29 -0
- package/src/claude/skills/ai-multimodal/scripts/check_setup.py +8 -68
- package/src/claude/skills/ai-multimodal/scripts/gemini_batch_process.py +6 -28
- package/src/claude/skills/brainstorm/SKILL.md +1 -1
- package/src/claude/skills/code-review/SKILL.md +4 -1
- package/src/claude/skills/code-review/references/pr-fix-reply.md +42 -0
- package/src/claude/skills/develop/SKILL.md +5 -3
- package/src/claude/skills/develop/references/quality-gate.md +1 -1
- package/src/claude/skills/fix/SKILL.md +2 -2
- package/src/claude/skills/git/SKILL.md +5 -3
- package/src/claude/skills/git/references/merge-pr.md +125 -0
- package/src/claude/skills/git/references/worktree-blueprint.md +1 -1
- package/src/claude/skills/specs/SKILL.md +2 -2
- package/src/claude/skills/specs/references/review.md +1 -1
- package/src/claude/skills/specs/references/templates.md +2 -0
- package/src/claude/skills/vibe/SKILL.md +69 -0
- package/src/claude/skills/vibe/references/pipeline-skill-integration.md +34 -0
- package/src/claude/skills/vibe/references/report-templates.md +84 -0
- package/src/claude/skills/vibe/references/subagent-orchestration.md +23 -0
- package/src/codex/AGENTS.md +9 -1
- package/src/codex/hooks/ask-guard.cjs +54 -0
- package/src/codex/hooks/inspect-block.cjs +15 -8
- package/src/codex/hooks/lib/hook-context.cjs +118 -4
- package/src/codex/hooks/lib/inspect-paths.cjs +1 -199
- package/src/codex/hooks/lib/plugin-options.cjs +2 -0
- package/src/codex/hooks/lib/privacy-paths.cjs +1 -0
- package/src/codex/hooks/lib/privacy-state.cjs +116 -17
- package/src/codex/hooks/lib/rules-reservation.cjs +38 -0
- package/src/codex/hooks/lib/secret-keywords.cjs +1 -0
- package/src/codex/hooks/lib/spec-session-touch.cjs +100 -0
- package/src/codex/hooks/lib/spec-touch.cjs +3 -0
- package/src/codex/hooks/lib/state-store.cjs +5 -4
- package/src/codex/hooks/lib/update-check.cjs +1 -0
- package/src/codex/hooks/precompact.cjs +40 -0
- package/src/codex/hooks/privacy-approval.cjs +28 -17
- package/src/codex/hooks/privacy-block.cjs +11 -216
- package/src/codex/hooks/rules.cjs +14 -34
- package/src/codex/hooks/secret-output-guardrail.cjs +1 -1
- package/src/codex/hooks/session.cjs +59 -11
- package/src/codex/hooks/spec-gate.cjs +21 -6
- package/src/codex/hooks/spec-state.cjs +32 -6
- package/src/codex/hooks/state.cjs +25 -4
- package/src/codex/hooks.json +39 -7
- package/src/codex/plugin/core.md +76 -0
- package/src/codex/plugin/hooks/plugin-context.cjs +63 -0
- package/src/codex/plugin/skills/setup-agents/SKILL.md +17 -0
- package/src/codex/rules/hook-protocols.md +13 -6
- package/src/codex/scripts/setup-agents.cjs +124 -0
- package/src/common/AGENTS.md +1 -1
- package/src/omp/extensions/{cafekit-bridge.mjs → cafekit-bridge.js} +85 -9
- package/src/omp/hooks/privacy-block.cjs +26 -207
- package/src/codex/rules/state-sync.md +0 -60
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { test, before, after } = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const os = require('node:os');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
const { spawnSync } = require('node:child_process');
|
|
9
|
+
|
|
10
|
+
const BUILD = path.resolve(__dirname, '../../../../scripts/build-claude-plugin.mjs');
|
|
11
|
+
const SAMPLE_PAYLOADS = {
|
|
12
|
+
SessionStart: { source: 'startup' },
|
|
13
|
+
UserPromptSubmit: { prompt: 'hello' },
|
|
14
|
+
PreToolUse: { tool_name: 'Read', tool_input: { file_path: 'README.md' } },
|
|
15
|
+
PostToolUse: { tool_name: 'Edit', tool_input: { file_path: 'README.md' }, tool_response: {} },
|
|
16
|
+
Stop: { stop_hook_active: false },
|
|
17
|
+
SubagentStart: { agent_type: 'cf:debugger' },
|
|
18
|
+
SubagentStop: { agent_type: 'cf:debugger', stop_hook_active: false, last_assistant_message: 'done' },
|
|
19
|
+
PreCompact: { trigger: 'manual' },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
let work;
|
|
23
|
+
let tree;
|
|
24
|
+
|
|
25
|
+
function walk(root) {
|
|
26
|
+
if (!fs.existsSync(root)) return [];
|
|
27
|
+
return fs.readdirSync(root, { withFileTypes: true }).flatMap((entry) => {
|
|
28
|
+
const full = path.join(root, entry.name);
|
|
29
|
+
return entry.isDirectory() ? [`${full}/`, ...walk(full)] : [full];
|
|
30
|
+
}).sort();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function makeRepo(name, withDotClaude) {
|
|
34
|
+
const repo = path.join(work, name);
|
|
35
|
+
fs.mkdirSync(repo, { recursive: true });
|
|
36
|
+
spawnSync('git', ['init', '-q'], { cwd: repo });
|
|
37
|
+
fs.writeFileSync(path.join(repo, 'README.md'), '# demo\n');
|
|
38
|
+
if (withDotClaude) {
|
|
39
|
+
fs.mkdirSync(path.join(repo, '.claude'));
|
|
40
|
+
fs.writeFileSync(path.join(repo, '.claude/settings.local.json'), '{}\n');
|
|
41
|
+
}
|
|
42
|
+
return repo;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Runs every hook the plugin registers, the way Claude Code would, and returns
|
|
46
|
+
// the SessionStart output so callers can inspect it.
|
|
47
|
+
function runAllHooks(repo, env) {
|
|
48
|
+
const { hooks } = JSON.parse(fs.readFileSync(path.join(tree, 'hooks/hooks.json'), 'utf8'));
|
|
49
|
+
let sessionOutput = '';
|
|
50
|
+
for (const [event, entries] of Object.entries(hooks)) {
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
for (const { command } of entry.hooks) {
|
|
53
|
+
const file = command.match(/hooks\/([^"]+)"$/)[1];
|
|
54
|
+
const payload = { session_id: 'isolation-test', hook_event_name: event, cwd: repo, transcript_path: path.join(repo, 'none.jsonl'), ...SAMPLE_PAYLOADS[event] };
|
|
55
|
+
const run = spawnSync('node', [path.join(tree, 'hooks', file)], {
|
|
56
|
+
cwd: repo, input: JSON.stringify(payload), encoding: 'utf8', timeout: 20000,
|
|
57
|
+
env: { ...process.env, HOME: path.join(work, 'home'), CLAUDE_PLUGIN_ROOT: tree, CLAUDE_PROJECT_DIR: repo, ...env },
|
|
58
|
+
});
|
|
59
|
+
assert.equal(run.status, 0, `${event} ${file}: ${run.stderr}`);
|
|
60
|
+
if (event === 'SessionStart') sessionOutput += run.stdout;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return sessionOutput;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
before(() => {
|
|
68
|
+
work = fs.mkdtempSync(path.join(os.tmpdir(), 'ck-plugin-isolation-'));
|
|
69
|
+
tree = path.join(work, 'cf');
|
|
70
|
+
const built = spawnSync('node', [BUILD, '--out', tree], { encoding: 'utf8' });
|
|
71
|
+
assert.equal(built.status, 0, built.stderr);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
after(() => fs.rmSync(work, { recursive: true, force: true }));
|
|
75
|
+
|
|
76
|
+
for (const [label, withDotClaude] of [['empty-repo', false], ['repo-with-dot-claude', true]]) {
|
|
77
|
+
test(`${label}: hooks write only under CLAUDE_PLUGIN_DATA`, () => {
|
|
78
|
+
const repo = makeRepo(label, withDotClaude);
|
|
79
|
+
const data = fs.mkdtempSync(path.join(work, `data-${label}-`));
|
|
80
|
+
const repoBefore = walk(repo);
|
|
81
|
+
const treeBefore = walk(tree);
|
|
82
|
+
runAllHooks(repo, { CLAUDE_PLUGIN_DATA: data });
|
|
83
|
+
assert.deepEqual(walk(repo), repoBefore, 'the project gained files');
|
|
84
|
+
assert.deepEqual(walk(tree), treeBefore, 'the plugin tree gained files');
|
|
85
|
+
assert.ok(walk(data).some((p) => p.endsWith('hook-log.jsonl') || p.includes('session-states')), 'no state under CLAUDE_PLUGIN_DATA');
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
test('no-data-env-uses-tmp: without CLAUDE_PLUGIN_DATA state goes to the OS temp directory', () => {
|
|
90
|
+
const repo = makeRepo('no-data', true);
|
|
91
|
+
const tmp = fs.mkdtempSync(path.join(work, 'tmp-'));
|
|
92
|
+
const repoBefore = walk(repo);
|
|
93
|
+
const treeBefore = walk(tree);
|
|
94
|
+
runAllHooks(repo, { CLAUDE_PLUGIN_DATA: '', TMPDIR: tmp });
|
|
95
|
+
assert.deepEqual(walk(repo), repoBefore, 'the project gained files');
|
|
96
|
+
assert.deepEqual(walk(tree), treeBefore, 'the plugin tree gained files');
|
|
97
|
+
const uid = typeof process.getuid === 'function' ? process.getuid() : 'user';
|
|
98
|
+
assert.ok(walk(path.join(tmp, `cafekit-plugin-${uid}`)).length > 0, 'no state under the temp directory');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('no-npx-notice: the plugin session hook prints no npx update notice and writes no update cache', () => {
|
|
102
|
+
const repo = makeRepo('npx-notice', true);
|
|
103
|
+
// An old npx install left version metadata behind; the plugin must ignore it.
|
|
104
|
+
fs.writeFileSync(path.join(repo, '.claude/cafekit.json'), JSON.stringify({ version: '0.0.1' }));
|
|
105
|
+
// A fresh cached "latest" makes the npx branch print its notice without any
|
|
106
|
+
// network access, so this test cannot pass merely because the registry is unreachable.
|
|
107
|
+
const cache = path.join(repo, '.claude/.cafekit-update-cache.json');
|
|
108
|
+
const cacheBytes = JSON.stringify({ ts: Date.now(), latest: '99.0.0' });
|
|
109
|
+
fs.writeFileSync(cache, cacheBytes);
|
|
110
|
+
const data = fs.mkdtempSync(path.join(work, 'data-notice-'));
|
|
111
|
+
const output = runAllHooks(repo, { CLAUDE_PLUGIN_DATA: data });
|
|
112
|
+
assert.doesNotMatch(output, /npx @haposoft\/cafekit/);
|
|
113
|
+
assert.equal(fs.readFileSync(cache, 'utf8'), cacheBytes, 'update cache rewritten');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('npx-branch-unchanged: outside a plugin, source-tree hooks keep the temp log directory', () => {
|
|
117
|
+
const script = `delete process.env.CLAUDE_PLUGIN_ROOT; delete process.env.CLAUDE_PLUGIN_DATA;
|
|
118
|
+
process.stdout.write(require(${JSON.stringify(path.resolve(__dirname, '../lib/hook-state-dir.cjs'))}).hookStateDir());`;
|
|
119
|
+
const env = { ...process.env };
|
|
120
|
+
delete env.CLAUDE_PLUGIN_ROOT;
|
|
121
|
+
delete env.CLAUDE_PLUGIN_DATA;
|
|
122
|
+
const run = spawnSync('node', ['-e', script], { encoding: 'utf8', env });
|
|
123
|
+
assert.equal(run.status, 0, run.stderr);
|
|
124
|
+
assert.equal(run.stdout, path.join(os.tmpdir(), 'cafekit-hook-logs', 'claude'));
|
|
125
|
+
});
|
|
@@ -5,17 +5,33 @@
|
|
|
5
5
|
// fixture; the capture is asserted from the file it writes and from what the
|
|
6
6
|
// next session actually prints.
|
|
7
7
|
|
|
8
|
-
const { test } = require('node:test');
|
|
8
|
+
const { test, after } = require('node:test');
|
|
9
9
|
const assert = require('node:assert');
|
|
10
10
|
const { spawnSync } = require('node:child_process');
|
|
11
11
|
const fs = require('node:fs');
|
|
12
12
|
const os = require('node:os');
|
|
13
13
|
const path = require('node:path');
|
|
14
|
+
const crypto = require('node:crypto');
|
|
15
|
+
|
|
16
|
+
// Hooks run from source keep state under os.tmpdir(); a private one per test process
|
|
17
|
+
// keeps a parallel run of this file from reading or clearing this run's state.
|
|
18
|
+
const PRIVATE_TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'ck-precompact-tmp-'));
|
|
19
|
+
for (const key of ['TMPDIR', 'TMP', 'TEMP']) process.env[key] = PRIVATE_TMP;
|
|
20
|
+
after(() => { fs.rmSync(PRIVATE_TMP, { recursive: true, force: true }); });
|
|
14
21
|
|
|
15
22
|
const PRECOMPACT = path.join(__dirname, '..', 'precompact.cjs');
|
|
16
23
|
const SESSION = path.join(__dirname, '..', 'session.cjs');
|
|
17
24
|
const { hookStateDir } = require(path.join(__dirname, '..', 'lib', 'hook-state-dir.cjs'));
|
|
18
|
-
const
|
|
25
|
+
const CAPTURES = new Set([path.join(hookStateDir(), 'compact-recovery.json')]);
|
|
26
|
+
function recoveryPath(session = 'compact-session') {
|
|
27
|
+
const key = crypto.createHash('sha256').update(session).digest('hex');
|
|
28
|
+
const file = path.join(hookStateDir(), `compact-recovery-${key}.json`);
|
|
29
|
+
CAPTURES.add(file);
|
|
30
|
+
return file;
|
|
31
|
+
}
|
|
32
|
+
const HOOK_ENV = { ...process.env };
|
|
33
|
+
delete HOOK_ENV.CLAUDE_PROJECT_DIR;
|
|
34
|
+
delete HOOK_ENV.PROJECT_ROOT;
|
|
19
35
|
|
|
20
36
|
function fixture() {
|
|
21
37
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ck-precompact-'));
|
|
@@ -36,21 +52,24 @@ function fixture() {
|
|
|
36
52
|
}
|
|
37
53
|
|
|
38
54
|
function run(hook, dir, payload) {
|
|
55
|
+
if (typeof payload?.session_id === 'string' && payload.session_id.trim()) recoveryPath(payload.session_id);
|
|
39
56
|
const result = spawnSync(process.execPath, [hook], {
|
|
40
57
|
cwd: dir,
|
|
41
58
|
input: JSON.stringify(payload),
|
|
42
59
|
encoding: 'utf8',
|
|
43
|
-
env:
|
|
60
|
+
env: HOOK_ENV,
|
|
44
61
|
});
|
|
45
62
|
return { code: result.status, stdout: result.stdout || '', stderr: result.stderr || '' };
|
|
46
63
|
}
|
|
47
64
|
|
|
48
65
|
function clearCapture() {
|
|
49
|
-
|
|
66
|
+
for (const file of CAPTURES) {
|
|
67
|
+
try { fs.unlinkSync(file); } catch { /* absent */ }
|
|
68
|
+
}
|
|
50
69
|
}
|
|
51
70
|
|
|
52
|
-
function readCapture() {
|
|
53
|
-
return JSON.parse(fs.readFileSync(
|
|
71
|
+
function readCapture(session) {
|
|
72
|
+
return JSON.parse(fs.readFileSync(recoveryPath(session), 'utf8'));
|
|
54
73
|
}
|
|
55
74
|
|
|
56
75
|
test('PreCompact records branch, HEAD, dirty count and the in-flight task', () => {
|
|
@@ -93,8 +112,8 @@ test('a compact restart prints the captured anchors alongside the authorization
|
|
|
93
112
|
const dir = fixture();
|
|
94
113
|
try {
|
|
95
114
|
clearCapture();
|
|
96
|
-
assert.strictEqual(run(PRECOMPACT, dir, { hook_event_name: 'PreCompact', trigger: 'manual', cwd: dir }).code, 0);
|
|
97
|
-
const restart = run(SESSION, dir, { hook_event_name: 'SessionStart', source: 'compact', cwd: dir });
|
|
115
|
+
assert.strictEqual(run(PRECOMPACT, dir, { hook_event_name: 'PreCompact', trigger: 'manual', session_id: 'compact-session', cwd: dir }).code, 0);
|
|
116
|
+
const restart = run(SESSION, dir, { hook_event_name: 'SessionStart', source: 'compact', session_id: 'compact-session', cwd: dir });
|
|
98
117
|
assert.strictEqual(restart.code, 0, restart.stderr);
|
|
99
118
|
assert.match(restart.stdout, /VERIFY PENDING AUTHORIZATIONS/, 'the existing warning must survive');
|
|
100
119
|
assert.match(restart.stdout, /State before compaction/);
|
|
@@ -119,6 +138,53 @@ test('a compact restart without a capture still prints the warning and nothing i
|
|
|
119
138
|
}
|
|
120
139
|
});
|
|
121
140
|
|
|
141
|
+
test('a compact restart in another session prints the warning without foreign anchors', () => {
|
|
142
|
+
const dir = fixture();
|
|
143
|
+
try {
|
|
144
|
+
clearCapture();
|
|
145
|
+
assert.strictEqual(run(PRECOMPACT, dir, { hook_event_name: 'PreCompact', session_id: 'session-a', cwd: dir }).code, 0);
|
|
146
|
+
const restart = run(SESSION, dir, { hook_event_name: 'SessionStart', source: 'compact', session_id: 'session-b', cwd: dir });
|
|
147
|
+
assert.strictEqual(restart.code, 0, restart.stderr);
|
|
148
|
+
assert.match(restart.stdout, /SESSION COMPRESSED/);
|
|
149
|
+
assert.doesNotMatch(restart.stdout, /State before compaction/);
|
|
150
|
+
} finally {
|
|
151
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
152
|
+
clearCapture();
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('a compact restart in the same session restores its captured anchors', () => {
|
|
157
|
+
const dir = fixture();
|
|
158
|
+
try {
|
|
159
|
+
clearCapture();
|
|
160
|
+
assert.strictEqual(run(PRECOMPACT, dir, { hook_event_name: 'PreCompact', session_id: 'session-a', cwd: dir }).code, 0);
|
|
161
|
+
const restart = run(SESSION, dir, { hook_event_name: 'SessionStart', source: 'compact', session_id: 'session-a', cwd: dir });
|
|
162
|
+
assert.strictEqual(restart.code, 0, restart.stderr);
|
|
163
|
+
assert.match(restart.stdout, /SESSION COMPRESSED/);
|
|
164
|
+
assert.match(restart.stdout, /State before compaction[\s\S]*branch work/);
|
|
165
|
+
} finally {
|
|
166
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
167
|
+
clearCapture();
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
for (const [captureId, restartId] of [[undefined, 'session-a'], ['session-a', undefined], [undefined, undefined]]) {
|
|
172
|
+
test(`a compact restart without ${captureId ? 'restart' : restartId ? 'capture' : 'either'} session identity keeps only the warning`, () => {
|
|
173
|
+
const dir = fixture();
|
|
174
|
+
try {
|
|
175
|
+
clearCapture();
|
|
176
|
+
assert.strictEqual(run(PRECOMPACT, dir, { hook_event_name: 'PreCompact', session_id: captureId, cwd: dir }).code, 0);
|
|
177
|
+
const restart = run(SESSION, dir, { hook_event_name: 'SessionStart', source: 'compact', session_id: restartId, cwd: dir });
|
|
178
|
+
assert.strictEqual(restart.code, 0, restart.stderr);
|
|
179
|
+
assert.match(restart.stdout, /SESSION COMPRESSED/);
|
|
180
|
+
assert.doesNotMatch(restart.stdout, /State before compaction/);
|
|
181
|
+
} finally {
|
|
182
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
183
|
+
clearCapture();
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
122
188
|
test('a normal session start prints no compaction anchors', () => {
|
|
123
189
|
const dir = fixture();
|
|
124
190
|
try {
|
|
@@ -138,7 +204,7 @@ test('PreCompact fails open outside a repository and on malformed input', () =>
|
|
|
138
204
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ck-precompact-bare-'));
|
|
139
205
|
try {
|
|
140
206
|
clearCapture();
|
|
141
|
-
const noGit = run(PRECOMPACT, dir, { hook_event_name: 'PreCompact', cwd: dir });
|
|
207
|
+
const noGit = run(PRECOMPACT, dir, { hook_event_name: 'PreCompact', session_id: 'compact-session', cwd: dir });
|
|
142
208
|
assert.strictEqual(noGit.code, 0, 'a directory outside git must not fail the hook');
|
|
143
209
|
const record = readCapture();
|
|
144
210
|
assert.strictEqual(record.branch, null, 'unavailable git facts are recorded as null, never guessed');
|
|
@@ -147,12 +213,148 @@ test('PreCompact fails open outside a repository and on malformed input', () =>
|
|
|
147
213
|
|
|
148
214
|
clearCapture();
|
|
149
215
|
const malformed = spawnSync(process.execPath, [PRECOMPACT], {
|
|
150
|
-
cwd: dir, input: 'not json', encoding: 'utf8', env:
|
|
216
|
+
cwd: dir, input: 'not json', encoding: 'utf8', env: HOOK_ENV,
|
|
151
217
|
});
|
|
152
218
|
assert.strictEqual(malformed.status, 0, 'malformed payload must fail open');
|
|
153
|
-
assert.strictEqual(fs.existsSync(
|
|
219
|
+
assert.strictEqual(fs.existsSync(recoveryPath()), false, 'a malformed payload writes no capture');
|
|
154
220
|
} finally {
|
|
155
221
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
156
222
|
clearCapture();
|
|
157
223
|
}
|
|
158
224
|
});
|
|
225
|
+
|
|
226
|
+
function installedFixture(callback) {
|
|
227
|
+
const root = fixture();
|
|
228
|
+
const source = path.resolve(__dirname, '../..');
|
|
229
|
+
const manifest = require('../../migration-manifest.json');
|
|
230
|
+
const sessions = new Set();
|
|
231
|
+
try {
|
|
232
|
+
for (const relative of manifest.runtime.files) {
|
|
233
|
+
const target = path.join(root, '.claude', relative);
|
|
234
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
235
|
+
fs.copyFileSync(path.join(source, relative), target);
|
|
236
|
+
}
|
|
237
|
+
for (const relative of manifest.scripts.required) {
|
|
238
|
+
const target = path.join(root, '.claude/scripts', relative);
|
|
239
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
240
|
+
fs.copyFileSync(path.join(source, 'scripts', relative), target);
|
|
241
|
+
}
|
|
242
|
+
const hooks = path.join(root, '.claude/hooks');
|
|
243
|
+
const invoke = (name, payload, overrides = {}) => {
|
|
244
|
+
if (typeof payload?.session_id === 'string' && payload.session_id.trim()) sessions.add(payload.session_id);
|
|
245
|
+
return spawnSync(process.execPath, [path.join(hooks, `${name}.cjs`)], {
|
|
246
|
+
cwd: root, env: HOOK_ENV, encoding: 'utf8', timeout: 10000,
|
|
247
|
+
input: typeof payload === 'string' ? payload : JSON.stringify(payload), ...overrides,
|
|
248
|
+
});
|
|
249
|
+
};
|
|
250
|
+
callback({ root: fs.realpathSync(root), hooks, invoke });
|
|
251
|
+
} finally {
|
|
252
|
+
for (const session of sessions) {
|
|
253
|
+
const key = crypto.createHash('sha256').update(session).digest('hex').slice(0, 16);
|
|
254
|
+
fs.rmSync(path.join(os.tmpdir(), `cafekit-rules-${key}.json`), { force: true });
|
|
255
|
+
}
|
|
256
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
test('two compacting sessions keep separate anchors and restore only their own branch', () => {
|
|
261
|
+
installedFixture(({ root, hooks, invoke }) => {
|
|
262
|
+
const sessions = [crypto.randomUUID(), crypto.randomUUID()];
|
|
263
|
+
const files = [];
|
|
264
|
+
for (const [index, session_id] of sessions.entries()) {
|
|
265
|
+
const branch = `anchor-${index === 0 ? 'first' : 'second'}`;
|
|
266
|
+
const checkout = spawnSync('git', ['-C', root, 'checkout', '-qb', branch], { encoding: 'utf8' });
|
|
267
|
+
assert.strictEqual(checkout.status, 0, checkout.stderr);
|
|
268
|
+
const capture = invoke('precompact', { hook_event_name: 'PreCompact', session_id, cwd: root });
|
|
269
|
+
assert.strictEqual(capture.status, 0, capture.stderr);
|
|
270
|
+
const hash = crypto.createHash('sha256').update(session_id).digest('hex');
|
|
271
|
+
files.push(path.join(hooks, '.logs', `compact-recovery-${hash}.json`));
|
|
272
|
+
}
|
|
273
|
+
for (const [index, file] of files.entries()) {
|
|
274
|
+
assert.ok(fs.existsSync(file), 'both session captures must survive');
|
|
275
|
+
const record = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
276
|
+
assert.strictEqual(record.session_id, sessions[index]);
|
|
277
|
+
assert.strictEqual(record.branch, `anchor-${index === 0 ? 'first' : 'second'}`);
|
|
278
|
+
}
|
|
279
|
+
for (const [index, session_id] of sessions.entries()) {
|
|
280
|
+
const compact = invoke('session', { hook_event_name: 'SessionStart', source: 'compact', session_id, cwd: root });
|
|
281
|
+
assert.strictEqual(compact.status, 0, compact.stderr);
|
|
282
|
+
assert.match(compact.stdout, /SESSION COMPRESSED/);
|
|
283
|
+
assert.match(compact.stdout, /State before compaction/);
|
|
284
|
+
assert.ok(compact.stdout.includes(`branch anchor-${index === 0 ? 'first' : 'second'}`));
|
|
285
|
+
assert.ok(!compact.stdout.includes(`branch anchor-${index === 0 ? 'second' : 'first'}`));
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test('PreCompact without a usable session identity creates no anchor or state directory', () => {
|
|
291
|
+
installedFixture(({ root, hooks, invoke }) => {
|
|
292
|
+
for (const session_id of [undefined, null, '', 42, ' ']) {
|
|
293
|
+
const capture = invoke('precompact', { hook_event_name: 'PreCompact', session_id, cwd: root });
|
|
294
|
+
assert.strictEqual(capture.status, 0, capture.stderr);
|
|
295
|
+
assert.strictEqual(fs.existsSync(path.join(hooks, '.logs')), false);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test('compact reinjects rules only for its own session even without matching anchors', () => {
|
|
301
|
+
installedFixture(({ root, hooks, invoke }) => {
|
|
302
|
+
const session = crypto.randomUUID();
|
|
303
|
+
const other = crypto.randomUUID();
|
|
304
|
+
const prompt = id => invoke('rules', { hook_event_name: 'UserPromptSubmit', session_id: id, cwd: root });
|
|
305
|
+
for (const id of [session, other]) {
|
|
306
|
+
const first = prompt(id);
|
|
307
|
+
assert.strictEqual(first.status, 0, first.stderr);
|
|
308
|
+
assert.match(first.stdout, /## Rules/);
|
|
309
|
+
assert.strictEqual(prompt(id).stdout, '');
|
|
310
|
+
}
|
|
311
|
+
fs.mkdirSync(path.join(hooks, '.logs'), { recursive: true });
|
|
312
|
+
const otherHash = crypto.createHash('sha256').update(other).digest('hex');
|
|
313
|
+
fs.writeFileSync(path.join(hooks, '.logs', `compact-recovery-${otherHash}.json`), JSON.stringify({ session_id: other }));
|
|
314
|
+
const compact = invoke('session', { hook_event_name: 'SessionStart', source: 'compact', session_id: session, cwd: root });
|
|
315
|
+
assert.strictEqual(compact.status, 0, compact.stderr);
|
|
316
|
+
assert.match(compact.stdout, /SESSION COMPRESSED/);
|
|
317
|
+
assert.doesNotMatch(compact.stdout, /State before compaction/);
|
|
318
|
+
const again = prompt(session);
|
|
319
|
+
assert.strictEqual(again.status, 0, again.stderr);
|
|
320
|
+
assert.match(again.stdout, /## Rules/);
|
|
321
|
+
assert.strictEqual(prompt(other).stdout, '');
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test('malformed session input fails open without a stack and logs its crash', () => {
|
|
326
|
+
installedFixture(({ hooks, invoke }) => {
|
|
327
|
+
const result = invoke('session', '{');
|
|
328
|
+
assert.strictEqual(result.status, 0);
|
|
329
|
+
assert.strictEqual(result.stderr, '');
|
|
330
|
+
const rows = fs.readFileSync(path.join(hooks, '.logs/hook-log.jsonl'), 'utf8').trim().split('\n').map(line => JSON.parse(line));
|
|
331
|
+
assert.strictEqual(rows.length, 1);
|
|
332
|
+
assert.strictEqual(rows[0].hook, 'session');
|
|
333
|
+
assert.strictEqual(rows[0].status, 'crash');
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test('installed session explains a nested cwd but omits the root line without a cwd', () => {
|
|
338
|
+
installedFixture(({ root, invoke }) => {
|
|
339
|
+
const cwd = path.join(root, 'sub/dir');
|
|
340
|
+
fs.mkdirSync(cwd, { recursive: true });
|
|
341
|
+
const nested = invoke('session', { source: 'startup', cwd }, { cwd });
|
|
342
|
+
assert.strictEqual(nested.status, 0, nested.stderr);
|
|
343
|
+
assert.ok(nested.stdout.includes(`CafeKit project root: ${root} (session working directory: ${cwd}).`));
|
|
344
|
+
const missing = invoke('session', { source: 'startup' }, { cwd });
|
|
345
|
+
assert.strictEqual(missing.status, 0, missing.stderr);
|
|
346
|
+
assert.doesNotMatch(missing.stdout, /CafeKit project root:/);
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test('PreCompact bounds slow git calls and finishes within ten seconds', { timeout: 15000 }, () => {
|
|
351
|
+
installedFixture(({ root, invoke }) => {
|
|
352
|
+
const bin = path.join(root, 'fake-bin');
|
|
353
|
+
fs.mkdirSync(bin);
|
|
354
|
+
fs.writeFileSync(path.join(bin, 'git'), '#!/bin/sh\nexec /bin/sleep 30\n', { mode: 0o755 });
|
|
355
|
+
const start = Date.now();
|
|
356
|
+
const result = invoke('precompact', { hook_event_name: 'PreCompact', session_id: crypto.randomUUID(), cwd: root }, { env: { ...HOOK_ENV, PATH: bin } });
|
|
357
|
+
assert.strictEqual(result.status, 0, 'slow git must not exhaust the hook timeout');
|
|
358
|
+
assert.ok(Date.now() - start < 10000);
|
|
359
|
+
});
|
|
360
|
+
});
|
|
@@ -11,12 +11,16 @@ const os = require('node:os');
|
|
|
11
11
|
const path = require('node:path');
|
|
12
12
|
|
|
13
13
|
const HOOK = path.join(__dirname, '..', 'privacy-block.cjs');
|
|
14
|
+
const HOOK_ENV = { ...process.env };
|
|
15
|
+
delete HOOK_ENV.CLAUDE_PROJECT_DIR;
|
|
16
|
+
delete HOOK_ENV.PROJECT_ROOT;
|
|
14
17
|
|
|
15
18
|
/** Run the hook with a tool payload; return process output. */
|
|
16
19
|
function runHook(payload, cwd) {
|
|
17
20
|
const res = spawnSync(process.execPath, [HOOK], {
|
|
18
21
|
input: JSON.stringify({ ...payload, cwd: cwd || payload.cwd }),
|
|
19
22
|
encoding: 'utf8',
|
|
23
|
+
env: HOOK_ENV,
|
|
20
24
|
});
|
|
21
25
|
return { code: res.status, stdout: res.stdout || '', stderr: res.stderr || '' };
|
|
22
26
|
}
|
|
@@ -30,6 +34,29 @@ function permissionDecision(stdout) {
|
|
|
30
34
|
return JSON.parse(stdout).hookSpecificOutput?.permissionDecision || null;
|
|
31
35
|
}
|
|
32
36
|
|
|
37
|
+
test('sensitive reads list sorted distinct basenames and the remaining count', () => {
|
|
38
|
+
const dir = tmpDir();
|
|
39
|
+
try {
|
|
40
|
+
const command = 'cat token.json nested/.env secrets.json credentials.json .env';
|
|
41
|
+
const { code, stdout } = runHook({ tool_name: 'Bash', tool_input: { command } }, dir);
|
|
42
|
+
assert.strictEqual(code, 0);
|
|
43
|
+
const output = JSON.parse(stdout).hookSpecificOutput;
|
|
44
|
+
assert.strictEqual(output.permissionDecision, 'ask');
|
|
45
|
+
assert.strictEqual(output.permissionDecisionReason,
|
|
46
|
+
'Sensitive file access requires approval: .env, credentials.json, secrets.json (+1 more)');
|
|
47
|
+
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('empty stdin asks because sensitive access cannot be evaluated safely', () => {
|
|
51
|
+
const result = spawnSync(process.execPath, [HOOK], { input: '', encoding: 'utf8', env: HOOK_ENV });
|
|
52
|
+
assert.strictEqual(result.status, 0);
|
|
53
|
+
assert.strictEqual(result.stderr, '');
|
|
54
|
+
assert.deepStrictEqual(JSON.parse(result.stdout).hookSpecificOutput, {
|
|
55
|
+
hookEventName: 'PreToolUse', permissionDecision: 'ask',
|
|
56
|
+
permissionDecisionReason: 'Sensitive access could not be evaluated safely.',
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
33
60
|
test('asks natively for a direct Read of .env', () => {
|
|
34
61
|
const dir = tmpDir();
|
|
35
62
|
try {
|
|
@@ -183,20 +210,17 @@ for (const command of [
|
|
|
183
210
|
}
|
|
184
211
|
|
|
185
212
|
// --- Accepted gap: pure indirection ---
|
|
186
|
-
// The target of `$FILE` is unknowable without running the shell. Treating every
|
|
213
|
+
// The target of a bare `$FILE` is unknowable without running the shell. Treating every
|
|
187
214
|
// unresolved expansion as sensitive asked on `cat ~/.zshrc`, `wc -l *.cjs` and
|
|
188
|
-
// `du -sh */` too, so these are allowed by design.
|
|
189
|
-
//
|
|
215
|
+
// `du -sh */` too, so these are allowed by design. A secret's name spelled out next to
|
|
216
|
+
// the expansion -- `"$HOME/.env"`, `$(echo .env)` -- is not indirection and asks below.
|
|
190
217
|
for (const command of [
|
|
191
218
|
'cat $FILE',
|
|
192
219
|
'cat "$FILE"',
|
|
193
220
|
'cat ${FILE}',
|
|
194
221
|
'cat $1',
|
|
195
|
-
'cat $(echo .env)',
|
|
196
|
-
'cat `echo .env`',
|
|
197
222
|
'head $SECRET',
|
|
198
223
|
'tail ${MY_FILE}',
|
|
199
|
-
'less "$HOME/.env"',
|
|
200
224
|
'cat README.md $EXTRA',
|
|
201
225
|
'echo ok; cat README.md $FILE',
|
|
202
226
|
'cat README.md | head $FILE',
|
|
@@ -288,14 +312,29 @@ for (const command of [
|
|
|
288
312
|
});
|
|
289
313
|
}
|
|
290
314
|
|
|
291
|
-
|
|
315
|
+
for (const command of ['cat $(echo .env)', 'cat `echo .env`', 'less "$HOME/.env"']) {
|
|
316
|
+
test(`asks when a secret's name is spelled out next to an expansion: ${command}`, () => {
|
|
317
|
+
const dir = tmpDir();
|
|
318
|
+
try {
|
|
319
|
+
const result = runHook({ tool_name: 'Bash', tool_input: { command } }, dir);
|
|
320
|
+
assert.strictEqual(permissionDecision(result.stdout), 'ask');
|
|
321
|
+
} finally {
|
|
322
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
test('an assignment naming a secret asks only when the same command reads it through the variable', () => {
|
|
292
328
|
const dir = tmpDir();
|
|
293
329
|
try {
|
|
294
330
|
const assigned = runHook({ tool_name: 'Bash', tool_input: { command: 'FILE=.env; cat README.md' } }, dir);
|
|
295
331
|
assert.strictEqual(permissionDecision(assigned.stdout), null);
|
|
296
|
-
//
|
|
332
|
+
// A literal assigned earlier in the same command is resolved, so reading it through the variable asks.
|
|
297
333
|
const read = runHook({ tool_name: 'Bash', tool_input: { command: 'FILE=.env; cat "$FILE"' } }, dir);
|
|
298
|
-
assert.strictEqual(permissionDecision(read.stdout),
|
|
334
|
+
assert.strictEqual(permissionDecision(read.stdout), 'ask');
|
|
335
|
+
// A prefix assignment does not reach the expansion: the shell expands $FILE first.
|
|
336
|
+
const prefix = runHook({ tool_name: 'Bash', tool_input: { command: 'FILE=.env cat "$FILE"' } }, dir);
|
|
337
|
+
assert.strictEqual(permissionDecision(prefix.stdout), null);
|
|
299
338
|
// Spelling the target out again does ask.
|
|
300
339
|
const literal = runHook({ tool_name: 'Bash', tool_input: { command: 'FILE=.env; cat .env' } }, dir);
|
|
301
340
|
assert.strictEqual(permissionDecision(literal.stdout), 'ask');
|
|
@@ -371,3 +410,15 @@ test('fails closed on malformed/looping symlink (hop limit)', () => {
|
|
|
371
410
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
372
411
|
}
|
|
373
412
|
});
|
|
413
|
+
|
|
414
|
+
test('a Claude Read of an ordinary path is not affected by omp selector handling', () => {
|
|
415
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ck-privacy-selector-'));
|
|
416
|
+
try {
|
|
417
|
+
fs.writeFileSync(path.join(root, 'notes.md'), 'hi\n');
|
|
418
|
+
const result = spawnSync(process.execPath, [HOOK], {
|
|
419
|
+
input: JSON.stringify({ tool_name: 'Read', tool_input: { file_path: path.join(root, 'notes.md') }, cwd: root }),
|
|
420
|
+
encoding: 'utf8', env: HOOK_ENV,
|
|
421
|
+
});
|
|
422
|
+
assert.equal(result.stdout.trim(), '');
|
|
423
|
+
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
424
|
+
});
|
|
@@ -17,6 +17,31 @@ const path = require('node:path');
|
|
|
17
17
|
|
|
18
18
|
const HOOK = path.join(__dirname, '..', 'session.cjs');
|
|
19
19
|
|
|
20
|
+
test('session.cjs does not export GIT_URL or remote userinfo', () => {
|
|
21
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ck-session-remote-'));
|
|
22
|
+
const envFile = path.join(dir, 'env.sh');
|
|
23
|
+
try {
|
|
24
|
+
git(['init', '-q'], dir);
|
|
25
|
+
const userinfo = 'fixture-user:fixture-placeholder';
|
|
26
|
+
const remote = `https://${userinfo}@example.invalid/r.git`;
|
|
27
|
+
const configured = spawnSync('git', ['remote', 'add', 'origin', remote], { cwd: dir, encoding: 'utf8' });
|
|
28
|
+
assert.strictEqual(configured.status, 0, 'fake remote setup must succeed');
|
|
29
|
+
const result = spawnSync(process.execPath, [HOOK], {
|
|
30
|
+
cwd: dir, input: JSON.stringify({ source: 'startup', cwd: dir }), encoding: 'utf8',
|
|
31
|
+
env: { ...process.env, CLAUDE_ENV_FILE: envFile },
|
|
32
|
+
});
|
|
33
|
+
assert.strictEqual(result.status, 0, 'session hook must exit successfully');
|
|
34
|
+
assert.ok(fs.existsSync(envFile), 'session env file must exist');
|
|
35
|
+
const content = fs.readFileSync(envFile, 'utf8');
|
|
36
|
+
assert.deepStrictEqual({
|
|
37
|
+
exportsGitUrl: /^\s*(?:export\s+)?GIT_URL=/m.test(content),
|
|
38
|
+
containsUserInfo: content.includes(userinfo),
|
|
39
|
+
}, { exportsGitUrl: false, containsUserInfo: false });
|
|
40
|
+
} finally {
|
|
41
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
20
45
|
function git(args, cwd) {
|
|
21
46
|
const res = spawnSync('git', args, { cwd, encoding: 'utf8' });
|
|
22
47
|
if (res.status !== 0) {
|