auto-mode 0.14.0 → 0.16.0
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 +4 -4
- package/dist/{classify-edit-D8GC6b91.js → classify-edit-LpoeenGt.js} +4 -1
- package/dist/cli.js +139 -121
- package/dist/index.d.ts +210 -15
- package/dist/index.js +7 -2
- package/dist/{render-verdict-QLyj-g7d.js → render-verdict-u3_ftSEp.js} +534 -450
- package/mods/auto-mode/hooks/testing.d.ts +76 -24
- package/mods/auto-mode/hooks/types.ts +15 -1
- package/package.json +25 -12
- package/policy/classifier.md +33 -22
- package/policy/decision.md +11 -5
- package/policy/rules.md +131 -137
- package/mods/auto-mode/hooks/build-prompt-context.claude-check.ts +0 -93
- package/mods/auto-mode/hooks/register.claude-check.ts +0 -1226
|
@@ -1,1226 +0,0 @@
|
|
|
1
|
-
import { expect, test } from 'claude-code/testing';
|
|
2
|
-
|
|
3
|
-
const NO_PROMPT_CONTEXT = {
|
|
4
|
-
agentID: null,
|
|
5
|
-
originalUserTask: null,
|
|
6
|
-
delegatedTask: null,
|
|
7
|
-
lastDirectUserMessage: null,
|
|
8
|
-
omittedTaskContext: [{ field: 'originalUserTask', reason: 'unavailable' }],
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
test('it preserves an existing denial and its rule', async ($, on) => {
|
|
12
|
-
const decided = {
|
|
13
|
-
decision: 'deny',
|
|
14
|
-
reason: 'Existing permission decision',
|
|
15
|
-
rule: 'Bash(git push:*)',
|
|
16
|
-
hook: 'PreToolUse',
|
|
17
|
-
} as const;
|
|
18
|
-
|
|
19
|
-
let calls = 0;
|
|
20
|
-
|
|
21
|
-
on('session.cwd', () => ({ value: '/repo' }));
|
|
22
|
-
on('tool.check', () => decided);
|
|
23
|
-
on('classic.SessionStart', () => ({}));
|
|
24
|
-
|
|
25
|
-
on('process.run', () => {
|
|
26
|
-
calls += 1;
|
|
27
|
-
|
|
28
|
-
return {
|
|
29
|
-
value: {
|
|
30
|
-
exitCode: 0,
|
|
31
|
-
stdout: '{"decision":"allow"}',
|
|
32
|
-
stderr: 'synthetic-private-fragment',
|
|
33
|
-
isStdoutTruncated: false,
|
|
34
|
-
isStderrTruncated: false,
|
|
35
|
-
},
|
|
36
|
-
};
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
// oxlint-disable-next-line new-cap -- The host event API retains its event spelling.
|
|
40
|
-
await $.classic.SessionStart({
|
|
41
|
-
source: 'startup',
|
|
42
|
-
session_id: 'session-1',
|
|
43
|
-
cwd: '/repo',
|
|
44
|
-
transcript_path: '/repo/transcript.jsonl',
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
const result = await $.tool.check({
|
|
48
|
-
tool: 'Bash',
|
|
49
|
-
input: { command: 'git push origin feature', timeout: 120_000 },
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
expect(result).toStrictEqual(decided);
|
|
53
|
-
expect(calls).toBe(0);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test('it preserves an existing allowance without a second evaluator', async ($, on) => {
|
|
57
|
-
const decided = {
|
|
58
|
-
decision: 'allow',
|
|
59
|
-
reason: 'Existing permission decision',
|
|
60
|
-
rule: 'Bash(git push:*)',
|
|
61
|
-
hook: 'PreToolUse',
|
|
62
|
-
} as const;
|
|
63
|
-
|
|
64
|
-
let calls = 0;
|
|
65
|
-
|
|
66
|
-
on('session.cwd', () => ({ value: '/repo' }));
|
|
67
|
-
on('tool.check', () => decided);
|
|
68
|
-
on('classic.SessionStart', () => ({}));
|
|
69
|
-
|
|
70
|
-
on('process.run', () => {
|
|
71
|
-
calls += 1;
|
|
72
|
-
|
|
73
|
-
return {
|
|
74
|
-
value: {
|
|
75
|
-
exitCode: 0,
|
|
76
|
-
stdout: '{"decision":"deny","reason":"[Data Exfiltration] Refuse the transfer."}',
|
|
77
|
-
stderr: 'synthetic-private-fragment',
|
|
78
|
-
isStdoutTruncated: false,
|
|
79
|
-
isStderrTruncated: false,
|
|
80
|
-
},
|
|
81
|
-
};
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
// oxlint-disable-next-line new-cap -- The host event API retains its event spelling.
|
|
85
|
-
await $.classic.SessionStart({
|
|
86
|
-
source: 'startup',
|
|
87
|
-
session_id: 'session-1',
|
|
88
|
-
cwd: '/repo',
|
|
89
|
-
transcript_path: '/repo/transcript.jsonl',
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
const result = await $.tool.check({
|
|
93
|
-
tool: 'Bash',
|
|
94
|
-
input: { command: 'git push origin feature', timeout: 120_000 },
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
expect(result).toStrictEqual(decided);
|
|
98
|
-
expect(calls).toBe(0);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
test('it approves an ask after Jev allows the action', async ($, on) => {
|
|
102
|
-
const decided = {
|
|
103
|
-
decision: 'ask',
|
|
104
|
-
reason: 'Existing permission decision',
|
|
105
|
-
rule: 'Bash(git push:*)',
|
|
106
|
-
hook: 'PreToolUse',
|
|
107
|
-
} as const;
|
|
108
|
-
|
|
109
|
-
let calls = 0;
|
|
110
|
-
let invocation: unknown;
|
|
111
|
-
let stdin = '';
|
|
112
|
-
let timeoutMs = 0;
|
|
113
|
-
|
|
114
|
-
on('session.cwd', () => ({ value: '/repo' }));
|
|
115
|
-
on('tool.check', () => decided);
|
|
116
|
-
on('classic.SessionStart', () => ({}));
|
|
117
|
-
|
|
118
|
-
on('process.run', (_api, e) => {
|
|
119
|
-
calls += 1;
|
|
120
|
-
invocation = e;
|
|
121
|
-
stdin = e.init?.stdin ?? '';
|
|
122
|
-
timeoutMs = e.init?.timeoutMs ?? 0;
|
|
123
|
-
|
|
124
|
-
return {
|
|
125
|
-
value: {
|
|
126
|
-
exitCode: 0,
|
|
127
|
-
stdout: '{"decision":"allow"}',
|
|
128
|
-
stderr: 'synthetic-private-fragment',
|
|
129
|
-
isStdoutTruncated: false,
|
|
130
|
-
isStderrTruncated: false,
|
|
131
|
-
},
|
|
132
|
-
};
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
// oxlint-disable-next-line new-cap -- The host event API retains its event spelling.
|
|
136
|
-
await $.classic.SessionStart({
|
|
137
|
-
source: 'startup',
|
|
138
|
-
session_id: 'session-1',
|
|
139
|
-
cwd: '/repo',
|
|
140
|
-
transcript_path: '/repo/transcript.jsonl',
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
const result = await $.tool.check({
|
|
144
|
-
tool: 'Bash',
|
|
145
|
-
input: { command: 'git push origin feature', timeout: 120_000 },
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
expect(result).toStrictEqual({ decision: 'allow' });
|
|
149
|
-
expect(calls).toBe(1);
|
|
150
|
-
expect(timeoutMs).toBeGreaterThan(0);
|
|
151
|
-
expect(timeoutMs).toBeLessThanOrEqual(8000);
|
|
152
|
-
|
|
153
|
-
expect(JSON.parse(stdin)).toStrictEqual({
|
|
154
|
-
sessionID: 'session-1',
|
|
155
|
-
cwd: '/repo',
|
|
156
|
-
toolName: 'Bash',
|
|
157
|
-
toolInput: { command: 'git push origin feature', timeout: 120_000 },
|
|
158
|
-
context: NO_PROMPT_CONTEXT,
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
expect(invocation).toMatchObject({
|
|
162
|
-
argv: ['auto-mode', 'run', '--jev-only', '--evaluation-deadline', expect.any(String)],
|
|
163
|
-
init: { timeoutMs: expect.any(Number), stdin: expect.any(String) },
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
test('it refuses an ask after Jev denies the action', async ($, on) => {
|
|
168
|
-
const decided = {
|
|
169
|
-
decision: 'ask',
|
|
170
|
-
reason: 'Existing permission decision',
|
|
171
|
-
rule: 'Bash(git push:*)',
|
|
172
|
-
hook: 'PreToolUse',
|
|
173
|
-
} as const;
|
|
174
|
-
|
|
175
|
-
let calls = 0;
|
|
176
|
-
let invocation: unknown;
|
|
177
|
-
let stdin = '';
|
|
178
|
-
let timeoutMs = 0;
|
|
179
|
-
|
|
180
|
-
on('session.cwd', () => ({ value: '/repo' }));
|
|
181
|
-
on('tool.check', () => decided);
|
|
182
|
-
on('classic.SessionStart', () => ({}));
|
|
183
|
-
|
|
184
|
-
on('process.run', (_api, e) => {
|
|
185
|
-
calls += 1;
|
|
186
|
-
invocation = e;
|
|
187
|
-
stdin = e.init?.stdin ?? '';
|
|
188
|
-
timeoutMs = e.init?.timeoutMs ?? 0;
|
|
189
|
-
|
|
190
|
-
return {
|
|
191
|
-
value: {
|
|
192
|
-
exitCode: 0,
|
|
193
|
-
stdout: '{"decision":"deny","reason":"[Data Exfiltration] Refuse the transfer."}',
|
|
194
|
-
stderr: 'synthetic-private-fragment',
|
|
195
|
-
isStdoutTruncated: false,
|
|
196
|
-
isStderrTruncated: false,
|
|
197
|
-
},
|
|
198
|
-
};
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
// oxlint-disable-next-line new-cap -- The host event API retains its event spelling.
|
|
202
|
-
await $.classic.SessionStart({
|
|
203
|
-
source: 'startup',
|
|
204
|
-
session_id: 'session-1',
|
|
205
|
-
cwd: '/repo',
|
|
206
|
-
transcript_path: '/repo/transcript.jsonl',
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
const result = await $.tool.check({
|
|
210
|
-
tool: 'Bash',
|
|
211
|
-
input: { command: 'git push origin feature', timeout: 120_000 },
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
expect(result).toStrictEqual({
|
|
215
|
-
decision: 'deny',
|
|
216
|
-
reason: '[Data Exfiltration] Refuse the transfer.',
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
expect(calls).toBe(1);
|
|
220
|
-
expect(timeoutMs).toBeGreaterThan(0);
|
|
221
|
-
expect(timeoutMs).toBeLessThanOrEqual(8000);
|
|
222
|
-
|
|
223
|
-
expect(JSON.parse(stdin)).toStrictEqual({
|
|
224
|
-
sessionID: 'session-1',
|
|
225
|
-
cwd: '/repo',
|
|
226
|
-
toolName: 'Bash',
|
|
227
|
-
toolInput: { command: 'git push origin feature', timeout: 120_000 },
|
|
228
|
-
context: NO_PROMPT_CONTEXT,
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
expect(invocation).toMatchObject({
|
|
232
|
-
argv: ['auto-mode', 'run', '--jev-only', '--evaluation-deadline', expect.any(String)],
|
|
233
|
-
init: { timeoutMs: expect.any(Number), stdin: expect.any(String) },
|
|
234
|
-
});
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
test('it retains manual approval when Jev returns no opinion', async ($, on) => {
|
|
238
|
-
const decided = {
|
|
239
|
-
decision: 'ask',
|
|
240
|
-
reason: 'Existing permission decision',
|
|
241
|
-
rule: 'Bash(git push:*)',
|
|
242
|
-
hook: 'PreToolUse',
|
|
243
|
-
} as const;
|
|
244
|
-
|
|
245
|
-
let calls = 0;
|
|
246
|
-
let invocation: unknown;
|
|
247
|
-
let stdin = '';
|
|
248
|
-
let timeoutMs = 0;
|
|
249
|
-
|
|
250
|
-
on('session.cwd', () => ({ value: '/repo' }));
|
|
251
|
-
on('tool.check', () => decided);
|
|
252
|
-
on('classic.SessionStart', () => ({}));
|
|
253
|
-
|
|
254
|
-
on('process.run', (_api, e) => {
|
|
255
|
-
calls += 1;
|
|
256
|
-
invocation = e;
|
|
257
|
-
stdin = e.init?.stdin ?? '';
|
|
258
|
-
timeoutMs = e.init?.timeoutMs ?? 0;
|
|
259
|
-
|
|
260
|
-
return {
|
|
261
|
-
value: {
|
|
262
|
-
exitCode: 0,
|
|
263
|
-
stdout: '',
|
|
264
|
-
stderr: 'synthetic-private-fragment',
|
|
265
|
-
isStdoutTruncated: false,
|
|
266
|
-
isStderrTruncated: false,
|
|
267
|
-
},
|
|
268
|
-
};
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
// oxlint-disable-next-line new-cap -- The host event API retains its event spelling.
|
|
272
|
-
await $.classic.SessionStart({
|
|
273
|
-
source: 'startup',
|
|
274
|
-
session_id: 'session-1',
|
|
275
|
-
cwd: '/repo',
|
|
276
|
-
transcript_path: '/repo/transcript.jsonl',
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
const result = await $.tool.check({
|
|
280
|
-
tool: 'Bash',
|
|
281
|
-
input: { command: 'git push origin feature', timeout: 120_000 },
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
expect(result).toStrictEqual(decided);
|
|
285
|
-
expect(calls).toBe(1);
|
|
286
|
-
expect(timeoutMs).toBeGreaterThan(0);
|
|
287
|
-
expect(timeoutMs).toBeLessThanOrEqual(8000);
|
|
288
|
-
|
|
289
|
-
expect(JSON.parse(stdin)).toStrictEqual({
|
|
290
|
-
sessionID: 'session-1',
|
|
291
|
-
cwd: '/repo',
|
|
292
|
-
toolName: 'Bash',
|
|
293
|
-
toolInput: { command: 'git push origin feature', timeout: 120_000 },
|
|
294
|
-
context: NO_PROMPT_CONTEXT,
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
expect(invocation).toMatchObject({
|
|
298
|
-
argv: ['auto-mode', 'run', '--jev-only', '--evaluation-deadline', expect.any(String)],
|
|
299
|
-
init: { timeoutMs: expect.any(Number), stdin: expect.any(String) },
|
|
300
|
-
});
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
test('it retains manual approval for malformed JSON without copying diagnostics', async ($, on) => {
|
|
304
|
-
const decided = {
|
|
305
|
-
decision: 'ask',
|
|
306
|
-
reason: 'Existing permission decision',
|
|
307
|
-
rule: 'Bash(git push:*)',
|
|
308
|
-
hook: 'PreToolUse',
|
|
309
|
-
} as const;
|
|
310
|
-
|
|
311
|
-
let calls = 0;
|
|
312
|
-
let invocation: unknown;
|
|
313
|
-
let stdin = '';
|
|
314
|
-
let timeoutMs = 0;
|
|
315
|
-
|
|
316
|
-
on('session.cwd', () => ({ value: '/repo' }));
|
|
317
|
-
on('tool.check', () => decided);
|
|
318
|
-
on('classic.SessionStart', () => ({}));
|
|
319
|
-
|
|
320
|
-
on('process.run', (_api, e) => {
|
|
321
|
-
calls += 1;
|
|
322
|
-
invocation = e;
|
|
323
|
-
stdin = e.init?.stdin ?? '';
|
|
324
|
-
timeoutMs = e.init?.timeoutMs ?? 0;
|
|
325
|
-
|
|
326
|
-
return {
|
|
327
|
-
value: {
|
|
328
|
-
exitCode: 0,
|
|
329
|
-
stdout: 'not-json synthetic-private-fragment',
|
|
330
|
-
stderr: 'synthetic-private-fragment',
|
|
331
|
-
isStdoutTruncated: false,
|
|
332
|
-
isStderrTruncated: false,
|
|
333
|
-
},
|
|
334
|
-
};
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
// oxlint-disable-next-line new-cap -- The host event API retains its event spelling.
|
|
338
|
-
await $.classic.SessionStart({
|
|
339
|
-
source: 'startup',
|
|
340
|
-
session_id: 'session-1',
|
|
341
|
-
cwd: '/repo',
|
|
342
|
-
transcript_path: '/repo/transcript.jsonl',
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
const result = await $.tool.check({
|
|
346
|
-
tool: 'Bash',
|
|
347
|
-
input: { command: 'git push origin feature', timeout: 120_000 },
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
expect(result).toStrictEqual(decided);
|
|
351
|
-
expect(calls).toBe(1);
|
|
352
|
-
expect(timeoutMs).toBeGreaterThan(0);
|
|
353
|
-
expect(timeoutMs).toBeLessThanOrEqual(8000);
|
|
354
|
-
|
|
355
|
-
expect(JSON.parse(stdin)).toStrictEqual({
|
|
356
|
-
sessionID: 'session-1',
|
|
357
|
-
cwd: '/repo',
|
|
358
|
-
toolName: 'Bash',
|
|
359
|
-
toolInput: { command: 'git push origin feature', timeout: 120_000 },
|
|
360
|
-
context: NO_PROMPT_CONTEXT,
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
expect(invocation).toMatchObject({
|
|
364
|
-
argv: ['auto-mode', 'run', '--jev-only', '--evaluation-deadline', expect.any(String)],
|
|
365
|
-
init: { timeoutMs: expect.any(Number), stdin: expect.any(String) },
|
|
366
|
-
});
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
test('it retains manual approval for a hook-shaped verdict', async ($, on) => {
|
|
370
|
-
const decided = {
|
|
371
|
-
decision: 'ask',
|
|
372
|
-
reason: 'Existing permission decision',
|
|
373
|
-
rule: 'Bash(git push:*)',
|
|
374
|
-
hook: 'PreToolUse',
|
|
375
|
-
} as const;
|
|
376
|
-
|
|
377
|
-
let calls = 0;
|
|
378
|
-
let invocation: unknown;
|
|
379
|
-
let stdin = '';
|
|
380
|
-
let timeoutMs = 0;
|
|
381
|
-
|
|
382
|
-
on('session.cwd', () => ({ value: '/repo' }));
|
|
383
|
-
on('tool.check', () => decided);
|
|
384
|
-
on('classic.SessionStart', () => ({}));
|
|
385
|
-
|
|
386
|
-
on('process.run', (_api, e) => {
|
|
387
|
-
calls += 1;
|
|
388
|
-
invocation = e;
|
|
389
|
-
stdin = e.init?.stdin ?? '';
|
|
390
|
-
timeoutMs = e.init?.timeoutMs ?? 0;
|
|
391
|
-
|
|
392
|
-
return {
|
|
393
|
-
value: {
|
|
394
|
-
exitCode: 0,
|
|
395
|
-
stdout:
|
|
396
|
-
'{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}',
|
|
397
|
-
stderr: 'synthetic-private-fragment',
|
|
398
|
-
isStdoutTruncated: false,
|
|
399
|
-
isStderrTruncated: false,
|
|
400
|
-
},
|
|
401
|
-
};
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
// oxlint-disable-next-line new-cap -- The host event API retains its event spelling.
|
|
405
|
-
await $.classic.SessionStart({
|
|
406
|
-
source: 'startup',
|
|
407
|
-
session_id: 'session-1',
|
|
408
|
-
cwd: '/repo',
|
|
409
|
-
transcript_path: '/repo/transcript.jsonl',
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
const result = await $.tool.check({
|
|
413
|
-
tool: 'Bash',
|
|
414
|
-
input: { command: 'git push origin feature', timeout: 120_000 },
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
expect(result).toStrictEqual(decided);
|
|
418
|
-
expect(calls).toBe(1);
|
|
419
|
-
expect(timeoutMs).toBeGreaterThan(0);
|
|
420
|
-
expect(timeoutMs).toBeLessThanOrEqual(8000);
|
|
421
|
-
|
|
422
|
-
expect(JSON.parse(stdin)).toStrictEqual({
|
|
423
|
-
sessionID: 'session-1',
|
|
424
|
-
cwd: '/repo',
|
|
425
|
-
toolName: 'Bash',
|
|
426
|
-
toolInput: { command: 'git push origin feature', timeout: 120_000 },
|
|
427
|
-
context: NO_PROMPT_CONTEXT,
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
expect(invocation).toMatchObject({
|
|
431
|
-
argv: ['auto-mode', 'run', '--jev-only', '--evaluation-deadline', expect.any(String)],
|
|
432
|
-
init: { timeoutMs: expect.any(Number), stdin: expect.any(String) },
|
|
433
|
-
});
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
test('it retains manual approval for a verdict with unknown or mistyped fields', async ($, on) => {
|
|
437
|
-
const outputs = [
|
|
438
|
-
'{"decision":"allow","reason":123}',
|
|
439
|
-
'{"decision":"allow","rule":"Read-only actions"}',
|
|
440
|
-
'{"decision":"deny","reason":"[Data Exfiltration] Refuse the transfer.","extra":true}',
|
|
441
|
-
'{"decision":"deny","reason":42}',
|
|
442
|
-
'["allow"]',
|
|
443
|
-
];
|
|
444
|
-
|
|
445
|
-
let stdout = '';
|
|
446
|
-
|
|
447
|
-
on('classic.SessionStart', () => ({}));
|
|
448
|
-
on('session.cwd', () => ({ value: '/repo' }));
|
|
449
|
-
on('tool.check', () => ({ decision: 'ask' }));
|
|
450
|
-
|
|
451
|
-
on('process.run', () => ({
|
|
452
|
-
value: {
|
|
453
|
-
exitCode: 0,
|
|
454
|
-
stdout,
|
|
455
|
-
stderr: '',
|
|
456
|
-
isStdoutTruncated: false,
|
|
457
|
-
isStderrTruncated: false,
|
|
458
|
-
},
|
|
459
|
-
}));
|
|
460
|
-
|
|
461
|
-
// oxlint-disable-next-line new-cap -- The host event API retains its event spelling.
|
|
462
|
-
await $.classic.SessionStart({ source: 'startup', session_id: 'session-1', cwd: '/repo' });
|
|
463
|
-
|
|
464
|
-
const results: unknown[] = [];
|
|
465
|
-
|
|
466
|
-
for (const output of outputs) {
|
|
467
|
-
stdout = output;
|
|
468
|
-
|
|
469
|
-
const result = await $.tool.check({ tool: 'Bash', input: { command: 'rm fixture.txt' } });
|
|
470
|
-
|
|
471
|
-
results.push(result);
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
expect(results).toStrictEqual(outputs.map(() => ({ decision: 'ask' })));
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
test('it retains manual approval for a malformed denial', async ($, on) => {
|
|
478
|
-
const decided = {
|
|
479
|
-
decision: 'ask',
|
|
480
|
-
reason: 'Existing permission decision',
|
|
481
|
-
rule: 'Bash(git push:*)',
|
|
482
|
-
hook: 'PreToolUse',
|
|
483
|
-
} as const;
|
|
484
|
-
|
|
485
|
-
let calls = 0;
|
|
486
|
-
let invocation: unknown;
|
|
487
|
-
let stdin = '';
|
|
488
|
-
let timeoutMs = 0;
|
|
489
|
-
|
|
490
|
-
on('session.cwd', () => ({ value: '/repo' }));
|
|
491
|
-
on('tool.check', () => decided);
|
|
492
|
-
on('classic.SessionStart', () => ({}));
|
|
493
|
-
|
|
494
|
-
on('process.run', (_api, e) => {
|
|
495
|
-
calls += 1;
|
|
496
|
-
invocation = e;
|
|
497
|
-
stdin = e.init?.stdin ?? '';
|
|
498
|
-
timeoutMs = e.init?.timeoutMs ?? 0;
|
|
499
|
-
|
|
500
|
-
return {
|
|
501
|
-
value: {
|
|
502
|
-
exitCode: 0,
|
|
503
|
-
stdout: '{"decision":"deny"}',
|
|
504
|
-
stderr: 'synthetic-private-fragment',
|
|
505
|
-
isStdoutTruncated: false,
|
|
506
|
-
isStderrTruncated: false,
|
|
507
|
-
},
|
|
508
|
-
};
|
|
509
|
-
});
|
|
510
|
-
|
|
511
|
-
// oxlint-disable-next-line new-cap -- The host event API retains its event spelling.
|
|
512
|
-
await $.classic.SessionStart({
|
|
513
|
-
source: 'startup',
|
|
514
|
-
session_id: 'session-1',
|
|
515
|
-
cwd: '/repo',
|
|
516
|
-
transcript_path: '/repo/transcript.jsonl',
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
const result = await $.tool.check({
|
|
520
|
-
tool: 'Bash',
|
|
521
|
-
input: { command: 'git push origin feature', timeout: 120_000 },
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
expect(result).toStrictEqual(decided);
|
|
525
|
-
expect(calls).toBe(1);
|
|
526
|
-
expect(timeoutMs).toBeGreaterThan(0);
|
|
527
|
-
expect(timeoutMs).toBeLessThanOrEqual(8000);
|
|
528
|
-
|
|
529
|
-
expect(JSON.parse(stdin)).toStrictEqual({
|
|
530
|
-
sessionID: 'session-1',
|
|
531
|
-
cwd: '/repo',
|
|
532
|
-
toolName: 'Bash',
|
|
533
|
-
toolInput: { command: 'git push origin feature', timeout: 120_000 },
|
|
534
|
-
context: NO_PROMPT_CONTEXT,
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
expect(invocation).toMatchObject({
|
|
538
|
-
argv: ['auto-mode', 'run', '--jev-only', '--evaluation-deadline', expect.any(String)],
|
|
539
|
-
init: { timeoutMs: expect.any(Number), stdin: expect.any(String) },
|
|
540
|
-
});
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
test('it retains manual approval for a truncated response', async ($, on) => {
|
|
544
|
-
const decided = {
|
|
545
|
-
decision: 'ask',
|
|
546
|
-
reason: 'Existing permission decision',
|
|
547
|
-
rule: 'Bash(git push:*)',
|
|
548
|
-
hook: 'PreToolUse',
|
|
549
|
-
} as const;
|
|
550
|
-
|
|
551
|
-
let calls = 0;
|
|
552
|
-
let invocation: unknown;
|
|
553
|
-
let stdin = '';
|
|
554
|
-
let timeoutMs = 0;
|
|
555
|
-
|
|
556
|
-
on('session.cwd', () => ({ value: '/repo' }));
|
|
557
|
-
on('tool.check', () => decided);
|
|
558
|
-
on('classic.SessionStart', () => ({}));
|
|
559
|
-
|
|
560
|
-
on('process.run', (_api, e) => {
|
|
561
|
-
calls += 1;
|
|
562
|
-
invocation = e;
|
|
563
|
-
stdin = e.init?.stdin ?? '';
|
|
564
|
-
timeoutMs = e.init?.timeoutMs ?? 0;
|
|
565
|
-
|
|
566
|
-
return {
|
|
567
|
-
value: {
|
|
568
|
-
exitCode: 0,
|
|
569
|
-
stdout: '{"decision":"allow"}',
|
|
570
|
-
stderr: 'synthetic-private-fragment',
|
|
571
|
-
isStdoutTruncated: true,
|
|
572
|
-
isStderrTruncated: false,
|
|
573
|
-
},
|
|
574
|
-
};
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
// oxlint-disable-next-line new-cap -- The host event API retains its event spelling.
|
|
578
|
-
await $.classic.SessionStart({
|
|
579
|
-
source: 'startup',
|
|
580
|
-
session_id: 'session-1',
|
|
581
|
-
cwd: '/repo',
|
|
582
|
-
transcript_path: '/repo/transcript.jsonl',
|
|
583
|
-
});
|
|
584
|
-
|
|
585
|
-
const result = await $.tool.check({
|
|
586
|
-
tool: 'Bash',
|
|
587
|
-
input: { command: 'git push origin feature', timeout: 120_000 },
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
expect(result).toStrictEqual(decided);
|
|
591
|
-
expect(calls).toBe(1);
|
|
592
|
-
expect(timeoutMs).toBeGreaterThan(0);
|
|
593
|
-
expect(timeoutMs).toBeLessThanOrEqual(8000);
|
|
594
|
-
|
|
595
|
-
expect(JSON.parse(stdin)).toStrictEqual({
|
|
596
|
-
sessionID: 'session-1',
|
|
597
|
-
cwd: '/repo',
|
|
598
|
-
toolName: 'Bash',
|
|
599
|
-
toolInput: { command: 'git push origin feature', timeout: 120_000 },
|
|
600
|
-
context: NO_PROMPT_CONTEXT,
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
expect(invocation).toMatchObject({
|
|
604
|
-
argv: ['auto-mode', 'run', '--jev-only', '--evaluation-deadline', expect.any(String)],
|
|
605
|
-
init: { timeoutMs: expect.any(Number), stdin: expect.any(String) },
|
|
606
|
-
});
|
|
607
|
-
});
|
|
608
|
-
|
|
609
|
-
test('it retains manual approval when the child exits nonzero', async ($, on) => {
|
|
610
|
-
const decided = {
|
|
611
|
-
decision: 'ask',
|
|
612
|
-
reason: 'Existing permission decision',
|
|
613
|
-
rule: 'Bash(git push:*)',
|
|
614
|
-
hook: 'PreToolUse',
|
|
615
|
-
} as const;
|
|
616
|
-
|
|
617
|
-
let calls = 0;
|
|
618
|
-
let invocation: unknown;
|
|
619
|
-
let stdin = '';
|
|
620
|
-
let timeoutMs = 0;
|
|
621
|
-
|
|
622
|
-
on('session.cwd', () => ({ value: '/repo' }));
|
|
623
|
-
on('tool.check', () => decided);
|
|
624
|
-
on('classic.SessionStart', () => ({}));
|
|
625
|
-
|
|
626
|
-
on('process.run', (_api, e) => {
|
|
627
|
-
calls += 1;
|
|
628
|
-
invocation = e;
|
|
629
|
-
stdin = e.init?.stdin ?? '';
|
|
630
|
-
timeoutMs = e.init?.timeoutMs ?? 0;
|
|
631
|
-
|
|
632
|
-
return {
|
|
633
|
-
value: {
|
|
634
|
-
exitCode: 1,
|
|
635
|
-
stdout: '{"decision":"allow"}',
|
|
636
|
-
stderr: 'synthetic-private-fragment',
|
|
637
|
-
isStdoutTruncated: false,
|
|
638
|
-
isStderrTruncated: false,
|
|
639
|
-
},
|
|
640
|
-
};
|
|
641
|
-
});
|
|
642
|
-
|
|
643
|
-
// oxlint-disable-next-line new-cap -- The host event API retains its event spelling.
|
|
644
|
-
await $.classic.SessionStart({
|
|
645
|
-
source: 'startup',
|
|
646
|
-
session_id: 'session-1',
|
|
647
|
-
cwd: '/repo',
|
|
648
|
-
transcript_path: '/repo/transcript.jsonl',
|
|
649
|
-
});
|
|
650
|
-
|
|
651
|
-
const result = await $.tool.check({
|
|
652
|
-
tool: 'Bash',
|
|
653
|
-
input: { command: 'git push origin feature', timeout: 120_000 },
|
|
654
|
-
});
|
|
655
|
-
|
|
656
|
-
expect(result).toStrictEqual(decided);
|
|
657
|
-
expect(calls).toBe(1);
|
|
658
|
-
expect(timeoutMs).toBeGreaterThan(0);
|
|
659
|
-
expect(timeoutMs).toBeLessThanOrEqual(8000);
|
|
660
|
-
|
|
661
|
-
expect(JSON.parse(stdin)).toStrictEqual({
|
|
662
|
-
sessionID: 'session-1',
|
|
663
|
-
cwd: '/repo',
|
|
664
|
-
toolName: 'Bash',
|
|
665
|
-
toolInput: { command: 'git push origin feature', timeout: 120_000 },
|
|
666
|
-
context: NO_PROMPT_CONTEXT,
|
|
667
|
-
});
|
|
668
|
-
|
|
669
|
-
expect(invocation).toMatchObject({
|
|
670
|
-
argv: ['auto-mode', 'run', '--jev-only', '--evaluation-deadline', expect.any(String)],
|
|
671
|
-
init: { timeoutMs: expect.any(Number), stdin: expect.any(String) },
|
|
672
|
-
});
|
|
673
|
-
});
|
|
674
|
-
|
|
675
|
-
test('it retains manual approval when the child times out', async ($, on) => {
|
|
676
|
-
const decided = {
|
|
677
|
-
decision: 'ask',
|
|
678
|
-
reason: 'Existing permission decision',
|
|
679
|
-
rule: 'Bash(git push:*)',
|
|
680
|
-
hook: 'PreToolUse',
|
|
681
|
-
} as const;
|
|
682
|
-
|
|
683
|
-
let calls = 0;
|
|
684
|
-
let invocation: unknown;
|
|
685
|
-
let stdin = '';
|
|
686
|
-
let timeoutMs = 0;
|
|
687
|
-
|
|
688
|
-
on('session.cwd', () => ({ value: '/repo' }));
|
|
689
|
-
on('tool.check', () => decided);
|
|
690
|
-
on('classic.SessionStart', () => ({}));
|
|
691
|
-
|
|
692
|
-
on('process.run', (_api, e) => {
|
|
693
|
-
calls += 1;
|
|
694
|
-
invocation = e;
|
|
695
|
-
stdin = e.init?.stdin ?? '';
|
|
696
|
-
timeoutMs = e.init?.timeoutMs ?? 0;
|
|
697
|
-
throw new Error('synthetic child timeout; private diagnostics');
|
|
698
|
-
});
|
|
699
|
-
|
|
700
|
-
// oxlint-disable-next-line new-cap -- The host event API retains its event spelling.
|
|
701
|
-
await $.classic.SessionStart({
|
|
702
|
-
source: 'startup',
|
|
703
|
-
session_id: 'session-1',
|
|
704
|
-
cwd: '/repo',
|
|
705
|
-
transcript_path: '/repo/transcript.jsonl',
|
|
706
|
-
});
|
|
707
|
-
|
|
708
|
-
const result = await $.tool.check({
|
|
709
|
-
tool: 'Bash',
|
|
710
|
-
input: { command: 'git push origin feature', timeout: 120_000 },
|
|
711
|
-
});
|
|
712
|
-
|
|
713
|
-
expect(result).toStrictEqual(decided);
|
|
714
|
-
expect(calls).toBe(1);
|
|
715
|
-
expect(timeoutMs).toBeGreaterThan(0);
|
|
716
|
-
expect(timeoutMs).toBeLessThanOrEqual(8000);
|
|
717
|
-
|
|
718
|
-
expect(JSON.parse(stdin)).toStrictEqual({
|
|
719
|
-
sessionID: 'session-1',
|
|
720
|
-
cwd: '/repo',
|
|
721
|
-
toolName: 'Bash',
|
|
722
|
-
toolInput: { command: 'git push origin feature', timeout: 120_000 },
|
|
723
|
-
context: NO_PROMPT_CONTEXT,
|
|
724
|
-
});
|
|
725
|
-
|
|
726
|
-
expect(invocation).toMatchObject({
|
|
727
|
-
argv: ['auto-mode', 'run', '--jev-only', '--evaluation-deadline', expect.any(String)],
|
|
728
|
-
init: { timeoutMs: expect.any(Number), stdin: expect.any(String) },
|
|
729
|
-
});
|
|
730
|
-
});
|
|
731
|
-
|
|
732
|
-
test('it retains manual approval before the session context arrives', async ($, on) => {
|
|
733
|
-
const decided = {
|
|
734
|
-
decision: 'ask',
|
|
735
|
-
reason: 'Existing permission decision',
|
|
736
|
-
rule: 'Bash(git push:*)',
|
|
737
|
-
hook: 'PreToolUse',
|
|
738
|
-
} as const;
|
|
739
|
-
|
|
740
|
-
let calls = 0;
|
|
741
|
-
|
|
742
|
-
on('session.cwd', () => ({ value: '/repo' }));
|
|
743
|
-
on('tool.check', () => decided);
|
|
744
|
-
|
|
745
|
-
on('process.run', () => {
|
|
746
|
-
calls += 1;
|
|
747
|
-
|
|
748
|
-
return {
|
|
749
|
-
value: {
|
|
750
|
-
exitCode: 0,
|
|
751
|
-
stdout: '{"decision":"allow"}',
|
|
752
|
-
stderr: 'synthetic-private-fragment',
|
|
753
|
-
isStdoutTruncated: false,
|
|
754
|
-
isStderrTruncated: false,
|
|
755
|
-
},
|
|
756
|
-
};
|
|
757
|
-
});
|
|
758
|
-
|
|
759
|
-
const result = await $.tool.check({
|
|
760
|
-
tool: 'Bash',
|
|
761
|
-
input: { command: 'git push origin feature', timeout: 120_000 },
|
|
762
|
-
});
|
|
763
|
-
|
|
764
|
-
expect(result).toStrictEqual(decided);
|
|
765
|
-
expect(calls).toBe(0);
|
|
766
|
-
});
|
|
767
|
-
|
|
768
|
-
test(
|
|
769
|
-
'it passes a configured executable as one argument without a shell',
|
|
770
|
-
{ options: { command: '/opt/auto mode/bin/auto-mode' } },
|
|
771
|
-
async ($, on) => {
|
|
772
|
-
const decided = {
|
|
773
|
-
decision: 'ask',
|
|
774
|
-
reason: 'Existing permission decision',
|
|
775
|
-
rule: 'Bash(git push:*)',
|
|
776
|
-
hook: 'PreToolUse',
|
|
777
|
-
} as const;
|
|
778
|
-
|
|
779
|
-
let calls = 0;
|
|
780
|
-
let invocation: unknown;
|
|
781
|
-
let stdin = '';
|
|
782
|
-
let timeoutMs = 0;
|
|
783
|
-
|
|
784
|
-
on('session.cwd', () => ({ value: '/repo' }));
|
|
785
|
-
on('tool.check', () => decided);
|
|
786
|
-
on('classic.SessionStart', () => ({}));
|
|
787
|
-
|
|
788
|
-
on('process.run', (_api, e) => {
|
|
789
|
-
calls += 1;
|
|
790
|
-
invocation = e;
|
|
791
|
-
stdin = e.init?.stdin ?? '';
|
|
792
|
-
timeoutMs = e.init?.timeoutMs ?? 0;
|
|
793
|
-
|
|
794
|
-
return {
|
|
795
|
-
value: {
|
|
796
|
-
exitCode: 0,
|
|
797
|
-
stdout: '{"decision":"allow"}',
|
|
798
|
-
stderr: 'synthetic-private-fragment',
|
|
799
|
-
isStdoutTruncated: false,
|
|
800
|
-
isStderrTruncated: false,
|
|
801
|
-
},
|
|
802
|
-
};
|
|
803
|
-
});
|
|
804
|
-
|
|
805
|
-
// oxlint-disable-next-line new-cap -- The host event API retains its event spelling.
|
|
806
|
-
await $.classic.SessionStart({
|
|
807
|
-
source: 'startup',
|
|
808
|
-
session_id: 'session-1',
|
|
809
|
-
cwd: '/repo',
|
|
810
|
-
transcript_path: '/repo/transcript.jsonl',
|
|
811
|
-
});
|
|
812
|
-
|
|
813
|
-
const result = await $.tool.check({
|
|
814
|
-
tool: 'Bash',
|
|
815
|
-
input: { command: 'git push origin feature', timeout: 120_000 },
|
|
816
|
-
});
|
|
817
|
-
|
|
818
|
-
expect(result).toStrictEqual({ decision: 'allow' });
|
|
819
|
-
expect(calls).toBe(1);
|
|
820
|
-
expect(timeoutMs).toBeGreaterThan(0);
|
|
821
|
-
expect(timeoutMs).toBeLessThanOrEqual(8000);
|
|
822
|
-
|
|
823
|
-
expect(JSON.parse(stdin)).toStrictEqual({
|
|
824
|
-
sessionID: 'session-1',
|
|
825
|
-
cwd: '/repo',
|
|
826
|
-
toolName: 'Bash',
|
|
827
|
-
toolInput: { command: 'git push origin feature', timeout: 120_000 },
|
|
828
|
-
context: NO_PROMPT_CONTEXT,
|
|
829
|
-
});
|
|
830
|
-
|
|
831
|
-
expect(invocation).toMatchObject({
|
|
832
|
-
argv: [
|
|
833
|
-
'/opt/auto mode/bin/auto-mode',
|
|
834
|
-
'run',
|
|
835
|
-
'--jev-only',
|
|
836
|
-
'--evaluation-deadline',
|
|
837
|
-
expect.any(String),
|
|
838
|
-
],
|
|
839
|
-
init: { timeoutMs: expect.any(Number), stdin: expect.any(String) },
|
|
840
|
-
});
|
|
841
|
-
},
|
|
842
|
-
);
|
|
843
|
-
|
|
844
|
-
test('it refreshes the context from the current user prompt after reload', async ($, on) => {
|
|
845
|
-
on('classic.UserPromptSubmit', () => ({}));
|
|
846
|
-
on('session.cwd', () => ({ value: '/new-repo' }));
|
|
847
|
-
on('tool.check', () => ({ decision: 'ask' }));
|
|
848
|
-
|
|
849
|
-
let stdin = '';
|
|
850
|
-
|
|
851
|
-
on('process.run', (_api, e) => {
|
|
852
|
-
stdin = e.init?.stdin ?? '';
|
|
853
|
-
|
|
854
|
-
return {
|
|
855
|
-
value: {
|
|
856
|
-
exitCode: 0,
|
|
857
|
-
stdout: '',
|
|
858
|
-
stderr: '',
|
|
859
|
-
isStdoutTruncated: false,
|
|
860
|
-
isStderrTruncated: false,
|
|
861
|
-
},
|
|
862
|
-
};
|
|
863
|
-
});
|
|
864
|
-
|
|
865
|
-
// oxlint-disable-next-line new-cap -- The host event API retains its event spelling.
|
|
866
|
-
await $.classic.UserPromptSubmit({
|
|
867
|
-
prompt: 'Edit the test fixture.',
|
|
868
|
-
session_id: 'new-session',
|
|
869
|
-
cwd: '/new-repo',
|
|
870
|
-
transcript_path: '/new-repo/current.jsonl',
|
|
871
|
-
});
|
|
872
|
-
|
|
873
|
-
const result = await $.tool.check({
|
|
874
|
-
tool: 'Write',
|
|
875
|
-
input: { file_path: '/new-repo/fixture', content: 'green' },
|
|
876
|
-
});
|
|
877
|
-
|
|
878
|
-
expect(result).toStrictEqual({ decision: 'ask' });
|
|
879
|
-
expect(JSON.parse(stdin)).toMatchObject({ sessionID: 'new-session', cwd: '/new-repo' });
|
|
880
|
-
expect(stdin.includes('transcript')).toBe(false);
|
|
881
|
-
});
|
|
882
|
-
|
|
883
|
-
test('it carries the session identity and the direct user message', async ($, on) => {
|
|
884
|
-
let stdin = '';
|
|
885
|
-
|
|
886
|
-
on('classic.SessionStart', () => ({}));
|
|
887
|
-
on('prompt.submit', (_api, e) => ({ text: e.text }));
|
|
888
|
-
on('session.cwd', () => ({ value: '/repo' }));
|
|
889
|
-
on('tool.check', () => ({ decision: 'ask' }));
|
|
890
|
-
|
|
891
|
-
on('process.run', (_api, e) => {
|
|
892
|
-
stdin = e.init?.stdin ?? '';
|
|
893
|
-
|
|
894
|
-
return {
|
|
895
|
-
value: {
|
|
896
|
-
exitCode: 0,
|
|
897
|
-
stdout: '',
|
|
898
|
-
stderr: '',
|
|
899
|
-
isStdoutTruncated: false,
|
|
900
|
-
isStderrTruncated: false,
|
|
901
|
-
},
|
|
902
|
-
};
|
|
903
|
-
});
|
|
904
|
-
|
|
905
|
-
// oxlint-disable-next-line new-cap -- The host event API retains its event spelling.
|
|
906
|
-
await $.classic.SessionStart({ source: 'startup', session_id: 'session-1', cwd: '/repo' });
|
|
907
|
-
await $.prompt.submit({ text: 'Clean the build output.', origin: { kind: 'composer' } });
|
|
908
|
-
await $.tool.check({ tool: 'Bash', input: { command: 'rm -rf dist' } });
|
|
909
|
-
|
|
910
|
-
expect(JSON.parse(stdin)).toMatchObject({
|
|
911
|
-
sessionID: 'session-1',
|
|
912
|
-
context: {
|
|
913
|
-
agentID: null,
|
|
914
|
-
originalUserTask: { text: 'Clean the build output.' },
|
|
915
|
-
lastDirectUserMessage: { text: 'Clean the build output.' },
|
|
916
|
-
omittedTaskContext: [],
|
|
917
|
-
},
|
|
918
|
-
});
|
|
919
|
-
});
|
|
920
|
-
|
|
921
|
-
test('it preserves the complete action without truncation', async ($, on) => {
|
|
922
|
-
const content = 'x'.repeat(120_000);
|
|
923
|
-
let stdin = '';
|
|
924
|
-
|
|
925
|
-
on('classic.SessionStart', () => ({}));
|
|
926
|
-
on('session.cwd', () => ({ value: '/repo' }));
|
|
927
|
-
on('tool.check', () => ({ decision: 'ask' }));
|
|
928
|
-
|
|
929
|
-
on('process.run', (_api, e) => {
|
|
930
|
-
stdin = e.init?.stdin ?? '';
|
|
931
|
-
|
|
932
|
-
return {
|
|
933
|
-
value: {
|
|
934
|
-
exitCode: 0,
|
|
935
|
-
stdout: '',
|
|
936
|
-
stderr: '',
|
|
937
|
-
isStdoutTruncated: false,
|
|
938
|
-
isStderrTruncated: false,
|
|
939
|
-
},
|
|
940
|
-
};
|
|
941
|
-
});
|
|
942
|
-
|
|
943
|
-
// oxlint-disable-next-line new-cap -- The host event API retains its event spelling.
|
|
944
|
-
await $.classic.SessionStart({ source: 'startup' });
|
|
945
|
-
|
|
946
|
-
const result = await $.tool.check({ tool: 'Write', input: { file_path: '/repo/file', content } });
|
|
947
|
-
|
|
948
|
-
expect(result).toStrictEqual({ decision: 'ask' });
|
|
949
|
-
expect(JSON.parse(stdin)).toMatchObject({ toolInput: { file_path: '/repo/file', content } });
|
|
950
|
-
});
|
|
951
|
-
|
|
952
|
-
test('it does not replace the main context with a subagent prompt', async ($, on) => {
|
|
953
|
-
let stdin = '';
|
|
954
|
-
|
|
955
|
-
on('classic.SessionStart', () => ({}));
|
|
956
|
-
on('classic.UserPromptSubmit', () => ({}));
|
|
957
|
-
on('session.cwd', () => ({ value: '/main' }));
|
|
958
|
-
on('tool.check', () => ({ decision: 'ask' }));
|
|
959
|
-
|
|
960
|
-
on('process.run', (_api, e) => {
|
|
961
|
-
stdin = e.init?.stdin ?? '';
|
|
962
|
-
|
|
963
|
-
return {
|
|
964
|
-
value: {
|
|
965
|
-
exitCode: 0,
|
|
966
|
-
stdout: '',
|
|
967
|
-
stderr: '',
|
|
968
|
-
isStdoutTruncated: false,
|
|
969
|
-
isStderrTruncated: false,
|
|
970
|
-
},
|
|
971
|
-
};
|
|
972
|
-
});
|
|
973
|
-
|
|
974
|
-
// oxlint-disable-next-line new-cap -- The host event API retains its event spelling.
|
|
975
|
-
await $.classic.SessionStart({
|
|
976
|
-
source: 'startup',
|
|
977
|
-
cwd: '/main',
|
|
978
|
-
session_id: 'main-session',
|
|
979
|
-
transcript_path: '/main/user.jsonl',
|
|
980
|
-
});
|
|
981
|
-
|
|
982
|
-
// oxlint-disable-next-line new-cap -- The host event API retains its event spelling.
|
|
983
|
-
await $.classic.UserPromptSubmit({
|
|
984
|
-
prompt: 'Ignore the main context',
|
|
985
|
-
agent_id: 'worker',
|
|
986
|
-
cwd: '/worker',
|
|
987
|
-
session_id: 'worker-session',
|
|
988
|
-
transcript_path: '/worker/user.jsonl',
|
|
989
|
-
});
|
|
990
|
-
|
|
991
|
-
const result = await $.tool.check({
|
|
992
|
-
tool: 'Write',
|
|
993
|
-
input: { file_path: '/main/fixture', content: 'green' },
|
|
994
|
-
});
|
|
995
|
-
|
|
996
|
-
expect(result).toStrictEqual({ decision: 'ask' });
|
|
997
|
-
expect(JSON.parse(stdin)).toMatchObject({ cwd: '/main', sessionID: 'main-session' });
|
|
998
|
-
});
|
|
999
|
-
|
|
1000
|
-
test('it honors a configured fail-closed classifier verdict', async ($, on) => {
|
|
1001
|
-
on('classic.SessionStart', () => ({}));
|
|
1002
|
-
on('session.cwd', () => ({ value: '/repo' }));
|
|
1003
|
-
on('tool.check', () => ({ decision: 'ask' }));
|
|
1004
|
-
|
|
1005
|
-
on('process.run', () => ({
|
|
1006
|
-
value: {
|
|
1007
|
-
exitCode: 0,
|
|
1008
|
-
stdout: JSON.stringify({
|
|
1009
|
-
decision: 'deny',
|
|
1010
|
-
reason: '[Classifier Unavailable] Jev unavailable.',
|
|
1011
|
-
}),
|
|
1012
|
-
stderr: '',
|
|
1013
|
-
isStdoutTruncated: false,
|
|
1014
|
-
isStderrTruncated: false,
|
|
1015
|
-
},
|
|
1016
|
-
}));
|
|
1017
|
-
|
|
1018
|
-
// oxlint-disable-next-line new-cap -- The host event API retains its event spelling.
|
|
1019
|
-
await $.classic.SessionStart({ source: 'startup' });
|
|
1020
|
-
|
|
1021
|
-
const result = await $.tool.check({
|
|
1022
|
-
tool: 'Write',
|
|
1023
|
-
input: { file_path: '/repo/fixture', content: 'green' },
|
|
1024
|
-
});
|
|
1025
|
-
|
|
1026
|
-
expect(result).toStrictEqual({
|
|
1027
|
-
decision: 'deny',
|
|
1028
|
-
reason: '[Classifier Unavailable] Jev unavailable.',
|
|
1029
|
-
});
|
|
1030
|
-
});
|
|
1031
|
-
|
|
1032
|
-
test('it reads the current directory between two calls without a new prompt', async ($, on) => {
|
|
1033
|
-
let cwd = '/first';
|
|
1034
|
-
const inputs: unknown[] = [];
|
|
1035
|
-
|
|
1036
|
-
on('classic.SessionStart', () => ({}));
|
|
1037
|
-
on('session.cwd', () => ({ value: cwd }));
|
|
1038
|
-
on('tool.check', () => ({ decision: 'ask' }));
|
|
1039
|
-
|
|
1040
|
-
on('process.run', (_api, e) => {
|
|
1041
|
-
inputs.push(JSON.parse(e.init?.stdin ?? ''));
|
|
1042
|
-
|
|
1043
|
-
return {
|
|
1044
|
-
value: {
|
|
1045
|
-
exitCode: 0,
|
|
1046
|
-
stdout: '',
|
|
1047
|
-
stderr: '',
|
|
1048
|
-
isStdoutTruncated: false,
|
|
1049
|
-
isStderrTruncated: false,
|
|
1050
|
-
},
|
|
1051
|
-
};
|
|
1052
|
-
});
|
|
1053
|
-
|
|
1054
|
-
// oxlint-disable-next-line new-cap -- The host event API retains its event spelling.
|
|
1055
|
-
await $.classic.SessionStart({ source: 'startup', cwd: '/first' });
|
|
1056
|
-
await $.tool.check({ tool: 'Bash', input: { command: 'rm fixture.txt' } });
|
|
1057
|
-
|
|
1058
|
-
cwd = '/second';
|
|
1059
|
-
|
|
1060
|
-
await $.tool.check({ tool: 'Bash', input: { command: 'rm fixture.txt' } });
|
|
1061
|
-
|
|
1062
|
-
expect(inputs).toMatchObject([{ cwd: '/first' }, { cwd: '/second' }]);
|
|
1063
|
-
});
|
|
1064
|
-
|
|
1065
|
-
test('it records invocation and fallback without copying child output or failure text', async ($, on) => {
|
|
1066
|
-
const logs: string[] = [];
|
|
1067
|
-
|
|
1068
|
-
on('ui.log', (_api, e) => {
|
|
1069
|
-
logs.push(e.text);
|
|
1070
|
-
|
|
1071
|
-
return { value: undefined };
|
|
1072
|
-
});
|
|
1073
|
-
|
|
1074
|
-
on('classic.SessionStart', () => ({}));
|
|
1075
|
-
on('session.cwd', () => ({ value: '/repo' }));
|
|
1076
|
-
on('tool.check', () => ({ decision: 'ask' }));
|
|
1077
|
-
|
|
1078
|
-
on('process.run', () => ({
|
|
1079
|
-
value: {
|
|
1080
|
-
exitCode: 0,
|
|
1081
|
-
stdout: 'private-stdout-canary',
|
|
1082
|
-
stderr: 'private-stderr-canary',
|
|
1083
|
-
isStdoutTruncated: false,
|
|
1084
|
-
isStderrTruncated: false,
|
|
1085
|
-
},
|
|
1086
|
-
}));
|
|
1087
|
-
|
|
1088
|
-
// oxlint-disable-next-line new-cap -- The host event API retains its event spelling.
|
|
1089
|
-
await $.classic.SessionStart({ source: 'startup' });
|
|
1090
|
-
|
|
1091
|
-
const result = await $.tool.check({ tool: 'Bash', input: { command: 'private-action-canary' } });
|
|
1092
|
-
|
|
1093
|
-
expect(result).toStrictEqual({ decision: 'ask' });
|
|
1094
|
-
|
|
1095
|
-
expect(logs).toStrictEqual([
|
|
1096
|
-
'auto-mode action unavailable: evaluator invoked',
|
|
1097
|
-
'auto-mode action unavailable: manual approval retained; no usable verdict; inspect action diagnostics',
|
|
1098
|
-
]);
|
|
1099
|
-
});
|
|
1100
|
-
|
|
1101
|
-
test('it identifies a subprocess failure without copying the exception', async ($, on) => {
|
|
1102
|
-
const logs: string[] = [];
|
|
1103
|
-
|
|
1104
|
-
on('ui.log', (_api, e) => {
|
|
1105
|
-
logs.push(e.text);
|
|
1106
|
-
|
|
1107
|
-
return { value: undefined };
|
|
1108
|
-
});
|
|
1109
|
-
|
|
1110
|
-
on('classic.SessionStart', () => ({}));
|
|
1111
|
-
on('session.cwd', () => ({ value: '/repo' }));
|
|
1112
|
-
on('tool.check', () => ({ decision: 'ask' }));
|
|
1113
|
-
|
|
1114
|
-
on('process.run', () => {
|
|
1115
|
-
throw new Error('private-exception-canary: aborted: still running after 8000ms');
|
|
1116
|
-
});
|
|
1117
|
-
|
|
1118
|
-
// oxlint-disable-next-line new-cap -- The host event API retains its event spelling.
|
|
1119
|
-
await $.classic.SessionStart({ source: 'startup' });
|
|
1120
|
-
|
|
1121
|
-
const result = await $.tool.check({ tool: 'Bash', input: {} });
|
|
1122
|
-
|
|
1123
|
-
expect(result).toStrictEqual({ decision: 'ask' });
|
|
1124
|
-
|
|
1125
|
-
expect(logs).toStrictEqual([
|
|
1126
|
-
'auto-mode action unavailable: evaluator invoked',
|
|
1127
|
-
'auto-mode action unavailable: manual approval retained; subprocess failure',
|
|
1128
|
-
]);
|
|
1129
|
-
});
|
|
1130
|
-
|
|
1131
|
-
test('it records a finished scope-creating Bash call with its result', async ($, on) => {
|
|
1132
|
-
let invocation: unknown;
|
|
1133
|
-
let stdin = '';
|
|
1134
|
-
|
|
1135
|
-
on('session.cwd', () => ({ value: '/repo' }));
|
|
1136
|
-
on('classic.SessionStart', () => ({}));
|
|
1137
|
-
|
|
1138
|
-
on('tool.call', () => ({
|
|
1139
|
-
result: { stdout: 'https://github.com/dev/app/pull/3\n', stderr: '' },
|
|
1140
|
-
text: 'https://github.com/dev/app/pull/3\n',
|
|
1141
|
-
}));
|
|
1142
|
-
|
|
1143
|
-
on('process.run', (_api, e) => {
|
|
1144
|
-
invocation = e;
|
|
1145
|
-
stdin = e.init?.stdin ?? '';
|
|
1146
|
-
|
|
1147
|
-
return {
|
|
1148
|
-
value: {
|
|
1149
|
-
exitCode: 0,
|
|
1150
|
-
stdout: '',
|
|
1151
|
-
stderr: '',
|
|
1152
|
-
isStdoutTruncated: false,
|
|
1153
|
-
isStderrTruncated: false,
|
|
1154
|
-
},
|
|
1155
|
-
};
|
|
1156
|
-
});
|
|
1157
|
-
|
|
1158
|
-
// oxlint-disable-next-line new-cap -- The host event API retains its event spelling.
|
|
1159
|
-
await $.classic.SessionStart({
|
|
1160
|
-
source: 'startup',
|
|
1161
|
-
session_id: 'session-1',
|
|
1162
|
-
cwd: '/repo',
|
|
1163
|
-
transcript_path: '/repo/transcript.jsonl',
|
|
1164
|
-
});
|
|
1165
|
-
|
|
1166
|
-
const result = await $.tool.call({
|
|
1167
|
-
tool: 'Bash',
|
|
1168
|
-
tool_use_id: 'call-1',
|
|
1169
|
-
command: 'gh pr create --fill',
|
|
1170
|
-
});
|
|
1171
|
-
|
|
1172
|
-
expect(result).toMatchObject({ text: 'https://github.com/dev/app/pull/3\n' });
|
|
1173
|
-
|
|
1174
|
-
expect(invocation).toMatchObject({
|
|
1175
|
-
argv: ['auto-mode', 'record'],
|
|
1176
|
-
init: { timeoutMs: 8000, stdin: expect.any(String) },
|
|
1177
|
-
});
|
|
1178
|
-
|
|
1179
|
-
expect(JSON.parse(stdin)).toStrictEqual({
|
|
1180
|
-
sessionID: 'session-1',
|
|
1181
|
-
cwd: '/repo',
|
|
1182
|
-
startedAt: expect.any(Number),
|
|
1183
|
-
command: 'gh pr create --fill',
|
|
1184
|
-
resultText: 'https://github.com/dev/app/pull/3\n',
|
|
1185
|
-
});
|
|
1186
|
-
});
|
|
1187
|
-
|
|
1188
|
-
test('it records nothing for a Bash call that cannot create scope or a call that was denied', async ($, on) => {
|
|
1189
|
-
let calls = 0;
|
|
1190
|
-
|
|
1191
|
-
on('session.cwd', () => ({ value: '/repo' }));
|
|
1192
|
-
on('classic.SessionStart', () => ({}));
|
|
1193
|
-
|
|
1194
|
-
on('tool.call', (_api, e) =>
|
|
1195
|
-
e.command === 'git checkout -b x'
|
|
1196
|
-
? { deny: 'no' }
|
|
1197
|
-
: { result: { stdout: '', stderr: '' }, text: '' },
|
|
1198
|
-
);
|
|
1199
|
-
|
|
1200
|
-
on('process.run', () => {
|
|
1201
|
-
calls += 1;
|
|
1202
|
-
|
|
1203
|
-
return {
|
|
1204
|
-
value: {
|
|
1205
|
-
exitCode: 0,
|
|
1206
|
-
stdout: '',
|
|
1207
|
-
stderr: '',
|
|
1208
|
-
isStdoutTruncated: false,
|
|
1209
|
-
isStderrTruncated: false,
|
|
1210
|
-
},
|
|
1211
|
-
};
|
|
1212
|
-
});
|
|
1213
|
-
|
|
1214
|
-
// oxlint-disable-next-line new-cap -- The host event API retains its event spelling.
|
|
1215
|
-
await $.classic.SessionStart({
|
|
1216
|
-
source: 'startup',
|
|
1217
|
-
session_id: 'session-1',
|
|
1218
|
-
cwd: '/repo',
|
|
1219
|
-
transcript_path: '/repo/transcript.jsonl',
|
|
1220
|
-
});
|
|
1221
|
-
|
|
1222
|
-
await $.tool.call({ tool: 'Bash', tool_use_id: 'call-1', command: 'bun test' });
|
|
1223
|
-
await $.tool.call({ tool: 'Bash', tool_use_id: 'call-2', command: 'git checkout -b x' });
|
|
1224
|
-
|
|
1225
|
-
expect(calls).toBe(0);
|
|
1226
|
-
});
|