1+ /**
2+ * @license
3+ * Copyright 2025 Kolosal Inc.
4+ * SPDX-License-Identifier: Apache-2.0
5+ */
6+
7+ import { describe , expect , it , vi , beforeEach , afterEach } from 'vitest' ;
8+ import type { Config , WorkspaceContext } from '@kolosal-ai/kolosal-ai-core' ;
9+ import { ApprovalMode } from '@kolosal-ai/kolosal-ai-core' ;
10+ import { GenerationService } from '../services/generation.service.js' ;
11+
12+ // Mock the core module
13+ vi . mock ( '@kolosal-ai/kolosal-ai-core' , async ( ) => {
14+ const actual = await vi . importActual ( '@kolosal-ai/kolosal-ai-core' ) ;
15+ return {
16+ ...actual ,
17+ WorkspaceContext : vi . fn ( ) . mockImplementation ( ( directory : string ) => ( {
18+ directory,
19+ getDirectories : vi . fn ( ) . mockReturnValue ( [ directory ] ) ,
20+ } ) ) ,
21+ } ;
22+ } ) ;
23+
24+ describe ( 'GenerationService - Working Directory' , ( ) => {
25+ let mockConfig : Partial < Config > ;
26+ let generationService : GenerationService ;
27+ let originalWorkspaceContext : Partial < WorkspaceContext > ;
28+
29+ beforeEach ( ( ) => {
30+ // Create original workspace context
31+ originalWorkspaceContext = {
32+ getDirectories : vi . fn ( ) . mockReturnValue ( [ '/original/path' ] ) ,
33+ } ;
34+
35+ // Create mock config
36+ mockConfig = {
37+ getApprovalMode : vi . fn ( ) . mockReturnValue ( ApprovalMode . DEFAULT ) ,
38+ setApprovalMode : vi . fn ( ) ,
39+ getWorkspaceContext : vi . fn ( ) . mockReturnValue ( originalWorkspaceContext ) ,
40+ setWorkspaceContext : vi . fn ( ) ,
41+ getGeminiClient : vi . fn ( ) . mockReturnValue ( {
42+ isInitialized : vi . fn ( ) . mockReturnValue ( true ) ,
43+ setHistory : vi . fn ( ) ,
44+ sendMessageStream : vi . fn ( ) . mockReturnValue ( [ ] ) ,
45+ } ) ,
46+ } ;
47+
48+ generationService = new GenerationService ( mockConfig as Config ) ;
49+ } ) ;
50+
51+ afterEach ( ( ) => {
52+ vi . clearAllMocks ( ) ;
53+ } ) ;
54+
55+ it ( 'should not change workspace context when no working directory is provided' , async ( ) => {
56+ const abortController = new AbortController ( ) ;
57+
58+ // Mock the generation flow to avoid actual AI calls
59+ const mockSendMessageStream = vi . fn ( ) . mockReturnValue ( [ ] ) ;
60+ mockConfig . getGeminiClient = vi . fn ( ) . mockReturnValue ( {
61+ isInitialized : vi . fn ( ) . mockReturnValue ( true ) ,
62+ setHistory : vi . fn ( ) ,
63+ sendMessageStream : mockSendMessageStream ,
64+ } ) ;
65+
66+ try {
67+ await generationService . generateResponse (
68+ 'test input' ,
69+ 'test-prompt-id' ,
70+ abortController . signal ,
71+ {
72+ // No working directory provided
73+ }
74+ ) ;
75+ } catch {
76+ // Ignore errors for this test - we're only testing workspace context behavior
77+ }
78+
79+ // Workspace context should not be changed
80+ expect ( mockConfig . setWorkspaceContext ) . not . toHaveBeenCalled ( ) ;
81+ } ) ;
82+
83+ it ( 'should temporarily change workspace context when working directory is provided' , async ( ) => {
84+ const abortController = new AbortController ( ) ;
85+ const testWorkingDirectory = '/test/working/directory' ;
86+
87+ // Mock the generation flow to avoid actual AI calls
88+ const mockSendMessageStream = vi . fn ( ) . mockReturnValue ( [ ] ) ;
89+ mockConfig . getGeminiClient = vi . fn ( ) . mockReturnValue ( {
90+ isInitialized : vi . fn ( ) . mockReturnValue ( true ) ,
91+ setHistory : vi . fn ( ) ,
92+ sendMessageStream : mockSendMessageStream ,
93+ } ) ;
94+
95+ try {
96+ await generationService . generateResponse (
97+ 'test input' ,
98+ 'test-prompt-id' ,
99+ abortController . signal ,
100+ {
101+ workingDirectory : testWorkingDirectory ,
102+ }
103+ ) ;
104+ } catch {
105+ // Ignore errors for this test - we're only testing workspace context behavior
106+ }
107+
108+ // Workspace context should be changed and then restored
109+ expect ( mockConfig . setWorkspaceContext ) . toHaveBeenCalledTimes ( 2 ) ;
110+
111+ // First call should set the new workspace context
112+ const firstCall = ( mockConfig . setWorkspaceContext as any ) . mock . calls [ 0 ] ;
113+ expect ( firstCall [ 0 ] ) . toEqual ( expect . objectContaining ( {
114+ getDirectories : expect . any ( Function ) ,
115+ } ) ) ;
116+
117+ // Second call should restore the original workspace context
118+ const secondCall = ( mockConfig . setWorkspaceContext as any ) . mock . calls [ 1 ] ;
119+ expect ( secondCall [ 0 ] ) . toBe ( originalWorkspaceContext ) ;
120+ } ) ;
121+
122+ it ( 'should restore original workspace context even if generation fails' , async ( ) => {
123+ const abortController = new AbortController ( ) ;
124+ const testWorkingDirectory = '/test/working/directory' ;
125+
126+ // Mock the generation to throw an error
127+ mockConfig . getGeminiClient = vi . fn ( ) . mockReturnValue ( {
128+ isInitialized : vi . fn ( ) . mockReturnValue ( true ) ,
129+ setHistory : vi . fn ( ) ,
130+ sendMessageStream : vi . fn ( ) . mockRejectedValue ( new Error ( 'Generation failed' ) ) ,
131+ } ) ;
132+
133+ try {
134+ await generationService . generateResponse (
135+ 'test input' ,
136+ 'test-prompt-id' ,
137+ abortController . signal ,
138+ {
139+ workingDirectory : testWorkingDirectory ,
140+ }
141+ ) ;
142+ } catch {
143+ // Expected to fail
144+ }
145+
146+ // Workspace context should still be restored even after error
147+ expect ( mockConfig . setWorkspaceContext ) . toHaveBeenCalledTimes ( 2 ) ;
148+
149+ // Second call should restore the original workspace context
150+ const secondCall = ( mockConfig . setWorkspaceContext as any ) . mock . calls [ 1 ] ;
151+ expect ( secondCall [ 0 ] ) . toBe ( originalWorkspaceContext ) ;
152+ } ) ;
153+
154+ it ( 'should handle WorkspaceContext creation failure gracefully' , async ( ) => {
155+ const abortController = new AbortController ( ) ;
156+ const testWorkingDirectory = '/test/working/directory' ;
157+
158+ // Mock the generation flow
159+ const mockSendMessageStream = vi . fn ( ) . mockReturnValue ( [ ] ) ;
160+ mockConfig . getGeminiClient = vi . fn ( ) . mockReturnValue ( {
161+ isInitialized : vi . fn ( ) . mockReturnValue ( true ) ,
162+ setHistory : vi . fn ( ) ,
163+ sendMessageStream : mockSendMessageStream ,
164+ } ) ;
165+
166+ // This test verifies that the code handles directory setup gracefully
167+ // Since WorkspaceContext is mocked to succeed, we just verify the workspace context was set
168+ try {
169+ await generationService . generateResponse (
170+ 'test input' ,
171+ 'test-prompt-id' ,
172+ abortController . signal ,
173+ {
174+ workingDirectory : testWorkingDirectory ,
175+ }
176+ ) ;
177+ } catch {
178+ // Ignore errors for this test
179+ }
180+
181+ // The workspace context should be changed even with mocked implementation
182+ expect ( mockConfig . setWorkspaceContext ) . toHaveBeenCalledTimes ( 2 ) ;
183+ } ) ;
184+
185+ it ( 'should set approval mode to YOLO and restore it afterwards' , async ( ) => {
186+ const abortController = new AbortController ( ) ;
187+
188+ // Mock the generation flow
189+ const mockSendMessageStream = vi . fn ( ) . mockReturnValue ( [ ] ) ;
190+ mockConfig . getGeminiClient = vi . fn ( ) . mockReturnValue ( {
191+ isInitialized : vi . fn ( ) . mockReturnValue ( true ) ,
192+ setHistory : vi . fn ( ) ,
193+ sendMessageStream : mockSendMessageStream ,
194+ } ) ;
195+
196+ try {
197+ await generationService . generateResponse (
198+ 'test input' ,
199+ 'test-prompt-id' ,
200+ abortController . signal ,
201+ { }
202+ ) ;
203+ } catch {
204+ // Ignore errors for this test
205+ }
206+
207+ // Should set to YOLO mode and then restore original
208+ expect ( mockConfig . setApprovalMode ) . toHaveBeenCalledWith ( ApprovalMode . YOLO ) ;
209+ expect ( mockConfig . setApprovalMode ) . toHaveBeenLastCalledWith ( ApprovalMode . DEFAULT ) ;
210+ } ) ;
211+ } ) ;
0 commit comments