@@ -13,9 +13,92 @@ 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 ( ( data , encoding , callback ) => {
20+ // Simulate successful write
21+ if ( callback ) callback ( null )
22+ } ) ,
23+ end : vi . fn ( ) ,
24+ }
25+
26+ // Mock process that simulates successful execution
27+ const createMockProcess = ( ) => {
28+ let resolveProcess : ( value : { exitCode : number } ) => void
29+ const processPromise = new Promise < { exitCode : number } > ( ( resolve ) => {
30+ resolveProcess = resolve
31+ } )
32+
33+ const mockProcess = {
34+ stdin : mockStdin ,
35+ stdout : {
36+ on : vi . fn ( ) ,
37+ } ,
38+ stderr : {
39+ on : vi . fn ( ( event , callback ) => {
40+ // Don't emit any stderr data in tests
41+ } ) ,
42+ } ,
43+ on : vi . fn ( ( event , callback ) => {
44+ if ( event === "close" ) {
45+ // Simulate successful process completion after a short delay
46+ setTimeout ( ( ) => {
47+ callback ( 0 )
48+ resolveProcess ( { exitCode : 0 } )
49+ } , 10 )
50+ }
51+ if ( event === "error" ) {
52+ // Don't emit any errors in tests
53+ }
54+ } ) ,
55+ killed : false ,
56+ kill : vi . fn ( ) ,
57+ then : processPromise . then . bind ( processPromise ) ,
58+ catch : processPromise . catch . bind ( processPromise ) ,
59+ finally : processPromise . finally . bind ( processPromise ) ,
60+ }
61+ return mockProcess
62+ }
63+
64+ vi . mock ( "execa" , ( ) => ( {
65+ execa : mockExeca ,
66+ } ) )
67+
68+ // Mock readline with proper interface simulation
69+ let mockReadlineInterface : any = null
70+
71+ vi . mock ( "readline" , ( ) => ( {
72+ default : {
73+ createInterface : vi . fn ( ( ) => {
74+ mockReadlineInterface = {
75+ async * [ Symbol . asyncIterator ] ( ) {
76+ // Simulate Claude CLI JSON output
77+ yield '{"type":"text","text":"Hello"}'
78+ yield '{"type":"text","text":" world"}'
79+ // Simulate end of stream - must return to terminate the iterator
80+ return
81+ } ,
82+ close : vi . fn ( ) ,
83+ }
84+ return mockReadlineInterface
85+ } ) ,
86+ } ,
87+ } ) )
88+
1689describe ( "runClaudeCode" , ( ) => {
1790 beforeEach ( ( ) => {
1891 vi . clearAllMocks ( )
92+ mockExeca . mockReturnValue ( createMockProcess ( ) )
93+ // Mock setImmediate to run synchronously in tests
94+ vi . spyOn ( global , "setImmediate" ) . mockImplementation ( ( callback : any ) => {
95+ callback ( )
96+ return { } as any
97+ } )
98+ } )
99+
100+ afterEach ( ( ) => {
101+ vi . restoreAllMocks ( )
19102 } )
20103
21104 test ( "should export runClaudeCode function" , async ( ) => {
@@ -34,4 +117,174 @@ describe("runClaudeCode", () => {
34117 expect ( Symbol . asyncIterator in result ) . toBe ( true )
35118 expect ( typeof result [ Symbol . asyncIterator ] ) . toBe ( "function" )
36119 } )
120+
121+ test ( "should use stdin instead of command line arguments for messages" , async ( ) => {
122+ const { runClaudeCode } = await import ( "../run" )
123+ const messages = [ { role : "user" as const , content : "Hello world!" } ]
124+ const options = {
125+ systemPrompt : "You are a helpful assistant" ,
126+ messages,
127+ }
128+
129+ const generator = runClaudeCode ( options )
130+
131+ // Consume the generator to completion
132+ const results = [ ]
133+ for await ( const chunk of generator ) {
134+ results . push ( chunk )
135+ }
136+
137+ // Verify execa was called with correct arguments (no JSON.stringify(messages) in args)
138+ expect ( mockExeca ) . toHaveBeenCalledWith (
139+ "claude" ,
140+ expect . arrayContaining ( [
141+ "-p" ,
142+ "--system-prompt" ,
143+ "You are a helpful assistant" ,
144+ "--verbose" ,
145+ "--output-format" ,
146+ "stream-json" ,
147+ "--disallowedTools" ,
148+ expect . any ( String ) ,
149+ "--max-turns" ,
150+ "1" ,
151+ ] ) ,
152+ expect . objectContaining ( {
153+ stdin : "pipe" ,
154+ stdout : "pipe" ,
155+ stderr : "pipe" ,
156+ } ) ,
157+ )
158+
159+ // Verify the arguments do NOT contain the stringified messages
160+ const [ , args ] = mockExeca . mock . calls [ 0 ]
161+ expect ( args ) . not . toContain ( JSON . stringify ( messages ) )
162+
163+ // Verify messages were written to stdin with callback
164+ expect ( mockStdin . write ) . toHaveBeenCalledWith ( JSON . stringify ( messages ) , "utf8" , expect . any ( Function ) )
165+ expect ( mockStdin . end ) . toHaveBeenCalled ( )
166+
167+ // Verify we got the expected mock output
168+ expect ( results ) . toHaveLength ( 2 )
169+ expect ( results [ 0 ] ) . toEqual ( { type : "text" , text : "Hello" } )
170+ expect ( results [ 1 ] ) . toEqual ( { type : "text" , text : " world" } )
171+ } )
172+
173+ test ( "should include model parameter when provided" , async ( ) => {
174+ const { runClaudeCode } = await import ( "../run" )
175+ const options = {
176+ systemPrompt : "You are a helpful assistant" ,
177+ messages : [ { role : "user" as const , content : "Hello" } ] ,
178+ modelId : "claude-3-5-sonnet-20241022" ,
179+ }
180+
181+ const generator = runClaudeCode ( options )
182+
183+ // Consume at least one item to trigger process spawn
184+ await generator . next ( )
185+
186+ // Clean up the generator
187+ await generator . return ( undefined )
188+
189+ const [ , args ] = mockExeca . mock . calls [ 0 ]
190+ expect ( args ) . toContain ( "--model" )
191+ expect ( args ) . toContain ( "claude-3-5-sonnet-20241022" )
192+ } )
193+
194+ test ( "should use custom claude path when provided" , async ( ) => {
195+ const { runClaudeCode } = await import ( "../run" )
196+ const options = {
197+ systemPrompt : "You are a helpful assistant" ,
198+ messages : [ { role : "user" as const , content : "Hello" } ] ,
199+ path : "/custom/path/to/claude" ,
200+ }
201+
202+ const generator = runClaudeCode ( options )
203+
204+ // Consume at least one item to trigger process spawn
205+ await generator . next ( )
206+
207+ // Clean up the generator
208+ await generator . return ( undefined )
209+
210+ const [ claudePath ] = mockExeca . mock . calls [ 0 ]
211+ expect ( claudePath ) . toBe ( "/custom/path/to/claude" )
212+ } )
213+
214+ test ( "should handle stdin write errors gracefully" , async ( ) => {
215+ const { runClaudeCode } = await import ( "../run" )
216+
217+ // Create a mock process with stdin that fails
218+ const mockProcessWithError = createMockProcess ( )
219+ mockProcessWithError . stdin . write = vi . fn ( ( data , encoding , callback ) => {
220+ // Simulate write error
221+ if ( callback ) callback ( new Error ( "EPIPE: broken pipe" ) )
222+ } )
223+
224+ // Mock console.error to verify error logging
225+ const consoleErrorSpy = vi . spyOn ( console , "error" ) . mockImplementation ( ( ) => { } )
226+
227+ mockExeca . mockReturnValueOnce ( mockProcessWithError )
228+
229+ const options = {
230+ systemPrompt : "You are a helpful assistant" ,
231+ messages : [ { role : "user" as const , content : "Hello" } ] ,
232+ }
233+
234+ const generator = runClaudeCode ( options )
235+
236+ // Try to consume the generator
237+ try {
238+ await generator . next ( )
239+ } catch ( error ) {
240+ // Expected to fail
241+ }
242+
243+ // Verify error was logged
244+ expect ( consoleErrorSpy ) . toHaveBeenCalledWith ( "Error writing to Claude Code stdin:" , expect . any ( Error ) )
245+
246+ // Verify process was killed
247+ expect ( mockProcessWithError . kill ) . toHaveBeenCalled ( )
248+
249+ // Clean up
250+ consoleErrorSpy . mockRestore ( )
251+ await generator . return ( undefined )
252+ } )
253+
254+ test ( "should handle stdin access errors gracefully" , async ( ) => {
255+ const { runClaudeCode } = await import ( "../run" )
256+
257+ // Create a mock process without stdin
258+ const mockProcessWithoutStdin = createMockProcess ( )
259+ mockProcessWithoutStdin . stdin = null as any
260+
261+ // Mock console.error to verify error logging
262+ const consoleErrorSpy = vi . spyOn ( console , "error" ) . mockImplementation ( ( ) => { } )
263+
264+ mockExeca . mockReturnValueOnce ( mockProcessWithoutStdin )
265+
266+ const options = {
267+ systemPrompt : "You are a helpful assistant" ,
268+ messages : [ { role : "user" as const , content : "Hello" } ] ,
269+ }
270+
271+ const generator = runClaudeCode ( options )
272+
273+ // Try to consume the generator
274+ try {
275+ await generator . next ( )
276+ } catch ( error ) {
277+ // Expected to fail
278+ }
279+
280+ // Verify error was logged
281+ expect ( consoleErrorSpy ) . toHaveBeenCalledWith ( "Error accessing Claude Code stdin:" , expect . any ( Error ) )
282+
283+ // Verify process was killed
284+ expect ( mockProcessWithoutStdin . kill ) . toHaveBeenCalled ( )
285+
286+ // Clean up
287+ consoleErrorSpy . mockRestore ( )
288+ await generator . return ( undefined )
289+ } )
37290} )
0 commit comments