1
- import { exec } from "child_process" ;
2
- import * as fs from "node:fs/promises" ;
3
- import * as path from "node:path" ;
4
- import { promisify } from "util" ;
5
-
6
- const execAsync = promisify ( exec ) ;
7
- const CLI_PATH = path . resolve ( process . cwd ( ) , "src/client/cli.ts" ) ;
8
- const TASK_MANAGER_FILE_PATH = path . resolve ( process . cwd ( ) , "tests/unit/test-tasks.json" ) ;
9
- const TEMP_DIR = path . resolve ( process . cwd ( ) , "tests/unit/temp" ) ;
10
-
11
- describe ( "CLI Unit Tests" , ( ) => {
12
- beforeEach ( async ( ) => {
13
- // Create a test file
14
- const testFile = path . join ( TEMP_DIR , "test-spec.txt" ) ;
15
- await fs . writeFile ( testFile , "Test specification content" ) ;
1
+ import { describe , it , expect , jest , beforeEach , beforeAll } from '@jest/globals' ;
2
+ import type { TaskManager as TaskManagerType } from '../../src/server/TaskManager.js' ;
3
+ import type { StandardResponse , ProjectCreationSuccessData } from '../../src/types/index.js' ;
4
+ import type { readFile as ReadFileType } from 'node:fs/promises' ;
5
+
6
+ // --- Mock Dependencies ---
7
+
8
+ // Mock TaskManager
9
+ const mockGenerateProjectPlan = jest . fn ( ) as jest . MockedFunction < typeof TaskManagerType . prototype . generateProjectPlan > ;
10
+ const mockReadProject = jest . fn ( ) as jest . MockedFunction < typeof TaskManagerType . prototype . readProject > ;
11
+ const mockListProjects = jest . fn ( ) as jest . MockedFunction < typeof TaskManagerType . prototype . listProjects > ;
12
+
13
+ jest . unstable_mockModule ( '../../src/server/TaskManager.js' , ( ) => ( {
14
+ TaskManager : jest . fn ( ) . mockImplementation ( ( ) => ( {
15
+ generateProjectPlan : mockGenerateProjectPlan ,
16
+ readProject : mockReadProject , // Include in mock
17
+ listProjects : mockListProjects , // Include in mock
18
+ // Add mocks for other methods used by other commands if testing them later
19
+ approveTaskCompletion : jest . fn ( ) ,
20
+ approveProjectCompletion : jest . fn ( ) ,
21
+ listTasks : jest . fn ( ) ,
22
+ // ... other methods
23
+ } ) ) ,
24
+ } ) ) ;
25
+
26
+ // Mock fs/promises
27
+ const mockReadFile = jest . fn ( ) ;
28
+ jest . unstable_mockModule ( 'node:fs/promises' , ( ) => ( {
29
+ readFile : mockReadFile ,
30
+ default : { readFile : mockReadFile } // Handle default export if needed
31
+ } ) ) ;
32
+
33
+ // Mock chalk - disable color codes
34
+ jest . unstable_mockModule ( 'chalk' , ( ) => ( {
35
+ default : {
36
+ blue : ( str : string ) => str ,
37
+ red : ( str : string ) => str ,
38
+ green : ( str : string ) => str ,
39
+ yellow : ( str : string ) => str ,
40
+ cyan : ( str : string ) => str ,
41
+ bold : ( str : string ) => str ,
42
+ gray : ( str : string ) => str ,
43
+ } ,
44
+ } ) ) ;
45
+
46
+ // --- Setup & Teardown ---
47
+
48
+ let program : any ; // To hold the imported commander program
49
+ let consoleLogSpy : ReturnType < typeof jest . spyOn > ; // Use inferred type
50
+ let consoleErrorSpy : ReturnType < typeof jest . spyOn > ; // Use inferred type
51
+ let processExitSpy : ReturnType < typeof jest . spyOn > ; // Use inferred type
52
+ let TaskManager : typeof TaskManagerType ;
53
+ let readFile : jest . MockedFunction < typeof ReadFileType > ;
54
+
55
+ beforeAll ( async ( ) => {
56
+ // Dynamically import the CLI module *after* mocks are set up
57
+ const cliModule = await import ( '../../src/client/cli.js' ) ;
58
+ program = cliModule . program ; // Assuming program is exported
59
+
60
+ // Import mocked types/modules
61
+ const TmModule = await import ( '../../src/server/TaskManager.js' ) ;
62
+ TaskManager = TmModule . TaskManager ;
63
+ const fsPromisesMock = await import ( 'node:fs/promises' ) ;
64
+ readFile = fsPromisesMock . readFile as jest . MockedFunction < typeof ReadFileType > ;
65
+ } ) ;
66
+
67
+ beforeEach ( ( ) => {
68
+ // Reset mocks and spies before each test
69
+ jest . clearAllMocks ( ) ;
70
+ mockGenerateProjectPlan . mockReset ( ) ;
71
+ mockReadFile . mockReset ( ) ;
72
+ mockReadProject . mockReset ( ) ; // Reset new mock
73
+ mockListProjects . mockReset ( ) ; // Reset new mock
74
+
75
+ // Spy on console and process.exit
76
+ consoleLogSpy = jest . spyOn ( console , 'log' ) . mockImplementation ( ( ) => { } ) ;
77
+ consoleErrorSpy = jest . spyOn ( console , 'error' ) . mockImplementation ( ( ) => { } ) ;
78
+ // Prevent tests from exiting and throw instead
79
+ processExitSpy = jest . spyOn ( process , 'exit' ) . mockImplementation ( ( code ?: string | number | null | undefined ) : never => { // Correct signature
80
+ throw new Error ( `process.exit called with code ${ code ?? 'undefined' } ` ) ;
16
81
} ) ;
82
+ } ) ;
17
83
18
- afterEach ( async ( ) => {
19
- await fs . rm ( TEMP_DIR , { recursive : true , force : true } ) ;
84
+ afterEach ( ( ) => {
85
+ // Restore spies
86
+ consoleLogSpy . mockRestore ( ) ;
87
+ consoleErrorSpy . mockRestore ( ) ;
88
+ processExitSpy . mockRestore ( ) ;
89
+ } ) ;
90
+
91
+ // --- Test Suites ---
92
+
93
+ describe ( 'CLI Commands' , ( ) => {
94
+ describe ( 'generate-plan' , ( ) => {
95
+ it ( 'should call TaskManager.generateProjectPlan with correct arguments and log success' , async ( ) => {
96
+ // Arrange: Mock TaskManager response
97
+ const mockSuccessResponse : StandardResponse < ProjectCreationSuccessData > = {
98
+ status : 'success' ,
99
+ data : {
100
+ projectId : 'proj-123' ,
101
+ totalTasks : 2 ,
102
+ tasks : [
103
+ { id : 'task-1' , title : 'Task 1' , description : 'Desc 1' } ,
104
+ { id : 'task-2' , title : 'Task 2' , description : 'Desc 2' } ,
105
+ ] ,
106
+ message : 'Project proj-123 created.' ,
107
+ } ,
108
+ } ;
109
+ mockGenerateProjectPlan . mockResolvedValue ( mockSuccessResponse ) ;
110
+
111
+ const testPrompt = 'Create a test plan' ;
112
+ const testProvider = 'openai' ;
113
+ const testModel = 'gpt-4o-mini' ;
114
+
115
+ // Act: Simulate running the CLI command
116
+ // Arguments: command, options...
117
+ await program . parseAsync (
118
+ [
119
+ 'generate-plan' ,
120
+ '--prompt' ,
121
+ testPrompt ,
122
+ '--provider' ,
123
+ testProvider ,
124
+ '--model' ,
125
+ testModel ,
126
+ ] ,
127
+ { from : 'user' } // Important: indicates these are user-provided args
128
+ ) ;
129
+
130
+ // Assert
131
+ // 1. TaskManager initialization (implicitly tested by mock setup)
132
+ // Ensure TaskManager constructor was called (likely once due to preAction hook)
133
+ expect ( TaskManager ) . toHaveBeenCalledTimes ( 1 ) ;
134
+
135
+ // 2. generateProjectPlan call
136
+ expect ( mockGenerateProjectPlan ) . toHaveBeenCalledTimes ( 1 ) ;
137
+ expect ( mockGenerateProjectPlan ) . toHaveBeenCalledWith ( {
138
+ prompt : testPrompt ,
139
+ provider : testProvider ,
140
+ model : testModel ,
141
+ attachments : [ ] , // No attachments in this test
142
+ } ) ;
143
+
144
+ // 3. Console output
145
+ expect ( consoleLogSpy ) . toHaveBeenCalledWith (
146
+ expect . stringContaining ( 'Generating project plan from prompt...' )
147
+ ) ;
148
+ expect ( consoleLogSpy ) . toHaveBeenCalledWith (
149
+ expect . stringContaining ( '✅ Project plan generated successfully!' )
150
+ ) ;
151
+ expect ( consoleLogSpy ) . toHaveBeenCalledWith (
152
+ expect . stringContaining ( 'Project ID: proj-123' )
153
+ ) ;
154
+ expect ( consoleLogSpy ) . toHaveBeenCalledWith (
155
+ expect . stringContaining ( 'Total Tasks: 2' )
156
+ ) ;
157
+ expect ( consoleLogSpy ) . toHaveBeenCalledWith (
158
+ expect . stringContaining ( 'task-1:' )
159
+ ) ;
160
+ expect ( consoleLogSpy ) . toHaveBeenCalledWith (
161
+ expect . stringContaining ( 'Title: Task 1' )
162
+ ) ;
163
+ expect ( consoleLogSpy ) . toHaveBeenCalledWith (
164
+ expect . stringContaining ( 'Description: Desc 1' )
165
+ ) ;
166
+ // Check for the TaskManager message as well
167
+ expect ( consoleLogSpy ) . toHaveBeenCalledWith (
168
+ expect . stringContaining ( 'Project proj-123 created.' )
169
+ ) ;
170
+
171
+
172
+ // 4. No errors or exits
173
+ expect ( consoleErrorSpy ) . not . toHaveBeenCalled ( ) ;
174
+ expect ( processExitSpy ) . not . toHaveBeenCalled ( ) ;
175
+ } ) ;
20
176
} ) ;
21
-
22
- // TODO: Rewrite these as unit tests
23
- it . skip ( "should generate a project plan with default options" , async ( ) => {
24
- const { stdout } = await execAsync (
25
- `TASK_MANAGER_FILE_PATH=${ TASK_MANAGER_FILE_PATH } tsx ${ CLI_PATH } generate-plan --prompt "Create a simple todo app"`
26
- ) ;
27
-
28
- expect ( stdout ) . toContain ( "Project plan generated successfully!" ) ;
29
- expect ( stdout ) . toContain ( "Project ID:" ) ;
30
- expect ( stdout ) . toContain ( "Total Tasks:" ) ;
31
- expect ( stdout ) . toContain ( "Tasks:" ) ;
32
- } , 10000 ) ;
33
-
34
- it . skip ( "should generate a plan with custom provider and model" , async ( ) => {
35
- const { stdout } = await execAsync (
36
- `TASK_MANAGER_FILE_PATH=${ TASK_MANAGER_FILE_PATH } tsx ${ CLI_PATH } generate-plan --prompt "Create a todo app" --provider google --model gemini-1.5-pro`
37
- ) ;
38
-
39
- expect ( stdout ) . toContain ( "Project plan generated successfully!" ) ;
40
- } , 10000 ) ;
41
-
42
- it . skip ( "should handle file attachments" , async ( ) => {
43
- // Create a test file
44
- const testFile = path . join ( TEMP_DIR , "test-spec.txt" ) ;
45
- await fs . writeFile ( testFile , "Test specification content" ) ;
46
-
47
- const { stdout } = await execAsync (
48
- `TASK_MANAGER_FILE_PATH=${ TASK_MANAGER_FILE_PATH } tsx ${ CLI_PATH } generate-plan --prompt "Create based on spec" --attachment ${ testFile } `
49
- ) ;
50
-
51
- expect ( stdout ) . toContain ( "Project plan generated successfully!" ) ;
52
- } , 10000 ) ;
53
- } ) ;
177
+
178
+ // Add describe blocks for other commands (approve, finalize, list) later
179
+ } ) ;
0 commit comments