1+ import { describe , it , expect , vi , beforeEach , afterEach } from 'vitest' ;
2+ import type { MockedFunction } from 'vitest' ;
3+ // Removed type-only import for fs
4+ import fetch from 'node-fetch' ;
5+ import type { Response } from 'node-fetch' ; // Separate type import
6+ import { Buffer } from 'node:buffer' ; // Explicitly import Buffer
7+
8+ // --- Mocking ---
9+ // Mock node-fetch
10+ // We need to mock the default export for ESM
11+ vi . mock ( 'node-fetch' , ( ) => ( {
12+ __esModule : true , // This is important for ESM modules
13+ default : vi . fn ( ) ,
14+ } ) ) ;
15+ const mockedFetch = fetch as MockedFunction < typeof fetch > ;
16+
17+
18+ // Mock fs-extra
19+ vi . mock ( 'fs-extra' , async ( ) => {
20+ // Define mocks entirely inside the factory, but without default implementations for readJson/pathExists
21+ const mockEnsureDir = vi . fn ( ) ;
22+ const mockPathExists = vi . fn ( ) ;
23+ const mockReadJson = vi . fn ( ) ;
24+ const mockWriteJson = vi . fn ( ) ;
25+
26+ return {
27+ __esModule : true ,
28+ ensureDir : mockEnsureDir ,
29+ pathExists : mockPathExists ,
30+ readJson : mockReadJson ,
31+ writeJson : mockWriteJson ,
32+ // Mock default export if necessary
33+ default : {
34+ ensureDir : mockEnsureDir ,
35+ pathExists : mockPathExists ,
36+ readJson : mockReadJson ,
37+ writeJson : mockWriteJson ,
38+ } ,
39+ } ;
40+ } ) ;
41+
42+ // Import the mocked module directly. Vitest ensures this is the mocked version.
43+ import * as fs from 'fs-extra' ;
44+
45+ // Import the functions to test
46+ import {
47+ fetchLatestInstructions ,
48+ readCustomModes ,
49+ updateModesData ,
50+ writeCustomModes ,
51+ } from './index.js' ; // Updated import path
52+
53+ // --- Test Suite ---
54+ describe ( '@sylphx/setup_mode core logic' , ( ) => {
55+
56+ // Define constants used in tests (or export from src/index.ts)
57+ const SYLPHX_MODE_SLUG = 'sylphx' ;
58+ const SYLPHX_MODE_NAME = '🪽 Sylphx' ;
59+ const SYLPHX_MODE_GROUPS = [ "read" , "edit" , "browser" , "command" , "mcp" ] ;
60+ const SYLPHX_MODE_SOURCE = 'global' ;
61+
62+ beforeEach ( ( ) => {
63+ // Reset mocks before each test
64+ vi . clearAllMocks ( ) ;
65+ vi . resetAllMocks ( ) ; // Resets all mocks including spies and call history
66+ // Mock console methods to prevent test output clutter
67+ vi . spyOn ( console , 'log' ) . mockImplementation ( ( ) => { } ) ;
68+ vi . spyOn ( console , 'warn' ) . mockImplementation ( ( ) => { } ) ;
69+ vi . spyOn ( console , 'error' ) . mockImplementation ( ( ) => { } ) ;
70+ } ) ;
71+
72+ afterEach ( ( ) => {
73+ // Restore console mocks
74+ vi . restoreAllMocks ( ) ;
75+ } ) ;
76+
77+ // --- fetchLatestInstructions Tests ---
78+ describe ( 'fetchLatestInstructions' , ( ) => {
79+ // No dynamic import needed here anymore
80+
81+ it ( 'should fetch and decode instructions successfully' , async ( ) => {
82+ const mockContent = 'Test Markdown Content' ;
83+ const encodedContent = Buffer . from ( mockContent ) . toString ( 'base64' ) ;
84+ mockedFetch . mockResolvedValueOnce ( {
85+ ok : true ,
86+ // Return the expected structure directly
87+ json : async ( ) => ( { content : encodedContent , encoding : 'base64' } ) ,
88+ } as Response ) ; // Use the imported Response type
89+
90+ const instructions = await fetchLatestInstructions ( ) ;
91+ expect ( instructions ) . toBe ( mockContent ) ;
92+ expect ( mockedFetch ) . toHaveBeenCalledTimes ( 1 ) ;
93+ // Add check for URL if needed: expect(mockedFetch).toHaveBeenCalledWith(EXPECTED_URL, expect.any(Object));
94+ } ) ;
95+
96+ it ( 'should throw error on fetch failure' , async ( ) => {
97+ mockedFetch . mockResolvedValueOnce ( {
98+ ok : false ,
99+ status : 404 ,
100+ statusText : 'Not Found' ,
101+ } as Response ) ;
102+
103+ await expect ( fetchLatestInstructions ( ) ) . rejects . toThrow ( / G i t H u b A P I r e q u e s t f a i l e d : 4 0 4 N o t F o u n d / ) ;
104+ expect ( console . error ) . toHaveBeenCalledWith ( 'Error fetching from GitHub:' , expect . any ( Error ) ) ;
105+ } ) ;
106+
107+ it ( 'should throw error on API error message' , async ( ) => {
108+ mockedFetch . mockResolvedValueOnce ( {
109+ ok : true ,
110+ json : async ( ) => ( { message : 'API rate limit exceeded' } ) ,
111+ } as Response ) ;
112+
113+ await expect ( fetchLatestInstructions ( ) ) . rejects . toThrow ( / G i t H u b A P I e r r o r : A P I r a t e l i m i t e x c e e d e d / ) ;
114+ expect ( console . error ) . toHaveBeenCalledWith ( 'Error fetching from GitHub:' , expect . any ( Error ) ) ;
115+ } ) ;
116+
117+ it ( 'should throw error on invalid content structure' , async ( ) => {
118+ mockedFetch . mockResolvedValueOnce ( {
119+ ok : true ,
120+ json : async ( ) => ( { content : 'some content' , encoding : 'utf-8' } ) , // Wrong encoding
121+ } as Response ) ;
122+
123+ await expect ( fetchLatestInstructions ( ) ) . rejects . toThrow ( / I n v a l i d c o n t e n t r e c e i v e d f r o m G i t H u b A P I / ) ;
124+ expect ( console . error ) . toHaveBeenCalledWith ( 'Error fetching from GitHub:' , expect . any ( Error ) ) ;
125+ } ) ;
126+ } ) ;
127+
128+ // --- readCustomModes Tests ---
129+ describe ( 'readCustomModes' , ( ) => {
130+ // No dynamic import needed here anymore
131+
132+ it ( 'should read existing valid JSON' , async ( ) => {
133+ const mockData = { customModes : [ { slug : 'test' , name : 'Test' , roleDefinition : '...' , groups : [ ] , source : 'user' } ] } ;
134+ // Explicitly set mocks for this test case
135+ vi . mocked ( fs . pathExists ) . mockImplementationOnce ( async ( ) => true ) ; // Ensure pathExists is true
136+ vi . mocked ( fs . readJson ) . mockResolvedValueOnce ( mockData ) ; // Use Once for safety
137+
138+ const data = await readCustomModes ( ) ;
139+ expect ( data ) . toEqual ( mockData ) ;
140+ expect ( fs . ensureDir ) . toHaveBeenCalledTimes ( 1 ) ;
141+ expect ( fs . readJson ) . toHaveBeenCalledTimes ( 1 ) ;
142+ expect ( console . log ) . toHaveBeenCalledWith ( expect . stringContaining ( 'Successfully read custom modes file.' ) ) ;
143+ } ) ;
144+
145+ it ( 'should create file if it does not exist' , async ( ) => {
146+ vi . mocked ( fs . pathExists ) . mockImplementationOnce ( async ( ) => false ) ; // Use mockImplementationOnce
147+ const initialData = { customModes : [ ] } ;
148+ // Ensure writeJson mock is reset/doesn't interfere if needed, though not strictly necessary here
149+ vi . mocked ( fs . writeJson ) . mockResolvedValueOnce ( undefined ) ;
150+
151+ const data = await readCustomModes ( ) ;
152+ expect ( data ) . toEqual ( initialData ) ;
153+ expect ( fs . ensureDir ) . toHaveBeenCalledTimes ( 1 ) ;
154+ expect ( fs . writeJson ) . toHaveBeenCalledWith ( expect . any ( String ) , initialData , { spaces : 2 } ) ;
155+ expect ( fs . readJson ) . not . toHaveBeenCalled ( ) ;
156+ expect ( console . log ) . toHaveBeenCalledWith ( expect . stringContaining ( 'custom_modes.json not found. Creating a new one.' ) ) ;
157+ } ) ;
158+
159+ it ( 'should return default and warn on invalid JSON' , async ( ) => {
160+ vi . mocked ( fs . pathExists ) . mockImplementationOnce ( async ( ) => true ) ; // Use mockImplementationOnce
161+ vi . mocked ( fs . readJson ) . mockRejectedValueOnce ( new SyntaxError ( 'Invalid JSON' ) ) ; // Use Once
162+ const initialData = { customModes : [ ] } ;
163+
164+ const data = await readCustomModes ( ) ;
165+ expect ( data ) . toEqual ( initialData ) ;
166+ expect ( console . error ) . toHaveBeenCalledWith ( expect . stringContaining ( 'Error parsing JSON' ) , expect . any ( SyntaxError ) ) ;
167+ expect ( console . warn ) . toHaveBeenCalledWith ( expect . stringContaining ( 'File content might be corrupted. Attempting to reset.' ) ) ;
168+ } ) ;
169+
170+ it ( 'should return default and warn on malformed structure' , async ( ) => {
171+ vi . mocked ( fs . pathExists ) . mockImplementationOnce ( async ( ) => true ) ; // Use mockImplementationOnce
172+ vi . mocked ( fs . readJson ) . mockResolvedValueOnce ( { someOtherProperty : [ ] } ) ; // Use Once, Missing customModes array
173+ const initialData = { customModes : [ ] } ;
174+
175+ const data = await readCustomModes ( ) ;
176+ expect ( data ) . toEqual ( initialData ) ;
177+ expect ( console . warn ) . toHaveBeenCalledWith ( expect . stringContaining ( 'custom_modes.json seems malformed. Resetting to default structure.' ) ) ;
178+ } ) ;
179+
180+ it ( 'should throw error on other fs read errors' , async ( ) => {
181+ vi . mocked ( fs . pathExists ) . mockImplementationOnce ( async ( ) => true ) ; // Use mockImplementationOnce
182+ const readError = new Error ( 'Permission denied' ) ;
183+ vi . mocked ( fs . readJson ) . mockRejectedValueOnce ( readError ) ; // Use Once
184+
185+ await expect ( readCustomModes ( ) ) . rejects . toThrow ( / F a i l e d t o r e a d o r p a r s e .* P e r m i s s i o n d e n i e d / ) ;
186+ expect ( console . error ) . toHaveBeenCalledWith ( 'Error reading custom modes file:' , readError ) ;
187+ } ) ;
188+ } ) ;
189+
190+ // --- updateModesData Tests ---
191+ describe ( 'updateModesData' , ( ) => {
192+ // No dynamic import needed here anymore
193+
194+ it ( 'should add new mode if not exists' , ( ) => {
195+ const initialData = { customModes : [ ] } ;
196+ const instructions = 'New Instructions' ;
197+ const updatedData = updateModesData ( initialData , instructions ) ;
198+
199+ expect ( updatedData . customModes ) . toHaveLength ( 1 ) ;
200+ // Assign to variable and check existence for type safety
201+ const addedMode = updatedData . customModes [ 0 ] ;
202+ expect ( addedMode ) . toBeDefined ( ) ;
203+ if ( addedMode ) {
204+ expect ( addedMode . slug ) . toBe ( SYLPHX_MODE_SLUG ) ;
205+ expect ( addedMode . roleDefinition ) . toBe ( instructions ) ;
206+ expect ( addedMode . name ) . toBe ( SYLPHX_MODE_NAME ) ;
207+ expect ( addedMode . groups ) . toEqual ( SYLPHX_MODE_GROUPS ) ;
208+ expect ( addedMode . source ) . toBe ( SYLPHX_MODE_SOURCE ) ;
209+ }
210+ expect ( console . log ) . toHaveBeenCalledWith ( expect . stringContaining ( `Adding new '${ SYLPHX_MODE_SLUG } ' mode definition.` ) ) ;
211+ } ) ;
212+
213+ it ( 'should update existing mode' , ( ) => {
214+ const initialData = { customModes : [ { slug : SYLPHX_MODE_SLUG , name : 'Old Name' , roleDefinition : 'Old Def' , groups : [ ] , source : 'user' } ] } ;
215+ const instructions = 'Updated Instructions' ;
216+ const updatedData = updateModesData ( initialData , instructions ) ;
217+
218+ expect ( updatedData . customModes ) . toHaveLength ( 1 ) ;
219+ // Assign to variable and check existence
220+ const updatedMode = updatedData . customModes [ 0 ] ;
221+ expect ( updatedMode ) . toBeDefined ( ) ;
222+ if ( updatedMode ) {
223+ expect ( updatedMode . slug ) . toBe ( SYLPHX_MODE_SLUG ) ;
224+ expect ( updatedMode . roleDefinition ) . toBe ( instructions ) ;
225+ expect ( updatedMode . name ) . toBe ( SYLPHX_MODE_NAME ) ; // Name gets updated
226+ expect ( updatedMode . groups ) . toEqual ( SYLPHX_MODE_GROUPS ) ; // Groups updated
227+ expect ( updatedMode . source ) . toBe ( SYLPHX_MODE_SOURCE ) ; // Source updated
228+ }
229+ expect ( console . log ) . toHaveBeenCalledWith ( expect . stringContaining ( `Updating existing '${ SYLPHX_MODE_SLUG } ' mode definition.` ) ) ;
230+ } ) ;
231+ } ) ;
232+
233+ // --- writeCustomModes Tests ---
234+ describe ( 'writeCustomModes' , ( ) => {
235+ // No dynamic import needed here anymore
236+
237+ it ( 'should call fs.writeJson with correct data' , async ( ) => {
238+ const dataToWrite = { customModes : [ { slug : SYLPHX_MODE_SLUG , name : SYLPHX_MODE_NAME , roleDefinition : 'Test Def' , groups : SYLPHX_MODE_GROUPS , source : SYLPHX_MODE_SOURCE } ] } ;
239+ await writeCustomModes ( dataToWrite ) ;
240+
241+ expect ( fs . writeJson ) . toHaveBeenCalledTimes ( 1 ) ;
242+ expect ( fs . writeJson ) . toHaveBeenCalledWith ( expect . any ( String ) , dataToWrite , { spaces : 2 } ) ;
243+ expect ( console . log ) . toHaveBeenCalledWith ( expect . stringContaining ( 'Successfully updated custom_modes.json.' ) ) ;
244+ } ) ;
245+
246+ it ( 'should throw error on fs write error' , async ( ) => {
247+ const writeError = new Error ( 'Disk full' ) ;
248+ vi . mocked ( fs . writeJson ) . mockRejectedValueOnce ( writeError ) ; // Use Once
249+ const dataToWrite = { customModes : [ ] } ;
250+
251+ await expect ( writeCustomModes ( dataToWrite ) ) . rejects . toThrow ( / F a i l e d t o w r i t e u p d a t e s .* D i s k f u l l / ) ;
252+ expect ( console . error ) . toHaveBeenCalledWith ( 'Error writing custom modes file:' , writeError ) ;
253+ } ) ;
254+ } ) ;
255+ } ) ;
0 commit comments