rafcode 2.1.0 → 2.1.1

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.
@@ -140,8 +140,7 @@ describe('ClaudeRunner', () => {
140
140
  const runPromise = runner.run('test prompt', { timeout: 5 });
141
141
 
142
142
  // Emit some output and close the process normally
143
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: '<promise>COMPLETE</promise>' }] } });
144
- mockProc.stdout.emit('data', Buffer.from(event + '\n'));
143
+ mockProc.stdout.emit('data', Buffer.from('<promise>COMPLETE</promise>'));
145
144
  mockProc.emit('close', 0);
146
145
 
147
146
  const result = await runPromise;
@@ -163,9 +162,8 @@ describe('ClaudeRunner', () => {
163
162
 
164
163
  const runPromise1 = runner.run('first prompt', { timeout: 2 });
165
164
 
166
- // Complete first call quickly (NDJSON event)
167
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'output' }] } });
168
- mockProc1.stdout.emit('data', Buffer.from(event + '\n'));
165
+ // Complete first call quickly
166
+ mockProc1.stdout.emit('data', Buffer.from('output'));
169
167
  mockProc1.emit('close', 0);
170
168
  await runPromise1;
171
169
 
@@ -213,9 +211,8 @@ describe('ClaudeRunner', () => {
213
211
  // Advance a bit but not to timeout
214
212
  jest.advanceTimersByTime(30000);
215
213
 
216
- // Complete process (NDJSON event)
217
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'output' }] } });
218
- mockProc.stdout.emit('data', Buffer.from(event + '\n'));
214
+ // Complete process
215
+ mockProc.stdout.emit('data', Buffer.from('output'));
219
216
  mockProc.emit('close', 0);
220
217
 
221
218
  const result = await runPromise;
@@ -235,9 +232,8 @@ describe('ClaudeRunner', () => {
235
232
  jest.advanceTimersByTime(1000);
236
233
  expect(mockProc.kill).not.toHaveBeenCalled();
237
234
 
238
- // Complete the process normally before reaching the 60 minute timeout (NDJSON event)
239
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'output' }] } });
240
- mockProc.stdout.emit('data', Buffer.from(event + '\n'));
235
+ // Complete the process normally before reaching the 60 minute timeout
236
+ mockProc.stdout.emit('data', Buffer.from('output'));
241
237
  mockProc.emit('close', 0);
242
238
 
243
239
  const result = await runPromise;
@@ -309,9 +305,8 @@ describe('ClaudeRunner', () => {
309
305
  const runner = new ClaudeRunner();
310
306
  const runPromise = runner.run('test prompt', { timeout: 60 });
311
307
 
312
- // Emit context overflow message as NDJSON assistant event (run() now uses stream-json)
313
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'Error: context length exceeded' }] } });
314
- mockProc.stdout.emit('data', Buffer.from(event + '\n'));
308
+ // Emit context overflow message
309
+ mockProc.stdout.emit('data', Buffer.from('Error: context length exceeded'));
315
310
 
316
311
  const result = await runPromise;
317
312
  expect(result.contextOverflow).toBe(true);
@@ -338,9 +333,7 @@ describe('ClaudeRunner', () => {
338
333
  const runner = new ClaudeRunner();
339
334
  const runPromise = runner.run('test prompt', { timeout: 60 });
340
335
 
341
- // Emit as NDJSON assistant event (run() now uses stream-json)
342
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: `Error: ${pattern}` }] } });
343
- mockProc.stdout.emit('data', Buffer.from(event + '\n'));
336
+ mockProc.stdout.emit('data', Buffer.from(`Error: ${pattern}`));
344
337
 
345
338
  const result = await runPromise;
346
339
  expect(result.contextOverflow).toBe(true);
@@ -362,18 +355,17 @@ describe('ClaudeRunner', () => {
362
355
  return proc;
363
356
  }
364
357
 
365
- it('should collect all stdout output from NDJSON events', async () => {
358
+ it('should collect all stdout output', async () => {
366
359
  const mockProc = createMockProcess();
367
360
  mockSpawn.mockReturnValue(mockProc);
368
361
 
369
362
  const runner = new ClaudeRunner();
370
363
  const runPromise = runner.run('test prompt', { timeout: 60 });
371
364
 
372
- // Emit NDJSON assistant events (run() now uses stream-json)
373
- const event1 = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'chunk1' }] } });
374
- const event2 = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'chunk2' }] } });
375
- const event3 = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'chunk3' }] } });
376
- mockProc.stdout.emit('data', Buffer.from(event1 + '\n' + event2 + '\n' + event3 + '\n'));
365
+ // Emit multiple chunks
366
+ mockProc.stdout.emit('data', Buffer.from('chunk1'));
367
+ mockProc.stdout.emit('data', Buffer.from('chunk2'));
368
+ mockProc.stdout.emit('data', Buffer.from('chunk3'));
377
369
  mockProc.emit('close', 0);
378
370
 
379
371
  const result = await runPromise;
@@ -704,7 +696,7 @@ describe('ClaudeRunner', () => {
704
696
  expect(spawnArgs).toContain('--verbose');
705
697
  });
706
698
 
707
- it('should include --output-format stream-json and --verbose flags in run()', async () => {
699
+ it('should NOT include --output-format or --verbose flags in run()', async () => {
708
700
  const mockProc = createMockProcess();
709
701
  mockSpawn.mockReturnValue(mockProc);
710
702
 
@@ -715,9 +707,9 @@ describe('ClaudeRunner', () => {
715
707
  await runPromise;
716
708
 
717
709
  const spawnArgs = mockSpawn.mock.calls[0][1] as string[];
718
- expect(spawnArgs).toContain('--output-format');
719
- expect(spawnArgs).toContain('stream-json');
720
- expect(spawnArgs).toContain('--verbose');
710
+ expect(spawnArgs).not.toContain('--output-format');
711
+ expect(spawnArgs).not.toContain('stream-json');
712
+ expect(spawnArgs).not.toContain('--verbose');
721
713
  });
722
714
 
723
715
  it('should extract text from NDJSON assistant events', async () => {
@@ -804,9 +796,8 @@ describe('ClaudeRunner', () => {
804
796
  jest.advanceTimersByTime(60000);
805
797
  expect(mockProc2.kill).not.toHaveBeenCalled();
806
798
 
807
- // Complete successfully (NDJSON event)
808
- const successEvent = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'success' }] } });
809
- mockProc2.stdout.emit('data', Buffer.from(successEvent + '\n'));
799
+ // Complete successfully
800
+ mockProc2.stdout.emit('data', Buffer.from('success'));
810
801
  mockProc2.emit('close', 0);
811
802
 
812
803
  const result2 = await runPromise2;
@@ -827,9 +818,8 @@ describe('ClaudeRunner', () => {
827
818
  // Run for 4 minutes (240000ms)
828
819
  jest.advanceTimersByTime(240000);
829
820
 
830
- // Fail without timeout (NDJSON event)
831
- const failEvent = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: '<promise>FAILED</promise>' }] } });
832
- mockProc1.stdout.emit('data', Buffer.from(failEvent + '\n'));
821
+ // Fail without timeout
822
+ mockProc1.stdout.emit('data', Buffer.from('<promise>FAILED</promise>'));
833
823
  mockProc1.emit('close', 1);
834
824
 
835
825
  const result1 = await runPromise1;
@@ -879,9 +869,8 @@ describe('ClaudeRunner', () => {
879
869
  const runner = new ClaudeRunner();
880
870
  const runPromise = runner.run('test prompt', { timeout: 60 });
881
871
 
882
- // Emit output with completion marker (NDJSON event)
883
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'Writing outcome...\n<promise>COMPLETE</promise>' }] } });
884
- mockProc.stdout.emit('data', Buffer.from(event + '\n'));
872
+ // Emit output with completion marker
873
+ mockProc.stdout.emit('data', Buffer.from('Writing outcome...\n<promise>COMPLETE</promise>\n'));
885
874
 
886
875
  // Process should not be killed immediately
887
876
  expect(mockProc.kill).not.toHaveBeenCalled();
@@ -906,9 +895,8 @@ describe('ClaudeRunner', () => {
906
895
  const runner = new ClaudeRunner();
907
896
  const runPromise = runner.run('test prompt', { timeout: 60 });
908
897
 
909
- // Emit output with failed marker (NDJSON event)
910
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: '<promise>FAILED</promise>\nReason: test error' }] } });
911
- mockProc.stdout.emit('data', Buffer.from(event + '\n'));
898
+ // Emit output with failed marker
899
+ mockProc.stdout.emit('data', Buffer.from('<promise>FAILED</promise>\nReason: test error'));
912
900
 
913
901
  // Advance past grace period
914
902
  jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS + 1);
@@ -925,15 +913,13 @@ describe('ClaudeRunner', () => {
925
913
  const runner = new ClaudeRunner();
926
914
  const runPromise = runner.run('test prompt', { timeout: 60 });
927
915
 
928
- // Emit partial output (no marker yet) as NDJSON
929
- const event1 = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'Working on task...' }] } });
930
- mockProc.stdout.emit('data', Buffer.from(event1 + '\n'));
916
+ // Emit partial output (no marker yet)
917
+ mockProc.stdout.emit('data', Buffer.from('Working on task...\n'));
931
918
  jest.advanceTimersByTime(10000);
932
919
  expect(mockProc.kill).not.toHaveBeenCalled();
933
920
 
934
- // Emit marker in second chunk as NDJSON
935
- const event2 = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: '<promise>COMPLETE</promise>' }] } });
936
- mockProc.stdout.emit('data', Buffer.from(event2 + '\n'));
921
+ // Emit marker in second chunk
922
+ mockProc.stdout.emit('data', Buffer.from('<promise>COMPLETE</promise>\n'));
937
923
 
938
924
  // Grace period starts now - advance past it
939
925
  jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS + 1);
@@ -951,9 +937,8 @@ describe('ClaudeRunner', () => {
951
937
  const runner = new ClaudeRunner();
952
938
  const runPromise = runner.run('test prompt', { timeout: 60 });
953
939
 
954
- // Emit completion marker as NDJSON
955
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: '<promise>COMPLETE</promise>' }] } });
956
- mockProc.stdout.emit('data', Buffer.from(event + '\n'));
940
+ // Emit completion marker
941
+ mockProc.stdout.emit('data', Buffer.from('<promise>COMPLETE</promise>\n'));
957
942
 
958
943
  // Process exits naturally before grace period
959
944
  jest.advanceTimersByTime(5000);
@@ -983,9 +968,8 @@ describe('ClaudeRunner', () => {
983
968
  outcomeFilePath: outcomePath,
984
969
  });
985
970
 
986
- // No output marker in stdout - just regular output (NDJSON event)
987
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'Working on task...' }] } });
988
- mockProc.stdout.emit('data', Buffer.from(event + '\n'));
971
+ // No output marker in stdout - just regular output
972
+ mockProc.stdout.emit('data', Buffer.from('Working on task...'));
989
973
 
990
974
  // After some time, outcome file appears with marker
991
975
  jest.advanceTimersByTime(OUTCOME_POLL_INTERVAL_MS - 1);
@@ -1029,9 +1013,8 @@ describe('ClaudeRunner', () => {
1029
1013
  jest.advanceTimersByTime(OUTCOME_POLL_INTERVAL_MS * 5);
1030
1014
  expect(mockProc.kill).not.toHaveBeenCalled();
1031
1015
 
1032
- // Complete normally (NDJSON event)
1033
- const doneEvent = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'done' }] } });
1034
- mockProc.stdout.emit('data', Buffer.from(doneEvent + '\n'));
1016
+ // Complete normally
1017
+ mockProc.stdout.emit('data', Buffer.from('done'));
1035
1018
  mockProc.emit('close', 0);
1036
1019
 
1037
1020
  await runPromise;
@@ -1064,16 +1047,14 @@ describe('ClaudeRunner', () => {
1064
1047
  const runner = new ClaudeRunner();
1065
1048
  const runPromise = runner.run('test prompt', { timeout: 60 });
1066
1049
 
1067
- // Emit first marker as NDJSON
1068
- const event1 = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: '<promise>COMPLETE</promise>' }] } });
1069
- mockProc.stdout.emit('data', Buffer.from(event1 + '\n'));
1050
+ // Emit first marker
1051
+ mockProc.stdout.emit('data', Buffer.from('<promise>COMPLETE</promise>\n'));
1070
1052
 
1071
1053
  // Advance halfway through grace period
1072
1054
  jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS / 2);
1073
1055
 
1074
- // Emit second marker (e.g., Claude reading back what it wrote) as NDJSON
1075
- const event2 = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'Verified: <promise>COMPLETE</promise>' }] } });
1076
- mockProc.stdout.emit('data', Buffer.from(event2 + '\n'));
1056
+ // Emit second marker (e.g., Claude reading back what it wrote)
1057
+ mockProc.stdout.emit('data', Buffer.from('Verified: <promise>COMPLETE</promise>\n'));
1077
1058
 
1078
1059
  // Advance remaining grace period from FIRST detection
1079
1060
  jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS / 2 + 1);
@@ -1129,9 +1110,8 @@ describe('ClaudeRunner', () => {
1129
1110
  commitContext,
1130
1111
  });
1131
1112
 
1132
- // Emit COMPLETE marker as NDJSON
1133
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: '<promise>COMPLETE</promise>' }] } });
1134
- mockProc.stdout.emit('data', Buffer.from(event + '\n'));
1113
+ // Emit COMPLETE marker
1114
+ mockProc.stdout.emit('data', Buffer.from('<promise>COMPLETE</promise>\n'));
1135
1115
 
1136
1116
  // Advance to grace period expiry - commit already verified
1137
1117
  jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS + 1);
@@ -1155,9 +1135,8 @@ describe('ClaudeRunner', () => {
1155
1135
  commitContext,
1156
1136
  });
1157
1137
 
1158
- // Emit COMPLETE marker as NDJSON
1159
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: '<promise>COMPLETE</promise>' }] } });
1160
- mockProc.stdout.emit('data', Buffer.from(event + '\n'));
1138
+ // Emit COMPLETE marker
1139
+ mockProc.stdout.emit('data', Buffer.from('<promise>COMPLETE</promise>\n'));
1161
1140
 
1162
1141
  // Advance past initial grace period - commit not found, should extend
1163
1142
  jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS + 1);
@@ -1191,9 +1170,8 @@ describe('ClaudeRunner', () => {
1191
1170
  commitContext,
1192
1171
  });
1193
1172
 
1194
- // Emit COMPLETE marker as NDJSON
1195
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: '<promise>COMPLETE</promise>' }] } });
1196
- mockProc.stdout.emit('data', Buffer.from(event + '\n'));
1173
+ // Emit COMPLETE marker
1174
+ mockProc.stdout.emit('data', Buffer.from('<promise>COMPLETE</promise>\n'));
1197
1175
 
1198
1176
  // Advance past initial grace period
1199
1177
  jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS + 1);
@@ -1219,9 +1197,8 @@ describe('ClaudeRunner', () => {
1219
1197
  commitContext,
1220
1198
  });
1221
1199
 
1222
- // Emit FAILED marker (not COMPLETE) as NDJSON
1223
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: '<promise>FAILED</promise>' }] } });
1224
- mockProc.stdout.emit('data', Buffer.from(event + '\n'));
1200
+ // Emit FAILED marker (not COMPLETE)
1201
+ mockProc.stdout.emit('data', Buffer.from('<promise>FAILED</promise>\n'));
1225
1202
 
1226
1203
  // Grace period should expire normally without extension
1227
1204
  jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS + 1);
@@ -1240,9 +1217,8 @@ describe('ClaudeRunner', () => {
1240
1217
  // No commitContext provided
1241
1218
  });
1242
1219
 
1243
- // Emit COMPLETE marker as NDJSON
1244
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: '<promise>COMPLETE</promise>' }] } });
1245
- mockProc.stdout.emit('data', Buffer.from(event + '\n'));
1220
+ // Emit COMPLETE marker
1221
+ mockProc.stdout.emit('data', Buffer.from('<promise>COMPLETE</promise>\n'));
1246
1222
 
1247
1223
  // Grace period should expire normally (no commit check)
1248
1224
  jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS + 1);
@@ -1267,9 +1243,8 @@ describe('ClaudeRunner', () => {
1267
1243
  commitContext,
1268
1244
  });
1269
1245
 
1270
- // Emit COMPLETE marker as NDJSON
1271
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: '<promise>COMPLETE</promise>' }] } });
1272
- mockProc.stdout.emit('data', Buffer.from(event + '\n'));
1246
+ // Emit COMPLETE marker
1247
+ mockProc.stdout.emit('data', Buffer.from('<promise>COMPLETE</promise>\n'));
1273
1248
 
1274
1249
  // Grace period expires - commit message doesn't match, should extend
1275
1250
  jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS + 1);
@@ -1302,9 +1277,8 @@ describe('ClaudeRunner', () => {
1302
1277
  commitContext,
1303
1278
  });
1304
1279
 
1305
- // Emit COMPLETE marker as NDJSON
1306
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: '<promise>COMPLETE</promise>' }] } });
1307
- mockProc.stdout.emit('data', Buffer.from(event + '\n'));
1280
+ // Emit COMPLETE marker
1281
+ mockProc.stdout.emit('data', Buffer.from('<promise>COMPLETE</promise>\n'));
1308
1282
 
1309
1283
  // Grace period expires - file not committed, should extend
1310
1284
  jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS + 1);
@@ -1321,329 +1295,4 @@ describe('ClaudeRunner', () => {
1321
1295
  await runPromise;
1322
1296
  });
1323
1297
  });
1324
-
1325
- describe('session ID extraction', () => {
1326
- function createMockProcess() {
1327
- const stdout = new EventEmitter();
1328
- const stderr = new EventEmitter();
1329
- const proc = new EventEmitter() as any;
1330
- proc.stdout = stdout;
1331
- proc.stderr = stderr;
1332
- proc.kill = jest.fn();
1333
- return proc;
1334
- }
1335
-
1336
- it('should extract sessionId from system init event in run()', async () => {
1337
- const mockProc = createMockProcess();
1338
- mockSpawn.mockReturnValue(mockProc);
1339
-
1340
- const runner = new ClaudeRunner();
1341
- const runPromise = runner.run('test prompt', { timeout: 60 });
1342
-
1343
- // Emit system init event with session_id
1344
- const initEvent = JSON.stringify({
1345
- type: 'system',
1346
- subtype: 'init',
1347
- session_id: 'test-session-123',
1348
- tools: ['Read', 'Write'],
1349
- model: 'claude-opus-4-6',
1350
- });
1351
- const textEvent = JSON.stringify({
1352
- type: 'assistant',
1353
- message: { content: [{ type: 'text', text: 'done' }] },
1354
- });
1355
- mockProc.stdout.emit('data', Buffer.from(initEvent + '\n' + textEvent + '\n'));
1356
- mockProc.emit('close', 0);
1357
-
1358
- const result = await runPromise;
1359
- expect(result.sessionId).toBe('test-session-123');
1360
- });
1361
-
1362
- it('should extract sessionId from system init event in runVerbose()', async () => {
1363
- const mockProc = createMockProcess();
1364
- mockSpawn.mockReturnValue(mockProc);
1365
-
1366
- const runner = new ClaudeRunner();
1367
- const runPromise = runner.runVerbose('test prompt', { timeout: 60 });
1368
-
1369
- // Emit system init event with session_id
1370
- const initEvent = JSON.stringify({
1371
- type: 'system',
1372
- subtype: 'init',
1373
- session_id: 'verbose-session-456',
1374
- tools: ['Read', 'Write'],
1375
- model: 'claude-opus-4-6',
1376
- });
1377
- mockProc.stdout.emit('data', Buffer.from(initEvent + '\n'));
1378
- mockProc.emit('close', 0);
1379
-
1380
- const result = await runPromise;
1381
- expect(result.sessionId).toBe('verbose-session-456');
1382
- });
1383
-
1384
- it('should return undefined sessionId when no system init event', async () => {
1385
- const mockProc = createMockProcess();
1386
- mockSpawn.mockReturnValue(mockProc);
1387
-
1388
- const runner = new ClaudeRunner();
1389
- const runPromise = runner.run('test prompt', { timeout: 60 });
1390
-
1391
- // Only emit assistant event, no system init
1392
- const textEvent = JSON.stringify({
1393
- type: 'assistant',
1394
- message: { content: [{ type: 'text', text: 'output' }] },
1395
- });
1396
- mockProc.stdout.emit('data', Buffer.from(textEvent + '\n'));
1397
- mockProc.emit('close', 0);
1398
-
1399
- const result = await runPromise;
1400
- expect(result.sessionId).toBeUndefined();
1401
- });
1402
-
1403
- it('should expose sessionId via getter on ClaudeRunner', async () => {
1404
- const mockProc = createMockProcess();
1405
- mockSpawn.mockReturnValue(mockProc);
1406
-
1407
- const runner = new ClaudeRunner();
1408
- expect(runner.sessionId).toBeUndefined();
1409
-
1410
- const runPromise = runner.run('test prompt', { timeout: 60 });
1411
-
1412
- const initEvent = JSON.stringify({
1413
- type: 'system',
1414
- subtype: 'init',
1415
- session_id: 'getter-session-789',
1416
- tools: [],
1417
- model: 'claude-opus-4-6',
1418
- });
1419
- mockProc.stdout.emit('data', Buffer.from(initEvent + '\n'));
1420
- mockProc.emit('close', 0);
1421
-
1422
- await runPromise;
1423
- expect(runner.sessionId).toBe('getter-session-789');
1424
- });
1425
-
1426
- it('should only capture the first sessionId (ignore duplicates)', async () => {
1427
- const mockProc = createMockProcess();
1428
- mockSpawn.mockReturnValue(mockProc);
1429
-
1430
- const runner = new ClaudeRunner();
1431
- const runPromise = runner.run('test prompt', { timeout: 60 });
1432
-
1433
- // Emit two system init events (shouldn't happen but test robustness)
1434
- const init1 = JSON.stringify({ type: 'system', subtype: 'init', session_id: 'first-session' });
1435
- const init2 = JSON.stringify({ type: 'system', subtype: 'init', session_id: 'second-session' });
1436
- mockProc.stdout.emit('data', Buffer.from(init1 + '\n' + init2 + '\n'));
1437
- mockProc.emit('close', 0);
1438
-
1439
- const result = await runPromise;
1440
- expect(result.sessionId).toBe('first-session');
1441
- });
1442
- });
1443
-
1444
- describe('runResume()', () => {
1445
- function createMockProcess() {
1446
- const stdout = new EventEmitter();
1447
- const stderr = new EventEmitter();
1448
- const proc = new EventEmitter() as any;
1449
- proc.stdout = stdout;
1450
- proc.stderr = stderr;
1451
- proc.kill = jest.fn().mockImplementation(() => {
1452
- setImmediate(() => proc.emit('close', 1));
1453
- });
1454
- return proc;
1455
- }
1456
-
1457
- it('should spawn Claude with --resume flag and session ID', async () => {
1458
- const mockProc = createMockProcess();
1459
- mockProc.kill = jest.fn();
1460
- mockSpawn.mockReturnValue(mockProc);
1461
-
1462
- const runner = new ClaudeRunner();
1463
- const runPromise = runner.runResume('test-session-abc', { timeout: 60 });
1464
-
1465
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'resumed output' }] } });
1466
- mockProc.stdout.emit('data', Buffer.from(event + '\n'));
1467
- mockProc.emit('close', 0);
1468
-
1469
- await runPromise;
1470
-
1471
- const spawnArgs = mockSpawn.mock.calls[0][1] as string[];
1472
- expect(spawnArgs).toContain('--resume');
1473
- expect(spawnArgs).toContain('test-session-abc');
1474
-
1475
- // Verify --resume comes before the session ID
1476
- const resumeIndex = spawnArgs.indexOf('--resume');
1477
- expect(spawnArgs[resumeIndex + 1]).toBe('test-session-abc');
1478
- });
1479
-
1480
- it('should NOT include --model, --append-system-prompt, or -p flags', async () => {
1481
- const mockProc = createMockProcess();
1482
- mockProc.kill = jest.fn();
1483
- mockSpawn.mockReturnValue(mockProc);
1484
-
1485
- const runner = new ClaudeRunner({ model: 'sonnet' });
1486
- const runPromise = runner.runResume('session-123', { timeout: 60 });
1487
-
1488
- mockProc.emit('close', 0);
1489
- await runPromise;
1490
-
1491
- const spawnArgs = mockSpawn.mock.calls[0][1] as string[];
1492
- expect(spawnArgs).not.toContain('--model');
1493
- expect(spawnArgs).not.toContain('--append-system-prompt');
1494
- expect(spawnArgs).not.toContain('-p');
1495
- });
1496
-
1497
- it('should include --dangerously-skip-permissions flag', async () => {
1498
- const mockProc = createMockProcess();
1499
- mockProc.kill = jest.fn();
1500
- mockSpawn.mockReturnValue(mockProc);
1501
-
1502
- const runner = new ClaudeRunner();
1503
- const runPromise = runner.runResume('session-123', { timeout: 60 });
1504
-
1505
- mockProc.emit('close', 0);
1506
- await runPromise;
1507
-
1508
- const spawnArgs = mockSpawn.mock.calls[0][1] as string[];
1509
- expect(spawnArgs).toContain('--dangerously-skip-permissions');
1510
- });
1511
-
1512
- it('should include --output-format stream-json and --verbose flags', async () => {
1513
- const mockProc = createMockProcess();
1514
- mockProc.kill = jest.fn();
1515
- mockSpawn.mockReturnValue(mockProc);
1516
-
1517
- const runner = new ClaudeRunner();
1518
- const runPromise = runner.runResume('session-123', { timeout: 60 });
1519
-
1520
- mockProc.emit('close', 0);
1521
- await runPromise;
1522
-
1523
- const spawnArgs = mockSpawn.mock.calls[0][1] as string[];
1524
- expect(spawnArgs).toContain('--output-format');
1525
- expect(spawnArgs).toContain('stream-json');
1526
- expect(spawnArgs).toContain('--verbose');
1527
- });
1528
-
1529
- it('should collect output from NDJSON events', async () => {
1530
- const mockProc = createMockProcess();
1531
- mockProc.kill = jest.fn();
1532
- mockSpawn.mockReturnValue(mockProc);
1533
-
1534
- const runner = new ClaudeRunner();
1535
- const runPromise = runner.runResume('session-123', { timeout: 60 });
1536
-
1537
- const event1 = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'chunk1' }] } });
1538
- const event2 = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'chunk2' }] } });
1539
- mockProc.stdout.emit('data', Buffer.from(event1 + '\n' + event2 + '\n'));
1540
- mockProc.emit('close', 0);
1541
-
1542
- const result = await runPromise;
1543
- expect(result.output).toBe('chunk1chunk2');
1544
- });
1545
-
1546
- it('should handle timeout correctly', async () => {
1547
- const mockProc = createMockProcess();
1548
- mockSpawn.mockReturnValue(mockProc);
1549
-
1550
- const runner = new ClaudeRunner();
1551
- const runPromise = runner.runResume('session-123', { timeout: 5 }); // 5 minutes
1552
-
1553
- // Fast forward past timeout
1554
- jest.advanceTimersByTime(300001);
1555
- expect(mockProc.kill).toHaveBeenCalledWith('SIGTERM');
1556
-
1557
- const result = await runPromise;
1558
- expect(result.timedOut).toBe(true);
1559
- });
1560
-
1561
- it('should detect completion markers', async () => {
1562
- const mockProc = createMockProcess();
1563
- mockSpawn.mockReturnValue(mockProc);
1564
-
1565
- const runner = new ClaudeRunner();
1566
- const runPromise = runner.runResume('session-123', { timeout: 60 });
1567
-
1568
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: '<promise>COMPLETE</promise>' }] } });
1569
- mockProc.stdout.emit('data', Buffer.from(event + '\n'));
1570
-
1571
- // Grace period should start
1572
- expect(mockProc.kill).not.toHaveBeenCalled();
1573
- jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS + 1);
1574
- expect(mockProc.kill).toHaveBeenCalledWith('SIGTERM');
1575
-
1576
- await runPromise;
1577
- });
1578
-
1579
- it('should extract session ID from resumed session', async () => {
1580
- const mockProc = createMockProcess();
1581
- mockProc.kill = jest.fn();
1582
- mockSpawn.mockReturnValue(mockProc);
1583
-
1584
- const runner = new ClaudeRunner();
1585
- const runPromise = runner.runResume('old-session-123', { timeout: 60 });
1586
-
1587
- const initEvent = JSON.stringify({ type: 'system', subtype: 'init', session_id: 'new-session-456' });
1588
- const textEvent = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'done' }] } });
1589
- mockProc.stdout.emit('data', Buffer.from(initEvent + '\n' + textEvent + '\n'));
1590
- mockProc.emit('close', 0);
1591
-
1592
- const result = await runPromise;
1593
- expect(result.sessionId).toBe('new-session-456');
1594
- expect(runner.sessionId).toBe('new-session-456');
1595
- });
1596
-
1597
- it('should pass cwd to spawn', async () => {
1598
- const mockProc = createMockProcess();
1599
- mockProc.kill = jest.fn();
1600
- mockSpawn.mockReturnValue(mockProc);
1601
-
1602
- const runner = new ClaudeRunner();
1603
- const runPromise = runner.runResume('session-123', { timeout: 60, cwd: '/worktree/path' });
1604
-
1605
- mockProc.emit('close', 0);
1606
- await runPromise;
1607
-
1608
- expect(mockSpawn).toHaveBeenCalledWith(
1609
- expect.any(String),
1610
- expect.any(Array),
1611
- expect.objectContaining({ cwd: '/worktree/path' })
1612
- );
1613
- });
1614
-
1615
- it('should detect context overflow', async () => {
1616
- jest.useRealTimers();
1617
-
1618
- const mockProc = createMockProcess();
1619
- mockSpawn.mockReturnValue(mockProc);
1620
-
1621
- const runner = new ClaudeRunner();
1622
- const runPromise = runner.runResume('session-123', { timeout: 60 });
1623
-
1624
- const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'Error: context length exceeded' }] } });
1625
- mockProc.stdout.emit('data', Buffer.from(event + '\n'));
1626
-
1627
- const result = await runPromise;
1628
- expect(result.contextOverflow).toBe(true);
1629
- expect(mockProc.kill).toHaveBeenCalledWith('SIGTERM');
1630
-
1631
- jest.useFakeTimers();
1632
- });
1633
-
1634
- it('should set CLAUDE_CODE_EFFORT_LEVEL env var when effortLevel is provided', async () => {
1635
- const mockProc = createMockProcess();
1636
- mockProc.kill = jest.fn();
1637
- mockSpawn.mockReturnValue(mockProc);
1638
-
1639
- const runner = new ClaudeRunner();
1640
- const runPromise = runner.runResume('session-123', { timeout: 60, effortLevel: 'medium' });
1641
-
1642
- mockProc.emit('close', 0);
1643
- await runPromise;
1644
-
1645
- const spawnOptions = mockSpawn.mock.calls[0][2];
1646
- expect(spawnOptions.env.CLAUDE_CODE_EFFORT_LEVEL).toBe('medium');
1647
- });
1648
- });
1649
1298
  });