Skip to content

Commit 16033a8

Browse files
Debugging and slop reduction
1 parent 98b6d2b commit 16033a8

File tree

14 files changed

+197
-395
lines changed

14 files changed

+197
-395
lines changed

.cursor/rules/errors.mdc

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ alwaysApply: false
99
graph TD
1010
subgraph Core_Logic
1111
FS[FileSystemService: e.g., FileReadError] --> TM[TaskManager: Throws App Errors, e.g., ProjectNotFound, TaskNotDone]
12-
TM -->|App Error with code APP-xxxx| CLI_Handler["cli.ts Command Handler"]
13-
TM -->|App Error with code APP-xxxx| ToolExec["toolExecutors.ts: execute"]
12+
TM -->|App Error with code ERR_xxxx| CLI_Handler["cli.ts Command Handler"]
13+
TM -->|App Error with code ERR_xxxx| ToolExec["toolExecutors.ts: execute"]
1414
end
1515

1616
subgraph CLI_Path
1717
CLI_Handler -->|App Error| CLI_Catch["cli.ts catch block"]
1818
CLI_Catch -->|Error Object| FormatCLI["client errors.ts formatCliError"]
19-
FormatCLI -->|"Error [APP-xxxx]: message"| ConsoleOut["console.error Output"]
19+
FormatCLI -->|"Error [ERR_xxxx]: message"| ConsoleOut["console.error Output"]
2020
end
2121

2222
subgraph MCP_Server_Path
@@ -25,13 +25,13 @@ graph TD
2525
end
2626

2727
subgraph App_Execution
28-
ToolExec -->|App Error with code APP-xxxx| ExecToolErrHandler["tools.ts executeToolAndHandleErrors catch block"]
28+
ToolExec -->|App Error with code ERR_xxxx| ExecToolErrHandler["tools.ts executeToolAndHandleErrors catch block"]
2929
ExecToolErrHandler -->|Map AppError to Protocol Error or Tool Result| ErrorMapping
30-
ErrorMapping -->|"If validation error (APP-1xxx)"| McpError["Create McpError with appropriate ErrorCode"]
31-
ErrorMapping -->|"If business logic error (APP-2xxx+)"| FormatResult["Format as isError true result"]
30+
ErrorMapping -->|"If validation error (ERR_1xxx)"| McpError["Create McpError with appropriate ErrorCode"]
31+
ErrorMapping -->|"If business logic error (ERR_2xxx+)"| FormatResult["Format as isError true result"]
3232

3333
McpError -->|Throw| SDKHandler["server index.ts SDK Handler"]
34-
FormatResult -->|"{ content: [{ text: Error [APP-xxxx]: message }], isError: true }"| SDKHandler
34+
FormatResult -->|"{ content: [{ text: Error [ERR_xxxx]: message }], isError: true }"| SDKHandler
3535
end
3636

3737
SDKHandler -- Protocol Error --> SDKFormatError["SDK Formats as JSON-RPC Error Response"]
@@ -46,12 +46,12 @@ graph TD
4646

4747
Errors are consistently through a unified `AppError` system:
4848

49-
1. **Validation Errors** (`APP-1xxx` series)
49+
1. **Validation Errors** (`ERR_1xxx` series)
5050
- Used for validation issues (e.g., MissingParameter, InvalidArgument)
5151
- Thrown by tool executors during parameter validation
5252
- Mapped to protocol-level McpErrors in `executeToolAndHandleErrors`
5353

54-
2. **Business Logic Errors** (`APP-2xxx` and higher)
54+
2. **Business Logic Errors** (`ERR_2xxx` and higher)
5555
- Used for all business logic and application-specific errors
56-
- Include specific error codes (e.g., APP-2000 for ProjectNotFoundError)
56+
- Include specific error codes (e.g., ERR_2000 for ProjectNotFoundError)
5757
- Returned as serialized CallToolResults with `isError: true`

.cursor/rules/tests.mdc

Lines changed: 0 additions & 193 deletions
Original file line numberDiff line numberDiff line change
@@ -3,197 +3,4 @@ description: Writing unit tests with `jest`
33
globs: tests/**/*
44
alwaysApply: false
55
---
6-
# Testing Guidelines for TypeScript + ES Modules + Jest
76

8-
This guide contains cumulative in-context learnings about working with this project's testing stack.
9-
10-
## Unit vs. Integration Tests
11-
12-
**Never Mix Test Types**: Separate integration tests from unit tests into different files:
13-
- Simple unit tests without mocks for validating rules (like state transitions)
14-
- Integration tests with mocks for filesystem and external dependencies
15-
16-
## File Path Handling in Tests
17-
18-
1. **Environment Variables**:
19-
- Use `process.env.TASK_MANAGER_FILE_PATH` for configuring file paths in tests
20-
- Set this in `beforeEach` and clean up in `afterEach`:
21-
```typescript
22-
beforeEach(async () => {
23-
tempDir = path.join(os.tmpdir(), `test-${Date.now()}`);
24-
await fs.mkdir(tempDir, { recursive: true });
25-
tasksFilePath = path.join(tempDir, "test-tasks.json");
26-
process.env.TASK_MANAGER_FILE_PATH = tasksFilePath;
27-
});
28-
29-
afterEach(async () => {
30-
await fs.rm(tempDir, { recursive: true, force: true });
31-
delete process.env.TASK_MANAGER_FILE_PATH;
32-
});
33-
```
34-
35-
2. **Temporary Files**:
36-
- Create unique temp directories for each test run
37-
- Use `os.tmpdir()` for platform-independent temp directories
38-
- Include timestamps in directory names to prevent conflicts
39-
- Always clean up temp files in `afterEach`
40-
41-
## Jest ESM Mocking, Step-by-Step
42-
43-
1. **Type-Only Import:**
44-
Import types for static analysis without actually executing the module code:
45-
```typescript
46-
import type { MyService as MyServiceType } from 'path/to/MyService.js';
47-
import type { readFile as ReadFileType } from 'node:fs/promises';
48-
```
49-
50-
2. **Register Mock:**
51-
Use `jest.unstable_mockModule` to replace the real module:
52-
```typescript
53-
jest.unstable_mockModule('node:fs/promises', () => ({
54-
__esModule: true,
55-
readFile: jest.fn(),
56-
}));
57-
```
58-
59-
3. **Set Default Mock Implementations, Then Dynamically Import Modules:**
60-
You must dynamically import the modules to be mocked and/or tested *after* registering mocks and setting any mock implementations. This ensures that when `MyService` attempts to import `node:fs/promises`, it gets your mocked version. Depending how you want to scope your mock implementations, you can do this in `beforeAll`, `beforeEach`, or at the top of each test.
61-
```typescript
62-
let MyService: typeof MyServiceType;
63-
let readFile: jest.MockedFunction<ReadFileType>;
64-
65-
beforeAll(async () => {
66-
const fsPromisesMock = await import('node:fs/promises');
67-
readFile = fsPromisesMock.readFile as jest.MockedFunction<ReadFileType>;
68-
69-
// Set default implementation
70-
readFile.mockResolvedValue('default mocked content');
71-
72-
const serviceModule = await import('path/to/MyService.js');
73-
MyService = serviceModule.MyService;
74-
});
75-
```
76-
77-
4. **Setup in `beforeEach`:**
78-
Reset mocks and set default behaviors before each test:
79-
```typescript
80-
beforeEach(() => {
81-
jest.clearAllMocks();
82-
readFile.mockResolvedValue('');
83-
});
84-
```
85-
86-
5. **Write a Test:**
87-
Now you can test your service with the mocked `readFile`:
88-
```typescript
89-
describe('MyService', () => {
90-
let myServiceInstance: MyServiceType;
91-
92-
beforeEach(() => {
93-
myServiceInstance = new MyService('somePath');
94-
});
95-
96-
it('should do something', async () => {
97-
readFile.mockResolvedValueOnce('some data');
98-
const result = await myServiceInstance.someMethod();
99-
expect(result).toBe('expected result');
100-
expect(readFile).toHaveBeenCalledWith('somePath', 'utf-8');
101-
});
102-
});
103-
```
104-
105-
### Mocking a Class with Methods
106-
107-
If you have a class `MyClass` that has both instance methods and static methods, you can mock it in an **ES Modules + TypeScript** setup using the same pattern. For instance:
108-
109-
```typescript
110-
// 1. Create typed jest mock functions using the original types
111-
type InitResult = { data: string };
112-
113-
const mockInit = jest.fn() as jest.MockedFunction<MyClass['init']>;
114-
const mockDoWork = jest.fn() as jest.MockedFunction<MyClass['doWork']>;
115-
const mockStaticHelper = jest.fn() as jest.MockedFunction<typeof MyClass.staticHelper>;
116-
117-
// 2. Use jest.unstable_mockModule with an ES6 class in the factory
118-
jest.unstable_mockModule('path/to/MyClass.js', () => {
119-
class MockMyClass {
120-
// Instance methods
121-
init = mockInit;
122-
doWork = mockDoWork;
123-
124-
// Static method
125-
static staticHelper = mockStaticHelper;
126-
}
127-
128-
return {
129-
__esModule: true,
130-
MyClass: MockMyClass, // same name/structure as real export
131-
};
132-
});
133-
134-
// 3. Import your class after mocking
135-
let MyClass: typeof import('path/to/MyClass.js')['MyClass'];
136-
137-
beforeAll(async () => {
138-
const myClassModule = await import('path/to/MyClass.js');
139-
MyClass = myClassModule.MyClass;
140-
});
141-
142-
// 4. Write tests and reset mocks
143-
beforeEach(() => {
144-
jest.clearAllMocks();
145-
mockInit.mockResolvedValue({ data: 'default' });
146-
mockStaticHelper.mockReturnValue(42);
147-
});
148-
149-
describe('MyClass', () => {
150-
it('should call init', async () => {
151-
const instance = new MyClass();
152-
const result = await instance.init();
153-
expect(result).toEqual({ data: 'default' });
154-
expect(mockInit).toHaveBeenCalledTimes(1);
155-
});
156-
157-
it('should call the static helper', () => {
158-
const val = MyClass.staticHelper();
159-
expect(val).toBe(42);
160-
expect(mockStaticHelper).toHaveBeenCalledTimes(1);
161-
});
162-
});
163-
```
164-
165-
### Best Practice: **Type** Your Mocked Functions
166-
167-
By default, `jest.fn()` is very generic and doesn't enforce parameter or return types. This can cause TypeScript errors like:
168-
169-
> `Argument of type 'undefined' is not assignable to parameter of type 'never'`
170-
171-
or
172-
173-
> `Type 'Promise<SomeType>' is not assignable to type 'FunctionLike'`
174-
175-
To avoid these, **use the original type with `jest.MockedFunction`**. For example, if your real function is:
176-
177-
```typescript
178-
async function loadStuff(id: string): Promise<string[]> {
179-
// ...
180-
}
181-
```
182-
183-
then you should type the mock as:
184-
185-
```typescript
186-
const mockLoadStuff = jest.fn() as jest.MockedFunction<typeof loadStuff>;
187-
```
188-
189-
For class methods, use the class type to get the method signature:
190-
191-
```typescript
192-
const mockClassMethod = jest.fn() as jest.MockedFunction<YourClass['classMethod']>;
193-
```
194-
195-
This helps TypeScript catch mistakes if you:
196-
- call the function with the wrong argument types
197-
- use `mockResolvedValue` with the wrong shape
198-
199-
Once typed properly, your `mockResolvedValue(...)`, `mockImplementation(...)`, etc. calls will be fully type-safe.

.github/workflows/npm-publish.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ jobs:
1313
node-version: 'latest'
1414
- run: npm ci
1515
- run: npm install -g tsx
16-
- run: npm run build
1716
- run: npm test
1817

1918
publish:

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"build": "tsc",
1919
"start": "node dist/src/server/index.js",
2020
"dev": "tsc && node dist/src/server/index.js",
21-
"test": "NODE_OPTIONS=--experimental-vm-modules jest",
21+
"test": "tsc && NODE_OPTIONS=--experimental-vm-modules jest",
2222
"cli": "node dist/src/cli.js"
2323
},
2424
"repository": {

0 commit comments

Comments
 (0)