@critical-path/client 0.20.5 → 0.20.6

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.
package/src/cli.test.ts DELETED
@@ -1,266 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { main, isDirectExecution } from './bin/cli.js';
3
-
4
- describe('critical-path CLI Subcommands', () => {
5
- const originalEnv = process.env;
6
- let fetchCalls: Array<{ url: string; method?: string; body?: any; headers?: any }> = [];
7
-
8
- beforeEach(() => {
9
- process.env = {
10
- ...originalEnv,
11
- CRITICAL_PATH_API: 'http://localhost:3000/api/critical-path',
12
- CRITICAL_PATH_KEY: 'test-sa-key',
13
- CRITICAL_PATH_TASK_ID: 'task-123',
14
- CRITICAL_PATH_PROJECT_ID: 'proj-456'
15
- };
16
- fetchCalls = [];
17
-
18
- vi.stubGlobal('fetch', async (url: string | URL | Request, init?: RequestInit) => {
19
- const urlStr = url.toString();
20
- const method = init?.method?.toUpperCase() || 'GET';
21
- const body = init?.body ? JSON.parse(init.body as string) : undefined;
22
- const headers = init?.headers;
23
-
24
- fetchCalls.push({ url: urlStr, method, body, headers });
25
-
26
- if (urlStr.endsWith('/status')) {
27
- return new Response(JSON.stringify({ success: true, status: body?.status, timestamp: Date.now() }), { status: 200 });
28
- }
29
- if (urlStr.endsWith('/comments')) {
30
- return new Response(JSON.stringify({ comment: { id: 'c1', ...body } }), { status: 201 });
31
- }
32
- if (urlStr.includes('/tasks/')) {
33
- return new Response(JSON.stringify({
34
- task: {
35
- id: 'task-123',
36
- projectId: 'proj-456',
37
- todos: [
38
- { id: 'todo-1', title: 'Existing checklist item', completed: false }
39
- ],
40
- ...body
41
- }
42
- }), { status: 200 });
43
- }
44
- if (urlStr.endsWith('/tasks') || urlStr.includes('/tasks?')) {
45
- if (method === 'GET') {
46
- return new Response(JSON.stringify({
47
- tasks: [
48
- { id: 'sub-1', title: 'Existing child subtask', parentId: 'task-123', status: 'todo' },
49
- { id: 'sub-2', title: 'Unrelated task', parentId: 'other-task', status: 'todo' }
50
- ]
51
- }), { status: 200 });
52
- }
53
- return new Response(JSON.stringify({ task: { id: 'task-new-1', ...body } }), { status: 201 });
54
- }
55
- if (urlStr.endsWith('/deliverables')) {
56
- return new Response(JSON.stringify({ deliverable: { id: 'deliv-1', ...body } }), { status: 201 });
57
- }
58
- return new Response(JSON.stringify({ success: true }), { status: 200 });
59
- });
60
- });
61
-
62
- afterEach(() => {
63
- process.env = originalEnv;
64
- vi.unstubAllGlobals();
65
- });
66
-
67
- it('handles "status" command', async () => {
68
- await main(['status', 'Running unit tests', '--details', 'Passed 12 tests']);
69
-
70
- expect(fetchCalls).toHaveLength(1);
71
- expect(fetchCalls[0].url).toContain('/status');
72
- expect(fetchCalls[0].method).toBe('POST');
73
- expect(fetchCalls[0].body).toEqual({
74
- status: 'Running unit tests',
75
- taskId: 'task-123',
76
- projectId: 'proj-456',
77
- details: 'Passed 12 tests',
78
- isEngaged: true
79
- });
80
- expect(fetchCalls[0].headers).toEqual(expect.objectContaining({
81
- Authorization: 'Bearer test-sa-key'
82
- }));
83
- });
84
-
85
- it('handles "block" command', async () => {
86
- await main(['block', '--reason', 'Waiting on upstream PR #123', '--pr', 'https://github.com/Pixerate/ai-core/pull/123']);
87
-
88
- // 1. addComment, 2. updateTask, 3. updateStatus
89
- expect(fetchCalls.length).toBe(3);
90
-
91
- const commentCall = fetchCalls.find(c => c.url.endsWith('/comments'));
92
- expect(commentCall?.body.content).toContain('Waiting on upstream PR #123');
93
- expect(commentCall?.body.content).toContain('https://github.com/Pixerate/ai-core/pull/123');
94
-
95
- const updateTaskCall = fetchCalls.find(c => c.url.includes('/tasks/task-123') && c.method === 'PATCH');
96
- expect(updateTaskCall?.body.isBlocked).toBe(true);
97
- expect(updateTaskCall?.body.customFields?.blockerReason).toBe('Waiting on upstream PR #123');
98
- expect(updateTaskCall?.body.customFields?.prUrl).toBe('https://github.com/Pixerate/ai-core/pull/123');
99
-
100
- const statusCall = fetchCalls.find(c => c.url.endsWith('/status'));
101
- expect(statusCall?.body.status).toBe('Blocked: Waiting on upstream PR #123');
102
- expect(statusCall?.body.isEngaged).toBe(false);
103
- });
104
-
105
- it('handles "clarify" command', async () => {
106
- await main([
107
- 'clarify',
108
- '--reason', 'Database schema ambiguous',
109
- '--question', 'Should we use SQLite or PostgreSQL?',
110
- '--question', 'Is soft-delete required?'
111
- ]);
112
-
113
- expect(fetchCalls.length).toBe(3);
114
-
115
- const commentCall = fetchCalls.find(c => c.url.endsWith('/comments'));
116
- expect(commentCall?.body.content).toContain('Database schema ambiguous');
117
- expect(commentCall?.body.content).toContain('Should we use SQLite or PostgreSQL?');
118
- expect(commentCall?.body.content).toContain('Is soft-delete required?');
119
-
120
- const updateTaskCall = fetchCalls.find(c => c.url.includes('/tasks/task-123') && c.method === 'PATCH');
121
- expect(updateTaskCall?.body.isBlocked).toBe(true);
122
- expect(updateTaskCall?.body.customFields?.needsClarification).toBe(true);
123
-
124
- const statusCall = fetchCalls.find(c => c.url.endsWith('/status'));
125
- expect(statusCall?.body.status).toBe('Awaiting clarification');
126
- expect(statusCall?.body.isEngaged).toBe(false);
127
- });
128
-
129
- it('handles "propose" command (creates top-level draft task by default)', async () => {
130
- await main([
131
- 'propose',
132
- '--title', 'Implement Redis caching layer',
133
- '--description', 'Caches frequent queries to optimize response times'
134
- ]);
135
-
136
- const createTaskCall = fetchCalls.find(c => c.url.endsWith('/tasks') && c.method === 'POST');
137
- expect(createTaskCall?.body.title).toBe('Implement Redis caching layer');
138
- expect(createTaskCall?.body.description).toBe('Caches frequent queries to optimize response times');
139
- expect(createTaskCall?.body.projectId).toBe('proj-456');
140
- expect(createTaskCall?.body.status).toBe('draft');
141
- expect(createTaskCall?.body.parentId).toBeUndefined();
142
- expect(createTaskCall?.body.customFields?.proposedByAgent).toBe(true);
143
- });
144
-
145
- it('handles "propose" command with explicit --parent flag', async () => {
146
- await main([
147
- 'propose',
148
- '--title', 'Implement cache invalidation logic',
149
- '--parent', 'task-custom-parent'
150
- ]);
151
-
152
- const createTaskCall = fetchCalls.find(c => c.url.endsWith('/tasks') && c.method === 'POST' && c.body.title === 'Implement cache invalidation logic');
153
- expect(createTaskCall?.body.parentId).toBe('task-custom-parent');
154
- expect(createTaskCall?.body.status).toBe('draft');
155
- });
156
-
157
- it('handles "deliverable" command', async () => {
158
- await main([
159
- 'deliverable',
160
- '--title', 'Pull Request #45',
161
- '--url', 'https://github.com/Pixerate/uchiage-runners/pull/45'
162
- ]);
163
-
164
- const commentCall = fetchCalls.find(c => c.url.endsWith('/comments'));
165
- expect(commentCall?.body.content).toContain('Deliverable recorded: [Pull Request #45](https://github.com/Pixerate/uchiage-runners/pull/45)');
166
-
167
- const updateTaskCall = fetchCalls.find(c => c.url.includes('/tasks/task-123') && c.method === 'PATCH');
168
- expect(updateTaskCall?.body.customFields?.deliverableUrl).toBe('https://github.com/Pixerate/uchiage-runners/pull/45');
169
- expect(updateTaskCall?.body.customFields?.deliverableTitle).toBe('Pull Request #45');
170
- });
171
-
172
- it('handles "comment" command', async () => {
173
- await main(['comment', 'Migration completed successfully, starting verification.']);
174
-
175
- const commentCall = fetchCalls.find(c => c.url.endsWith('/comments'));
176
- expect(commentCall?.body.taskId).toBe('task-123');
177
- expect(commentCall?.body.content).toBe('Migration completed successfully, starting verification.');
178
- });
179
-
180
- it('handles "subtask" command (creates active subtask)', async () => {
181
- await main([
182
- 'subtask',
183
- '--title', 'Implement database migrations',
184
- '--description', 'Run Knex migrations for users table',
185
- '--priority', 'high'
186
- ]);
187
-
188
- const createTaskCall = fetchCalls.find(c => c.url.endsWith('/tasks') && c.method === 'POST');
189
- expect(createTaskCall).toBeDefined();
190
- expect(createTaskCall?.body.title).toBe('Implement database migrations');
191
- expect(createTaskCall?.body.description).toBe('Run Knex migrations for users table');
192
- expect(createTaskCall?.body.projectId).toBe('proj-456');
193
- expect(createTaskCall?.body.parentId).toBe('task-123');
194
- expect(createTaskCall?.body.status).toBe('todo');
195
- expect(createTaskCall?.body.priority).toBe('high');
196
- expect(createTaskCall?.body.customFields?.createdByAgent).toBe(true);
197
- });
198
-
199
- it('handles "subtask list" command', async () => {
200
- const logSpy = vi.spyOn(console, 'log');
201
- await main(['subtask', 'list']);
202
-
203
- const getTasksCall = fetchCalls.find(c => (c.url.endsWith('/tasks') || c.url.includes('/tasks?')) && c.method === 'GET');
204
- expect(getTasksCall).toBeDefined();
205
- expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Subtasks for parent task task-123 (1):'));
206
- expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Existing child subtask'));
207
- logSpy.mockRestore();
208
- });
209
-
210
- it('handles "checklist add" command', async () => {
211
- await main(['checklist', 'add', 'Write comprehensive unit tests']);
212
-
213
- const updateTaskCall = fetchCalls.find(c => c.url.includes('/tasks/task-123') && c.method === 'PATCH');
214
- expect(updateTaskCall).toBeDefined();
215
- expect(updateTaskCall?.body.todos).toHaveLength(2);
216
- expect(updateTaskCall?.body.todos[1].title).toBe('Write comprehensive unit tests');
217
- expect(updateTaskCall?.body.todos[1].completed).toBe(false);
218
- });
219
-
220
- it('handles "checklist check" command', async () => {
221
- await main(['checklist', 'check', 'Existing checklist item']);
222
-
223
- const updateTaskCall = fetchCalls.find(c => c.url.includes('/tasks/task-123') && c.method === 'PATCH');
224
- expect(updateTaskCall).toBeDefined();
225
- expect(updateTaskCall?.body.todos[0].completed).toBe(true);
226
- expect(updateTaskCall?.body.todos[0].completedAt).toBeDefined();
227
- });
228
-
229
- it('handles "checklist list" command', async () => {
230
- const logSpy = vi.spyOn(console, 'log');
231
- await main(['checklist', 'list']);
232
-
233
- expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Checklist for task task-123 (1):'));
234
- expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Existing checklist item'));
235
- logSpy.mockRestore();
236
- });
237
-
238
- it('handles "checklist set" command', async () => {
239
- await main(['checklist', 'set', 'Step 1', 'Step 2']);
240
-
241
- const updateTaskCall = fetchCalls.find(c => c.url.includes('/tasks/task-123') && c.method === 'PATCH');
242
- expect(updateTaskCall).toBeDefined();
243
- expect(updateTaskCall?.body.todos).toHaveLength(2);
244
- expect(updateTaskCall?.body.todos[0].title).toBe('Step 1');
245
- expect(updateTaskCall?.body.todos[1].title).toBe('Step 2');
246
- });
247
-
248
- describe('isDirectExecution', () => {
249
- it('returns false when argv1 is undefined', () => {
250
- expect(isDirectExecution('file:///path/to/cli.js', undefined)).toBe(false);
251
- });
252
-
253
- it('returns true when paths match directly', () => {
254
- // Using an existing file path for realpathSync
255
- const realFile = process.cwd() + '/package.json';
256
- const fileUrl = new URL(`file://${realFile}`).href;
257
- expect(isDirectExecution(fileUrl, realFile)).toBe(true);
258
- });
259
-
260
- it('returns false when target file does not match argv1', () => {
261
- const realFile = process.cwd() + '/package.json';
262
- const fileUrl = new URL(`file://${realFile}`).href;
263
- expect(isDirectExecution(fileUrl, process.cwd() + '/tsconfig.json')).toBe(false);
264
- });
265
- });
266
- });
@@ -1,88 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { CriticalPathRouter } from '@critical-path/server';
3
- import { CriticalPathClient } from './index.js';
4
-
5
- /**
6
- * Runs the client against the real router (no network), so client request shapes are checked
7
- * against the server's validation schemas rather than hand-written mocks.
8
- */
9
- function connect(router: CriticalPathRouter, token?: string) {
10
- return new CriticalPathClient({
11
- baseUrl: 'http://localhost/api/critical-path',
12
- headers: token ? { Authorization: `Bearer ${token}` } : {},
13
- fetch: ((input: RequestInfo | URL, init?: RequestInit) => router.handleRequest(new Request(input, init))) as typeof fetch
14
- });
15
- }
16
-
17
- describe('client ↔ server contract', () => {
18
- const router = new CriticalPathRouter(undefined, {
19
- getContext: (request) => {
20
- const token = request.headers.get('Authorization')?.replace('Bearer ', '');
21
- return token ? { userId: token } : null;
22
- }
23
- });
24
-
25
- it('round-trips the main resources with identity taken from the token', async () => {
26
- const client = connect(router, 'alice');
27
-
28
- const project = await client.createProject({ key: 'CON', name: 'Contract' });
29
- const renamed = await client.updateProject(project.id, { name: 'Contract v2' });
30
- expect(renamed.name).toBe('Contract v2');
31
-
32
- const task = await client.createTask({ projectId: project.id, title: 'Wire it up' });
33
- expect(task.reporterId).toBe('alice');
34
- const other = await client.createTask({ projectId: project.id, title: 'Upstream' });
35
- const dependency = await client.addDependency(task.id, { dependsOnTaskId: other.id });
36
- const unrelated = await client.createTask({ projectId: project.id, title: 'Unrelated' });
37
- expect(await client.removeDependency(unrelated.id, dependency.id)).toBe(false);
38
- expect(await client.removeDependency(task.id, dependency.id)).toBe(true);
39
-
40
- const child = await client.createTask({ projectId: project.id, title: 'Child', parentId: other.id });
41
- expect(await client.deleteTask(other.id, { subtasks: 'detach' })).toBe(true);
42
- expect((await client.getTask(child.id)).parentId).toBeUndefined();
43
-
44
- await client.addTodo(task.id, 'Write tests');
45
- const toggled = await client.toggleTodo(task.id, 'Write tests', true);
46
- expect(toggled.todos?.[0].completed).toBe(true);
47
-
48
- const comment = await client.addComment({ taskId: task.id, content: 'On it' });
49
- expect(comment.authorId).toBe('alice');
50
- const reacted = await client.addCommentReaction(comment.id, { emoji: '🚀' });
51
- expect(reacted.reactions?.[0].userId).toBe('alice');
52
- const unreacted = await client.removeCommentReaction(comment.id, { emoji: '🚀' });
53
- expect(unreacted.reactions ?? []).toHaveLength(0);
54
-
55
- const attachment = await client.createAttachment({ taskId: task.id, filename: 'spec.md', url: 'https://example.com/spec.md' });
56
- expect(attachment.uploaderId).toBe('alice');
57
-
58
- const entry = await client.logTime({ taskId: task.id, hours: 1.5 });
59
- expect(entry.userId).toBe('alice');
60
-
61
- const { webhook, secret } = await client.createWebhook({
62
- name: 'Hook',
63
- url: 'https://hooks.example.com/contract',
64
- events: ['task.created']
65
- });
66
- expect(secret).toMatch(/^whsec_/);
67
- expect((await client.updateWebhook(webhook.id, { active: false })).active).toBe(false);
68
- expect((await client.getWebhooks()).map((w) => w.id)).toContain(webhook.id);
69
- expect(await client.deleteWebhook(webhook.id)).toBe(true);
70
-
71
- const page = await client.queryTasks({ projectId: project.id, limit: 1, parentId: null });
72
- expect(page.items).toHaveLength(1);
73
- expect(page.nextCursor).toBeTruthy();
74
- expect((await client.getTasks(project.id)).length).toBeGreaterThan(1);
75
- expect((await client.getActivities({ projectId: project.id })).length).toBeGreaterThan(0);
76
-
77
- expect(await client.deleteProject(project.id)).toBe(true);
78
- expect(await client.deleteProject(project.id)).toBe(false);
79
- });
80
-
81
- it('surfaces validation errors for fields the server rejects', async () => {
82
- const client = connect(router);
83
- const project = await client.createProject({ name: 'Strict' });
84
- const task = await client.createTask({ projectId: project.id, title: 'T' });
85
-
86
- await expect(client.updateTask(task.id, { projectId: 'elsewhere' } as any)).rejects.toThrow(/Unrecognized key/);
87
- });
88
- });