rafcode 2.0.0 → 2.1.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/.claude/settings.local.json +3 -1
- package/RAF/ahrren-turbo-finder/decisions.md +19 -0
- package/RAF/ahrren-turbo-finder/input.md +2 -0
- package/RAF/ahrren-turbo-finder/outcomes/01-worktree-auto-detect.md +40 -0
- package/RAF/ahrren-turbo-finder/outcomes/02-medium-effort-do.md +34 -0
- package/RAF/ahrren-turbo-finder/plans/01-worktree-auto-detect.md +44 -0
- package/RAF/ahrren-turbo-finder/plans/02-medium-effort-do.md +39 -0
- package/RAF/ahrtxf-session-sentinel/decisions.md +19 -0
- package/RAF/ahrtxf-session-sentinel/input.md +1 -0
- package/RAF/ahrtxf-session-sentinel/outcomes/01-capture-session-id.md +37 -0
- package/RAF/ahrtxf-session-sentinel/outcomes/02-resume-flag.md +45 -0
- package/RAF/ahrtxf-session-sentinel/plans/01-capture-session-id.md +41 -0
- package/RAF/ahrtxf-session-sentinel/plans/02-resume-flag.md +51 -0
- package/dist/commands/do.d.ts.map +1 -1
- package/dist/commands/do.js +61 -20
- package/dist/commands/do.js.map +1 -1
- package/dist/core/claude-runner.d.ts +19 -0
- package/dist/core/claude-runner.d.ts.map +1 -1
- package/dist/core/claude-runner.js +199 -29
- 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 +4 -0
- package/dist/core/shutdown-handler.js.map +1 -1
- package/dist/core/worktree.d.ts +18 -0
- package/dist/core/worktree.d.ts.map +1 -1
- package/dist/core/worktree.js +61 -0
- package/dist/core/worktree.js.map +1 -1
- package/dist/parsers/stream-renderer.d.ts +3 -0
- 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 +1 -0
- package/dist/types/config.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/commands/do.ts +67 -21
- package/src/core/claude-runner.ts +244 -31
- package/src/core/shutdown-handler.ts +5 -0
- package/src/core/worktree.ts +77 -0
- package/src/parsers/stream-renderer.ts +4 -1
- package/src/types/config.ts +1 -0
- package/tests/unit/claude-runner-interactive.test.ts +24 -0
- package/tests/unit/claude-runner.test.ts +509 -55
- package/tests/unit/post-execution-picker.test.ts +1 -0
- package/tests/unit/stream-renderer.test.ts +30 -0
- package/tests/unit/worktree.test.ts +102 -0
|
@@ -140,7 +140,8 @@ 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
|
-
|
|
143
|
+
const event = JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: '<promise>COMPLETE</promise>' }] } });
|
|
144
|
+
mockProc.stdout.emit('data', Buffer.from(event + '\n'));
|
|
144
145
|
mockProc.emit('close', 0);
|
|
145
146
|
|
|
146
147
|
const result = await runPromise;
|
|
@@ -162,8 +163,9 @@ describe('ClaudeRunner', () => {
|
|
|
162
163
|
|
|
163
164
|
const runPromise1 = runner.run('first prompt', { timeout: 2 });
|
|
164
165
|
|
|
165
|
-
// Complete first call quickly
|
|
166
|
-
|
|
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'));
|
|
167
169
|
mockProc1.emit('close', 0);
|
|
168
170
|
await runPromise1;
|
|
169
171
|
|
|
@@ -211,8 +213,9 @@ describe('ClaudeRunner', () => {
|
|
|
211
213
|
// Advance a bit but not to timeout
|
|
212
214
|
jest.advanceTimersByTime(30000);
|
|
213
215
|
|
|
214
|
-
// Complete process
|
|
215
|
-
|
|
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'));
|
|
216
219
|
mockProc.emit('close', 0);
|
|
217
220
|
|
|
218
221
|
const result = await runPromise;
|
|
@@ -232,8 +235,9 @@ describe('ClaudeRunner', () => {
|
|
|
232
235
|
jest.advanceTimersByTime(1000);
|
|
233
236
|
expect(mockProc.kill).not.toHaveBeenCalled();
|
|
234
237
|
|
|
235
|
-
// Complete the process normally before reaching the 60 minute timeout
|
|
236
|
-
|
|
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'));
|
|
237
241
|
mockProc.emit('close', 0);
|
|
238
242
|
|
|
239
243
|
const result = await runPromise;
|
|
@@ -305,8 +309,9 @@ describe('ClaudeRunner', () => {
|
|
|
305
309
|
const runner = new ClaudeRunner();
|
|
306
310
|
const runPromise = runner.run('test prompt', { timeout: 60 });
|
|
307
311
|
|
|
308
|
-
// Emit context overflow message
|
|
309
|
-
|
|
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'));
|
|
310
315
|
|
|
311
316
|
const result = await runPromise;
|
|
312
317
|
expect(result.contextOverflow).toBe(true);
|
|
@@ -333,7 +338,9 @@ describe('ClaudeRunner', () => {
|
|
|
333
338
|
const runner = new ClaudeRunner();
|
|
334
339
|
const runPromise = runner.run('test prompt', { timeout: 60 });
|
|
335
340
|
|
|
336
|
-
|
|
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'));
|
|
337
344
|
|
|
338
345
|
const result = await runPromise;
|
|
339
346
|
expect(result.contextOverflow).toBe(true);
|
|
@@ -355,17 +362,18 @@ describe('ClaudeRunner', () => {
|
|
|
355
362
|
return proc;
|
|
356
363
|
}
|
|
357
364
|
|
|
358
|
-
it('should collect all stdout output', async () => {
|
|
365
|
+
it('should collect all stdout output from NDJSON events', async () => {
|
|
359
366
|
const mockProc = createMockProcess();
|
|
360
367
|
mockSpawn.mockReturnValue(mockProc);
|
|
361
368
|
|
|
362
369
|
const runner = new ClaudeRunner();
|
|
363
370
|
const runPromise = runner.run('test prompt', { timeout: 60 });
|
|
364
371
|
|
|
365
|
-
// Emit
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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'));
|
|
369
377
|
mockProc.emit('close', 0);
|
|
370
378
|
|
|
371
379
|
const result = await runPromise;
|
|
@@ -472,6 +480,109 @@ describe('ClaudeRunner', () => {
|
|
|
472
480
|
});
|
|
473
481
|
});
|
|
474
482
|
|
|
483
|
+
describe('effort level', () => {
|
|
484
|
+
function createMockProcess() {
|
|
485
|
+
const stdout = new EventEmitter();
|
|
486
|
+
const stderr = new EventEmitter();
|
|
487
|
+
const proc = new EventEmitter() as any;
|
|
488
|
+
proc.stdout = stdout;
|
|
489
|
+
proc.stderr = stderr;
|
|
490
|
+
proc.kill = jest.fn();
|
|
491
|
+
return proc;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
it('should set CLAUDE_CODE_EFFORT_LEVEL env var in run() when effortLevel is provided', async () => {
|
|
495
|
+
const mockProc = createMockProcess();
|
|
496
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
497
|
+
|
|
498
|
+
const runner = new ClaudeRunner();
|
|
499
|
+
const runPromise = runner.run('test prompt', { timeout: 60, effortLevel: 'medium' });
|
|
500
|
+
|
|
501
|
+
mockProc.emit('close', 0);
|
|
502
|
+
await runPromise;
|
|
503
|
+
|
|
504
|
+
const spawnOptions = mockSpawn.mock.calls[0][2];
|
|
505
|
+
expect(spawnOptions.env.CLAUDE_CODE_EFFORT_LEVEL).toBe('medium');
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it('should set CLAUDE_CODE_EFFORT_LEVEL env var in runVerbose() when effortLevel is provided', async () => {
|
|
509
|
+
const mockProc = createMockProcess();
|
|
510
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
511
|
+
|
|
512
|
+
const runner = new ClaudeRunner();
|
|
513
|
+
const runPromise = runner.runVerbose('test prompt', { timeout: 60, effortLevel: 'medium' });
|
|
514
|
+
|
|
515
|
+
mockProc.emit('close', 0);
|
|
516
|
+
await runPromise;
|
|
517
|
+
|
|
518
|
+
const spawnOptions = mockSpawn.mock.calls[0][2];
|
|
519
|
+
expect(spawnOptions.env.CLAUDE_CODE_EFFORT_LEVEL).toBe('medium');
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it('should NOT set CLAUDE_CODE_EFFORT_LEVEL when effortLevel is not provided in run()', async () => {
|
|
523
|
+
const mockProc = createMockProcess();
|
|
524
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
525
|
+
|
|
526
|
+
const runner = new ClaudeRunner();
|
|
527
|
+
const runPromise = runner.run('test prompt', { timeout: 60 });
|
|
528
|
+
|
|
529
|
+
mockProc.emit('close', 0);
|
|
530
|
+
await runPromise;
|
|
531
|
+
|
|
532
|
+
const spawnOptions = mockSpawn.mock.calls[0][2];
|
|
533
|
+
// env should be process.env directly (no CLAUDE_CODE_EFFORT_LEVEL override)
|
|
534
|
+
expect(spawnOptions.env).toBe(process.env);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it('should NOT set CLAUDE_CODE_EFFORT_LEVEL when effortLevel is not provided in runVerbose()', async () => {
|
|
538
|
+
const mockProc = createMockProcess();
|
|
539
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
540
|
+
|
|
541
|
+
const runner = new ClaudeRunner();
|
|
542
|
+
const runPromise = runner.runVerbose('test prompt', { timeout: 60 });
|
|
543
|
+
|
|
544
|
+
mockProc.emit('close', 0);
|
|
545
|
+
await runPromise;
|
|
546
|
+
|
|
547
|
+
const spawnOptions = mockSpawn.mock.calls[0][2];
|
|
548
|
+
// env should be process.env directly (no CLAUDE_CODE_EFFORT_LEVEL override)
|
|
549
|
+
expect(spawnOptions.env).toBe(process.env);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it('should support different effort levels', async () => {
|
|
553
|
+
for (const level of ['low', 'medium', 'high'] as const) {
|
|
554
|
+
const mockProc = createMockProcess();
|
|
555
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
556
|
+
|
|
557
|
+
const runner = new ClaudeRunner();
|
|
558
|
+
const runPromise = runner.run('test prompt', { timeout: 60, effortLevel: level });
|
|
559
|
+
|
|
560
|
+
mockProc.emit('close', 0);
|
|
561
|
+
await runPromise;
|
|
562
|
+
|
|
563
|
+
const spawnOptions = mockSpawn.mock.calls[mockSpawn.mock.calls.length - 1][2];
|
|
564
|
+
expect(spawnOptions.env.CLAUDE_CODE_EFFORT_LEVEL).toBe(level);
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
it('should preserve other env vars when effortLevel is set', async () => {
|
|
569
|
+
const mockProc = createMockProcess();
|
|
570
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
571
|
+
|
|
572
|
+
const runner = new ClaudeRunner();
|
|
573
|
+
const runPromise = runner.run('test prompt', { timeout: 60, effortLevel: 'medium' });
|
|
574
|
+
|
|
575
|
+
mockProc.emit('close', 0);
|
|
576
|
+
await runPromise;
|
|
577
|
+
|
|
578
|
+
const spawnOptions = mockSpawn.mock.calls[0][2];
|
|
579
|
+
// Should have PATH from process.env
|
|
580
|
+
expect(spawnOptions.env.PATH).toBe(process.env.PATH);
|
|
581
|
+
// And the injected effort level
|
|
582
|
+
expect(spawnOptions.env.CLAUDE_CODE_EFFORT_LEVEL).toBe('medium');
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
|
|
475
586
|
describe('system prompt append flag', () => {
|
|
476
587
|
function createMockProcess() {
|
|
477
588
|
const stdout = new EventEmitter();
|
|
@@ -593,7 +704,7 @@ describe('ClaudeRunner', () => {
|
|
|
593
704
|
expect(spawnArgs).toContain('--verbose');
|
|
594
705
|
});
|
|
595
706
|
|
|
596
|
-
it('should
|
|
707
|
+
it('should include --output-format stream-json and --verbose flags in run()', async () => {
|
|
597
708
|
const mockProc = createMockProcess();
|
|
598
709
|
mockSpawn.mockReturnValue(mockProc);
|
|
599
710
|
|
|
@@ -604,9 +715,9 @@ describe('ClaudeRunner', () => {
|
|
|
604
715
|
await runPromise;
|
|
605
716
|
|
|
606
717
|
const spawnArgs = mockSpawn.mock.calls[0][1] as string[];
|
|
607
|
-
expect(spawnArgs).
|
|
608
|
-
expect(spawnArgs).
|
|
609
|
-
expect(spawnArgs).
|
|
718
|
+
expect(spawnArgs).toContain('--output-format');
|
|
719
|
+
expect(spawnArgs).toContain('stream-json');
|
|
720
|
+
expect(spawnArgs).toContain('--verbose');
|
|
610
721
|
});
|
|
611
722
|
|
|
612
723
|
it('should extract text from NDJSON assistant events', async () => {
|
|
@@ -693,8 +804,9 @@ describe('ClaudeRunner', () => {
|
|
|
693
804
|
jest.advanceTimersByTime(60000);
|
|
694
805
|
expect(mockProc2.kill).not.toHaveBeenCalled();
|
|
695
806
|
|
|
696
|
-
// Complete successfully
|
|
697
|
-
|
|
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'));
|
|
698
810
|
mockProc2.emit('close', 0);
|
|
699
811
|
|
|
700
812
|
const result2 = await runPromise2;
|
|
@@ -715,8 +827,9 @@ describe('ClaudeRunner', () => {
|
|
|
715
827
|
// Run for 4 minutes (240000ms)
|
|
716
828
|
jest.advanceTimersByTime(240000);
|
|
717
829
|
|
|
718
|
-
// Fail without timeout
|
|
719
|
-
|
|
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'));
|
|
720
833
|
mockProc1.emit('close', 1);
|
|
721
834
|
|
|
722
835
|
const result1 = await runPromise1;
|
|
@@ -766,8 +879,9 @@ describe('ClaudeRunner', () => {
|
|
|
766
879
|
const runner = new ClaudeRunner();
|
|
767
880
|
const runPromise = runner.run('test prompt', { timeout: 60 });
|
|
768
881
|
|
|
769
|
-
// Emit output with completion marker
|
|
770
|
-
|
|
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'));
|
|
771
885
|
|
|
772
886
|
// Process should not be killed immediately
|
|
773
887
|
expect(mockProc.kill).not.toHaveBeenCalled();
|
|
@@ -792,8 +906,9 @@ describe('ClaudeRunner', () => {
|
|
|
792
906
|
const runner = new ClaudeRunner();
|
|
793
907
|
const runPromise = runner.run('test prompt', { timeout: 60 });
|
|
794
908
|
|
|
795
|
-
// Emit output with failed marker
|
|
796
|
-
|
|
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'));
|
|
797
912
|
|
|
798
913
|
// Advance past grace period
|
|
799
914
|
jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS + 1);
|
|
@@ -810,13 +925,15 @@ describe('ClaudeRunner', () => {
|
|
|
810
925
|
const runner = new ClaudeRunner();
|
|
811
926
|
const runPromise = runner.run('test prompt', { timeout: 60 });
|
|
812
927
|
|
|
813
|
-
// Emit partial output (no marker yet)
|
|
814
|
-
|
|
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'));
|
|
815
931
|
jest.advanceTimersByTime(10000);
|
|
816
932
|
expect(mockProc.kill).not.toHaveBeenCalled();
|
|
817
933
|
|
|
818
|
-
// Emit marker in second chunk
|
|
819
|
-
|
|
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'));
|
|
820
937
|
|
|
821
938
|
// Grace period starts now - advance past it
|
|
822
939
|
jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS + 1);
|
|
@@ -834,8 +951,9 @@ describe('ClaudeRunner', () => {
|
|
|
834
951
|
const runner = new ClaudeRunner();
|
|
835
952
|
const runPromise = runner.run('test prompt', { timeout: 60 });
|
|
836
953
|
|
|
837
|
-
// Emit completion marker
|
|
838
|
-
|
|
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'));
|
|
839
957
|
|
|
840
958
|
// Process exits naturally before grace period
|
|
841
959
|
jest.advanceTimersByTime(5000);
|
|
@@ -865,8 +983,9 @@ describe('ClaudeRunner', () => {
|
|
|
865
983
|
outcomeFilePath: outcomePath,
|
|
866
984
|
});
|
|
867
985
|
|
|
868
|
-
// No output marker in stdout - just regular output
|
|
869
|
-
|
|
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'));
|
|
870
989
|
|
|
871
990
|
// After some time, outcome file appears with marker
|
|
872
991
|
jest.advanceTimersByTime(OUTCOME_POLL_INTERVAL_MS - 1);
|
|
@@ -910,8 +1029,9 @@ describe('ClaudeRunner', () => {
|
|
|
910
1029
|
jest.advanceTimersByTime(OUTCOME_POLL_INTERVAL_MS * 5);
|
|
911
1030
|
expect(mockProc.kill).not.toHaveBeenCalled();
|
|
912
1031
|
|
|
913
|
-
// Complete normally
|
|
914
|
-
|
|
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'));
|
|
915
1035
|
mockProc.emit('close', 0);
|
|
916
1036
|
|
|
917
1037
|
await runPromise;
|
|
@@ -944,14 +1064,16 @@ describe('ClaudeRunner', () => {
|
|
|
944
1064
|
const runner = new ClaudeRunner();
|
|
945
1065
|
const runPromise = runner.run('test prompt', { timeout: 60 });
|
|
946
1066
|
|
|
947
|
-
// Emit first marker
|
|
948
|
-
|
|
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'));
|
|
949
1070
|
|
|
950
1071
|
// Advance halfway through grace period
|
|
951
1072
|
jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS / 2);
|
|
952
1073
|
|
|
953
|
-
// Emit second marker (e.g., Claude reading back what it wrote)
|
|
954
|
-
|
|
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'));
|
|
955
1077
|
|
|
956
1078
|
// Advance remaining grace period from FIRST detection
|
|
957
1079
|
jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS / 2 + 1);
|
|
@@ -1007,8 +1129,9 @@ describe('ClaudeRunner', () => {
|
|
|
1007
1129
|
commitContext,
|
|
1008
1130
|
});
|
|
1009
1131
|
|
|
1010
|
-
// Emit COMPLETE marker
|
|
1011
|
-
|
|
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'));
|
|
1012
1135
|
|
|
1013
1136
|
// Advance to grace period expiry - commit already verified
|
|
1014
1137
|
jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS + 1);
|
|
@@ -1032,8 +1155,9 @@ describe('ClaudeRunner', () => {
|
|
|
1032
1155
|
commitContext,
|
|
1033
1156
|
});
|
|
1034
1157
|
|
|
1035
|
-
// Emit COMPLETE marker
|
|
1036
|
-
|
|
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'));
|
|
1037
1161
|
|
|
1038
1162
|
// Advance past initial grace period - commit not found, should extend
|
|
1039
1163
|
jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS + 1);
|
|
@@ -1067,8 +1191,9 @@ describe('ClaudeRunner', () => {
|
|
|
1067
1191
|
commitContext,
|
|
1068
1192
|
});
|
|
1069
1193
|
|
|
1070
|
-
// Emit COMPLETE marker
|
|
1071
|
-
|
|
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'));
|
|
1072
1197
|
|
|
1073
1198
|
// Advance past initial grace period
|
|
1074
1199
|
jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS + 1);
|
|
@@ -1094,8 +1219,9 @@ describe('ClaudeRunner', () => {
|
|
|
1094
1219
|
commitContext,
|
|
1095
1220
|
});
|
|
1096
1221
|
|
|
1097
|
-
// Emit FAILED marker (not COMPLETE)
|
|
1098
|
-
|
|
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'));
|
|
1099
1225
|
|
|
1100
1226
|
// Grace period should expire normally without extension
|
|
1101
1227
|
jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS + 1);
|
|
@@ -1114,8 +1240,9 @@ describe('ClaudeRunner', () => {
|
|
|
1114
1240
|
// No commitContext provided
|
|
1115
1241
|
});
|
|
1116
1242
|
|
|
1117
|
-
// Emit COMPLETE marker
|
|
1118
|
-
|
|
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'));
|
|
1119
1246
|
|
|
1120
1247
|
// Grace period should expire normally (no commit check)
|
|
1121
1248
|
jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS + 1);
|
|
@@ -1140,8 +1267,9 @@ describe('ClaudeRunner', () => {
|
|
|
1140
1267
|
commitContext,
|
|
1141
1268
|
});
|
|
1142
1269
|
|
|
1143
|
-
// Emit COMPLETE marker
|
|
1144
|
-
|
|
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'));
|
|
1145
1273
|
|
|
1146
1274
|
// Grace period expires - commit message doesn't match, should extend
|
|
1147
1275
|
jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS + 1);
|
|
@@ -1174,8 +1302,9 @@ describe('ClaudeRunner', () => {
|
|
|
1174
1302
|
commitContext,
|
|
1175
1303
|
});
|
|
1176
1304
|
|
|
1177
|
-
// Emit COMPLETE marker
|
|
1178
|
-
|
|
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'));
|
|
1179
1308
|
|
|
1180
1309
|
// Grace period expires - file not committed, should extend
|
|
1181
1310
|
jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS + 1);
|
|
@@ -1192,4 +1321,329 @@ describe('ClaudeRunner', () => {
|
|
|
1192
1321
|
await runPromise;
|
|
1193
1322
|
});
|
|
1194
1323
|
});
|
|
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
|
+
});
|
|
1195
1649
|
});
|