rafcode 2.1.0 → 2.2.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 +4 -1
- package/CLAUDE.md +59 -11
- package/RAF/ahslfe-config-wizard/decisions.md +34 -0
- package/RAF/ahslfe-config-wizard/input.md +1 -0
- package/RAF/ahslfe-config-wizard/outcomes/01-define-config-schema.md +38 -0
- package/RAF/ahslfe-config-wizard/outcomes/02-refactor-codebase-to-use-config.md +67 -0
- package/RAF/ahslfe-config-wizard/outcomes/03-create-config-documentation.md +37 -0
- package/RAF/ahslfe-config-wizard/outcomes/04-implement-raf-config-command.md +47 -0
- package/RAF/ahslfe-config-wizard/outcomes/05-update-claude-md.md +26 -0
- package/RAF/ahslfe-config-wizard/plans/01-define-config-schema.md +73 -0
- package/RAF/ahslfe-config-wizard/plans/02-refactor-codebase-to-use-config.md +74 -0
- package/RAF/ahslfe-config-wizard/plans/03-create-config-documentation.md +57 -0
- package/RAF/ahslfe-config-wizard/plans/04-implement-raf-config-command.md +66 -0
- package/RAF/ahslfe-config-wizard/plans/05-update-claude-md.md +60 -0
- package/RAF/ahstvo-token-tracker/decisions.md +44 -0
- package/RAF/ahstvo-token-tracker/input.md +3 -0
- package/RAF/ahstvo-token-tracker/outcomes/01-full-model-id-support.md +43 -0
- package/RAF/ahstvo-token-tracker/outcomes/02-name-generation-no-session.md +33 -0
- package/RAF/ahstvo-token-tracker/outcomes/03-unify-stream-json-execution.md +48 -0
- package/RAF/ahstvo-token-tracker/outcomes/04-token-tracking-cost-calculation.md +53 -0
- package/RAF/ahstvo-token-tracker/outcomes/05-token-cost-console-reporting.md +57 -0
- package/RAF/ahstvo-token-tracker/outcomes/06-runtime-verbose-toggle.md +53 -0
- package/RAF/ahstvo-token-tracker/outcomes/07-readme-config-docs.md +36 -0
- package/RAF/ahstvo-token-tracker/plans/01-full-model-id-support.md +35 -0
- package/RAF/ahstvo-token-tracker/plans/02-name-generation-no-session.md +36 -0
- package/RAF/ahstvo-token-tracker/plans/03-unify-stream-json-execution.md +44 -0
- package/RAF/ahstvo-token-tracker/plans/04-token-tracking-cost-calculation.md +56 -0
- package/RAF/ahstvo-token-tracker/plans/05-token-cost-console-reporting.md +55 -0
- package/RAF/ahstvo-token-tracker/plans/06-runtime-verbose-toggle.md +48 -0
- package/RAF/ahstvo-token-tracker/plans/07-readme-config-docs.md +44 -0
- package/README.md +34 -0
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +173 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/do.d.ts.map +1 -1
- package/dist/commands/do.js +50 -28
- package/dist/commands/do.js.map +1 -1
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/plan.js +3 -2
- package/dist/commands/plan.js.map +1 -1
- package/dist/core/claude-runner.d.ts +17 -13
- package/dist/core/claude-runner.d.ts.map +1 -1
- package/dist/core/claude-runner.js +42 -257
- package/dist/core/claude-runner.js.map +1 -1
- package/dist/core/failure-analyzer.d.ts.map +1 -1
- package/dist/core/failure-analyzer.js +6 -3
- package/dist/core/failure-analyzer.js.map +1 -1
- package/dist/core/git.d.ts.map +1 -1
- package/dist/core/git.js +10 -3
- package/dist/core/git.js.map +1 -1
- package/dist/core/pull-request.d.ts +1 -1
- package/dist/core/pull-request.d.ts.map +1 -1
- package/dist/core/pull-request.js +7 -4
- package/dist/core/pull-request.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/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/parsers/stream-renderer.d.ts +16 -4
- package/dist/parsers/stream-renderer.d.ts.map +1 -1
- package/dist/parsers/stream-renderer.js +35 -5
- package/dist/parsers/stream-renderer.js.map +1 -1
- package/dist/prompts/execution.d.ts.map +1 -1
- package/dist/prompts/execution.js +11 -1
- package/dist/prompts/execution.js.map +1 -1
- package/dist/types/config.d.ts +95 -5
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +63 -3
- package/dist/types/config.js.map +1 -1
- package/dist/utils/config.d.ts +59 -7
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +276 -21
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/name-generator.d.ts +3 -7
- package/dist/utils/name-generator.d.ts.map +1 -1
- package/dist/utils/name-generator.js +75 -61
- package/dist/utils/name-generator.js.map +1 -1
- package/dist/utils/terminal-symbols.d.ts +21 -0
- package/dist/utils/terminal-symbols.d.ts.map +1 -1
- package/dist/utils/terminal-symbols.js +62 -0
- package/dist/utils/terminal-symbols.js.map +1 -1
- package/dist/utils/token-tracker.d.ts +45 -0
- package/dist/utils/token-tracker.d.ts.map +1 -0
- package/dist/utils/token-tracker.js +107 -0
- package/dist/utils/token-tracker.js.map +1 -0
- package/dist/utils/validation.d.ts +5 -5
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +10 -6
- package/dist/utils/validation.js.map +1 -1
- package/dist/utils/verbose-toggle.d.ts +33 -0
- package/dist/utils/verbose-toggle.d.ts.map +1 -0
- package/dist/utils/verbose-toggle.js +94 -0
- package/dist/utils/verbose-toggle.js.map +1 -0
- package/package.json +1 -1
- package/src/commands/config.ts +204 -0
- package/src/commands/do.ts +59 -27
- package/src/commands/plan.ts +3 -2
- package/src/core/claude-runner.ts +58 -311
- package/src/core/failure-analyzer.ts +6 -3
- package/src/core/git.ts +10 -3
- package/src/core/pull-request.ts +7 -4
- package/src/core/shutdown-handler.ts +0 -5
- package/src/index.ts +2 -0
- package/src/parsers/stream-renderer.ts +55 -8
- package/src/prompts/config-docs.md +331 -0
- package/src/prompts/execution.ts +13 -1
- package/src/types/config.ts +156 -8
- package/src/utils/config.ts +335 -21
- package/src/utils/name-generator.ts +84 -71
- package/src/utils/terminal-symbols.ts +68 -0
- package/src/utils/token-tracker.ts +135 -0
- package/src/utils/validation.ts +15 -10
- package/src/utils/verbose-toggle.ts +103 -0
- package/tests/unit/claude-runner.test.ts +216 -403
- package/tests/unit/config-command.test.ts +163 -0
- package/tests/unit/config.test.ts +608 -30
- package/tests/unit/name-generator.test.ts +99 -75
- package/tests/unit/pull-request.test.ts +2 -0
- package/tests/unit/stream-renderer.test.ts +83 -30
- package/tests/unit/terminal-symbols.test.ts +157 -0
- package/tests/unit/token-tracker.test.ts +352 -0
- package/tests/unit/verbose-toggle.test.ts +204 -0
- 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 (with newline so NDJSON line buffer processes it)
|
|
309
|
+
mockProc.stdout.emit('data', Buffer.from('Error: context length exceeded\n'));
|
|
315
310
|
|
|
316
311
|
const result = await runPromise;
|
|
317
312
|
expect(result.contextOverflow).toBe(true);
|
|
@@ -338,9 +333,8 @@ describe('ClaudeRunner', () => {
|
|
|
338
333
|
const runner = new ClaudeRunner();
|
|
339
334
|
const runPromise = runner.run('test prompt', { timeout: 60 });
|
|
340
335
|
|
|
341
|
-
//
|
|
342
|
-
|
|
343
|
-
mockProc.stdout.emit('data', Buffer.from(event + '\n'));
|
|
336
|
+
// Trailing newline needed for NDJSON line buffer processing
|
|
337
|
+
mockProc.stdout.emit('data', Buffer.from(`Error: ${pattern}\n`));
|
|
344
338
|
|
|
345
339
|
const result = await runPromise;
|
|
346
340
|
expect(result.contextOverflow).toBe(true);
|
|
@@ -362,18 +356,17 @@ describe('ClaudeRunner', () => {
|
|
|
362
356
|
return proc;
|
|
363
357
|
}
|
|
364
358
|
|
|
365
|
-
it('should collect all stdout output
|
|
359
|
+
it('should collect all stdout output', async () => {
|
|
366
360
|
const mockProc = createMockProcess();
|
|
367
361
|
mockSpawn.mockReturnValue(mockProc);
|
|
368
362
|
|
|
369
363
|
const runner = new ClaudeRunner();
|
|
370
364
|
const runPromise = runner.run('test prompt', { timeout: 60 });
|
|
371
365
|
|
|
372
|
-
// Emit
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
mockProc.stdout.emit('data', Buffer.from(event1 + '\n' + event2 + '\n' + event3 + '\n'));
|
|
366
|
+
// Emit multiple chunks
|
|
367
|
+
mockProc.stdout.emit('data', Buffer.from('chunk1'));
|
|
368
|
+
mockProc.stdout.emit('data', Buffer.from('chunk2'));
|
|
369
|
+
mockProc.stdout.emit('data', Buffer.from('chunk3'));
|
|
377
370
|
mockProc.emit('close', 0);
|
|
378
371
|
|
|
379
372
|
const result = await runPromise;
|
|
@@ -704,7 +697,7 @@ describe('ClaudeRunner', () => {
|
|
|
704
697
|
expect(spawnArgs).toContain('--verbose');
|
|
705
698
|
});
|
|
706
699
|
|
|
707
|
-
it('should include --output-format stream-json and --verbose flags in run()', async () => {
|
|
700
|
+
it('should include --output-format stream-json and --verbose flags in run() (unified stream-json)', async () => {
|
|
708
701
|
const mockProc = createMockProcess();
|
|
709
702
|
mockSpawn.mockReturnValue(mockProc);
|
|
710
703
|
|
|
@@ -764,6 +757,169 @@ describe('ClaudeRunner', () => {
|
|
|
764
757
|
});
|
|
765
758
|
});
|
|
766
759
|
|
|
760
|
+
describe('usage data extraction', () => {
|
|
761
|
+
function createMockProcess() {
|
|
762
|
+
const stdout = new EventEmitter();
|
|
763
|
+
const stderr = new EventEmitter();
|
|
764
|
+
const proc = new EventEmitter() as any;
|
|
765
|
+
proc.stdout = stdout;
|
|
766
|
+
proc.stderr = stderr;
|
|
767
|
+
proc.kill = jest.fn();
|
|
768
|
+
return proc;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
it('should return usageData from run() when result event has usage', async () => {
|
|
772
|
+
const mockProc = createMockProcess();
|
|
773
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
774
|
+
|
|
775
|
+
const runner = new ClaudeRunner();
|
|
776
|
+
const runPromise = runner.run('test prompt', { timeout: 60 });
|
|
777
|
+
|
|
778
|
+
const assistantEvent = JSON.stringify({
|
|
779
|
+
type: 'assistant',
|
|
780
|
+
message: { content: [{ type: 'text', text: 'Done' }] },
|
|
781
|
+
});
|
|
782
|
+
const resultEvent = JSON.stringify({
|
|
783
|
+
type: 'result',
|
|
784
|
+
usage: { input_tokens: 1000, output_tokens: 500, cache_read_input_tokens: 200, cache_creation_input_tokens: 100 },
|
|
785
|
+
modelUsage: { 'claude-opus-4-6': { inputTokens: 1000, outputTokens: 500, cacheReadInputTokens: 200, cacheCreationInputTokens: 100 } },
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
mockProc.stdout.emit('data', Buffer.from(assistantEvent + '\n' + resultEvent + '\n'));
|
|
789
|
+
mockProc.emit('close', 0);
|
|
790
|
+
|
|
791
|
+
const result = await runPromise;
|
|
792
|
+
expect(result.usageData).toBeDefined();
|
|
793
|
+
expect(result.usageData!.inputTokens).toBe(1000);
|
|
794
|
+
expect(result.usageData!.outputTokens).toBe(500);
|
|
795
|
+
expect(result.usageData!.modelUsage['claude-opus-4-6']).toEqual({
|
|
796
|
+
inputTokens: 1000,
|
|
797
|
+
outputTokens: 500,
|
|
798
|
+
cacheReadInputTokens: 200,
|
|
799
|
+
cacheCreationInputTokens: 100,
|
|
800
|
+
});
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
it('should return usageData from runVerbose() when result event has usage', async () => {
|
|
804
|
+
const mockProc = createMockProcess();
|
|
805
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
806
|
+
|
|
807
|
+
const runner = new ClaudeRunner();
|
|
808
|
+
const runPromise = runner.runVerbose('test prompt', { timeout: 60 });
|
|
809
|
+
|
|
810
|
+
const resultEvent = JSON.stringify({
|
|
811
|
+
type: 'result',
|
|
812
|
+
usage: { input_tokens: 2000, output_tokens: 800 },
|
|
813
|
+
modelUsage: { 'claude-sonnet-4-5-20250929': { inputTokens: 2000, outputTokens: 800 } },
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
mockProc.stdout.emit('data', Buffer.from(resultEvent + '\n'));
|
|
817
|
+
mockProc.emit('close', 0);
|
|
818
|
+
|
|
819
|
+
const result = await runPromise;
|
|
820
|
+
expect(result.usageData).toBeDefined();
|
|
821
|
+
expect(result.usageData!.inputTokens).toBe(2000);
|
|
822
|
+
expect(result.usageData!.outputTokens).toBe(800);
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
it('should return undefined usageData when no result event', async () => {
|
|
826
|
+
const mockProc = createMockProcess();
|
|
827
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
828
|
+
|
|
829
|
+
const runner = new ClaudeRunner();
|
|
830
|
+
const runPromise = runner.run('test prompt', { timeout: 60 });
|
|
831
|
+
|
|
832
|
+
const assistantEvent = JSON.stringify({
|
|
833
|
+
type: 'assistant',
|
|
834
|
+
message: { content: [{ type: 'text', text: 'Output' }] },
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
mockProc.stdout.emit('data', Buffer.from(assistantEvent + '\n'));
|
|
838
|
+
mockProc.emit('close', 0);
|
|
839
|
+
|
|
840
|
+
const result = await runPromise;
|
|
841
|
+
expect(result.usageData).toBeUndefined();
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
it('should suppress display in run() but still capture usage data', async () => {
|
|
845
|
+
const mockProc = createMockProcess();
|
|
846
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
847
|
+
|
|
848
|
+
// Spy on stdout.write to verify no display output in non-verbose mode
|
|
849
|
+
const writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
850
|
+
|
|
851
|
+
const runner = new ClaudeRunner();
|
|
852
|
+
const runPromise = runner.run('test prompt', { timeout: 60 });
|
|
853
|
+
|
|
854
|
+
const toolEvent = JSON.stringify({
|
|
855
|
+
type: 'assistant',
|
|
856
|
+
message: { content: [
|
|
857
|
+
{ type: 'text', text: 'Working...' },
|
|
858
|
+
{ type: 'tool_use', name: 'Read', input: { file_path: '/test.ts' } },
|
|
859
|
+
] },
|
|
860
|
+
});
|
|
861
|
+
const resultEvent = JSON.stringify({
|
|
862
|
+
type: 'result',
|
|
863
|
+
usage: { input_tokens: 100, output_tokens: 50 },
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
mockProc.stdout.emit('data', Buffer.from(toolEvent + '\n' + resultEvent + '\n'));
|
|
867
|
+
mockProc.emit('close', 0);
|
|
868
|
+
|
|
869
|
+
const result = await runPromise;
|
|
870
|
+
|
|
871
|
+
// No display output (run() is non-verbose)
|
|
872
|
+
expect(writeSpy).not.toHaveBeenCalled();
|
|
873
|
+
|
|
874
|
+
// But usage data is still captured
|
|
875
|
+
expect(result.usageData).toBeDefined();
|
|
876
|
+
expect(result.usageData!.inputTokens).toBe(100);
|
|
877
|
+
|
|
878
|
+
// And text content is captured
|
|
879
|
+
expect(result.output).toContain('Working...');
|
|
880
|
+
|
|
881
|
+
writeSpy.mockRestore();
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
it('should use verboseCheck callback to dynamically control display', async () => {
|
|
885
|
+
const mockProc = createMockProcess();
|
|
886
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
887
|
+
|
|
888
|
+
const writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
889
|
+
|
|
890
|
+
let dynamicVerbose = false;
|
|
891
|
+
const runner = new ClaudeRunner();
|
|
892
|
+
const runPromise = runner.run('test prompt', {
|
|
893
|
+
timeout: 60,
|
|
894
|
+
verboseCheck: () => dynamicVerbose,
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
// First event with verbose OFF — should not display
|
|
898
|
+
const event1 = JSON.stringify({
|
|
899
|
+
type: 'assistant',
|
|
900
|
+
message: { content: [{ type: 'tool_use', name: 'Read', input: { file_path: '/a.ts' } }] },
|
|
901
|
+
});
|
|
902
|
+
mockProc.stdout.emit('data', Buffer.from(event1 + '\n'));
|
|
903
|
+
expect(writeSpy).not.toHaveBeenCalled();
|
|
904
|
+
|
|
905
|
+
// Toggle verbose ON
|
|
906
|
+
dynamicVerbose = true;
|
|
907
|
+
|
|
908
|
+
// Second event with verbose ON — should display
|
|
909
|
+
const event2 = JSON.stringify({
|
|
910
|
+
type: 'assistant',
|
|
911
|
+
message: { content: [{ type: 'tool_use', name: 'Read', input: { file_path: '/b.ts' } }] },
|
|
912
|
+
});
|
|
913
|
+
mockProc.stdout.emit('data', Buffer.from(event2 + '\n'));
|
|
914
|
+
expect(writeSpy).toHaveBeenCalled();
|
|
915
|
+
|
|
916
|
+
mockProc.emit('close', 0);
|
|
917
|
+
await runPromise;
|
|
918
|
+
|
|
919
|
+
writeSpy.mockRestore();
|
|
920
|
+
});
|
|
921
|
+
});
|
|
922
|
+
|
|
767
923
|
describe('retry isolation (timeout per attempt)', () => {
|
|
768
924
|
function createMockProcess() {
|
|
769
925
|
const stdout = new EventEmitter();
|
|
@@ -804,9 +960,8 @@ describe('ClaudeRunner', () => {
|
|
|
804
960
|
jest.advanceTimersByTime(60000);
|
|
805
961
|
expect(mockProc2.kill).not.toHaveBeenCalled();
|
|
806
962
|
|
|
807
|
-
// Complete successfully
|
|
808
|
-
|
|
809
|
-
mockProc2.stdout.emit('data', Buffer.from(successEvent + '\n'));
|
|
963
|
+
// Complete successfully
|
|
964
|
+
mockProc2.stdout.emit('data', Buffer.from('success'));
|
|
810
965
|
mockProc2.emit('close', 0);
|
|
811
966
|
|
|
812
967
|
const result2 = await runPromise2;
|
|
@@ -827,9 +982,8 @@ describe('ClaudeRunner', () => {
|
|
|
827
982
|
// Run for 4 minutes (240000ms)
|
|
828
983
|
jest.advanceTimersByTime(240000);
|
|
829
984
|
|
|
830
|
-
// Fail without timeout
|
|
831
|
-
|
|
832
|
-
mockProc1.stdout.emit('data', Buffer.from(failEvent + '\n'));
|
|
985
|
+
// Fail without timeout
|
|
986
|
+
mockProc1.stdout.emit('data', Buffer.from('<promise>FAILED</promise>'));
|
|
833
987
|
mockProc1.emit('close', 1);
|
|
834
988
|
|
|
835
989
|
const result1 = await runPromise1;
|
|
@@ -879,9 +1033,8 @@ describe('ClaudeRunner', () => {
|
|
|
879
1033
|
const runner = new ClaudeRunner();
|
|
880
1034
|
const runPromise = runner.run('test prompt', { timeout: 60 });
|
|
881
1035
|
|
|
882
|
-
// Emit output with completion marker
|
|
883
|
-
|
|
884
|
-
mockProc.stdout.emit('data', Buffer.from(event + '\n'));
|
|
1036
|
+
// Emit output with completion marker
|
|
1037
|
+
mockProc.stdout.emit('data', Buffer.from('Writing outcome...\n<promise>COMPLETE</promise>\n'));
|
|
885
1038
|
|
|
886
1039
|
// Process should not be killed immediately
|
|
887
1040
|
expect(mockProc.kill).not.toHaveBeenCalled();
|
|
@@ -906,9 +1059,8 @@ describe('ClaudeRunner', () => {
|
|
|
906
1059
|
const runner = new ClaudeRunner();
|
|
907
1060
|
const runPromise = runner.run('test prompt', { timeout: 60 });
|
|
908
1061
|
|
|
909
|
-
// Emit output with failed marker
|
|
910
|
-
|
|
911
|
-
mockProc.stdout.emit('data', Buffer.from(event + '\n'));
|
|
1062
|
+
// Emit output with failed marker
|
|
1063
|
+
mockProc.stdout.emit('data', Buffer.from('<promise>FAILED</promise>\nReason: test error'));
|
|
912
1064
|
|
|
913
1065
|
// Advance past grace period
|
|
914
1066
|
jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS + 1);
|
|
@@ -925,15 +1077,13 @@ describe('ClaudeRunner', () => {
|
|
|
925
1077
|
const runner = new ClaudeRunner();
|
|
926
1078
|
const runPromise = runner.run('test prompt', { timeout: 60 });
|
|
927
1079
|
|
|
928
|
-
// Emit partial output (no marker yet)
|
|
929
|
-
|
|
930
|
-
mockProc.stdout.emit('data', Buffer.from(event1 + '\n'));
|
|
1080
|
+
// Emit partial output (no marker yet)
|
|
1081
|
+
mockProc.stdout.emit('data', Buffer.from('Working on task...\n'));
|
|
931
1082
|
jest.advanceTimersByTime(10000);
|
|
932
1083
|
expect(mockProc.kill).not.toHaveBeenCalled();
|
|
933
1084
|
|
|
934
|
-
// Emit marker in second chunk
|
|
935
|
-
|
|
936
|
-
mockProc.stdout.emit('data', Buffer.from(event2 + '\n'));
|
|
1085
|
+
// Emit marker in second chunk
|
|
1086
|
+
mockProc.stdout.emit('data', Buffer.from('<promise>COMPLETE</promise>\n'));
|
|
937
1087
|
|
|
938
1088
|
// Grace period starts now - advance past it
|
|
939
1089
|
jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS + 1);
|
|
@@ -951,9 +1101,8 @@ describe('ClaudeRunner', () => {
|
|
|
951
1101
|
const runner = new ClaudeRunner();
|
|
952
1102
|
const runPromise = runner.run('test prompt', { timeout: 60 });
|
|
953
1103
|
|
|
954
|
-
// Emit completion marker
|
|
955
|
-
|
|
956
|
-
mockProc.stdout.emit('data', Buffer.from(event + '\n'));
|
|
1104
|
+
// Emit completion marker
|
|
1105
|
+
mockProc.stdout.emit('data', Buffer.from('<promise>COMPLETE</promise>\n'));
|
|
957
1106
|
|
|
958
1107
|
// Process exits naturally before grace period
|
|
959
1108
|
jest.advanceTimersByTime(5000);
|
|
@@ -983,9 +1132,8 @@ describe('ClaudeRunner', () => {
|
|
|
983
1132
|
outcomeFilePath: outcomePath,
|
|
984
1133
|
});
|
|
985
1134
|
|
|
986
|
-
// No output marker in stdout - just regular output
|
|
987
|
-
|
|
988
|
-
mockProc.stdout.emit('data', Buffer.from(event + '\n'));
|
|
1135
|
+
// No output marker in stdout - just regular output
|
|
1136
|
+
mockProc.stdout.emit('data', Buffer.from('Working on task...'));
|
|
989
1137
|
|
|
990
1138
|
// After some time, outcome file appears with marker
|
|
991
1139
|
jest.advanceTimersByTime(OUTCOME_POLL_INTERVAL_MS - 1);
|
|
@@ -1029,9 +1177,8 @@ describe('ClaudeRunner', () => {
|
|
|
1029
1177
|
jest.advanceTimersByTime(OUTCOME_POLL_INTERVAL_MS * 5);
|
|
1030
1178
|
expect(mockProc.kill).not.toHaveBeenCalled();
|
|
1031
1179
|
|
|
1032
|
-
// Complete normally
|
|
1033
|
-
|
|
1034
|
-
mockProc.stdout.emit('data', Buffer.from(doneEvent + '\n'));
|
|
1180
|
+
// Complete normally
|
|
1181
|
+
mockProc.stdout.emit('data', Buffer.from('done'));
|
|
1035
1182
|
mockProc.emit('close', 0);
|
|
1036
1183
|
|
|
1037
1184
|
await runPromise;
|
|
@@ -1064,16 +1211,14 @@ describe('ClaudeRunner', () => {
|
|
|
1064
1211
|
const runner = new ClaudeRunner();
|
|
1065
1212
|
const runPromise = runner.run('test prompt', { timeout: 60 });
|
|
1066
1213
|
|
|
1067
|
-
// Emit first marker
|
|
1068
|
-
|
|
1069
|
-
mockProc.stdout.emit('data', Buffer.from(event1 + '\n'));
|
|
1214
|
+
// Emit first marker
|
|
1215
|
+
mockProc.stdout.emit('data', Buffer.from('<promise>COMPLETE</promise>\n'));
|
|
1070
1216
|
|
|
1071
1217
|
// Advance halfway through grace period
|
|
1072
1218
|
jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS / 2);
|
|
1073
1219
|
|
|
1074
|
-
// Emit second marker (e.g., Claude reading back what it wrote)
|
|
1075
|
-
|
|
1076
|
-
mockProc.stdout.emit('data', Buffer.from(event2 + '\n'));
|
|
1220
|
+
// Emit second marker (e.g., Claude reading back what it wrote)
|
|
1221
|
+
mockProc.stdout.emit('data', Buffer.from('Verified: <promise>COMPLETE</promise>\n'));
|
|
1077
1222
|
|
|
1078
1223
|
// Advance remaining grace period from FIRST detection
|
|
1079
1224
|
jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS / 2 + 1);
|
|
@@ -1129,9 +1274,8 @@ describe('ClaudeRunner', () => {
|
|
|
1129
1274
|
commitContext,
|
|
1130
1275
|
});
|
|
1131
1276
|
|
|
1132
|
-
// Emit COMPLETE marker
|
|
1133
|
-
|
|
1134
|
-
mockProc.stdout.emit('data', Buffer.from(event + '\n'));
|
|
1277
|
+
// Emit COMPLETE marker
|
|
1278
|
+
mockProc.stdout.emit('data', Buffer.from('<promise>COMPLETE</promise>\n'));
|
|
1135
1279
|
|
|
1136
1280
|
// Advance to grace period expiry - commit already verified
|
|
1137
1281
|
jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS + 1);
|
|
@@ -1155,9 +1299,8 @@ describe('ClaudeRunner', () => {
|
|
|
1155
1299
|
commitContext,
|
|
1156
1300
|
});
|
|
1157
1301
|
|
|
1158
|
-
// Emit COMPLETE marker
|
|
1159
|
-
|
|
1160
|
-
mockProc.stdout.emit('data', Buffer.from(event + '\n'));
|
|
1302
|
+
// Emit COMPLETE marker
|
|
1303
|
+
mockProc.stdout.emit('data', Buffer.from('<promise>COMPLETE</promise>\n'));
|
|
1161
1304
|
|
|
1162
1305
|
// Advance past initial grace period - commit not found, should extend
|
|
1163
1306
|
jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS + 1);
|
|
@@ -1191,9 +1334,8 @@ describe('ClaudeRunner', () => {
|
|
|
1191
1334
|
commitContext,
|
|
1192
1335
|
});
|
|
1193
1336
|
|
|
1194
|
-
// Emit COMPLETE marker
|
|
1195
|
-
|
|
1196
|
-
mockProc.stdout.emit('data', Buffer.from(event + '\n'));
|
|
1337
|
+
// Emit COMPLETE marker
|
|
1338
|
+
mockProc.stdout.emit('data', Buffer.from('<promise>COMPLETE</promise>\n'));
|
|
1197
1339
|
|
|
1198
1340
|
// Advance past initial grace period
|
|
1199
1341
|
jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS + 1);
|
|
@@ -1219,9 +1361,8 @@ describe('ClaudeRunner', () => {
|
|
|
1219
1361
|
commitContext,
|
|
1220
1362
|
});
|
|
1221
1363
|
|
|
1222
|
-
// Emit FAILED marker (not COMPLETE)
|
|
1223
|
-
|
|
1224
|
-
mockProc.stdout.emit('data', Buffer.from(event + '\n'));
|
|
1364
|
+
// Emit FAILED marker (not COMPLETE)
|
|
1365
|
+
mockProc.stdout.emit('data', Buffer.from('<promise>FAILED</promise>\n'));
|
|
1225
1366
|
|
|
1226
1367
|
// Grace period should expire normally without extension
|
|
1227
1368
|
jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS + 1);
|
|
@@ -1240,9 +1381,8 @@ describe('ClaudeRunner', () => {
|
|
|
1240
1381
|
// No commitContext provided
|
|
1241
1382
|
});
|
|
1242
1383
|
|
|
1243
|
-
// Emit COMPLETE marker
|
|
1244
|
-
|
|
1245
|
-
mockProc.stdout.emit('data', Buffer.from(event + '\n'));
|
|
1384
|
+
// Emit COMPLETE marker
|
|
1385
|
+
mockProc.stdout.emit('data', Buffer.from('<promise>COMPLETE</promise>\n'));
|
|
1246
1386
|
|
|
1247
1387
|
// Grace period should expire normally (no commit check)
|
|
1248
1388
|
jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS + 1);
|
|
@@ -1267,9 +1407,8 @@ describe('ClaudeRunner', () => {
|
|
|
1267
1407
|
commitContext,
|
|
1268
1408
|
});
|
|
1269
1409
|
|
|
1270
|
-
// Emit COMPLETE marker
|
|
1271
|
-
|
|
1272
|
-
mockProc.stdout.emit('data', Buffer.from(event + '\n'));
|
|
1410
|
+
// Emit COMPLETE marker
|
|
1411
|
+
mockProc.stdout.emit('data', Buffer.from('<promise>COMPLETE</promise>\n'));
|
|
1273
1412
|
|
|
1274
1413
|
// Grace period expires - commit message doesn't match, should extend
|
|
1275
1414
|
jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS + 1);
|
|
@@ -1302,9 +1441,8 @@ describe('ClaudeRunner', () => {
|
|
|
1302
1441
|
commitContext,
|
|
1303
1442
|
});
|
|
1304
1443
|
|
|
1305
|
-
// Emit COMPLETE marker
|
|
1306
|
-
|
|
1307
|
-
mockProc.stdout.emit('data', Buffer.from(event + '\n'));
|
|
1444
|
+
// Emit COMPLETE marker
|
|
1445
|
+
mockProc.stdout.emit('data', Buffer.from('<promise>COMPLETE</promise>\n'));
|
|
1308
1446
|
|
|
1309
1447
|
// Grace period expires - file not committed, should extend
|
|
1310
1448
|
jest.advanceTimersByTime(COMPLETION_GRACE_PERIOD_MS + 1);
|
|
@@ -1321,329 +1459,4 @@ describe('ClaudeRunner', () => {
|
|
|
1321
1459
|
await runPromise;
|
|
1322
1460
|
});
|
|
1323
1461
|
});
|
|
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
1462
|
});
|