@monotykamary/pi-loop 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +21 -0
  3. package/README.md +285 -0
  4. package/media/demo.mp4 +0 -0
  5. package/media/pi-loop.jpg +0 -0
  6. package/media/screenshot.png +0 -0
  7. package/package.json +89 -0
  8. package/src/core/analyzer.ts +51 -0
  9. package/src/core/content-extractor.ts +79 -0
  10. package/src/core/inference.ts +137 -0
  11. package/src/core/prompt-builder.ts +217 -0
  12. package/src/core/prompt-loader.ts +126 -0
  13. package/src/core/reframe.ts +30 -0
  14. package/src/core/snapshot-builder.ts +252 -0
  15. package/src/global-config.ts +38 -0
  16. package/src/index.ts +532 -0
  17. package/src/session/client.ts +47 -0
  18. package/src/session/loop-session.ts +102 -0
  19. package/src/session/response-parser.ts +37 -0
  20. package/src/state/manager.ts +164 -0
  21. package/src/state/patterns.ts +81 -0
  22. package/src/state/reframe.ts +33 -0
  23. package/src/subagent-detector.ts +94 -0
  24. package/src/types.ts +83 -0
  25. package/src/ui/animations.ts +70 -0
  26. package/src/ui/model-picker.ts +79 -0
  27. package/src/ui/renderer.ts +257 -0
  28. package/src/ui/status-widget.ts +30 -0
  29. package/src/ui/types.ts +48 -0
  30. package/tests/compaction.test.ts +754 -0
  31. package/tests/continue-action-regression.test.ts +456 -0
  32. package/tests/engine.test.ts +770 -0
  33. package/tests/ephemeral-supervision.test.ts +391 -0
  34. package/tests/full-fidelity-snapshot.test.ts +843 -0
  35. package/tests/parsing.test.ts +303 -0
  36. package/tests/state.test.ts +525 -0
  37. package/tests/status-widget.test.ts +703 -0
  38. package/tests/subagent-detector.test.ts +191 -0
  39. package/tests/supervise-command.test.ts +381 -0
  40. package/tsconfig.json +14 -0
  41. package/vitest.config.ts +15 -0
@@ -0,0 +1,191 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { exec } from 'node:child_process';
3
+ import { checkChildPiProcesses, waitForSubagents } from '../src/subagent-detector.js';
4
+
5
+ // Mock child_process
6
+ vi.mock('node:child_process', () => ({
7
+ exec: vi.fn(),
8
+ }));
9
+
10
+ describe('subagent-detector', () => {
11
+ const mockExec = vi.mocked(exec);
12
+
13
+ beforeEach(() => {
14
+ vi.clearAllMocks();
15
+ });
16
+
17
+ afterEach(() => {
18
+ vi.restoreAllMocks();
19
+ });
20
+
21
+ describe('checkChildPiProcesses', () => {
22
+ it('returns no subagents when ps output is empty', async () => {
23
+ mockExec.mockImplementation((cmd, callback) => {
24
+ callback(null, { stdout: '', stderr: '' }, null as any);
25
+ return {} as any;
26
+ });
27
+
28
+ const result = await checkChildPiProcesses();
29
+
30
+ expect(result.hasActiveSubagents).toBe(false);
31
+ expect(result.count).toBe(0);
32
+ expect(result.pids).toEqual([]);
33
+ });
34
+
35
+ it('detects child pi processes', async () => {
36
+ const parentPid = process.pid;
37
+ mockExec.mockImplementation((cmd, callback) => {
38
+ callback(
39
+ null,
40
+ {
41
+ stdout: `${parentPid} 12345 pi\n${parentPid} 12346 pi\n99999 12347 other`,
42
+ stderr: '',
43
+ },
44
+ null as any
45
+ );
46
+ return {} as any;
47
+ });
48
+
49
+ const result = await checkChildPiProcesses();
50
+
51
+ expect(result.hasActiveSubagents).toBe(true);
52
+ expect(result.count).toBe(2);
53
+ expect(result.pids).toContain(12345);
54
+ expect(result.pids).toContain(12346);
55
+ expect(result.pids).not.toContain(12347);
56
+ });
57
+
58
+ it('ignores non-pi processes', async () => {
59
+ const parentPid = process.pid;
60
+ mockExec.mockImplementation((cmd, callback) => {
61
+ callback(
62
+ null,
63
+ {
64
+ stdout: `${parentPid} 12345 node\n${parentPid} 12346 bash`,
65
+ stderr: '',
66
+ },
67
+ null as any
68
+ );
69
+ return {} as any;
70
+ });
71
+
72
+ const result = await checkChildPiProcesses();
73
+
74
+ expect(result.hasActiveSubagents).toBe(false);
75
+ expect(result.count).toBe(0);
76
+ });
77
+
78
+ it('ignores pi processes from other parents', async () => {
79
+ mockExec.mockImplementation((cmd, callback) => {
80
+ callback(
81
+ null,
82
+ {
83
+ stdout: '99999 12345 pi\n99999 12346 pi',
84
+ stderr: '',
85
+ },
86
+ null as any
87
+ );
88
+ return {} as any;
89
+ });
90
+
91
+ const result = await checkChildPiProcesses();
92
+
93
+ expect(result.hasActiveSubagents).toBe(false);
94
+ expect(result.count).toBe(0);
95
+ });
96
+
97
+ it('handles exec errors gracefully', async () => {
98
+ mockExec.mockImplementation((cmd, callback) => {
99
+ callback(new Error('ps command failed'), { stdout: '', stderr: '' }, null as any);
100
+ return {} as any;
101
+ });
102
+
103
+ const result = await checkChildPiProcesses();
104
+
105
+ expect(result.hasActiveSubagents).toBe(false);
106
+ expect(result.count).toBe(0);
107
+ expect(result.pids).toEqual([]);
108
+ });
109
+
110
+ it('handles malformed ps output', async () => {
111
+ mockExec.mockImplementation((cmd, callback) => {
112
+ callback(
113
+ null,
114
+ {
115
+ stdout: 'garbage line\n \n123 abc def extra',
116
+ stderr: '',
117
+ },
118
+ null as any
119
+ );
120
+ return {} as any;
121
+ });
122
+
123
+ const result = await checkChildPiProcesses();
124
+
125
+ expect(result.hasActiveSubagents).toBe(false);
126
+ expect(result.count).toBe(0);
127
+ });
128
+ });
129
+
130
+ describe('waitForSubagents', () => {
131
+ it('returns immediately when no subagents', async () => {
132
+ mockExec.mockImplementation((cmd, callback) => {
133
+ callback(null, { stdout: '', stderr: '' }, null as any);
134
+ return {} as any;
135
+ });
136
+
137
+ const result = await waitForSubagents(100, 1000);
138
+
139
+ expect(result.completed).toBe(true);
140
+ expect(result.finalStatus.hasActiveSubagents).toBe(false);
141
+ });
142
+
143
+ it('waits for subagents to complete', async () => {
144
+ const parentPid = process.pid;
145
+ let calls = 0;
146
+ mockExec.mockImplementation((cmd, callback) => {
147
+ calls++;
148
+ if (calls < 3) {
149
+ callback(
150
+ null,
151
+ {
152
+ stdout: `${parentPid} 12345 pi`,
153
+ stderr: '',
154
+ },
155
+ null as any
156
+ );
157
+ } else {
158
+ callback(null, { stdout: '', stderr: '' }, null as any);
159
+ }
160
+ return {} as any;
161
+ });
162
+
163
+ const result = await waitForSubagents(50, 1000);
164
+
165
+ expect(result.completed).toBe(true);
166
+ expect(result.finalStatus.hasActiveSubagents).toBe(false);
167
+ expect(calls).toBe(3);
168
+ });
169
+
170
+ it("times out if subagents don't complete", async () => {
171
+ const parentPid = process.pid;
172
+ mockExec.mockImplementation((cmd, callback) => {
173
+ callback(
174
+ null,
175
+ {
176
+ stdout: `${parentPid} 12345 pi`,
177
+ stderr: '',
178
+ },
179
+ null as any
180
+ );
181
+ return {} as any;
182
+ });
183
+
184
+ const result = await waitForSubagents(50, 100);
185
+
186
+ expect(result.completed).toBe(false);
187
+ expect(result.finalStatus.hasActiveSubagents).toBe(true);
188
+ expect(result.finalStatus.count).toBe(1);
189
+ });
190
+ });
191
+ });
@@ -0,0 +1,381 @@
1
+ import { describe, expect, it, vi, beforeEach } from 'vitest';
2
+ import { LoopStateManager } from '../src/state/manager.js';
3
+
4
+ // Mock dependencies
5
+ vi.mock('../src/core/analyzer.js', () => ({
6
+ analyze: vi.fn(),
7
+ }));
8
+
9
+ vi.mock('../src/core/inference.js', () => ({
10
+ inferOutcome: vi.fn(),
11
+ }));
12
+
13
+ vi.mock('../src/core/prompt-loader.js', () => ({
14
+ loadSystemPrompt: vi.fn().mockReturnValue({ prompt: 'test prompt', source: 'built-in' }),
15
+ }));
16
+
17
+ vi.mock('../src/ui/renderer.js', () => ({
18
+ updateUI: vi.fn(),
19
+ toggleWidget: vi.fn(),
20
+ }));
21
+
22
+ vi.mock('../src/ui/model-picker.js', () => ({
23
+ pickModel: vi.fn(),
24
+ }));
25
+
26
+ vi.mock('../src/global-config.js', () => ({
27
+ loadGlobalModel: vi.fn().mockReturnValue(null),
28
+ }));
29
+
30
+ vi.mock('../src/session/client.js', () => ({
31
+ disposeSession: vi.fn(),
32
+ }));
33
+
34
+ vi.mock('../src/subagent-detector.js', () => ({
35
+ checkChildPiProcesses: vi.fn().mockResolvedValue({ hasActiveSubagents: false, count: 0 }),
36
+ waitForSubagents: vi
37
+ .fn()
38
+ .mockResolvedValue({ completed: true, finalStatus: { hasActiveSubagents: false, count: 0 } }),
39
+ }));
40
+
41
+ import { updateUI } from '../src/ui/renderer.js';
42
+ import { pickModel } from '../src/ui/model-picker.js';
43
+ import { loadGlobalModel } from '../src/global-config.js';
44
+ import { inferOutcome } from '../src/core/inference.js';
45
+
46
+ describe('LoopStateManager - goal append feature', () => {
47
+ function createMockApi() {
48
+ return {
49
+ appendEntry: vi.fn(),
50
+ on: vi.fn(),
51
+ registerCommand: vi.fn(),
52
+ registerTool: vi.fn(),
53
+ sendUserMessage: vi.fn(),
54
+ sendMessage: vi.fn(),
55
+ events: { emit: vi.fn(), on: vi.fn() },
56
+ } as any;
57
+ }
58
+
59
+ it('appends to existing goal when supervision is already active', () => {
60
+ const api = createMockApi();
61
+ const state = new LoopStateManager(api);
62
+
63
+ // Start initial supervision
64
+ state.start('Initial goal', 'anthropic', 'claude-haiku');
65
+ expect(state.getState()!.outcome).toBe('Initial goal');
66
+
67
+ // Simulate appending (behavior from command handler)
68
+ const trimmed = 'Additional requirement';
69
+ const existing = state.getState();
70
+ const appendedOutcome = `${existing!.outcome}. Additionally: ${trimmed}`;
71
+ state.updateOutcome(appendedOutcome);
72
+
73
+ expect(state.getState()!.outcome).toBe('Initial goal. Additionally: Additional requirement');
74
+ expect(api.appendEntry).toHaveBeenCalledTimes(2); // start + update
75
+ });
76
+
77
+ it('persists appended goal to session', () => {
78
+ const api = createMockApi();
79
+ const state = new LoopStateManager(api);
80
+
81
+ state.start('First part', 'anthropic', 'claude-haiku');
82
+ state.updateOutcome('First part. Additionally: Second part');
83
+
84
+ const lastCall = api.appendEntry.mock.calls[api.appendEntry.mock.calls.length - 1];
85
+ expect(lastCall[1].outcome).toBe('First part. Additionally: Second part');
86
+ });
87
+
88
+ it('maintains active state when appending', () => {
89
+ const api = createMockApi();
90
+ const state = new LoopStateManager(api);
91
+
92
+ state.start('Original', 'anthropic', 'claude-haiku');
93
+ expect(state.isActive()).toBe(true);
94
+
95
+ state.updateOutcome('Original. Additionally: More');
96
+ expect(state.isActive()).toBe(true);
97
+ });
98
+
99
+ it('keeps other state intact when appending', () => {
100
+ const api = createMockApi();
101
+ const state = new LoopStateManager(api);
102
+
103
+ state.start('Goal', 'openai', 'gpt-4o');
104
+ state.incrementTurnCount();
105
+ state.addIntervention({
106
+ turnCount: 1,
107
+ message: 'Focus',
108
+ reasoning: 'Test',
109
+ timestamp: Date.now(),
110
+ });
111
+
112
+ const originalProvider = state.getState()!.provider;
113
+ const originalModelId = state.getState()!.modelId;
114
+ const originalInterventions = state.getState()!.interventions.length;
115
+
116
+ state.updateOutcome('Goal. Additionally: Extended');
117
+
118
+ expect(state.getState()!.provider).toBe(originalProvider);
119
+ expect(state.getState()!.modelId).toBe(originalModelId);
120
+ expect(state.getState()!.interventions.length).toBe(originalInterventions);
121
+ });
122
+ });
123
+
124
+ describe('Supervise command kickstart behavior', () => {
125
+ function createMockCommandContext(overrides: { isIdle?: boolean; hasUI?: boolean } = {}) {
126
+ return {
127
+ ui: {
128
+ notify: vi.fn(),
129
+ select: vi.fn(),
130
+ input: vi.fn(),
131
+ confirm: vi.fn(),
132
+ setStatus: vi.fn(),
133
+ setWorkingMessage: vi.fn(),
134
+ setWidget: vi.fn(),
135
+ setFooter: vi.fn(),
136
+ setHeader: vi.fn(),
137
+ custom: vi.fn(),
138
+ pasteToEditor: vi.fn(),
139
+ setEditorText: vi.fn(),
140
+ getEditorText: vi.fn().mockReturnValue(''),
141
+ editor: vi.fn(),
142
+ setEditorComponent: vi.fn(),
143
+ theme: {},
144
+ getAllThemes: vi.fn().mockReturnValue([]),
145
+ getTheme: vi.fn(),
146
+ setTheme: vi.fn().mockReturnValue({ success: true }),
147
+ getToolsExpanded: vi.fn().mockReturnValue(false),
148
+ setToolsExpanded: vi.fn(),
149
+ onTerminalInput: vi.fn().mockReturnValue(() => {}),
150
+ },
151
+ hasUI: overrides.hasUI ?? true,
152
+ cwd: '/test',
153
+ sessionManager: {
154
+ getBranch: vi.fn().mockReturnValue([]),
155
+ },
156
+ modelRegistry: {
157
+ getApiKeyForProvider: vi.fn().mockResolvedValue('test-key'),
158
+ },
159
+ model: {
160
+ provider: 'anthropic',
161
+ id: 'claude-haiku',
162
+ },
163
+ isIdle: vi.fn().mockReturnValue(overrides.isIdle ?? true),
164
+ abort: vi.fn(),
165
+ hasPendingMessages: vi.fn().mockReturnValue(false),
166
+ shutdown: vi.fn(),
167
+ getContextUsage: vi.fn(),
168
+ compact: vi.fn(),
169
+ getSystemPrompt: vi.fn().mockReturnValue('test system prompt'),
170
+ } as any;
171
+ }
172
+
173
+ function createMockExtensionAPI() {
174
+ const sendUserMessage = vi.fn();
175
+ return {
176
+ appendEntry: vi.fn(),
177
+ on: vi.fn(),
178
+ registerCommand: vi.fn(),
179
+ registerTool: vi.fn(),
180
+ sendUserMessage,
181
+ sendMessage: vi.fn(),
182
+ events: { emit: vi.fn(), on: vi.fn() },
183
+ } as any;
184
+ }
185
+
186
+ beforeEach(() => {
187
+ vi.clearAllMocks();
188
+ });
189
+
190
+ describe('when agent is idle', () => {
191
+ it('should kickstart with goal when running explicit /supervise <outcome>', async () => {
192
+ // This tests the behavior pattern from the command handler
193
+ const pi = createMockExtensionAPI();
194
+ const ctx = createMockCommandContext({ isIdle: true });
195
+ const state = new LoopStateManager(pi);
196
+
197
+ // Simulate the command handler behavior
198
+ const trimmed = 'Implement JWT authentication';
199
+ state.start(trimmed, 'anthropic', 'claude-haiku');
200
+
201
+ // Kickstart behavior
202
+ if (ctx.isIdle()) {
203
+ pi.sendUserMessage(`Please start working on this goal: ${trimmed}`, {
204
+ deliverAs: 'followUp',
205
+ });
206
+ }
207
+
208
+ expect(pi.sendUserMessage).toHaveBeenCalledTimes(1);
209
+ expect(pi.sendUserMessage).toHaveBeenCalledWith(
210
+ 'Please start working on this goal: Implement JWT authentication',
211
+ { deliverAs: 'followUp' }
212
+ );
213
+ });
214
+
215
+ it('should kickstart with inferred goal when agent is idle', async () => {
216
+ vi.mocked(inferOutcome).mockResolvedValue('Fix the memory leak in handler');
217
+
218
+ const pi = createMockExtensionAPI();
219
+ const ctx = createMockCommandContext({ isIdle: true });
220
+ const state = new LoopStateManager(pi);
221
+
222
+ // Simulate inferred goal behavior
223
+ const inferred = 'Fix the memory leak in handler';
224
+ state.start(inferred, 'anthropic', 'claude-haiku');
225
+
226
+ if (ctx.isIdle()) {
227
+ pi.sendUserMessage(`Please start working on this goal: ${inferred}`, {
228
+ deliverAs: 'followUp',
229
+ });
230
+ }
231
+
232
+ expect(pi.sendUserMessage).toHaveBeenCalledWith(
233
+ 'Please start working on this goal: Fix the memory leak in handler',
234
+ { deliverAs: 'followUp' }
235
+ );
236
+ });
237
+
238
+ it('should kickstart when tool initiates supervision', async () => {
239
+ const pi = createMockExtensionAPI();
240
+ const ctx = createMockCommandContext({ isIdle: true });
241
+ const state = new LoopStateManager(pi);
242
+
243
+ // Simulate tool behavior
244
+ const outcome = 'Refactor database layer';
245
+ state.start(outcome, 'anthropic', 'claude-haiku');
246
+
247
+ if (ctx.isIdle()) {
248
+ pi.sendUserMessage(`Please start working on this goal: ${outcome}`, {
249
+ deliverAs: 'followUp',
250
+ });
251
+ }
252
+
253
+ expect(pi.sendUserMessage).toHaveBeenCalledWith(
254
+ 'Please start working on this goal: Refactor database layer',
255
+ { deliverAs: 'followUp' }
256
+ );
257
+ });
258
+ });
259
+
260
+ describe('when agent is busy', () => {
261
+ it('should NOT kickstart when running /supervise and agent is working', async () => {
262
+ const pi = createMockExtensionAPI();
263
+ const ctx = createMockCommandContext({ isIdle: false }); // Agent is busy
264
+ const state = new LoopStateManager(pi);
265
+
266
+ const trimmed = 'Implement feature X';
267
+ state.start(trimmed, 'anthropic', 'claude-haiku');
268
+
269
+ // Kickstart behavior (should NOT trigger)
270
+ if (ctx.isIdle()) {
271
+ pi.sendUserMessage(`Please start working on this goal: ${trimmed}`, {
272
+ deliverAs: 'followUp',
273
+ });
274
+ }
275
+
276
+ expect(pi.sendUserMessage).not.toHaveBeenCalled();
277
+ });
278
+
279
+ it('should NOT kickstart when tool initiates supervision but agent is busy', async () => {
280
+ const pi = createMockExtensionAPI();
281
+ const ctx = createMockCommandContext({ isIdle: false });
282
+ const state = new LoopStateManager(pi);
283
+
284
+ const outcome = 'Add test coverage';
285
+ state.start(outcome, 'anthropic', 'claude-haiku');
286
+
287
+ if (ctx.isIdle()) {
288
+ pi.sendUserMessage(`Please start working on this goal: ${outcome}`, {
289
+ deliverAs: 'followUp',
290
+ });
291
+ }
292
+
293
+ expect(pi.sendUserMessage).not.toHaveBeenCalled();
294
+ });
295
+ });
296
+
297
+ describe('append behavior', () => {
298
+ it('should NOT kickstart when appending to existing supervision', async () => {
299
+ const pi = createMockExtensionAPI();
300
+ const ctx = createMockCommandContext({ isIdle: true });
301
+ const state = new LoopStateManager(pi);
302
+
303
+ // First start supervision
304
+ state.start('Original goal', 'anthropic', 'claude-haiku');
305
+
306
+ // Reset mock to track only append behavior
307
+ pi.sendUserMessage.mockClear();
308
+
309
+ // Now simulate append (when supervision is already active)
310
+ const trimmed = 'Additional requirement';
311
+ const existing = state.getState();
312
+ const appendedOutcome = `${existing!.outcome}. Additionally: ${trimmed}`;
313
+ state.updateOutcome(appendedOutcome);
314
+
315
+ // Append behavior does NOT kickstart - it just updates the goal
316
+ expect(pi.sendUserMessage).not.toHaveBeenCalled();
317
+ expect(state.getState()!.outcome).toBe('Original goal. Additionally: Additional requirement');
318
+ });
319
+
320
+ it('should notify user when appending to goal', async () => {
321
+ const pi = createMockExtensionAPI();
322
+ const ctx = createMockCommandContext({ isIdle: true });
323
+ const state = new LoopStateManager(pi);
324
+
325
+ state.start('Original', 'anthropic', 'claude-haiku');
326
+
327
+ const trimmed = 'More work';
328
+ const existing = state.getState();
329
+ const appendedOutcome = `${existing!.outcome}. Additionally: ${trimmed}`;
330
+ state.updateOutcome(appendedOutcome);
331
+
332
+ // In real implementation, this would call ctx.ui.notify
333
+ // We verify the outcome was updated
334
+ expect(state.getState()!.outcome).toContain('Additionally: More work');
335
+ });
336
+ });
337
+
338
+ describe('edge cases', () => {
339
+ it('should handle kickstart with very long goals (truncated in UI but full in message)', async () => {
340
+ const pi = createMockExtensionAPI();
341
+ const ctx = createMockCommandContext({ isIdle: true });
342
+ const state = new LoopStateManager(pi);
343
+
344
+ const longGoal = 'A'.repeat(200);
345
+ state.start(longGoal, 'anthropic', 'claude-haiku');
346
+
347
+ if (ctx.isIdle()) {
348
+ pi.sendUserMessage(`Please start working on this goal: ${longGoal}`, {
349
+ deliverAs: 'followUp',
350
+ });
351
+ }
352
+
353
+ // Full goal should be in the kickstart message
354
+ expect(pi.sendUserMessage).toHaveBeenCalledWith(
355
+ `Please start working on this goal: ${longGoal}`,
356
+ { deliverAs: 'followUp' }
357
+ );
358
+ });
359
+
360
+ it('should handle empty isIdle() result gracefully', async () => {
361
+ const pi = createMockExtensionAPI();
362
+ const ctx = {
363
+ ...createMockCommandContext(),
364
+ isIdle: vi.fn().mockReturnValue(undefined), // falsy value
365
+ };
366
+ const state = new LoopStateManager(pi);
367
+
368
+ const trimmed = 'Test goal';
369
+ state.start(trimmed, 'anthropic', 'claude-haiku');
370
+
371
+ // Should not kickstart when isIdle returns falsy
372
+ if (ctx.isIdle()) {
373
+ pi.sendUserMessage(`Please start working on this goal: ${trimmed}`, {
374
+ deliverAs: 'followUp',
375
+ });
376
+ }
377
+
378
+ expect(pi.sendUserMessage).not.toHaveBeenCalled();
379
+ });
380
+ });
381
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "outDir": "dist",
10
+ "rootDir": "src",
11
+ "declaration": true
12
+ },
13
+ "include": ["src/**/*.ts"]
14
+ }
@@ -0,0 +1,15 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: 'node',
6
+ include: ['tests/**/*.test.ts'],
7
+ clearMocks: true,
8
+ restoreMocks: true,
9
+ coverage: {
10
+ provider: 'v8',
11
+ reporter: ['text', 'json', 'html'],
12
+ exclude: ['node_modules/', 'tests/', '**/*.d.ts', '**/*.config.ts'],
13
+ },
14
+ },
15
+ });