@vibelet/cli 0.1.34 → 0.1.36
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/app.json +5 -0
- package/dist/advertised-hosts.d.ts +34 -0
- package/dist/advertised-hosts.d.ts.map +1 -0
- package/dist/advertised-hosts.js +176 -0
- package/dist/advertised-hosts.js.map +1 -0
- package/dist/advertised-hosts.test.d.ts +2 -0
- package/dist/advertised-hosts.test.d.ts.map +1 -0
- package/dist/advertised-hosts.test.js +96 -0
- package/dist/advertised-hosts.test.js.map +1 -0
- package/dist/audit.d.ts +30 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +73 -0
- package/dist/audit.js.map +1 -0
- package/dist/audit.test.d.ts +2 -0
- package/dist/audit.test.d.ts.map +1 -0
- package/dist/audit.test.js +33 -0
- package/dist/audit.test.js.map +1 -0
- package/dist/auth.d.ts +6 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +27 -0
- package/dist/auth.js.map +1 -0
- package/dist/claude-hooks.d.ts +58 -0
- package/dist/claude-hooks.d.ts.map +1 -0
- package/dist/claude-hooks.js +129 -0
- package/dist/claude-hooks.js.map +1 -0
- package/dist/cli-version.d.ts +3 -0
- package/dist/cli-version.d.ts.map +1 -0
- package/dist/cli-version.js +35 -0
- package/dist/cli-version.js.map +1 -0
- package/dist/cli-version.test.d.ts +2 -0
- package/dist/cli-version.test.d.ts.map +1 -0
- package/dist/cli-version.test.js +38 -0
- package/dist/cli-version.test.js.map +1 -0
- package/dist/config.d.ts +30 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +327 -0
- package/dist/config.js.map +1 -0
- package/dist/config.test.d.ts +2 -0
- package/dist/config.test.d.ts.map +1 -0
- package/dist/config.test.js +184 -0
- package/dist/config.test.js.map +1 -0
- package/dist/dev-auth.test.d.ts +2 -0
- package/dist/dev-auth.test.d.ts.map +1 -0
- package/dist/dev-auth.test.js +154 -0
- package/dist/dev-auth.test.js.map +1 -0
- package/dist/dev-script.test.d.ts +2 -0
- package/dist/dev-script.test.d.ts.map +1 -0
- package/dist/dev-script.test.js +412 -0
- package/dist/dev-script.test.js.map +1 -0
- package/dist/drivers/claude.d.ts +34 -0
- package/dist/drivers/claude.d.ts.map +1 -0
- package/dist/drivers/claude.js +413 -0
- package/dist/drivers/claude.js.map +1 -0
- package/dist/drivers/claude.test.d.ts +2 -0
- package/dist/drivers/claude.test.d.ts.map +1 -0
- package/dist/drivers/claude.test.js +951 -0
- package/dist/drivers/claude.test.js.map +1 -0
- package/dist/drivers/codex.d.ts +38 -0
- package/dist/drivers/codex.d.ts.map +1 -0
- package/dist/drivers/codex.js +771 -0
- package/dist/drivers/codex.js.map +1 -0
- package/dist/drivers/codex.test.d.ts +2 -0
- package/dist/drivers/codex.test.d.ts.map +1 -0
- package/dist/drivers/codex.test.js +939 -0
- package/dist/drivers/codex.test.js.map +1 -0
- package/dist/drivers/types.d.ts +14 -0
- package/dist/drivers/types.d.ts.map +1 -0
- package/dist/drivers/types.js +2 -0
- package/dist/drivers/types.js.map +1 -0
- package/dist/e2e.test.d.ts +2 -0
- package/dist/e2e.test.d.ts.map +1 -0
- package/dist/e2e.test.js +111 -0
- package/dist/e2e.test.js.map +1 -0
- package/dist/identity.d.ts +10 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +66 -0
- package/dist/identity.js.map +1 -0
- package/dist/identity.test.d.ts +2 -0
- package/dist/identity.test.d.ts.map +1 -0
- package/dist/identity.test.js +25 -0
- package/dist/identity.test.js.map +1 -0
- package/dist/index-entry.test.d.ts +2 -0
- package/dist/index-entry.test.d.ts.map +1 -0
- package/dist/index-entry.test.js +272 -0
- package/dist/index-entry.test.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +707 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +31 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +75 -0
- package/dist/logger.js.map +1 -0
- package/dist/metrics.d.ts +52 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +89 -0
- package/dist/metrics.js.map +1 -0
- package/dist/pairing-store.d.ts +29 -0
- package/dist/pairing-store.d.ts.map +1 -0
- package/dist/pairing-store.js +131 -0
- package/dist/pairing-store.js.map +1 -0
- package/dist/pairing-store.test.d.ts +2 -0
- package/dist/pairing-store.test.d.ts.map +1 -0
- package/dist/pairing-store.test.js +47 -0
- package/dist/pairing-store.test.js.map +1 -0
- package/dist/paths.d.ts +16 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +18 -0
- package/dist/paths.js.map +1 -0
- package/dist/perf-compare.d.ts +13 -0
- package/dist/perf-compare.d.ts.map +1 -0
- package/dist/perf-compare.js +125 -0
- package/dist/perf-compare.js.map +1 -0
- package/dist/port-conflict.d.ts +9 -0
- package/dist/port-conflict.d.ts.map +1 -0
- package/dist/port-conflict.js +33 -0
- package/dist/port-conflict.js.map +1 -0
- package/dist/port-conflict.test.d.ts +2 -0
- package/dist/port-conflict.test.d.ts.map +1 -0
- package/dist/port-conflict.test.js +38 -0
- package/dist/port-conflict.test.js.map +1 -0
- package/dist/process-scanner.d.ts +43 -0
- package/dist/process-scanner.d.ts.map +1 -0
- package/dist/process-scanner.js +453 -0
- package/dist/process-scanner.js.map +1 -0
- package/dist/process-scanner.perf.test.d.ts +2 -0
- package/dist/process-scanner.perf.test.d.ts.map +1 -0
- package/dist/process-scanner.perf.test.js +186 -0
- package/dist/process-scanner.perf.test.js.map +1 -0
- package/dist/process-scanner.test.d.ts +2 -0
- package/dist/process-scanner.test.d.ts.map +1 -0
- package/dist/process-scanner.test.js +399 -0
- package/dist/process-scanner.test.js.map +1 -0
- package/dist/push-protocol.d.ts +15 -0
- package/dist/push-protocol.d.ts.map +1 -0
- package/dist/push-protocol.js +23 -0
- package/dist/push-protocol.js.map +1 -0
- package/dist/push-protocol.test.d.ts +2 -0
- package/dist/push-protocol.test.d.ts.map +1 -0
- package/dist/push-protocol.test.js +57 -0
- package/dist/push-protocol.test.js.map +1 -0
- package/dist/push-store.d.ts +22 -0
- package/dist/push-store.d.ts.map +1 -0
- package/dist/push-store.js +103 -0
- package/dist/push-store.js.map +1 -0
- package/dist/push-store.test.d.ts +2 -0
- package/dist/push-store.test.d.ts.map +1 -0
- package/dist/push-store.test.js +79 -0
- package/dist/push-store.test.js.map +1 -0
- package/dist/push.d.ts +65 -0
- package/dist/push.d.ts.map +1 -0
- package/dist/push.js +202 -0
- package/dist/push.js.map +1 -0
- package/dist/push.test.d.ts +2 -0
- package/dist/push.test.d.ts.map +1 -0
- package/dist/push.test.js +199 -0
- package/dist/push.test.js.map +1 -0
- package/dist/safe-stdio.d.ts +3 -0
- package/dist/safe-stdio.d.ts.map +1 -0
- package/dist/safe-stdio.js +46 -0
- package/dist/safe-stdio.js.map +1 -0
- package/dist/scanner.d.ts +30 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +859 -0
- package/dist/scanner.js.map +1 -0
- package/dist/scanner.perf.test.d.ts +2 -0
- package/dist/scanner.perf.test.d.ts.map +1 -0
- package/dist/scanner.perf.test.js +320 -0
- package/dist/scanner.perf.test.js.map +1 -0
- package/dist/scanner.test.d.ts +2 -0
- package/dist/scanner.test.d.ts.map +1 -0
- package/dist/scanner.test.js +948 -0
- package/dist/scanner.test.js.map +1 -0
- package/dist/session-inventory.d.ts +63 -0
- package/dist/session-inventory.d.ts.map +1 -0
- package/dist/session-inventory.js +525 -0
- package/dist/session-inventory.js.map +1 -0
- package/dist/session-inventory.perf.test.d.ts +2 -0
- package/dist/session-inventory.perf.test.d.ts.map +1 -0
- package/dist/session-inventory.perf.test.js +220 -0
- package/dist/session-inventory.perf.test.js.map +1 -0
- package/dist/session-inventory.test.d.ts +2 -0
- package/dist/session-inventory.test.d.ts.map +1 -0
- package/dist/session-inventory.test.js +712 -0
- package/dist/session-inventory.test.js.map +1 -0
- package/dist/session-manager.d.ts +75 -0
- package/dist/session-manager.d.ts.map +1 -0
- package/dist/session-manager.js +1515 -0
- package/dist/session-manager.js.map +1 -0
- package/dist/session-manager.test.d.ts +2 -0
- package/dist/session-manager.test.d.ts.map +1 -0
- package/dist/session-manager.test.js +2861 -0
- package/dist/session-manager.test.js.map +1 -0
- package/dist/session-store.d.ts +42 -0
- package/dist/session-store.d.ts.map +1 -0
- package/dist/session-store.js +163 -0
- package/dist/session-store.js.map +1 -0
- package/dist/session-store.test.d.ts +2 -0
- package/dist/session-store.test.d.ts.map +1 -0
- package/dist/session-store.test.js +236 -0
- package/dist/session-store.test.js.map +1 -0
- package/dist/session-title.d.ts +6 -0
- package/dist/session-title.d.ts.map +1 -0
- package/dist/session-title.js +105 -0
- package/dist/session-title.js.map +1 -0
- package/dist/session-title.perf.test.d.ts +2 -0
- package/dist/session-title.perf.test.d.ts.map +1 -0
- package/dist/session-title.perf.test.js +99 -0
- package/dist/session-title.perf.test.js.map +1 -0
- package/dist/session-title.test.d.ts +2 -0
- package/dist/session-title.test.d.ts.map +1 -0
- package/dist/session-title.test.js +199 -0
- package/dist/session-title.test.js.map +1 -0
- package/dist/shutdown-endpoint.test.d.ts +2 -0
- package/dist/shutdown-endpoint.test.d.ts.map +1 -0
- package/dist/shutdown-endpoint.test.js +93 -0
- package/dist/shutdown-endpoint.test.js.map +1 -0
- package/dist/storage-housekeeping.d.ts +28 -0
- package/dist/storage-housekeeping.d.ts.map +1 -0
- package/dist/storage-housekeeping.js +76 -0
- package/dist/storage-housekeeping.js.map +1 -0
- package/dist/storage-housekeeping.test.d.ts +2 -0
- package/dist/storage-housekeeping.test.d.ts.map +1 -0
- package/dist/storage-housekeeping.test.js +65 -0
- package/dist/storage-housekeeping.test.js.map +1 -0
- package/dist/test-daemon-harness.d.ts +31 -0
- package/dist/test-daemon-harness.d.ts.map +1 -0
- package/dist/test-daemon-harness.js +337 -0
- package/dist/test-daemon-harness.js.map +1 -0
- package/dist/token-auth.test.d.ts +2 -0
- package/dist/token-auth.test.d.ts.map +1 -0
- package/dist/token-auth.test.js +52 -0
- package/dist/token-auth.test.js.map +1 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +40 -0
- package/dist/utils.js.map +1 -0
- package/dist/utils.test.d.ts +2 -0
- package/dist/utils.test.d.ts.map +1 -0
- package/dist/utils.test.js +54 -0
- package/dist/utils.test.js.map +1 -0
- package/dist/ws-data.d.ts +4 -0
- package/dist/ws-data.d.ts.map +1 -0
- package/dist/ws-data.js +20 -0
- package/dist/ws-data.js.map +1 -0
- package/dist/ws-data.test.d.ts +2 -0
- package/dist/ws-data.test.d.ts.map +1 -0
- package/dist/ws-data.test.js +17 -0
- package/dist/ws-data.test.js.map +1 -0
- package/package.json +24 -27
- package/perf-reporter.mjs +138 -0
- package/scripts/build-release.mjs +41 -0
- package/scripts/dev.mjs +537 -0
- package/src/advertised-hosts.test.ts +125 -0
- package/src/advertised-hosts.ts +225 -0
- package/src/audit.test.ts +38 -0
- package/src/audit.ts +117 -0
- package/src/auth.ts +31 -0
- package/src/claude-hooks.ts +195 -0
- package/src/cli-version.test.ts +36 -0
- package/src/cli-version.ts +46 -0
- package/src/config.test.ts +254 -0
- package/src/config.ts +324 -0
- package/src/dev-auth.test.ts +183 -0
- package/src/dev-script.test.ts +511 -0
- package/src/drivers/claude.test.ts +1186 -0
- package/src/drivers/claude.ts +443 -0
- package/src/drivers/codex.test.ts +1096 -0
- package/src/drivers/codex.ts +879 -0
- package/src/drivers/types.ts +15 -0
- package/src/e2e.test.ts +139 -0
- package/src/identity.test.ts +26 -0
- package/src/identity.ts +82 -0
- package/src/index-entry.test.ts +336 -0
- package/src/index.ts +781 -0
- package/src/logger.ts +112 -0
- package/src/metrics.ts +117 -0
- package/src/pairing-store.test.ts +53 -0
- package/src/pairing-store.ts +154 -0
- package/src/paths.ts +19 -0
- package/src/perf-compare.ts +164 -0
- package/src/port-conflict.test.ts +45 -0
- package/src/port-conflict.ts +44 -0
- package/src/process-scanner.perf.test.ts +222 -0
- package/src/process-scanner.test.ts +575 -0
- package/src/process-scanner.ts +514 -0
- package/src/push-protocol.test.ts +74 -0
- package/src/push-protocol.ts +36 -0
- package/src/push-store.test.ts +89 -0
- package/src/push-store.ts +126 -0
- package/src/push.test.ts +234 -0
- package/src/push.ts +318 -0
- package/src/safe-stdio.ts +51 -0
- package/src/scanner.perf.test.ts +359 -0
- package/src/scanner.test.ts +1045 -0
- package/src/scanner.ts +924 -0
- package/src/session-inventory.perf.test.ts +250 -0
- package/src/session-inventory.test.ts +1002 -0
- package/src/session-inventory.ts +721 -0
- package/src/session-manager.test.ts +3430 -0
- package/src/session-manager.ts +1775 -0
- package/src/session-store.test.ts +276 -0
- package/src/session-store.ts +202 -0
- package/src/session-title.perf.test.ts +118 -0
- package/src/session-title.test.ts +286 -0
- package/src/session-title.ts +108 -0
- package/src/shutdown-endpoint.test.ts +95 -0
- package/src/storage-housekeeping.test.ts +78 -0
- package/src/storage-housekeeping.ts +111 -0
- package/src/test-daemon-harness.ts +410 -0
- package/src/token-auth.test.ts +67 -0
- package/src/utils.test.ts +65 -0
- package/src/utils.ts +47 -0
- package/src/ws-data.test.ts +20 -0
- package/src/ws-data.ts +26 -0
- package/tsconfig.json +12 -0
- package/README.md +0 -80
- package/bin/cloudflared-quick-tunnel.mjs +0 -11
- package/bin/cloudflared-resolver.mjs +0 -68
- package/bin/vibelet-runtime-policy.mjs +0 -36
- package/bin/vibelet.cjs +0 -12
- package/bin/vibelet.mjs +0 -1019
- package/dist/index.cjs +0 -123
|
@@ -0,0 +1,951 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { writeFile, rm, chmod, mkdtemp, readFile } from 'fs/promises';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { tmpdir } from 'os';
|
|
6
|
+
import { ClaudeDriver, isClaudeSyntheticApprovalRequestId } from './claude.js';
|
|
7
|
+
import { config } from '../config.js';
|
|
8
|
+
// Helper: access private members via `any` cast
|
|
9
|
+
function getDriver() {
|
|
10
|
+
const driver = new ClaudeDriver();
|
|
11
|
+
const d = driver;
|
|
12
|
+
return { driver, d };
|
|
13
|
+
}
|
|
14
|
+
async function withMockClaudePath(claudePath, fn) {
|
|
15
|
+
const originalDescriptor = Object.getOwnPropertyDescriptor(config, 'claudePath');
|
|
16
|
+
Object.defineProperty(config, 'claudePath', {
|
|
17
|
+
configurable: true,
|
|
18
|
+
get() {
|
|
19
|
+
return claudePath;
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
try {
|
|
23
|
+
await fn();
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
if (originalDescriptor) {
|
|
27
|
+
Object.defineProperty(config, 'claudePath', originalDescriptor);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function withMockSanitizedEnv(buildEnv, fn) {
|
|
32
|
+
const originalBuildSanitizedEnv = config.buildSanitizedEnv;
|
|
33
|
+
config.buildSanitizedEnv = buildEnv;
|
|
34
|
+
try {
|
|
35
|
+
await fn();
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
config.buildSanitizedEnv = originalBuildSanitizedEnv;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
let sendPromptTestQueue = Promise.resolve();
|
|
42
|
+
async function runSerializedSendPromptTest(fn) {
|
|
43
|
+
const previous = sendPromptTestQueue;
|
|
44
|
+
let release;
|
|
45
|
+
sendPromptTestQueue = new Promise((resolve) => {
|
|
46
|
+
release = resolve;
|
|
47
|
+
});
|
|
48
|
+
await previous;
|
|
49
|
+
try {
|
|
50
|
+
await fn();
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
release();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function testSendPrompt(name, fn) {
|
|
57
|
+
test(name, async () => {
|
|
58
|
+
await runSerializedSendPromptTest(fn);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
// ─── system init ─────────────────────────────────────────────────────
|
|
62
|
+
test('handleRaw updates sessionId on system init', () => {
|
|
63
|
+
const { d } = getDriver();
|
|
64
|
+
d.sessionId = 'old-id';
|
|
65
|
+
d.handleRaw({ type: 'system', subtype: 'init', session_id: 'new-session-123' });
|
|
66
|
+
assert.equal(d.sessionId, 'new-session-123');
|
|
67
|
+
});
|
|
68
|
+
test('handleRaw does not update sessionId when new id matches current', () => {
|
|
69
|
+
const { driver, d } = getDriver();
|
|
70
|
+
d.sessionId = 'same-id';
|
|
71
|
+
const messages = [];
|
|
72
|
+
driver.onMessage((msg) => messages.push(msg));
|
|
73
|
+
d.handleRaw({ type: 'system', subtype: 'init', session_id: 'same-id' });
|
|
74
|
+
assert.equal(d.sessionId, 'same-id');
|
|
75
|
+
assert.equal(messages.length, 0);
|
|
76
|
+
});
|
|
77
|
+
test('handleRaw does not update sessionId when session_id is empty', () => {
|
|
78
|
+
const { d } = getDriver();
|
|
79
|
+
d.sessionId = 'keep-me';
|
|
80
|
+
d.handleRaw({ type: 'system', subtype: 'init', session_id: '' });
|
|
81
|
+
assert.equal(d.sessionId, 'keep-me');
|
|
82
|
+
});
|
|
83
|
+
test('handleRaw system init does not call handler', () => {
|
|
84
|
+
const { driver, d } = getDriver();
|
|
85
|
+
const messages = [];
|
|
86
|
+
driver.onMessage((msg) => messages.push(msg));
|
|
87
|
+
d.handleRaw({ type: 'system', subtype: 'init', session_id: 'abc' });
|
|
88
|
+
assert.equal(messages.length, 0);
|
|
89
|
+
});
|
|
90
|
+
// ─── assistant with tool_use ─────────────────────────────────────────
|
|
91
|
+
test('handleRaw emits tool.call for assistant message with tool_use blocks', () => {
|
|
92
|
+
const { driver, d } = getDriver();
|
|
93
|
+
d.sessionId = 'sess-1';
|
|
94
|
+
const messages = [];
|
|
95
|
+
driver.onMessage((msg) => messages.push(msg));
|
|
96
|
+
d.handleRaw({
|
|
97
|
+
type: 'assistant',
|
|
98
|
+
message: {
|
|
99
|
+
content: [
|
|
100
|
+
{ type: 'tool_use', name: 'bash', input: { command: 'ls' }, id: 'tu-1' },
|
|
101
|
+
{ type: 'tool_use', name: 'write', input: { path: '/tmp/x' }, id: 'tu-2' },
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
assert.equal(messages.length, 2);
|
|
106
|
+
assert.deepEqual(messages[0], {
|
|
107
|
+
type: 'tool.call',
|
|
108
|
+
sessionId: 'sess-1',
|
|
109
|
+
toolName: 'bash',
|
|
110
|
+
input: { command: 'ls' },
|
|
111
|
+
toolCallId: 'tu-1',
|
|
112
|
+
});
|
|
113
|
+
assert.deepEqual(messages[1], {
|
|
114
|
+
type: 'tool.call',
|
|
115
|
+
sessionId: 'sess-1',
|
|
116
|
+
toolName: 'write',
|
|
117
|
+
input: { path: '/tmp/x' },
|
|
118
|
+
toolCallId: 'tu-2',
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
test('handleRaw uses empty object when tool_use has no input', () => {
|
|
122
|
+
const { driver, d } = getDriver();
|
|
123
|
+
d.sessionId = 'sess-1';
|
|
124
|
+
const messages = [];
|
|
125
|
+
driver.onMessage((msg) => messages.push(msg));
|
|
126
|
+
d.handleRaw({
|
|
127
|
+
type: 'assistant',
|
|
128
|
+
message: {
|
|
129
|
+
content: [{ type: 'tool_use', name: 'read', id: 'tu-3' }],
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
assert.equal(messages.length, 1);
|
|
133
|
+
assert.deepEqual(messages[0].input, {});
|
|
134
|
+
});
|
|
135
|
+
test('handleRaw ignores assistant message without content array', () => {
|
|
136
|
+
const { driver, d } = getDriver();
|
|
137
|
+
d.sessionId = 'sess-1';
|
|
138
|
+
const messages = [];
|
|
139
|
+
driver.onMessage((msg) => messages.push(msg));
|
|
140
|
+
d.handleRaw({ type: 'assistant', message: { content: 'just text' } });
|
|
141
|
+
assert.equal(messages.length, 0);
|
|
142
|
+
});
|
|
143
|
+
test('handleRaw ignores assistant message without message field', () => {
|
|
144
|
+
const { driver, d } = getDriver();
|
|
145
|
+
d.sessionId = 'sess-1';
|
|
146
|
+
const messages = [];
|
|
147
|
+
driver.onMessage((msg) => messages.push(msg));
|
|
148
|
+
d.handleRaw({ type: 'assistant' });
|
|
149
|
+
assert.equal(messages.length, 0);
|
|
150
|
+
});
|
|
151
|
+
test('handleRaw skips non-tool_use blocks in assistant content', () => {
|
|
152
|
+
const { driver, d } = getDriver();
|
|
153
|
+
d.sessionId = 'sess-1';
|
|
154
|
+
const messages = [];
|
|
155
|
+
driver.onMessage((msg) => messages.push(msg));
|
|
156
|
+
d.handleRaw({
|
|
157
|
+
type: 'assistant',
|
|
158
|
+
message: {
|
|
159
|
+
content: [
|
|
160
|
+
{ type: 'text', text: 'I will run a command' },
|
|
161
|
+
{ type: 'tool_use', name: 'bash', input: { command: 'echo hi' }, id: 'tu-4' },
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
assert.equal(messages.length, 1);
|
|
166
|
+
assert.equal(messages[0].toolName, 'bash');
|
|
167
|
+
});
|
|
168
|
+
// ─── user with tool_result ───────────────────────────────────────────
|
|
169
|
+
test('handleRaw emits tool.result for user message with string content', () => {
|
|
170
|
+
const { driver, d } = getDriver();
|
|
171
|
+
d.sessionId = 'sess-1';
|
|
172
|
+
const messages = [];
|
|
173
|
+
driver.onMessage((msg) => messages.push(msg));
|
|
174
|
+
d.handleRaw({
|
|
175
|
+
type: 'user',
|
|
176
|
+
message: {
|
|
177
|
+
content: [
|
|
178
|
+
{ type: 'tool_result', tool_use_id: 'tu-1', content: 'command output here' },
|
|
179
|
+
],
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
assert.equal(messages.length, 1);
|
|
183
|
+
assert.deepEqual(messages[0], {
|
|
184
|
+
type: 'tool.result',
|
|
185
|
+
sessionId: 'sess-1',
|
|
186
|
+
toolCallId: 'tu-1',
|
|
187
|
+
output: 'command output here',
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
test('handleRaw emits tool.result with JSON-stringified object content', () => {
|
|
191
|
+
const { driver, d } = getDriver();
|
|
192
|
+
d.sessionId = 'sess-1';
|
|
193
|
+
const messages = [];
|
|
194
|
+
driver.onMessage((msg) => messages.push(msg));
|
|
195
|
+
const objContent = { status: 'ok', data: [1, 2, 3] };
|
|
196
|
+
d.handleRaw({
|
|
197
|
+
type: 'user',
|
|
198
|
+
message: {
|
|
199
|
+
content: [
|
|
200
|
+
{ type: 'tool_result', tool_use_id: 'tu-2', content: objContent },
|
|
201
|
+
],
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
assert.equal(messages.length, 1);
|
|
205
|
+
assert.equal(messages[0].output, JSON.stringify(objContent));
|
|
206
|
+
});
|
|
207
|
+
test('handleRaw skips tool_result blocks that only contain tool references', () => {
|
|
208
|
+
const { driver, d } = getDriver();
|
|
209
|
+
d.sessionId = 'sess-1';
|
|
210
|
+
const messages = [];
|
|
211
|
+
driver.onMessage((msg) => messages.push(msg));
|
|
212
|
+
d.handleRaw({
|
|
213
|
+
type: 'user',
|
|
214
|
+
message: {
|
|
215
|
+
content: [
|
|
216
|
+
{
|
|
217
|
+
type: 'tool_result',
|
|
218
|
+
tool_use_id: 'tu-ref',
|
|
219
|
+
content: [{ type: 'tool_reference', tool_name: 'WebSearch' }],
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
assert.equal(messages.length, 0);
|
|
225
|
+
});
|
|
226
|
+
test('handleRaw ignores user message without content array', () => {
|
|
227
|
+
const { driver, d } = getDriver();
|
|
228
|
+
d.sessionId = 'sess-1';
|
|
229
|
+
const messages = [];
|
|
230
|
+
driver.onMessage((msg) => messages.push(msg));
|
|
231
|
+
d.handleRaw({ type: 'user', message: { content: 'raw string' } });
|
|
232
|
+
assert.equal(messages.length, 0);
|
|
233
|
+
});
|
|
234
|
+
test('handleRaw skips non-tool_result blocks in user content', () => {
|
|
235
|
+
const { driver, d } = getDriver();
|
|
236
|
+
d.sessionId = 'sess-1';
|
|
237
|
+
const messages = [];
|
|
238
|
+
driver.onMessage((msg) => messages.push(msg));
|
|
239
|
+
d.handleRaw({
|
|
240
|
+
type: 'user',
|
|
241
|
+
message: {
|
|
242
|
+
content: [
|
|
243
|
+
{ type: 'text', text: 'some user text' },
|
|
244
|
+
],
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
assert.equal(messages.length, 0);
|
|
248
|
+
});
|
|
249
|
+
// ─── control_request ─────────────────────────────────────────────────
|
|
250
|
+
test('handleRaw emits approval.request for control_request can_use_tool', () => {
|
|
251
|
+
const { driver, d } = getDriver();
|
|
252
|
+
d.sessionId = 'sess-1';
|
|
253
|
+
const messages = [];
|
|
254
|
+
driver.onMessage((msg) => messages.push(msg));
|
|
255
|
+
d.handleRaw({
|
|
256
|
+
type: 'control_request',
|
|
257
|
+
request_id: 'req-42',
|
|
258
|
+
request: {
|
|
259
|
+
subtype: 'can_use_tool',
|
|
260
|
+
tool_name: 'bash',
|
|
261
|
+
input: { command: 'rm -rf /' },
|
|
262
|
+
description: 'Dangerous command',
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
assert.equal(messages.length, 1);
|
|
266
|
+
assert.deepEqual(messages[0], {
|
|
267
|
+
type: 'approval.request',
|
|
268
|
+
sessionId: 'sess-1',
|
|
269
|
+
requestId: 'req-42',
|
|
270
|
+
toolName: 'bash',
|
|
271
|
+
input: { command: 'rm -rf /' },
|
|
272
|
+
description: 'Dangerous command',
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
test('handleRaw approval.request falls back to title when description is missing', () => {
|
|
276
|
+
const { driver, d } = getDriver();
|
|
277
|
+
d.sessionId = 'sess-1';
|
|
278
|
+
const messages = [];
|
|
279
|
+
driver.onMessage((msg) => messages.push(msg));
|
|
280
|
+
d.handleRaw({
|
|
281
|
+
type: 'control_request',
|
|
282
|
+
request_id: 'req-43',
|
|
283
|
+
request: {
|
|
284
|
+
subtype: 'can_use_tool',
|
|
285
|
+
tool_name: 'write',
|
|
286
|
+
input: {},
|
|
287
|
+
title: 'Write file',
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
assert.equal(messages.length, 1);
|
|
291
|
+
assert.equal(messages[0].description, 'Write file');
|
|
292
|
+
});
|
|
293
|
+
test('handleRaw approval.request uses defaults for missing fields', () => {
|
|
294
|
+
const { driver, d } = getDriver();
|
|
295
|
+
d.sessionId = 'sess-1';
|
|
296
|
+
const messages = [];
|
|
297
|
+
driver.onMessage((msg) => messages.push(msg));
|
|
298
|
+
d.handleRaw({
|
|
299
|
+
type: 'control_request',
|
|
300
|
+
request_id: 'req-44',
|
|
301
|
+
request: { subtype: 'can_use_tool' },
|
|
302
|
+
});
|
|
303
|
+
assert.equal(messages.length, 1);
|
|
304
|
+
const msg = messages[0];
|
|
305
|
+
assert.equal(msg.toolName, 'unknown');
|
|
306
|
+
assert.deepEqual(msg.input, {});
|
|
307
|
+
assert.equal(msg.description, '');
|
|
308
|
+
});
|
|
309
|
+
test('handleRaw ignores control_request with non-can_use_tool subtype', () => {
|
|
310
|
+
const { driver, d } = getDriver();
|
|
311
|
+
d.sessionId = 'sess-1';
|
|
312
|
+
const messages = [];
|
|
313
|
+
driver.onMessage((msg) => messages.push(msg));
|
|
314
|
+
d.handleRaw({
|
|
315
|
+
type: 'control_request',
|
|
316
|
+
request_id: 'req-45',
|
|
317
|
+
request: { subtype: 'other_thing' },
|
|
318
|
+
});
|
|
319
|
+
assert.equal(messages.length, 0);
|
|
320
|
+
});
|
|
321
|
+
// ─── stream_event text_delta ─────────────────────────────────────────
|
|
322
|
+
test('handleRaw emits text.delta for stream_event with text_delta', () => {
|
|
323
|
+
const { driver, d } = getDriver();
|
|
324
|
+
d.sessionId = 'sess-1';
|
|
325
|
+
const messages = [];
|
|
326
|
+
driver.onMessage((msg) => messages.push(msg));
|
|
327
|
+
d.handleRaw({
|
|
328
|
+
type: 'stream_event',
|
|
329
|
+
event: {
|
|
330
|
+
type: 'content_block_delta',
|
|
331
|
+
delta: { type: 'text_delta', text: 'Hello, world!' },
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
assert.equal(messages.length, 1);
|
|
335
|
+
assert.deepEqual(messages[0], {
|
|
336
|
+
type: 'text.delta',
|
|
337
|
+
sessionId: 'sess-1',
|
|
338
|
+
content: 'Hello, world!',
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
test('handleRaw ignores stream_event with non-text_delta delta type', () => {
|
|
342
|
+
const { driver, d } = getDriver();
|
|
343
|
+
d.sessionId = 'sess-1';
|
|
344
|
+
const messages = [];
|
|
345
|
+
driver.onMessage((msg) => messages.push(msg));
|
|
346
|
+
d.handleRaw({
|
|
347
|
+
type: 'stream_event',
|
|
348
|
+
event: {
|
|
349
|
+
type: 'content_block_delta',
|
|
350
|
+
delta: { type: 'input_json_delta', partial_json: '{"x":1}' },
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
assert.equal(messages.length, 0);
|
|
354
|
+
});
|
|
355
|
+
test('handleRaw ignores stream_event with non-content_block_delta event type', () => {
|
|
356
|
+
const { driver, d } = getDriver();
|
|
357
|
+
d.sessionId = 'sess-1';
|
|
358
|
+
const messages = [];
|
|
359
|
+
driver.onMessage((msg) => messages.push(msg));
|
|
360
|
+
d.handleRaw({
|
|
361
|
+
type: 'stream_event',
|
|
362
|
+
event: { type: 'content_block_start' },
|
|
363
|
+
});
|
|
364
|
+
assert.equal(messages.length, 0);
|
|
365
|
+
});
|
|
366
|
+
test('handleRaw ignores stream_event with empty text', () => {
|
|
367
|
+
const { driver, d } = getDriver();
|
|
368
|
+
d.sessionId = 'sess-1';
|
|
369
|
+
const messages = [];
|
|
370
|
+
driver.onMessage((msg) => messages.push(msg));
|
|
371
|
+
d.handleRaw({
|
|
372
|
+
type: 'stream_event',
|
|
373
|
+
event: {
|
|
374
|
+
type: 'content_block_delta',
|
|
375
|
+
delta: { type: 'text_delta', text: '' },
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
assert.equal(messages.length, 0);
|
|
379
|
+
});
|
|
380
|
+
// ─── result success ──────────────────────────────────────────────────
|
|
381
|
+
test('handleRaw emits session.done on successful result with cost and usage', () => {
|
|
382
|
+
const { driver, d } = getDriver();
|
|
383
|
+
d.sessionId = 'sess-1';
|
|
384
|
+
const messages = [];
|
|
385
|
+
driver.onMessage((msg) => messages.push(msg));
|
|
386
|
+
d.handleRaw({
|
|
387
|
+
type: 'result',
|
|
388
|
+
result: 'Task completed successfully',
|
|
389
|
+
total_cost_usd: 0.042,
|
|
390
|
+
usage: { input_tokens: 1000, output_tokens: 500 },
|
|
391
|
+
});
|
|
392
|
+
assert.equal(messages.length, 1);
|
|
393
|
+
assert.deepEqual(messages[0], {
|
|
394
|
+
type: 'session.done',
|
|
395
|
+
sessionId: 'sess-1',
|
|
396
|
+
cost: 0.042,
|
|
397
|
+
usage: { inputTokens: 1000, outputTokens: 500 },
|
|
398
|
+
});
|
|
399
|
+
assert.equal(d.sawFinalResult, true);
|
|
400
|
+
});
|
|
401
|
+
test('handleRaw emits session.done without usage when not provided', () => {
|
|
402
|
+
const { driver, d } = getDriver();
|
|
403
|
+
d.sessionId = 'sess-1';
|
|
404
|
+
const messages = [];
|
|
405
|
+
driver.onMessage((msg) => messages.push(msg));
|
|
406
|
+
d.handleRaw({
|
|
407
|
+
type: 'result',
|
|
408
|
+
result: 'done',
|
|
409
|
+
total_cost_usd: 0.01,
|
|
410
|
+
});
|
|
411
|
+
assert.equal(messages.length, 1);
|
|
412
|
+
const msg = messages[0];
|
|
413
|
+
assert.equal(msg.type, 'session.done');
|
|
414
|
+
assert.equal(msg.cost, 0.01);
|
|
415
|
+
assert.equal(msg.usage, undefined);
|
|
416
|
+
});
|
|
417
|
+
test('handleRaw emits synthetic approval.request when result includes permission_denials', () => {
|
|
418
|
+
const { driver, d } = getDriver();
|
|
419
|
+
d.sessionId = 'sess-1';
|
|
420
|
+
const messages = [];
|
|
421
|
+
driver.onMessage((msg) => messages.push(msg));
|
|
422
|
+
d.handleRaw({
|
|
423
|
+
type: 'user',
|
|
424
|
+
message: {
|
|
425
|
+
content: [
|
|
426
|
+
{
|
|
427
|
+
type: 'tool_result',
|
|
428
|
+
tool_use_id: 'tu-1',
|
|
429
|
+
is_error: true,
|
|
430
|
+
content: 'Claude requested permissions to write to /tmp/probe.txt, but you haven\'t granted it yet.',
|
|
431
|
+
},
|
|
432
|
+
],
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
d.handleRaw({
|
|
436
|
+
type: 'result',
|
|
437
|
+
result: 'Waiting for approval.',
|
|
438
|
+
permission_denials: [
|
|
439
|
+
{
|
|
440
|
+
tool_name: 'Write',
|
|
441
|
+
tool_use_id: 'tu-1',
|
|
442
|
+
tool_input: { file_path: '/tmp/probe.txt', content: 'hi' },
|
|
443
|
+
},
|
|
444
|
+
],
|
|
445
|
+
});
|
|
446
|
+
assert.equal(messages.length, 2);
|
|
447
|
+
assert.equal(messages[0]?.type, 'approval.request');
|
|
448
|
+
assert.ok(isClaudeSyntheticApprovalRequestId(messages[0].requestId));
|
|
449
|
+
assert.deepEqual(messages[0], {
|
|
450
|
+
type: 'approval.request',
|
|
451
|
+
sessionId: 'sess-1',
|
|
452
|
+
requestId: messages[0].requestId,
|
|
453
|
+
toolName: 'Write',
|
|
454
|
+
input: { file_path: '/tmp/probe.txt', content: 'hi' },
|
|
455
|
+
description: 'Claude requested permissions to write to /tmp/probe.txt, but you haven\'t granted it yet.',
|
|
456
|
+
});
|
|
457
|
+
assert.deepEqual(messages[1], {
|
|
458
|
+
type: 'session.done',
|
|
459
|
+
sessionId: 'sess-1',
|
|
460
|
+
cost: undefined,
|
|
461
|
+
usage: undefined,
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
// ─── result error ────────────────────────────────────────────────────
|
|
465
|
+
test('handleRaw preserves non-limit result error text', () => {
|
|
466
|
+
const { driver, d } = getDriver();
|
|
467
|
+
d.sessionId = 'sess-1';
|
|
468
|
+
const messages = [];
|
|
469
|
+
driver.onMessage((msg) => messages.push(msg));
|
|
470
|
+
d.handleRaw({
|
|
471
|
+
type: 'result',
|
|
472
|
+
is_error: true,
|
|
473
|
+
result: 'Authentication failed',
|
|
474
|
+
});
|
|
475
|
+
assert.equal(messages.length, 1);
|
|
476
|
+
assert.deepEqual(messages[0], {
|
|
477
|
+
type: 'error',
|
|
478
|
+
sessionId: 'sess-1',
|
|
479
|
+
message: 'Authentication failed',
|
|
480
|
+
});
|
|
481
|
+
assert.equal(d.sawFinalResult, true);
|
|
482
|
+
});
|
|
483
|
+
test('handleRaw normalizes usage limit result errors', () => {
|
|
484
|
+
const { driver, d } = getDriver();
|
|
485
|
+
d.sessionId = 'sess-1';
|
|
486
|
+
const messages = [];
|
|
487
|
+
driver.onMessage((msg) => messages.push(msg));
|
|
488
|
+
d.handleRaw({
|
|
489
|
+
type: 'result',
|
|
490
|
+
is_error: true,
|
|
491
|
+
result: 'Rate limit exceeded',
|
|
492
|
+
});
|
|
493
|
+
assert.equal(messages.length, 1);
|
|
494
|
+
assert.deepEqual(messages[0], {
|
|
495
|
+
type: 'error',
|
|
496
|
+
sessionId: 'sess-1',
|
|
497
|
+
message: 'Claude usage limit reached. Rate limit exceeded',
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
test('handleRaw emits error with default message when result text is empty', () => {
|
|
501
|
+
const { driver, d } = getDriver();
|
|
502
|
+
d.sessionId = 'sess-1';
|
|
503
|
+
const messages = [];
|
|
504
|
+
driver.onMessage((msg) => messages.push(msg));
|
|
505
|
+
d.handleRaw({
|
|
506
|
+
type: 'result',
|
|
507
|
+
is_error: true,
|
|
508
|
+
result: '',
|
|
509
|
+
});
|
|
510
|
+
assert.equal(messages.length, 1);
|
|
511
|
+
assert.equal(messages[0].message, 'Claude returned an error result.');
|
|
512
|
+
});
|
|
513
|
+
test('handleRaw emits error with default message when result is not a string', () => {
|
|
514
|
+
const { driver, d } = getDriver();
|
|
515
|
+
d.sessionId = 'sess-1';
|
|
516
|
+
const messages = [];
|
|
517
|
+
driver.onMessage((msg) => messages.push(msg));
|
|
518
|
+
d.handleRaw({
|
|
519
|
+
type: 'result',
|
|
520
|
+
is_error: true,
|
|
521
|
+
result: { some: 'object' },
|
|
522
|
+
});
|
|
523
|
+
assert.equal(messages.length, 1);
|
|
524
|
+
assert.equal(messages[0].message, 'Claude returned an error result.');
|
|
525
|
+
});
|
|
526
|
+
// ─── No handler ──────────────────────────────────────────────────────
|
|
527
|
+
test('handleRaw does not throw when handler is null', () => {
|
|
528
|
+
const { d } = getDriver();
|
|
529
|
+
// Do NOT set handler
|
|
530
|
+
assert.doesNotThrow(() => {
|
|
531
|
+
d.handleRaw({ type: 'assistant', message: { content: [{ type: 'tool_use', name: 'bash', input: {}, id: 'x' }] } });
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
test('handleRaw does not throw for stream_event when handler is null', () => {
|
|
535
|
+
const { d } = getDriver();
|
|
536
|
+
assert.doesNotThrow(() => {
|
|
537
|
+
d.handleRaw({
|
|
538
|
+
type: 'stream_event',
|
|
539
|
+
event: { type: 'content_block_delta', delta: { type: 'text_delta', text: 'hi' } },
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
test('handleRaw does not throw for result when handler is null', () => {
|
|
544
|
+
const { d } = getDriver();
|
|
545
|
+
assert.doesNotThrow(() => {
|
|
546
|
+
d.handleRaw({ type: 'result', result: 'ok' });
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
test('handleRaw system init still updates sessionId even without handler', () => {
|
|
550
|
+
const { d } = getDriver();
|
|
551
|
+
d.sessionId = 'old';
|
|
552
|
+
d.handleRaw({ type: 'system', subtype: 'init', session_id: 'new-id' });
|
|
553
|
+
assert.equal(d.sessionId, 'new-id');
|
|
554
|
+
});
|
|
555
|
+
// ─── Unknown type ────────────────────────────────────────────────────
|
|
556
|
+
test('handleRaw does not emit for unknown type', () => {
|
|
557
|
+
const { driver, d } = getDriver();
|
|
558
|
+
d.sessionId = 'sess-1';
|
|
559
|
+
const messages = [];
|
|
560
|
+
driver.onMessage((msg) => messages.push(msg));
|
|
561
|
+
d.handleRaw({ type: 'unknown_event', data: {} });
|
|
562
|
+
assert.equal(messages.length, 0);
|
|
563
|
+
});
|
|
564
|
+
// ─── start() ─────────────────────────────────────────────────────────
|
|
565
|
+
test('start() returns resumeSessionId when provided', async () => {
|
|
566
|
+
const { driver } = getDriver();
|
|
567
|
+
const id = await driver.start('/tmp', 'resume-abc');
|
|
568
|
+
assert.equal(id, 'resume-abc');
|
|
569
|
+
});
|
|
570
|
+
test('start() generates pending_ sessionId when no resume id', async () => {
|
|
571
|
+
const { driver } = getDriver();
|
|
572
|
+
const id = await driver.start('/tmp');
|
|
573
|
+
assert.ok(id.startsWith('pending_'));
|
|
574
|
+
});
|
|
575
|
+
test('start() stores cwd and approvalMode', async () => {
|
|
576
|
+
const { driver, d } = getDriver();
|
|
577
|
+
await driver.start('/home/user', undefined, 'autoApprove');
|
|
578
|
+
assert.equal(d.cwd, '/home/user');
|
|
579
|
+
assert.equal(d.approvalMode, 'autoApprove');
|
|
580
|
+
});
|
|
581
|
+
// ─── stop() ──────────────────────────────────────────────────────────
|
|
582
|
+
test('stop() does nothing when proc is null', () => {
|
|
583
|
+
const { driver } = getDriver();
|
|
584
|
+
assert.doesNotThrow(() => driver.stop());
|
|
585
|
+
});
|
|
586
|
+
// ─── interrupt() ─────────────────────────────────────────────────────
|
|
587
|
+
test('interrupt() does nothing when proc is null', () => {
|
|
588
|
+
const { driver } = getDriver();
|
|
589
|
+
assert.doesNotThrow(() => driver.interrupt());
|
|
590
|
+
});
|
|
591
|
+
// ─── respondApproval() ───────────────────────────────────────────────
|
|
592
|
+
test('respondApproval() does not throw when proc is null', () => {
|
|
593
|
+
const { driver } = getDriver();
|
|
594
|
+
assert.doesNotThrow(() => driver.respondApproval('req-1', true));
|
|
595
|
+
assert.doesNotThrow(() => driver.respondApproval('req-2', false));
|
|
596
|
+
});
|
|
597
|
+
test('respondApproval() writes control_response JSON to stdin', async () => {
|
|
598
|
+
const { driver, d } = getDriver();
|
|
599
|
+
await driver.start('/tmp');
|
|
600
|
+
let written = '';
|
|
601
|
+
d.proc = {
|
|
602
|
+
stdin: {
|
|
603
|
+
writable: true,
|
|
604
|
+
write(chunk) {
|
|
605
|
+
written += chunk;
|
|
606
|
+
return true;
|
|
607
|
+
},
|
|
608
|
+
},
|
|
609
|
+
};
|
|
610
|
+
driver.respondApproval('req-42', true);
|
|
611
|
+
const parsed = JSON.parse(written.trim());
|
|
612
|
+
assert.equal(parsed.type, 'control_response');
|
|
613
|
+
assert.equal(parsed.request_id, 'req-42');
|
|
614
|
+
assert.equal(parsed.permission_granted, true);
|
|
615
|
+
});
|
|
616
|
+
test('respondApproval() sends permission_granted=false for denied requests', async () => {
|
|
617
|
+
const { driver, d } = getDriver();
|
|
618
|
+
await driver.start('/tmp');
|
|
619
|
+
let written = '';
|
|
620
|
+
d.proc = {
|
|
621
|
+
stdin: {
|
|
622
|
+
writable: true,
|
|
623
|
+
write(chunk) {
|
|
624
|
+
written += chunk;
|
|
625
|
+
return true;
|
|
626
|
+
},
|
|
627
|
+
},
|
|
628
|
+
};
|
|
629
|
+
driver.respondApproval('req-99', false);
|
|
630
|
+
const parsed = JSON.parse(written.trim());
|
|
631
|
+
assert.equal(parsed.type, 'control_response');
|
|
632
|
+
assert.equal(parsed.request_id, 'req-99');
|
|
633
|
+
assert.equal(parsed.permission_granted, false);
|
|
634
|
+
});
|
|
635
|
+
// ─── onMessage() ─────────────────────────────────────────────────────
|
|
636
|
+
test('onMessage() sets the handler', () => {
|
|
637
|
+
const { driver, d } = getDriver();
|
|
638
|
+
assert.equal(d.handler, null);
|
|
639
|
+
const handler = () => { };
|
|
640
|
+
driver.onMessage(handler);
|
|
641
|
+
assert.equal(d.handler, handler);
|
|
642
|
+
});
|
|
643
|
+
// ─── sendPrompt() integration tests ─────────────────────────────────
|
|
644
|
+
/** Helper: create a temp dir with a fake claude script and point config.claudePath at it. */
|
|
645
|
+
async function withFakeClaudeScript(scriptBody, fn) {
|
|
646
|
+
const tmpDir = await mkdtemp(join(tmpdir(), 'claude-test-'));
|
|
647
|
+
const scriptPath = join(tmpDir, 'fake-claude');
|
|
648
|
+
await writeFile(scriptPath, `#!/bin/sh\n${scriptBody}\n`, 'utf-8');
|
|
649
|
+
await chmod(scriptPath, 0o755);
|
|
650
|
+
try {
|
|
651
|
+
await withMockClaudePath(scriptPath, fn);
|
|
652
|
+
}
|
|
653
|
+
finally {
|
|
654
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
async function waitForFile(path, timeoutMs = 1000) {
|
|
658
|
+
const deadline = Date.now() + timeoutMs;
|
|
659
|
+
while (Date.now() < deadline) {
|
|
660
|
+
try {
|
|
661
|
+
return await readFile(path, 'utf-8');
|
|
662
|
+
}
|
|
663
|
+
catch (error) {
|
|
664
|
+
if (error.code !== 'ENOENT') {
|
|
665
|
+
throw error;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
669
|
+
}
|
|
670
|
+
return await readFile(path, 'utf-8');
|
|
671
|
+
}
|
|
672
|
+
/** Wait until a message of the given type appears, or timeout */
|
|
673
|
+
function waitForMessage(driver, messages, predicate, timeoutMs = 10_000) {
|
|
674
|
+
return new Promise((resolve, reject) => {
|
|
675
|
+
if (predicate(messages)) {
|
|
676
|
+
resolve();
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
const originalHandler = driver.handler;
|
|
680
|
+
const timer = setTimeout(() => { reject(new Error('Timed out waiting for message')); }, timeoutMs);
|
|
681
|
+
driver.handler = (msg) => {
|
|
682
|
+
originalHandler?.(msg);
|
|
683
|
+
messages.push(msg);
|
|
684
|
+
if (predicate(messages)) {
|
|
685
|
+
clearTimeout(timer);
|
|
686
|
+
resolve();
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
/** Wait for the spawned process to exit */
|
|
692
|
+
function waitForExit(driver, timeoutMs = 10_000) {
|
|
693
|
+
return new Promise((resolve, reject) => {
|
|
694
|
+
const proc = driver.proc;
|
|
695
|
+
if (!proc || proc.exitCode !== null || proc.signalCode !== null || proc.killed) {
|
|
696
|
+
resolve();
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
let settled = false;
|
|
700
|
+
const finish = () => {
|
|
701
|
+
if (settled)
|
|
702
|
+
return;
|
|
703
|
+
settled = true;
|
|
704
|
+
clearTimeout(timer);
|
|
705
|
+
resolve();
|
|
706
|
+
};
|
|
707
|
+
const timer = setTimeout(() => {
|
|
708
|
+
if (!driver.proc || proc.exitCode !== null || proc.signalCode !== null || proc.killed) {
|
|
709
|
+
finish();
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
reject(new Error('Timed out waiting for exit'));
|
|
713
|
+
}, timeoutMs);
|
|
714
|
+
proc.once('exit', finish);
|
|
715
|
+
proc.once('close', finish);
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
testSendPrompt('sendPrompt: spawns process and emits text.delta and session.done', async () => {
|
|
719
|
+
await withFakeClaudeScript(`echo '{"type":"system","subtype":"init","session_id":"test-sess-123"}'
|
|
720
|
+
echo '{"type":"stream_event","event":{"type":"content_block_delta","delta":{"type":"text_delta","text":"Hello"}}}'
|
|
721
|
+
echo '{"type":"result","result":"done","total_cost_usd":0.01,"usage":{"input_tokens":10,"output_tokens":5}}'`, async () => {
|
|
722
|
+
const driver = new ClaudeDriver();
|
|
723
|
+
await driver.start('/tmp');
|
|
724
|
+
const messages = [];
|
|
725
|
+
driver.onMessage((msg) => messages.push(msg));
|
|
726
|
+
driver.sendPrompt('test message');
|
|
727
|
+
await waitForMessage(driver, messages, (msgs) => msgs.some(m => m.type === 'session.done'));
|
|
728
|
+
const types = messages.map(m => m.type);
|
|
729
|
+
assert.ok(types.includes('text.delta'), `Expected text.delta in ${JSON.stringify(types)}`);
|
|
730
|
+
assert.ok(types.includes('session.done'), `Expected session.done in ${JSON.stringify(types)}`);
|
|
731
|
+
});
|
|
732
|
+
});
|
|
733
|
+
testSendPrompt('sendPrompt: emits error on non-zero exit code without result', async () => {
|
|
734
|
+
await withFakeClaudeScript('exit 1', async () => {
|
|
735
|
+
const driver = new ClaudeDriver();
|
|
736
|
+
await driver.start('/tmp');
|
|
737
|
+
const messages = [];
|
|
738
|
+
driver.onMessage((msg) => messages.push(msg));
|
|
739
|
+
driver.sendPrompt('test message');
|
|
740
|
+
await waitForMessage(driver, messages, (msgs) => msgs.some(m => m.type === 'error'));
|
|
741
|
+
const errors = messages.filter(m => m.type === 'error');
|
|
742
|
+
assert.ok(errors.length > 0, 'Should emit error on non-zero exit');
|
|
743
|
+
});
|
|
744
|
+
});
|
|
745
|
+
testSendPrompt('sendPrompt: includes stderr detail in non-zero exit errors', async () => {
|
|
746
|
+
await withFakeClaudeScript(`echo 'authentication failed for current account' >&2
|
|
747
|
+
exit 1`, async () => {
|
|
748
|
+
const driver = new ClaudeDriver();
|
|
749
|
+
await driver.start('/tmp');
|
|
750
|
+
const messages = [];
|
|
751
|
+
driver.onMessage((msg) => messages.push(msg));
|
|
752
|
+
driver.sendPrompt('test message');
|
|
753
|
+
await waitForMessage(driver, messages, (msgs) => msgs.some(m => m.type === 'error'));
|
|
754
|
+
const error = messages.find(m => m.type === 'error');
|
|
755
|
+
assert.equal(error.message, 'Claude exited with code 1: authentication failed for current account');
|
|
756
|
+
});
|
|
757
|
+
});
|
|
758
|
+
testSendPrompt('sendPrompt: normalizes usage limit stderr on non-zero exit', async () => {
|
|
759
|
+
await withFakeClaudeScript(`echo 'Rate limit exceeded' >&2
|
|
760
|
+
exit 1`, async () => {
|
|
761
|
+
const driver = new ClaudeDriver();
|
|
762
|
+
await driver.start('/tmp');
|
|
763
|
+
const messages = [];
|
|
764
|
+
driver.onMessage((msg) => messages.push(msg));
|
|
765
|
+
driver.sendPrompt('test message');
|
|
766
|
+
await waitForMessage(driver, messages, (msgs) => msgs.some(m => m.type === 'error'));
|
|
767
|
+
const error = messages.find(m => m.type === 'error');
|
|
768
|
+
assert.equal(error.message, 'Claude usage limit reached. Rate limit exceeded');
|
|
769
|
+
});
|
|
770
|
+
});
|
|
771
|
+
testSendPrompt('sendPrompt: does not emit error on non-zero exit if result was already seen', async () => {
|
|
772
|
+
await withFakeClaudeScript(`echo '{"type":"result","result":"ok"}'
|
|
773
|
+
exit 1`, async () => {
|
|
774
|
+
const driver = new ClaudeDriver();
|
|
775
|
+
await driver.start('/tmp');
|
|
776
|
+
const messages = [];
|
|
777
|
+
driver.onMessage((msg) => messages.push(msg));
|
|
778
|
+
driver.sendPrompt('test message');
|
|
779
|
+
// Wait for process to fully exit
|
|
780
|
+
await waitForExit(driver);
|
|
781
|
+
// Small extra delay for any pending callbacks
|
|
782
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
783
|
+
const errors = messages.filter(m => m.type === 'error');
|
|
784
|
+
assert.equal(errors.length, 0, 'Should not emit error when result was already seen');
|
|
785
|
+
});
|
|
786
|
+
});
|
|
787
|
+
testSendPrompt('sendPrompt: handles spawn error for nonexistent binary', async () => {
|
|
788
|
+
await withMockClaudePath('/nonexistent/path/to/claude-binary', async () => {
|
|
789
|
+
const driver = new ClaudeDriver();
|
|
790
|
+
await driver.start('/tmp');
|
|
791
|
+
const messages = [];
|
|
792
|
+
driver.onMessage((msg) => messages.push(msg));
|
|
793
|
+
driver.sendPrompt('test message');
|
|
794
|
+
await waitForMessage(driver, messages, (msgs) => msgs.some(m => m.type === 'error'));
|
|
795
|
+
const errors = messages.filter(m => m.type === 'error');
|
|
796
|
+
assert.ok(errors.length > 0, 'Should emit error on spawn failure');
|
|
797
|
+
});
|
|
798
|
+
});
|
|
799
|
+
testSendPrompt('sendPrompt: uses --resume flag for existing session', async () => {
|
|
800
|
+
await withFakeClaudeScript(`echo '{"type":"result","result":"ok"}'`, async () => {
|
|
801
|
+
const driver = new ClaudeDriver();
|
|
802
|
+
await driver.start('/tmp', 'existing-session-123');
|
|
803
|
+
driver.onMessage(() => { });
|
|
804
|
+
driver.sendPrompt('test');
|
|
805
|
+
await waitForExit(driver);
|
|
806
|
+
});
|
|
807
|
+
});
|
|
808
|
+
testSendPrompt('sendPrompt: uses --dangerously-skip-permissions for autoApprove mode', async () => {
|
|
809
|
+
await withFakeClaudeScript(`echo '{"type":"result","result":"ok"}'`, async () => {
|
|
810
|
+
const driver = new ClaudeDriver();
|
|
811
|
+
await driver.start('/tmp', undefined, 'autoApprove');
|
|
812
|
+
driver.onMessage(() => { });
|
|
813
|
+
driver.sendPrompt('test');
|
|
814
|
+
await waitForExit(driver);
|
|
815
|
+
});
|
|
816
|
+
});
|
|
817
|
+
testSendPrompt('sendPrompt: injects --settings hook file for normal mode when hook bridge is configured', async () => {
|
|
818
|
+
const tmpDir = await mkdtemp(join(tmpdir(), 'claude-args-'));
|
|
819
|
+
const argLogPath = join(tmpDir, 'args.log');
|
|
820
|
+
let settingsPath = '';
|
|
821
|
+
try {
|
|
822
|
+
await withFakeClaudeScript(`printf '%s\n' "$@" > ${JSON.stringify(argLogPath)}
|
|
823
|
+
echo '{"type":"result","result":"ok"}'`, async () => {
|
|
824
|
+
const driver = new ClaudeDriver();
|
|
825
|
+
driver.configureHookBridge(4321, 'secret-1');
|
|
826
|
+
await driver.start('/tmp', undefined, 'normal');
|
|
827
|
+
driver.onMessage(() => { });
|
|
828
|
+
driver.sendPrompt('test');
|
|
829
|
+
settingsPath = driver.hookFiles?.settingsPath ?? '';
|
|
830
|
+
assert.ok(settingsPath?.endsWith('settings.json'));
|
|
831
|
+
const settings = await readFile(settingsPath, 'utf-8');
|
|
832
|
+
assert.match(settings, /"PreToolUse"/);
|
|
833
|
+
assert.doesNotMatch(settings, /"PermissionRequest"/);
|
|
834
|
+
await waitForExit(driver);
|
|
835
|
+
});
|
|
836
|
+
const args = (await waitForFile(argLogPath)).trim().split('\n').filter(Boolean);
|
|
837
|
+
const settingsIndex = args.indexOf('--settings');
|
|
838
|
+
assert.ok(settingsIndex >= 0, `Expected --settings in ${JSON.stringify(args)}`);
|
|
839
|
+
assert.equal(args[settingsIndex + 1], settingsPath);
|
|
840
|
+
assert.equal(args.includes('--dangerously-skip-permissions'), false);
|
|
841
|
+
}
|
|
842
|
+
finally {
|
|
843
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
testSendPrompt('sendPrompt: strips CMUX environment variables before spawning Claude', async () => {
|
|
847
|
+
const tmpDir = await mkdtemp(join(tmpdir(), 'claude-env-'));
|
|
848
|
+
const envLogPath = join(tmpDir, 'env.log');
|
|
849
|
+
try {
|
|
850
|
+
const sanitizedEnvWithCmux = {
|
|
851
|
+
...config.buildSanitizedEnv(),
|
|
852
|
+
CMUX_PORT: '9310',
|
|
853
|
+
};
|
|
854
|
+
await withMockSanitizedEnv(() => ({ ...sanitizedEnvWithCmux }), async () => await withFakeClaudeScript(`printf '%s' "$CMUX_PORT" > ${JSON.stringify(envLogPath)}
|
|
855
|
+
echo '{"type":"result","result":"ok"}'`, async () => {
|
|
856
|
+
const driver = new ClaudeDriver();
|
|
857
|
+
await driver.start('/tmp', undefined, 'normal');
|
|
858
|
+
driver.onMessage(() => { });
|
|
859
|
+
driver.sendPrompt('test');
|
|
860
|
+
await waitForExit(driver);
|
|
861
|
+
}));
|
|
862
|
+
const value = await readFile(envLogPath, 'utf-8');
|
|
863
|
+
assert.equal(value, '');
|
|
864
|
+
}
|
|
865
|
+
finally {
|
|
866
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
867
|
+
}
|
|
868
|
+
});
|
|
869
|
+
testSendPrompt('sendPrompt: skips hook --settings when acceptEdits retry mode is used', async () => {
|
|
870
|
+
const tmpDir = await mkdtemp(join(tmpdir(), 'claude-args-'));
|
|
871
|
+
const argLogPath = join(tmpDir, 'args.log');
|
|
872
|
+
try {
|
|
873
|
+
await withFakeClaudeScript(`printf '%s\n' "$@" > ${JSON.stringify(argLogPath)}
|
|
874
|
+
echo '{"type":"result","result":"ok"}'`, async () => {
|
|
875
|
+
const driver = new ClaudeDriver();
|
|
876
|
+
driver.configureHookBridge(4321, 'secret-2');
|
|
877
|
+
await driver.start('/tmp', undefined, 'acceptEdits');
|
|
878
|
+
driver.onMessage(() => { });
|
|
879
|
+
driver.sendPrompt('test');
|
|
880
|
+
await waitForExit(driver);
|
|
881
|
+
});
|
|
882
|
+
const args = (await waitForFile(argLogPath)).trim().split('\n').filter(Boolean);
|
|
883
|
+
assert.equal(args.includes('--settings'), false);
|
|
884
|
+
assert.deepEqual(args.slice(args.indexOf('--permission-mode'), args.indexOf('--permission-mode') + 2), [
|
|
885
|
+
'--permission-mode',
|
|
886
|
+
'acceptEdits',
|
|
887
|
+
]);
|
|
888
|
+
}
|
|
889
|
+
finally {
|
|
890
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
891
|
+
}
|
|
892
|
+
});
|
|
893
|
+
testSendPrompt('sendPrompt: stderr output is handled without crashing', async () => {
|
|
894
|
+
await withFakeClaudeScript(`echo 'some warning' >&2
|
|
895
|
+
echo '{"type":"result","result":"ok"}'`, async () => {
|
|
896
|
+
const driver = new ClaudeDriver();
|
|
897
|
+
await driver.start('/tmp');
|
|
898
|
+
const messages = [];
|
|
899
|
+
driver.onMessage((msg) => messages.push(msg));
|
|
900
|
+
driver.sendPrompt('test');
|
|
901
|
+
await waitForMessage(driver, messages, (msgs) => msgs.some(m => m.type === 'session.done'));
|
|
902
|
+
const done = messages.filter(m => m.type === 'session.done');
|
|
903
|
+
assert.ok(done.length > 0, 'Should still emit session.done after stderr output');
|
|
904
|
+
});
|
|
905
|
+
});
|
|
906
|
+
testSendPrompt('stop: kills process and sets proc to null', async () => {
|
|
907
|
+
await withFakeClaudeScript('sleep 30', async () => {
|
|
908
|
+
const driver = new ClaudeDriver();
|
|
909
|
+
await driver.start('/tmp');
|
|
910
|
+
driver.onMessage(() => { });
|
|
911
|
+
driver.sendPrompt('test');
|
|
912
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
913
|
+
assert.ok(driver.proc !== null, 'proc should be set after sendPrompt');
|
|
914
|
+
driver.stop();
|
|
915
|
+
assert.equal(driver.proc, null, 'proc should be null after stop');
|
|
916
|
+
});
|
|
917
|
+
});
|
|
918
|
+
test('setApprovalMode: changes mode for subsequent sendPrompt calls', async () => {
|
|
919
|
+
const driver = new ClaudeDriver();
|
|
920
|
+
await driver.start('/tmp', undefined, 'normal');
|
|
921
|
+
assert.equal(driver.approvalMode, 'normal');
|
|
922
|
+
driver.setApprovalMode('autoApprove');
|
|
923
|
+
assert.equal(driver.approvalMode, 'autoApprove');
|
|
924
|
+
driver.setApprovalMode('normal');
|
|
925
|
+
assert.equal(driver.approvalMode, 'normal');
|
|
926
|
+
});
|
|
927
|
+
testSendPrompt('sendPrompt: ENOENT with invalid cwd reports cwd error', async () => {
|
|
928
|
+
const driver = new ClaudeDriver();
|
|
929
|
+
await driver.start('/nonexistent/cwd/path/that/does/not/exist');
|
|
930
|
+
const messages = [];
|
|
931
|
+
driver.onMessage((msg) => messages.push(msg));
|
|
932
|
+
driver.sendPrompt('test message');
|
|
933
|
+
await waitForMessage(driver, messages, (msgs) => msgs.some(m => m.type === 'error'));
|
|
934
|
+
const errors = messages.filter(m => m.type === 'error');
|
|
935
|
+
assert.ok(errors.length > 0, 'Should emit error');
|
|
936
|
+
const errorMsg = errors[0].message;
|
|
937
|
+
assert.ok(errorMsg.includes('Working directory does not exist'), `Error should mention cwd, got: ${errorMsg}`);
|
|
938
|
+
});
|
|
939
|
+
testSendPrompt('interrupt: kills running process', async () => {
|
|
940
|
+
await withFakeClaudeScript('sleep 30', async () => {
|
|
941
|
+
const driver = new ClaudeDriver();
|
|
942
|
+
await driver.start('/tmp');
|
|
943
|
+
const messages = [];
|
|
944
|
+
driver.onMessage((msg) => messages.push(msg));
|
|
945
|
+
driver.sendPrompt('test');
|
|
946
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
947
|
+
driver.interrupt();
|
|
948
|
+
await waitForMessage(driver, messages, (msgs) => msgs.some((msg) => msg.type === 'session.interrupted'));
|
|
949
|
+
});
|
|
950
|
+
});
|
|
951
|
+
//# sourceMappingURL=claude.test.js.map
|