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.
- package/dist/commands/do.d.ts.map +1 -1
- package/dist/commands/do.js +5 -24
- package/dist/commands/do.js.map +1 -1
- package/dist/core/claude-runner.d.ts +0 -13
- package/dist/core/claude-runner.d.ts.map +1 -1
- package/dist/core/claude-runner.js +25 -187
- package/dist/core/claude-runner.js.map +1 -1
- package/dist/core/shutdown-handler.d.ts.map +1 -1
- package/dist/core/shutdown-handler.js +0 -4
- package/dist/core/shutdown-handler.js.map +1 -1
- package/dist/parsers/stream-renderer.d.ts +0 -3
- package/dist/parsers/stream-renderer.d.ts.map +1 -1
- package/dist/parsers/stream-renderer.js +1 -1
- package/dist/parsers/stream-renderer.js.map +1 -1
- package/dist/types/config.d.ts +0 -1
- package/dist/types/config.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/commands/do.ts +5 -24
- package/src/core/claude-runner.ts +27 -224
- package/src/core/shutdown-handler.ts +0 -5
- package/src/parsers/stream-renderer.ts +1 -4
- package/src/types/config.ts +0 -1
- package/tests/unit/claude-runner.test.ts +55 -406
- package/tests/unit/stream-renderer.test.ts +0 -30
- package/RAF/ahrtxf-session-sentinel/decisions.md +0 -19
- package/RAF/ahrtxf-session-sentinel/input.md +0 -1
- package/RAF/ahrtxf-session-sentinel/outcomes/01-capture-session-id.md +0 -37
- package/RAF/ahrtxf-session-sentinel/outcomes/02-resume-flag.md +0 -45
- package/RAF/ahrtxf-session-sentinel/plans/01-capture-session-id.md +0 -41
- package/RAF/ahrtxf-session-sentinel/plans/02-resume-flag.md +0 -51
|
@@ -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
|
-
|
|
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
|
|
167
|
-
|
|
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
|
|
217
|
-
|
|
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
|
|
239
|
-
|
|
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
|
|
313
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
|
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
|
|
808
|
-
|
|
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
|
|
831
|
-
|
|
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
|
|
883
|
-
|
|
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
|
|
910
|
-
|
|
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)
|
|
929
|
-
|
|
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
|
|
935
|
-
|
|
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
|
|
955
|
-
|
|
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
|
|
987
|
-
|
|
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
|
|
1033
|
-
|
|
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
|
|
1068
|
-
|
|
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)
|
|
1075
|
-
|
|
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
|
|
1133
|
-
|
|
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
|
|
1159
|
-
|
|
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
|
|
1195
|
-
|
|
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)
|
|
1223
|
-
|
|
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
|
|
1244
|
-
|
|
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
|
|
1271
|
-
|
|
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
|
|
1306
|
-
|
|
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
|
});
|