@@ -13,9 +13,80 @@ vi.mock("vscode", () => ({
1313 } ,
1414} ) )
1515
16+ // Mock execa to test stdin behavior
17+ const mockExeca = vi . fn ( )
18+ const mockStdin = {
19+ write : vi . fn ( ) ,
20+ end : vi . fn ( ) ,
21+ }
22+
23+ // Mock process that simulates successful execution
24+ const createMockProcess = ( ) => {
25+ let resolveProcess : ( value : { exitCode : number } ) => void
26+ const processPromise = new Promise < { exitCode : number } > ( ( resolve ) => {
27+ resolveProcess = resolve
28+ } )
29+
30+ const mockProcess = {
31+ stdin : mockStdin ,
32+ stdout : {
33+ on : vi . fn ( ) ,
34+ } ,
35+ stderr : {
36+ on : vi . fn ( ( event , callback ) => {
37+ // Don't emit any stderr data in tests
38+ } ) ,
39+ } ,
40+ on : vi . fn ( ( event , callback ) => {
41+ if ( event === "close" ) {
42+ // Simulate successful process completion after a short delay
43+ setTimeout ( ( ) => {
44+ callback ( 0 )
45+ resolveProcess ( { exitCode : 0 } )
46+ } , 10 )
47+ }
48+ if ( event === "error" ) {
49+ // Don't emit any errors in tests
50+ }
51+ } ) ,
52+ killed : false ,
53+ kill : vi . fn ( ) ,
54+ then : processPromise . then . bind ( processPromise ) ,
55+ catch : processPromise . catch . bind ( processPromise ) ,
56+ finally : processPromise . finally . bind ( processPromise ) ,
57+ }
58+ return mockProcess
59+ }
60+
61+ vi . mock ( "execa" , ( ) => ( {
62+ execa : mockExeca ,
63+ } ) )
64+
65+ // Mock readline with proper interface simulation
66+ let mockReadlineInterface : any = null
67+
68+ vi . mock ( "readline" , ( ) => ( {
69+ default : {
70+ createInterface : vi . fn ( ( ) => {
71+ mockReadlineInterface = {
72+ async * [ Symbol . asyncIterator ] ( ) {
73+ // Simulate Claude CLI JSON output
74+ yield '{"type":"text","text":"Hello"}'
75+ yield '{"type":"text","text":" world"}'
76+ // Simulate end of stream - must return to terminate the iterator
77+ return
78+ } ,
79+ close : vi . fn ( ) ,
80+ }
81+ return mockReadlineInterface
82+ } ) ,
83+ } ,
84+ } ) )
85+
1686describe ( "runClaudeCode" , ( ) => {
1787 beforeEach ( ( ) => {
1888 vi . clearAllMocks ( )
89+ mockExeca . mockReturnValue ( createMockProcess ( ) )
1990 } )
2091
2192 test ( "should export runClaudeCode function" , async ( ) => {
@@ -34,4 +105,99 @@ describe("runClaudeCode", () => {
34105 expect ( Symbol . asyncIterator in result ) . toBe ( true )
35106 expect ( typeof result [ Symbol . asyncIterator ] ) . toBe ( "function" )
36107 } )
108+
109+ test ( "should use stdin instead of command line arguments for messages" , async ( ) => {
110+ const { runClaudeCode } = await import ( "../run" )
111+ const messages = [ { role : "user" as const , content : "Hello world!" } ]
112+ const options = {
113+ systemPrompt : "You are a helpful assistant" ,
114+ messages,
115+ }
116+
117+ const generator = runClaudeCode ( options )
118+
119+ // Consume the generator to completion
120+ const results = [ ]
121+ for await ( const chunk of generator ) {
122+ results . push ( chunk )
123+ }
124+
125+ // Verify execa was called with correct arguments (no JSON.stringify(messages) in args)
126+ expect ( mockExeca ) . toHaveBeenCalledWith (
127+ "claude" ,
128+ expect . arrayContaining ( [
129+ "-p" ,
130+ "--input-format" ,
131+ "text" ,
132+ "--system-prompt" ,
133+ "You are a helpful assistant" ,
134+ "--verbose" ,
135+ "--output-format" ,
136+ "stream-json" ,
137+ "--disallowedTools" ,
138+ expect . any ( String ) ,
139+ "--max-turns" ,
140+ "1" ,
141+ ] ) ,
142+ expect . objectContaining ( {
143+ stdin : "pipe" ,
144+ stdout : "pipe" ,
145+ stderr : "pipe" ,
146+ } ) ,
147+ )
148+
149+ // Verify the arguments do NOT contain the stringified messages
150+ const [ , args ] = mockExeca . mock . calls [ 0 ]
151+ expect ( args ) . not . toContain ( JSON . stringify ( messages ) )
152+
153+ // Verify messages were written to stdin
154+ expect ( mockStdin . write ) . toHaveBeenCalledWith ( JSON . stringify ( messages ) , "utf8" )
155+ expect ( mockStdin . end ) . toHaveBeenCalled ( )
156+
157+ // Verify we got the expected mock output
158+ expect ( results ) . toHaveLength ( 2 )
159+ expect ( results [ 0 ] ) . toEqual ( { type : "text" , text : "Hello" } )
160+ expect ( results [ 1 ] ) . toEqual ( { type : "text" , text : " world" } )
161+ } )
162+
163+ test ( "should include model parameter when provided" , async ( ) => {
164+ const { runClaudeCode } = await import ( "../run" )
165+ const options = {
166+ systemPrompt : "You are a helpful assistant" ,
167+ messages : [ { role : "user" as const , content : "Hello" } ] ,
168+ modelId : "claude-3-5-sonnet-20241022" ,
169+ }
170+
171+ const generator = runClaudeCode ( options )
172+
173+ // Consume at least one item to trigger process spawn
174+ await generator . next ( )
175+
176+ // Clean up the generator
177+ await generator . return ( undefined )
178+
179+ const [ , args ] = mockExeca . mock . calls [ 0 ]
180+ expect ( args ) . toContain ( "--model" )
181+ expect ( args ) . toContain ( "claude-3-5-sonnet-20241022" )
182+ } )
183+
184+ test ( "should use custom claude path when provided" , async ( ) => {
185+ const { runClaudeCode } = await import ( "../run" )
186+ const options = {
187+ systemPrompt : "You are a helpful assistant" ,
188+ messages : [ { role : "user" as const , content : "Hello" } ] ,
189+ path : "/custom/path/to/claude" ,
190+ }
191+
192+ const generator = runClaudeCode ( options )
193+
194+ // Consume at least one item to trigger process spawn
195+ await generator . next ( )
196+
197+ // Clean up the generator
198+ await generator . return ( undefined )
199+
200+ const [ claudePath ] = mockExeca . mock . calls [ 0 ]
201+ expect ( claudePath ) . toBe ( "/custom/path/to/claude" )
202+ } )
37203} )
0 commit comments