@critical-path/client 0.12.1 → 0.13.1

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.
@@ -0,0 +1,169 @@
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({ task: { id: 'task-123', ...body } }), { status: 200 });
34
+ }
35
+ if (urlStr.endsWith('/tasks')) {
36
+ return new Response(JSON.stringify({ task: { id: 'task-new-1', ...body } }), { status: 201 });
37
+ }
38
+ if (urlStr.endsWith('/deliverables')) {
39
+ return new Response(JSON.stringify({ deliverable: { id: 'deliv-1', ...body } }), { status: 201 });
40
+ }
41
+ return new Response(JSON.stringify({ success: true }), { status: 200 });
42
+ });
43
+ });
44
+
45
+ afterEach(() => {
46
+ process.env = originalEnv;
47
+ vi.unstubAllGlobals();
48
+ });
49
+
50
+ it('handles "status" command', async () => {
51
+ await main(['status', 'Running unit tests', '--details', 'Passed 12 tests']);
52
+
53
+ expect(fetchCalls).toHaveLength(1);
54
+ expect(fetchCalls[0].url).toContain('/status');
55
+ expect(fetchCalls[0].method).toBe('POST');
56
+ expect(fetchCalls[0].body).toEqual({
57
+ status: 'Running unit tests',
58
+ taskId: 'task-123',
59
+ projectId: 'proj-456',
60
+ details: 'Passed 12 tests',
61
+ isEngaged: true
62
+ });
63
+ expect(fetchCalls[0].headers).toEqual(expect.objectContaining({
64
+ Authorization: 'Bearer test-sa-key'
65
+ }));
66
+ });
67
+
68
+ it('handles "block" command', async () => {
69
+ await main(['block', '--reason', 'Waiting on upstream PR #123', '--pr', 'https://github.com/Pixerate/ai-core/pull/123']);
70
+
71
+ // 1. addComment, 2. updateTask, 3. updateStatus
72
+ expect(fetchCalls.length).toBe(3);
73
+
74
+ const commentCall = fetchCalls.find(c => c.url.endsWith('/comments'));
75
+ expect(commentCall?.body.content).toContain('Waiting on upstream PR #123');
76
+ expect(commentCall?.body.content).toContain('https://github.com/Pixerate/ai-core/pull/123');
77
+
78
+ const updateTaskCall = fetchCalls.find(c => c.url.includes('/tasks/task-123') && c.method === 'PATCH');
79
+ expect(updateTaskCall?.body.isBlocked).toBe(true);
80
+ expect(updateTaskCall?.body.customFields?.blockerReason).toBe('Waiting on upstream PR #123');
81
+ expect(updateTaskCall?.body.customFields?.prUrl).toBe('https://github.com/Pixerate/ai-core/pull/123');
82
+
83
+ const statusCall = fetchCalls.find(c => c.url.endsWith('/status'));
84
+ expect(statusCall?.body.status).toBe('Blocked: Waiting on upstream PR #123');
85
+ expect(statusCall?.body.isEngaged).toBe(false);
86
+ });
87
+
88
+ it('handles "clarify" command', async () => {
89
+ await main([
90
+ 'clarify',
91
+ '--reason', 'Database schema ambiguous',
92
+ '--question', 'Should we use SQLite or PostgreSQL?',
93
+ '--question', 'Is soft-delete required?'
94
+ ]);
95
+
96
+ expect(fetchCalls.length).toBe(3);
97
+
98
+ const commentCall = fetchCalls.find(c => c.url.endsWith('/comments'));
99
+ expect(commentCall?.body.content).toContain('Database schema ambiguous');
100
+ expect(commentCall?.body.content).toContain('Should we use SQLite or PostgreSQL?');
101
+ expect(commentCall?.body.content).toContain('Is soft-delete required?');
102
+
103
+ const updateTaskCall = fetchCalls.find(c => c.url.includes('/tasks/task-123') && c.method === 'PATCH');
104
+ expect(updateTaskCall?.body.isBlocked).toBe(true);
105
+ expect(updateTaskCall?.body.customFields?.needsClarification).toBe(true);
106
+
107
+ const statusCall = fetchCalls.find(c => c.url.endsWith('/status'));
108
+ expect(statusCall?.body.status).toBe('Awaiting clarification');
109
+ expect(statusCall?.body.isEngaged).toBe(false);
110
+ });
111
+
112
+ it('handles "propose" command', async () => {
113
+ await main([
114
+ 'propose',
115
+ '--title', 'Implement Redis caching layer',
116
+ '--description', 'Caches frequent queries to optimize response times'
117
+ ]);
118
+
119
+ const createTaskCall = fetchCalls.find(c => c.url.endsWith('/tasks') && c.method === 'POST');
120
+ expect(createTaskCall?.body.title).toBe('Implement Redis caching layer');
121
+ expect(createTaskCall?.body.description).toBe('Caches frequent queries to optimize response times');
122
+ expect(createTaskCall?.body.projectId).toBe('proj-456');
123
+ expect(createTaskCall?.body.status).toBe('draft');
124
+ expect(createTaskCall?.body.parentId).toBe('task-123');
125
+ expect(createTaskCall?.body.customFields?.proposedByAgent).toBe(true);
126
+ });
127
+
128
+ it('handles "deliverable" command', async () => {
129
+ await main([
130
+ 'deliverable',
131
+ '--title', 'Pull Request #45',
132
+ '--url', 'https://github.com/Pixerate/uchiage-runners/pull/45'
133
+ ]);
134
+
135
+ const commentCall = fetchCalls.find(c => c.url.endsWith('/comments'));
136
+ expect(commentCall?.body.content).toContain('Deliverable recorded: [Pull Request #45](https://github.com/Pixerate/uchiage-runners/pull/45)');
137
+
138
+ const updateTaskCall = fetchCalls.find(c => c.url.includes('/tasks/task-123') && c.method === 'PATCH');
139
+ expect(updateTaskCall?.body.customFields?.deliverableUrl).toBe('https://github.com/Pixerate/uchiage-runners/pull/45');
140
+ expect(updateTaskCall?.body.customFields?.deliverableTitle).toBe('Pull Request #45');
141
+ });
142
+
143
+ it('handles "comment" command', async () => {
144
+ await main(['comment', 'Migration completed successfully, starting verification.']);
145
+
146
+ const commentCall = fetchCalls.find(c => c.url.endsWith('/comments'));
147
+ expect(commentCall?.body.taskId).toBe('task-123');
148
+ expect(commentCall?.body.content).toBe('Migration completed successfully, starting verification.');
149
+ });
150
+
151
+ describe('isDirectExecution', () => {
152
+ it('returns false when argv1 is undefined', () => {
153
+ expect(isDirectExecution('file:///path/to/cli.js', undefined)).toBe(false);
154
+ });
155
+
156
+ it('returns true when paths match directly', () => {
157
+ // Using an existing file path for realpathSync
158
+ const realFile = process.cwd() + '/package.json';
159
+ const fileUrl = new URL(`file://${realFile}`).href;
160
+ expect(isDirectExecution(fileUrl, realFile)).toBe(true);
161
+ });
162
+
163
+ it('returns false when target file does not match argv1', () => {
164
+ const realFile = process.cwd() + '/package.json';
165
+ const fileUrl = new URL(`file://${realFile}`).href;
166
+ expect(isDirectExecution(fileUrl, process.cwd() + '/tsconfig.json')).toBe(false);
167
+ });
168
+ });
169
+ });
package/src/index.test.ts CHANGED
@@ -362,5 +362,39 @@ describe('@critical-path/client Tests', () => {
362
362
  expect(workload.seriesKeys).toEqual(['u1', 'u2']);
363
363
  expect(workload.buckets[0].values.u1).toBe(12);
364
364
  });
365
+
366
+ it('posts agent status updates via client.updateStatus', async () => {
367
+ let capturedBody: any = null;
368
+ const mockFetch = async (url: string | URL | Request, init?: RequestInit) => {
369
+ const urlStr = url.toString();
370
+ if (urlStr.endsWith('/status') && init?.method === 'POST') {
371
+ capturedBody = JSON.parse(init.body as string);
372
+ return new Response(JSON.stringify({ success: true, status: capturedBody.status, timestamp: 123456789 }), { status: 200 });
373
+ }
374
+ return new Response(JSON.stringify({ error: 'Not found' }), { status: 404 });
375
+ };
376
+
377
+ const client = new CriticalPathClient({
378
+ baseUrl: 'http://localhost:3000/api/critical-path',
379
+ fetch: mockFetch as typeof fetch
380
+ });
381
+
382
+ const result = await client.updateStatus('Running unit tests', {
383
+ taskId: 't1',
384
+ projectId: 'p1',
385
+ details: 'vitest run 15 passed'
386
+ });
387
+
388
+ expect(result.success).toBe(true);
389
+ expect(result.status).toBe('Running unit tests');
390
+ expect(capturedBody).toEqual({
391
+ status: 'Running unit tests',
392
+ taskId: 't1',
393
+ projectId: 'p1',
394
+ details: 'vitest run 15 passed',
395
+ isEngaged: true
396
+ });
397
+ });
365
398
  });
366
399
 
400
+
package/src/index.ts CHANGED
@@ -73,6 +73,19 @@ export type {
73
73
  WorkloadMetric
74
74
  };
75
75
 
76
+ export interface UpdateStatusOptions {
77
+ taskId?: string;
78
+ projectId?: string;
79
+ details?: string;
80
+ isEngaged?: boolean;
81
+ }
82
+
83
+ export interface UpdateStatusResult {
84
+ success: boolean;
85
+ status: string;
86
+ timestamp: number;
87
+ }
88
+
76
89
  export interface ClientOptions {
77
90
  baseUrl: string; // e.g. "http://localhost:3000/api/critical-path"
78
91
  headers?: Record<string, string>;
@@ -553,6 +566,24 @@ export class CriticalPathClient {
553
566
  });
554
567
  return res.timeEntry;
555
568
  }
569
+
570
+ // Agent Status / Telemetry
571
+ async updateStatus(
572
+ status: string,
573
+ options?: UpdateStatusOptions
574
+ ): Promise<UpdateStatusResult> {
575
+ const res = await this.request<UpdateStatusResult>('/status', {
576
+ method: 'POST',
577
+ body: JSON.stringify({
578
+ status,
579
+ taskId: options?.taskId,
580
+ projectId: options?.projectId,
581
+ details: options?.details,
582
+ isEngaged: options?.isEngaged ?? true
583
+ })
584
+ });
585
+ return res;
586
+ }
556
587
  }
557
588
 
558
589
  export type { CommentReaction };
@@ -1 +1 @@
1
- {"root":["./src/index.test.ts","./src/index.ts"],"version":"5.9.3"}
1
+ {"root":["./src/cli.test.ts","./src/index.test.ts","./src/index.ts","./src/bin/cli.ts"],"version":"5.9.3"}