agentproc 0.1.1 → 0.3.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/package.json +8 -5
- package/src/cli.js +633 -0
- package/src/hub.js +310 -0
- package/src/hub.test.js +327 -0
- package/src/runner.js +395 -0
- package/src/runner.test.js +439 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* Tests for runner.js — the AgentProc canonical bridge implementation.
|
|
4
|
+
*
|
|
5
|
+
* Run with: `node --test src/runner.test.js`
|
|
6
|
+
*
|
|
7
|
+
* Strategy:
|
|
8
|
+
* 1. Pure-function tests: classifyLine, decodeJsonValue, substitute,
|
|
9
|
+
* normalizeProfile, expandEnvRef — no subprocess.
|
|
10
|
+
* 2. run() end-to-end tests: spawn a tiny bash/node helper script that
|
|
11
|
+
* emits protocol lines, assert the runner classifies them correctly.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { test, describe } = require('node:test');
|
|
15
|
+
const assert = require('node:assert');
|
|
16
|
+
const fs = require('node:fs');
|
|
17
|
+
const os = require('node:os');
|
|
18
|
+
const path = require('node:path');
|
|
19
|
+
|
|
20
|
+
const {
|
|
21
|
+
run,
|
|
22
|
+
classifyLine,
|
|
23
|
+
decodeJsonValue,
|
|
24
|
+
substitute,
|
|
25
|
+
normalizeProfile,
|
|
26
|
+
expandEnvRef,
|
|
27
|
+
expandPath,
|
|
28
|
+
PROTOCOL_VERSION,
|
|
29
|
+
} = require('./runner.js');
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// 1. Pure-function tests
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
describe('classifyLine', () => {
|
|
36
|
+
test('identifies AGENT_SESSION:', () => {
|
|
37
|
+
assert.deepStrictEqual(classifyLine('AGENT_SESSION:abc-123'), { kind: 'session', value: 'abc-123' });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('strips whitespace from session id', () => {
|
|
41
|
+
assert.deepStrictEqual(classifyLine('AGENT_SESSION: abc-123 '), { kind: 'session', value: 'abc-123' });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('identifies AGENT_PARTIAL: with JSON string', () => {
|
|
45
|
+
assert.deepStrictEqual(classifyLine('AGENT_PARTIAL:"hello"'), { kind: 'partial', value: 'hello' });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('AGENT_PARTIAL: with newline in JSON', () => {
|
|
49
|
+
assert.deepStrictEqual(classifyLine('AGENT_PARTIAL:"line1\\nline2"'), { kind: 'partial', value: 'line1\nline2' });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('AGENT_PARTIAL: lenient on bad JSON — treats as raw text', () => {
|
|
53
|
+
assert.deepStrictEqual(classifyLine('AGENT_PARTIAL:not json'), { kind: 'partial', value: 'not json' });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('AGENT_PARTIAL: empty value', () => {
|
|
57
|
+
assert.deepStrictEqual(classifyLine('AGENT_PARTIAL:'), { kind: 'partial', value: '' });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('identifies AGENT_ERROR:', () => {
|
|
61
|
+
assert.deepStrictEqual(classifyLine('AGENT_ERROR:"rate limited"'), { kind: 'error', value: 'rate limited' });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('AGENT_ERROR: lenient on bad JSON', () => {
|
|
65
|
+
assert.deepStrictEqual(classifyLine('AGENT_ERROR:boom'), { kind: 'error', value: 'boom' });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('body line: anything else', () => {
|
|
69
|
+
assert.deepStrictEqual(classifyLine('hello world'), { kind: 'body', value: 'hello world' });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('body line: line starting with space is NOT a protocol line', () => {
|
|
73
|
+
// Per spec: agents that want their text to NOT be a protocol line should prefix with space.
|
|
74
|
+
assert.deepStrictEqual(classifyLine(' AGENT_SESSION:foo'), { kind: 'body', value: ' AGENT_SESSION:foo' });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('body line: prefix-like but not exact prefix', () => {
|
|
78
|
+
assert.deepStrictEqual(classifyLine('AGENT_SESSION'), { kind: 'body', value: 'AGENT_SESSION' });
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('decodeJsonValue', () => {
|
|
83
|
+
test('JSON string', () => {
|
|
84
|
+
assert.strictEqual(decodeJsonValue('"hi"'), 'hi');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('JSON string with newline', () => {
|
|
88
|
+
assert.strictEqual(decodeJsonValue('"a\\nb"'), 'a\nb');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('empty', () => {
|
|
92
|
+
assert.strictEqual(decodeJsonValue(''), '');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('lenient: non-JSON returns trimmed raw', () => {
|
|
96
|
+
assert.strictEqual(decodeJsonValue(' not json '), 'not json');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('JSON number becomes string', () => {
|
|
100
|
+
assert.strictEqual(decodeJsonValue('42'), '42');
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('substitute', () => {
|
|
105
|
+
test('replaces MESSAGE', () => {
|
|
106
|
+
assert.strictEqual(substitute('You said: {{MESSAGE}}', { message: 'hi' }), 'You said: hi');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('replaces SESSION_ID', () => {
|
|
110
|
+
assert.strictEqual(substitute('s={{SESSION_ID}}', { sessionId: 'abc' }), 's=abc');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('replaces SESSION_NAME', () => {
|
|
114
|
+
assert.strictEqual(substitute('n={{SESSION_NAME}}', { sessionName: 'work' }), 'n=work');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('empty SESSION_ID when new session', () => {
|
|
118
|
+
assert.strictEqual(substitute('s={{SESSION_ID}}', { sessionId: '' }), 's=');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('multiple placeholders', () => {
|
|
122
|
+
assert.strictEqual(
|
|
123
|
+
substitute('{{MESSAGE}} [{{SESSION_ID}}] ({{SESSION_NAME}})', {
|
|
124
|
+
message: 'hi', sessionId: 's1', sessionName: 'work',
|
|
125
|
+
}),
|
|
126
|
+
'hi [s1] (work)',
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('expandEnvRef', () => {
|
|
132
|
+
test('expands ${VAR} from env', () => {
|
|
133
|
+
assert.strictEqual(expandEnvRef('${HOME}', { HOME: '/u/x' }), '/u/x');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('unknown var → empty', () => {
|
|
137
|
+
assert.strictEqual(expandEnvRef('${NOPE}', {}), '');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('no refs', () => {
|
|
141
|
+
assert.strictEqual(expandEnvRef('plain value', {}), 'plain value');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('mixed', () => {
|
|
145
|
+
assert.strictEqual(
|
|
146
|
+
expandEnvRef('key=${HOME} and ${missing}', { HOME: '/h' }),
|
|
147
|
+
'key=/h and ',
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('expandPath', () => {
|
|
153
|
+
test('~ expands to homedir', () => {
|
|
154
|
+
assert.strictEqual(expandPath('~'), os.homedir());
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('~/foo expands', () => {
|
|
158
|
+
assert.strictEqual(expandPath('~/foo'), path.join(os.homedir(), 'foo'));
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('absolute path unchanged', () => {
|
|
162
|
+
assert.strictEqual(expandPath('/usr/bin'), '/usr/bin');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('relative path unchanged', () => {
|
|
166
|
+
assert.strictEqual(expandPath('./foo'), './foo');
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('normalizeProfile', () => {
|
|
171
|
+
test('minimal valid profile', () => {
|
|
172
|
+
const p = normalizeProfile({ command: 'bash ./x.sh' });
|
|
173
|
+
assert.deepStrictEqual(p.argv, ['bash', './x.sh']);
|
|
174
|
+
assert.strictEqual(p.stdin, 'none');
|
|
175
|
+
assert.strictEqual(p.streaming, true);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('hub form: profile nested under agentproc:', () => {
|
|
179
|
+
const p = normalizeProfile({ agentproc: { command: 'node ./x.js' } });
|
|
180
|
+
assert.deepStrictEqual(p.argv, ['node', './x.js']);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('rejects missing command', () => {
|
|
184
|
+
assert.throws(() => normalizeProfile({}), /command must be a non-empty string/);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('rejects empty command', () => {
|
|
188
|
+
assert.throws(() => normalizeProfile({ command: ' ' }), /command must be a non-empty string/);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('rejects non-object', () => {
|
|
192
|
+
assert.throws(() => normalizeProfile(null), /must be an object/);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('argv splits on whitespace, multiple spaces', () => {
|
|
196
|
+
const p = normalizeProfile({ command: 'bash ./spaced.sh' });
|
|
197
|
+
assert.deepStrictEqual(p.argv, ['bash', './spaced.sh']);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('args field defaults to empty array', () => {
|
|
201
|
+
const p = normalizeProfile({ command: 'x' });
|
|
202
|
+
assert.deepStrictEqual(p.args, []);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('args field is preserved (cast to string)', () => {
|
|
206
|
+
const p = normalizeProfile({ command: 'x', args: ['--foo', 42] });
|
|
207
|
+
assert.deepStrictEqual(p.args, ['--foo', '42']);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('cwd ~ is expanded', () => {
|
|
211
|
+
const p = normalizeProfile({ command: 'x', cwd: '~/proj' });
|
|
212
|
+
assert.strictEqual(p.cwd, path.join(os.homedir(), 'proj'));
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('stdin: message → message, anything else → none', () => {
|
|
216
|
+
assert.strictEqual(normalizeProfile({ command: 'x', stdin: 'message' }).stdin, 'message');
|
|
217
|
+
assert.strictEqual(normalizeProfile({ command: 'x', stdin: 'none' }).stdin, 'none');
|
|
218
|
+
assert.strictEqual(normalizeProfile({ command: 'x', stdin: 'bogus' }).stdin, 'none');
|
|
219
|
+
assert.strictEqual(normalizeProfile({ command: 'x' }).stdin, 'none');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('streaming: false is honored, anything else true', () => {
|
|
223
|
+
assert.strictEqual(normalizeProfile({ command: 'x', streaming: false }).streaming, false);
|
|
224
|
+
assert.strictEqual(normalizeProfile({ command: 'x', streaming: true }).streaming, true);
|
|
225
|
+
assert.strictEqual(normalizeProfile({ command: 'x' }).streaming, true);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
// 2. run() end-to-end tests with tiny agent scripts
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
function writeScript(content) {
|
|
234
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ap-runner-'));
|
|
235
|
+
const file = path.join(dir, 'agent.sh');
|
|
236
|
+
fs.writeFileSync(file, content, { mode: 0o755 });
|
|
237
|
+
return file;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
describe('run() — end-to-end', () => {
|
|
241
|
+
test('simple reply body', async () => {
|
|
242
|
+
const agent = writeScript('#!/usr/bin/env bash\necho "hello"\n');
|
|
243
|
+
const r = await run(
|
|
244
|
+
{ command: agent },
|
|
245
|
+
{ message: 'hi' },
|
|
246
|
+
);
|
|
247
|
+
assert.strictEqual(r.reply.trim(), 'hello');
|
|
248
|
+
assert.strictEqual(r.sessionId, '');
|
|
249
|
+
assert.strictEqual(r.error, '');
|
|
250
|
+
assert.strictEqual(r.exitCode, 0);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test('AGENT_SESSION: last wins', async () => {
|
|
254
|
+
const agent = writeScript(
|
|
255
|
+
'#!/usr/bin/env bash\n' +
|
|
256
|
+
'echo "AGENT_SESSION:first"\n' +
|
|
257
|
+
'echo "AGENT_SESSION:second"\n' +
|
|
258
|
+
'echo "done"\n'
|
|
259
|
+
);
|
|
260
|
+
const r = await run({ command: agent }, { message: 'hi' });
|
|
261
|
+
assert.strictEqual(r.sessionId, 'second');
|
|
262
|
+
assert.strictEqual(r.reply.trim(), 'done');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test('AGENT_PARTIAL: triggers onPartial callback', async () => {
|
|
266
|
+
const agent = writeScript(
|
|
267
|
+
'#!/usr/bin/env bash\n' +
|
|
268
|
+
'echo "AGENT_PARTIAL:\\"chunk1\\""\n' +
|
|
269
|
+
'echo "AGENT_PARTIAL:\\"chunk2\\""\n' +
|
|
270
|
+
'echo "final"\n'
|
|
271
|
+
);
|
|
272
|
+
const partials = [];
|
|
273
|
+
const r = await run(
|
|
274
|
+
{ command: agent },
|
|
275
|
+
{ message: 'hi', onPartial: (t) => partials.push(t) },
|
|
276
|
+
);
|
|
277
|
+
assert.deepStrictEqual(partials, ['chunk1', 'chunk2']);
|
|
278
|
+
assert.strictEqual(r.reply.trim(), 'final');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test('AGENT_PARTIAL: when streaming=false, onPartial NOT called', async () => {
|
|
282
|
+
const agent = writeScript(
|
|
283
|
+
'#!/usr/bin/env bash\n' +
|
|
284
|
+
'echo "AGENT_PARTIAL:\\"chunk1\\""\n' +
|
|
285
|
+
'echo "final"\n'
|
|
286
|
+
);
|
|
287
|
+
const partials = [];
|
|
288
|
+
const r = await run(
|
|
289
|
+
{ command: agent },
|
|
290
|
+
{ message: 'hi', streaming: false, onPartial: (t) => partials.push(t) },
|
|
291
|
+
);
|
|
292
|
+
assert.deepStrictEqual(partials, []);
|
|
293
|
+
// partial lines are still NOT added to reply body (they're protocol lines)
|
|
294
|
+
assert.strictEqual(r.reply.trim(), 'final');
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test('AGENT_ERROR: surfaces in result.error', async () => {
|
|
298
|
+
const agent = writeScript(
|
|
299
|
+
'#!/usr/bin/env bash\n' +
|
|
300
|
+
'echo "AGENT_PARTIAL:\\"thinking...\\""\n' +
|
|
301
|
+
'echo "AGENT_ERROR:\\"rate limited\\""\n' +
|
|
302
|
+
'exit 1\n'
|
|
303
|
+
);
|
|
304
|
+
const r = await run({ command: agent }, { message: 'hi' });
|
|
305
|
+
assert.strictEqual(r.error, 'rate limited');
|
|
306
|
+
assert.strictEqual(r.exitCode, 1);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test('AGENT_ERROR: marks exit 1 even if process exits 0', async () => {
|
|
310
|
+
const agent = writeScript(
|
|
311
|
+
'#!/usr/bin/env bash\n' +
|
|
312
|
+
'echo "AGENT_ERROR:\\"soft fail\\""\n' +
|
|
313
|
+
'exit 0\n'
|
|
314
|
+
);
|
|
315
|
+
const r = await run({ command: agent }, { message: 'hi' });
|
|
316
|
+
assert.strictEqual(r.error, 'soft fail');
|
|
317
|
+
assert.strictEqual(r.exitCode, 1);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test('reply body lines are NOT prefixed with protocol markers', async () => {
|
|
321
|
+
const agent = writeScript(
|
|
322
|
+
'#!/usr/bin/env bash\n' +
|
|
323
|
+
'echo " AGENT_SESSION:foo"\n' + // leading space → body
|
|
324
|
+
'echo "real reply"\n'
|
|
325
|
+
);
|
|
326
|
+
const r = await run({ command: agent }, { message: 'hi' });
|
|
327
|
+
assert.strictEqual(r.sessionId, '');
|
|
328
|
+
assert.strictEqual(r.reply.trim().split('\n').length, 2);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test('exit code propagates from agent', async () => {
|
|
332
|
+
const agent = writeScript('#!/usr/bin/env bash\nexit 3\n');
|
|
333
|
+
const r = await run({ command: agent }, { message: 'hi' });
|
|
334
|
+
assert.strictEqual(r.exitCode, 3);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test('message is injected as AGENT_MESSAGE', async () => {
|
|
338
|
+
const agent = writeScript('#!/usr/bin/env bash\necho "got: $AGENT_MESSAGE"\n');
|
|
339
|
+
const r = await run({ command: agent }, { message: 'payload' });
|
|
340
|
+
assert.strictEqual(r.reply.trim(), 'got: payload');
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test('AGENT_SESSION_ID injected from options.sessionId', async () => {
|
|
344
|
+
const agent = writeScript('#!/usr/bin/env bash\necho "prev: $AGENT_SESSION_ID"\n');
|
|
345
|
+
const r = await run({ command: agent }, { message: 'hi', sessionId: 'prev-123' });
|
|
346
|
+
assert.strictEqual(r.reply.trim(), 'prev: prev-123');
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test('AGENT_PROTOCOL_VERSION injected', async () => {
|
|
350
|
+
const agent = writeScript('#!/usr/bin/env bash\necho "pv=$AGENT_PROTOCOL_VERSION"\n');
|
|
351
|
+
const r = await run({ command: agent }, { message: 'hi' });
|
|
352
|
+
assert.strictEqual(r.reply.trim(), `pv=${PROTOCOL_VERSION}`);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test('AGENT_STREAMING reflects streaming option', async () => {
|
|
356
|
+
const agent = writeScript('#!/usr/bin/env bash\necho "stream=$AGENT_STREAMING"\n');
|
|
357
|
+
const r1 = await run({ command: agent }, { message: 'hi' });
|
|
358
|
+
assert.strictEqual(r1.reply.trim(), 'stream=1');
|
|
359
|
+
const r2 = await run({ command: agent }, { message: 'hi', streaming: false });
|
|
360
|
+
assert.strictEqual(r2.reply.trim(), 'stream=0');
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test('profile.env injects env vars with ${VAR} expansion', async () => {
|
|
364
|
+
const agent = writeScript('#!/usr/bin/env bash\necho "MY_KEY=$MY_KEY"\n');
|
|
365
|
+
const r = await run(
|
|
366
|
+
{ command: agent, env: { MY_KEY: '${HOME}' } },
|
|
367
|
+
{ message: 'hi' },
|
|
368
|
+
);
|
|
369
|
+
assert.strictEqual(r.reply.trim(), `MY_KEY=${process.env.HOME}`);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test('{{MESSAGE}} placeholder in args', async () => {
|
|
373
|
+
const agent = writeScript(
|
|
374
|
+
'#!/usr/bin/env bash\necho "args: $1"\n'
|
|
375
|
+
);
|
|
376
|
+
const r = await run(
|
|
377
|
+
{ command: agent, args: ['{{MESSAGE}}'] },
|
|
378
|
+
{ message: 'hello' },
|
|
379
|
+
);
|
|
380
|
+
assert.strictEqual(r.reply.trim(), 'args: hello');
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test('extraEnv from options is applied', async () => {
|
|
384
|
+
const agent = writeScript('#!/usr/bin/env bash\necho "x=$X"\n');
|
|
385
|
+
const r = await run(
|
|
386
|
+
{ command: agent },
|
|
387
|
+
{ message: 'hi', extraEnv: { X: 'extra' } },
|
|
388
|
+
);
|
|
389
|
+
assert.strictEqual(r.reply.trim(), 'x=extra');
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test('stdin: message — agent reads AGENT_MESSAGE from stdin', async () => {
|
|
393
|
+
const agent = writeScript(
|
|
394
|
+
'#!/usr/bin/env bash\nread line\necho "stdin: $line"\n'
|
|
395
|
+
);
|
|
396
|
+
const r = await run(
|
|
397
|
+
{ command: agent, stdin: 'message' },
|
|
398
|
+
{ message: 'via-stdin' },
|
|
399
|
+
);
|
|
400
|
+
assert.strictEqual(r.reply.trim(), 'stdin: via-stdin');
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test('timeout kills long-running agent', async () => {
|
|
404
|
+
const agent = writeScript(
|
|
405
|
+
'#!/usr/bin/env bash\nsleep 30\necho "should not reach"\n'
|
|
406
|
+
);
|
|
407
|
+
const r = await run(
|
|
408
|
+
{ command: agent, kill_grace_secs: 1 },
|
|
409
|
+
{ message: 'hi', timeoutSecs: 1 },
|
|
410
|
+
);
|
|
411
|
+
assert.strictEqual(r.timedOut, true);
|
|
412
|
+
assert.strictEqual(r.exitCode, 124);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test('multiline reply body preserves newlines', async () => {
|
|
416
|
+
const agent = writeScript(
|
|
417
|
+
'#!/usr/bin/env bash\n' +
|
|
418
|
+
'echo "line 1"\n' +
|
|
419
|
+
'echo "line 2"\n' +
|
|
420
|
+
'echo "line 3"\n'
|
|
421
|
+
);
|
|
422
|
+
const r = await run({ command: agent }, { message: 'hi' });
|
|
423
|
+
// Lines are joined with \n; the trailing newline is the caller's responsibility
|
|
424
|
+
// (the CLI adds it when printing).
|
|
425
|
+
assert.strictEqual(r.reply, 'line 1\nline 2\nline 3');
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test('spawn error (command not found) → exit 1', async () => {
|
|
429
|
+
const r = await run(
|
|
430
|
+
{ command: '/nonexistent/command/xyz' },
|
|
431
|
+
{ message: 'hi' },
|
|
432
|
+
);
|
|
433
|
+
assert.strictEqual(r.exitCode, 1);
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
test('PROTOCOL_VERSION is "0.1"', () => {
|
|
438
|
+
assert.strictEqual(PROTOCOL_VERSION, '0.1');
|
|
439
|
+
});
|