@claudetools/tools 0.8.4 → 0.8.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/dist/cli.js +14 -0
- package/dist/handlers/tool-handlers.js +140 -19
- package/dist/helpers/api-client.d.ts +2 -0
- package/dist/helpers/api-client.js +12 -1
- package/dist/helpers/circuit-breaker.d.ts +28 -0
- package/dist/helpers/circuit-breaker.js +97 -0
- package/dist/helpers/compact-formatter.d.ts +2 -0
- package/dist/helpers/compact-formatter.js +6 -0
- package/dist/helpers/tasks-retry.d.ts +9 -0
- package/dist/helpers/tasks-retry.js +30 -0
- package/dist/helpers/tasks.d.ts +91 -5
- package/dist/helpers/tasks.js +261 -16
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/test-runner.d.ts +33 -0
- package/dist/test-runner.js +149 -0
- package/dist/testing/example.d.ts +1 -0
- package/dist/testing/example.js +63 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +13 -0
- package/dist/testing/test-runner.d.ts +220 -0
- package/dist/testing/test-runner.js +302 -0
- package/dist/tools.js +103 -2
- package/package.json +1 -1
package/dist/helpers/tasks.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// =============================================================================
|
|
4
4
|
import { apiRequest } from './api-client.js';
|
|
5
5
|
import { matchTaskToWorker, buildWorkerPrompt } from './workers.js';
|
|
6
|
+
import { isCircuitOpen } from './circuit-breaker.js';
|
|
6
7
|
export function parseJsonArray(value) {
|
|
7
8
|
if (!value)
|
|
8
9
|
return [];
|
|
@@ -51,11 +52,12 @@ export async function getTask(userId, projectId, taskId, full = false) {
|
|
|
51
52
|
const endpoint = full ? 'full' : '';
|
|
52
53
|
return apiRequest(`/api/v1/tasks/${userId}/${projectId}/${taskId}${endpoint ? `/${endpoint}` : ''}`);
|
|
53
54
|
}
|
|
54
|
-
export async function claimTask(userId, projectId, taskId, agentId, lockDurationMinutes
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
lock_duration_minutes
|
|
58
|
-
}
|
|
55
|
+
export async function claimTask(userId, projectId, taskId, agentId, lockDurationMinutes) {
|
|
56
|
+
const body = { agent_id: agentId };
|
|
57
|
+
if (lockDurationMinutes !== undefined) {
|
|
58
|
+
body.lock_duration_minutes = lockDurationMinutes;
|
|
59
|
+
}
|
|
60
|
+
return apiRequest(`/api/v1/tasks/${userId}/${projectId}/${taskId}/claim`, 'POST', body);
|
|
59
61
|
}
|
|
60
62
|
export async function releaseTask(userId, projectId, taskId, agentId, newStatus, workLog) {
|
|
61
63
|
return apiRequest(`/api/v1/tasks/${userId}/${projectId}/${taskId}/release`, 'POST', {
|
|
@@ -90,11 +92,29 @@ export async function heartbeatTask(userId, projectId, taskId, agentId, extendMi
|
|
|
90
92
|
// -----------------------------------------------------------------------------
|
|
91
93
|
// Orchestration Functions
|
|
92
94
|
// -----------------------------------------------------------------------------
|
|
93
|
-
//
|
|
94
|
-
export const
|
|
95
|
+
// Effort weights for capacity planning
|
|
96
|
+
export const EFFORT_WEIGHTS = {
|
|
97
|
+
xs: 1,
|
|
98
|
+
s: 2,
|
|
99
|
+
m: 4,
|
|
100
|
+
l: 8,
|
|
101
|
+
xl: 16,
|
|
102
|
+
};
|
|
103
|
+
// Default lock duration by effort size (in minutes)
|
|
104
|
+
// Used when no explicit lockDurationMinutes is provided to claimTask
|
|
105
|
+
export const LOCK_DURATION_BY_EFFORT = {
|
|
106
|
+
xs: 15, // Extra small: 15 minutes
|
|
107
|
+
s: 30, // Small: 30 minutes (default)
|
|
108
|
+
m: 60, // Medium: 1 hour
|
|
109
|
+
l: 120, // Large: 2 hours
|
|
110
|
+
xl: 240, // Extra large: 4 hours
|
|
111
|
+
};
|
|
112
|
+
// Configuration: Default capacity budget in effort units
|
|
113
|
+
// 20 units allows: 20 xs tasks OR 1 xl + 1 m task OR 5 m tasks, etc.
|
|
114
|
+
export const DEFAULT_CAPACITY_BUDGET = 20;
|
|
95
115
|
/**
|
|
96
|
-
* Get
|
|
97
|
-
* Returns both in_progress and claimed (locked) task counts
|
|
116
|
+
* Get effort-weighted load of currently active tasks
|
|
117
|
+
* Returns both in_progress and claimed (locked) task counts, plus effort-weighted load
|
|
98
118
|
*/
|
|
99
119
|
export async function getActiveTaskCount(userId, projectId, epicId) {
|
|
100
120
|
// Get all in_progress tasks
|
|
@@ -105,10 +125,15 @@ export async function getActiveTaskCount(userId, projectId, epicId) {
|
|
|
105
125
|
});
|
|
106
126
|
let inProgressCount = 0;
|
|
107
127
|
let claimedCount = 0;
|
|
128
|
+
let effortLoad = 0;
|
|
108
129
|
if (inProgressResult.success && inProgressResult.data.length > 0) {
|
|
109
130
|
const now = new Date();
|
|
110
131
|
for (const task of inProgressResult.data) {
|
|
111
132
|
inProgressCount++;
|
|
133
|
+
// Calculate effort weight (default to 'm' = 4 units if not specified)
|
|
134
|
+
const effort = task.estimated_effort || 'm';
|
|
135
|
+
const weight = EFFORT_WEIGHTS[effort] || EFFORT_WEIGHTS.m;
|
|
136
|
+
effortLoad += weight;
|
|
112
137
|
// Check if task is also claimed (has valid lock)
|
|
113
138
|
if (task.assigned_to && task.lock_expires_at) {
|
|
114
139
|
const lockExpires = new Date(task.lock_expires_at);
|
|
@@ -122,6 +147,7 @@ export async function getActiveTaskCount(userId, projectId, epicId) {
|
|
|
122
147
|
inProgress: inProgressCount,
|
|
123
148
|
claimed: claimedCount,
|
|
124
149
|
total: inProgressCount,
|
|
150
|
+
effortLoad,
|
|
125
151
|
};
|
|
126
152
|
}
|
|
127
153
|
/**
|
|
@@ -130,13 +156,13 @@ export async function getActiveTaskCount(userId, projectId, epicId) {
|
|
|
130
156
|
* - Excludes already claimed tasks
|
|
131
157
|
* - Resolves dependencies (only returns unblocked tasks)
|
|
132
158
|
* - Matches each to appropriate expert worker
|
|
133
|
-
* - Respects
|
|
159
|
+
* - Respects effort-weighted capacity budget
|
|
134
160
|
*/
|
|
135
|
-
export async function getDispatchableTasks(userId, projectId, epicId,
|
|
136
|
-
// Get current active task
|
|
161
|
+
export async function getDispatchableTasks(userId, projectId, epicId, capacityBudget = DEFAULT_CAPACITY_BUDGET) {
|
|
162
|
+
// Get current active task effort load
|
|
137
163
|
const activeCount = await getActiveTaskCount(userId, projectId, epicId);
|
|
138
|
-
// Calculate remaining capacity
|
|
139
|
-
|
|
164
|
+
// Calculate remaining effort capacity
|
|
165
|
+
let remainingCapacity = capacityBudget - activeCount.effortLoad;
|
|
140
166
|
if (remainingCapacity <= 0) {
|
|
141
167
|
// No capacity available, return empty array
|
|
142
168
|
return [];
|
|
@@ -178,6 +204,13 @@ export async function getDispatchableTasks(userId, projectId, epicId, maxParalle
|
|
|
178
204
|
continue; // Still blocked
|
|
179
205
|
}
|
|
180
206
|
}
|
|
207
|
+
// Calculate task effort weight
|
|
208
|
+
const effort = task.estimated_effort || 'm';
|
|
209
|
+
const taskEffort = EFFORT_WEIGHTS[effort] || EFFORT_WEIGHTS.m;
|
|
210
|
+
// Check if this task fits within remaining capacity
|
|
211
|
+
if (taskEffort > remainingCapacity) {
|
|
212
|
+
continue; // Task too large for remaining capacity
|
|
213
|
+
}
|
|
181
214
|
// Match to expert worker
|
|
182
215
|
const worker = matchTaskToWorker({
|
|
183
216
|
title: task.title,
|
|
@@ -185,6 +218,11 @@ export async function getDispatchableTasks(userId, projectId, epicId, maxParalle
|
|
|
185
218
|
domain: task.domain || undefined,
|
|
186
219
|
tags: parseJsonArray(task.tags),
|
|
187
220
|
});
|
|
221
|
+
// Circuit breaker check: skip if worker circuit is open
|
|
222
|
+
if (isCircuitOpen(worker.id)) {
|
|
223
|
+
console.warn(`[Circuit Breaker] Skipping task ${task.id} - circuit OPEN for worker type: ${worker.id}`);
|
|
224
|
+
continue; // Skip this task
|
|
225
|
+
}
|
|
188
226
|
// Get task context
|
|
189
227
|
const fullTask = await getTask(userId, projectId, task.id, true);
|
|
190
228
|
const taskData = fullTask.data;
|
|
@@ -195,13 +233,48 @@ export async function getDispatchableTasks(userId, projectId, epicId, maxParalle
|
|
|
195
233
|
parentContext: taskData.parent,
|
|
196
234
|
dependencies: [],
|
|
197
235
|
});
|
|
198
|
-
//
|
|
199
|
-
|
|
236
|
+
// Reduce remaining capacity by the effort of this task
|
|
237
|
+
remainingCapacity -= taskEffort;
|
|
238
|
+
// Stop if no capacity remains
|
|
239
|
+
if (remainingCapacity <= 0) {
|
|
200
240
|
break;
|
|
201
241
|
}
|
|
202
242
|
}
|
|
243
|
+
// Sort by priority: critical → high → medium → low
|
|
244
|
+
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
245
|
+
dispatchable.sort((a, b) => (priorityOrder[a.task.priority] ?? 2) - (priorityOrder[b.task.priority] ?? 2));
|
|
203
246
|
return dispatchable;
|
|
204
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* Check if task lock is about to expire and return warning
|
|
250
|
+
*/
|
|
251
|
+
export function getLockExpiryWarning(lockExpiresAt) {
|
|
252
|
+
if (!lockExpiresAt) {
|
|
253
|
+
return { warning: false, minutes_remaining: 0 };
|
|
254
|
+
}
|
|
255
|
+
const now = new Date();
|
|
256
|
+
const expiryTime = new Date(lockExpiresAt);
|
|
257
|
+
const secondsRemaining = (expiryTime.getTime() - now.getTime()) / 1000;
|
|
258
|
+
const minutesRemaining = Math.floor(secondsRemaining / 60);
|
|
259
|
+
// Expired (lock time has passed)
|
|
260
|
+
if (secondsRemaining <= 0) {
|
|
261
|
+
return {
|
|
262
|
+
warning: true,
|
|
263
|
+
minutes_remaining: 0,
|
|
264
|
+
message: '🚨 Lock has EXPIRED. Task may be claimed by another agent.',
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
// Warn if lock expires within 5 minutes (but not yet expired)
|
|
268
|
+
if (minutesRemaining <= 5 && minutesRemaining >= 1) {
|
|
269
|
+
return {
|
|
270
|
+
warning: true,
|
|
271
|
+
minutes_remaining: minutesRemaining,
|
|
272
|
+
message: `⚠️ Lock expires in ${minutesRemaining} minute${minutesRemaining !== 1 ? 's' : ''}. Call task_heartbeat to extend.`,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
// Less than 1 minute but not expired - no warning (edge case)
|
|
276
|
+
return { warning: false, minutes_remaining: minutesRemaining };
|
|
277
|
+
}
|
|
205
278
|
/**
|
|
206
279
|
* Get full execution context for a worker agent
|
|
207
280
|
*/
|
|
@@ -251,6 +324,8 @@ export async function getExecutionContext(userId, projectId, taskId) {
|
|
|
251
324
|
status: s.status,
|
|
252
325
|
})),
|
|
253
326
|
});
|
|
327
|
+
// Check for lock expiry warning
|
|
328
|
+
const lockWarning = getLockExpiryWarning(task.lock_expires_at);
|
|
254
329
|
return {
|
|
255
330
|
task,
|
|
256
331
|
worker,
|
|
@@ -258,6 +333,7 @@ export async function getExecutionContext(userId, projectId, taskId) {
|
|
|
258
333
|
context: taskData.context,
|
|
259
334
|
parentTask: taskData.parent,
|
|
260
335
|
siblingTasks,
|
|
336
|
+
lockWarning,
|
|
261
337
|
};
|
|
262
338
|
}
|
|
263
339
|
/**
|
|
@@ -303,6 +379,81 @@ export async function resolveTaskDependencies(userId, projectId, completedTaskId
|
|
|
303
379
|
}
|
|
304
380
|
return newlyUnblocked;
|
|
305
381
|
}
|
|
382
|
+
/**
|
|
383
|
+
* Aggregate work_log context from all tasks in an epic
|
|
384
|
+
* Used by orchestrator to synthesize final results
|
|
385
|
+
*/
|
|
386
|
+
export async function getEpicAggregate(userId, projectId, epicId, includePending = false) {
|
|
387
|
+
// Get the epic
|
|
388
|
+
const epicResult = await getTask(userId, projectId, epicId);
|
|
389
|
+
if (!epicResult.success) {
|
|
390
|
+
throw new Error(`Epic not found: ${epicId}`);
|
|
391
|
+
}
|
|
392
|
+
const epic = epicResult.data;
|
|
393
|
+
// Verify it's an epic
|
|
394
|
+
if (epic.type !== 'epic') {
|
|
395
|
+
throw new Error(`Task ${epicId} is not an epic (type: ${epic.type})`);
|
|
396
|
+
}
|
|
397
|
+
// Get all child tasks
|
|
398
|
+
const tasksResult = await listTasks(userId, projectId, {
|
|
399
|
+
parent_id: epicId,
|
|
400
|
+
limit: 100,
|
|
401
|
+
});
|
|
402
|
+
if (!tasksResult.success) {
|
|
403
|
+
return {
|
|
404
|
+
epic: {
|
|
405
|
+
id: epic.id,
|
|
406
|
+
title: epic.title,
|
|
407
|
+
description: epic.description,
|
|
408
|
+
status: epic.status,
|
|
409
|
+
},
|
|
410
|
+
tasks: [],
|
|
411
|
+
summary_stats: {
|
|
412
|
+
total: 0,
|
|
413
|
+
completed: 0,
|
|
414
|
+
in_progress: 0,
|
|
415
|
+
pending: 0,
|
|
416
|
+
},
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
const allTasks = tasksResult.data;
|
|
420
|
+
// Get work_log context for each task
|
|
421
|
+
const tasksWithLogs = await Promise.all(allTasks.map(async (task) => {
|
|
422
|
+
// Get full task data to access context
|
|
423
|
+
const fullTaskResult = await getTask(userId, projectId, task.id, true);
|
|
424
|
+
const fullTaskData = fullTaskResult.data;
|
|
425
|
+
// Find work_log context
|
|
426
|
+
const workLogContext = fullTaskData.context?.find(ctx => ctx.context_type === 'work_log');
|
|
427
|
+
return {
|
|
428
|
+
id: task.id,
|
|
429
|
+
title: task.title,
|
|
430
|
+
status: task.status,
|
|
431
|
+
work_log: workLogContext?.content || null,
|
|
432
|
+
completed_at: task.completed_at,
|
|
433
|
+
};
|
|
434
|
+
}));
|
|
435
|
+
// Filter based on includePending
|
|
436
|
+
const filteredTasks = includePending
|
|
437
|
+
? tasksWithLogs
|
|
438
|
+
: tasksWithLogs.filter(t => t.status === 'done' || t.status === 'in_progress');
|
|
439
|
+
// Calculate stats
|
|
440
|
+
const stats = {
|
|
441
|
+
total: allTasks.length,
|
|
442
|
+
completed: allTasks.filter(t => t.status === 'done').length,
|
|
443
|
+
in_progress: allTasks.filter(t => t.status === 'in_progress').length,
|
|
444
|
+
pending: allTasks.filter(t => t.status === 'ready' || t.status === 'backlog').length,
|
|
445
|
+
};
|
|
446
|
+
return {
|
|
447
|
+
epic: {
|
|
448
|
+
id: epic.id,
|
|
449
|
+
title: epic.title,
|
|
450
|
+
description: epic.description,
|
|
451
|
+
status: epic.status,
|
|
452
|
+
},
|
|
453
|
+
tasks: filteredTasks,
|
|
454
|
+
summary_stats: stats,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
306
457
|
/**
|
|
307
458
|
* Get epic status with progress tracking
|
|
308
459
|
* Auto-completes epic if all child tasks are done
|
|
@@ -368,3 +519,97 @@ export async function getEpicStatus(userId, projectId, epicId) {
|
|
|
368
519
|
autoCompleted,
|
|
369
520
|
};
|
|
370
521
|
}
|
|
522
|
+
export async function handoffTask(userId, projectId, taskId, agentId, newWorkerType, reason) {
|
|
523
|
+
return apiRequest(`/api/v1/tasks/${userId}/${projectId}/${taskId}/handoff`, 'POST', {
|
|
524
|
+
agent_id: agentId,
|
|
525
|
+
new_worker_type: newWorkerType,
|
|
526
|
+
reason,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Revise an existing epic by adding/removing/updating tasks
|
|
531
|
+
* Enables iterative planning without recreating the entire epic
|
|
532
|
+
*/
|
|
533
|
+
export async function reviseEpic(userId, projectId, epicId, changes) {
|
|
534
|
+
// Verify epic exists and is not done
|
|
535
|
+
const epicResult = await getTask(userId, projectId, epicId);
|
|
536
|
+
if (!epicResult.success) {
|
|
537
|
+
throw new Error(`Epic not found: ${epicId}`);
|
|
538
|
+
}
|
|
539
|
+
const epic = epicResult.data;
|
|
540
|
+
if (epic.type !== 'epic') {
|
|
541
|
+
throw new Error(`Task ${epicId} is not an epic (type: ${epic.type})`);
|
|
542
|
+
}
|
|
543
|
+
if (epic.status === 'done') {
|
|
544
|
+
throw new Error(`Cannot revise completed epic: ${epicId}`);
|
|
545
|
+
}
|
|
546
|
+
const added = [];
|
|
547
|
+
const removed = [];
|
|
548
|
+
const updated = [];
|
|
549
|
+
// Add new tasks
|
|
550
|
+
if (changes.add_tasks && changes.add_tasks.length > 0) {
|
|
551
|
+
for (const taskDef of changes.add_tasks) {
|
|
552
|
+
const taskResult = await createTask(userId, projectId, {
|
|
553
|
+
type: 'task',
|
|
554
|
+
title: taskDef.title,
|
|
555
|
+
description: taskDef.description,
|
|
556
|
+
parent_id: epicId,
|
|
557
|
+
priority: epic.priority,
|
|
558
|
+
estimated_effort: taskDef.effort,
|
|
559
|
+
blocked_by: taskDef.blocked_by,
|
|
560
|
+
domain: taskDef.domain,
|
|
561
|
+
});
|
|
562
|
+
added.push(taskResult.data);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
// Remove tasks (set to cancelled status)
|
|
566
|
+
if (changes.remove_task_ids && changes.remove_task_ids.length > 0) {
|
|
567
|
+
for (const taskId of changes.remove_task_ids) {
|
|
568
|
+
try {
|
|
569
|
+
await updateTaskStatus(userId, projectId, taskId, 'cancelled', 'system:epic-revision');
|
|
570
|
+
removed.push(taskId);
|
|
571
|
+
}
|
|
572
|
+
catch (error) {
|
|
573
|
+
// Log but continue - task may not exist
|
|
574
|
+
console.warn(`Failed to cancel task ${taskId}:`, error);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
// Update existing tasks
|
|
579
|
+
// Note: The API doesn't support PATCH for tasks, so we can only update status
|
|
580
|
+
// For title/description/effort updates, we'll need to add context explaining the change
|
|
581
|
+
if (changes.update_tasks && changes.update_tasks.length > 0) {
|
|
582
|
+
for (const update of changes.update_tasks) {
|
|
583
|
+
try {
|
|
584
|
+
const taskResult = await getTask(userId, projectId, update.task_id);
|
|
585
|
+
if (!taskResult.success)
|
|
586
|
+
continue;
|
|
587
|
+
const task = taskResult.data;
|
|
588
|
+
// Add context about the updates since we can't modify fields directly
|
|
589
|
+
const updates = [];
|
|
590
|
+
if (update.title && update.title !== task.title) {
|
|
591
|
+
updates.push(`Title updated: "${task.title}" → "${update.title}"`);
|
|
592
|
+
}
|
|
593
|
+
if (update.description && update.description !== task.description) {
|
|
594
|
+
updates.push(`Description updated: "${update.description}"`);
|
|
595
|
+
}
|
|
596
|
+
if (update.effort && update.effort !== task.estimated_effort) {
|
|
597
|
+
updates.push(`Effort updated: ${task.estimated_effort || 'unset'} → ${update.effort}`);
|
|
598
|
+
}
|
|
599
|
+
if (updates.length > 0) {
|
|
600
|
+
await addTaskContext(userId, projectId, update.task_id, 'decision', updates.join('\n'), 'task_plan_revise', 'epic-revision');
|
|
601
|
+
updated.push(task);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
console.warn(`Failed to update task ${update.task_id}:`, error);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return {
|
|
610
|
+
epic,
|
|
611
|
+
added,
|
|
612
|
+
removed,
|
|
613
|
+
updated,
|
|
614
|
+
};
|
|
615
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
export type { ExpertWorker } from './helpers/workers.js';
|
|
2
2
|
export type { Task, TaskContext, DispatchableTask } from './helpers/tasks.js';
|
|
3
3
|
export type { DeduplicationTracker } from './context/index.js';
|
|
4
|
+
export type { CircuitState } from './helpers/circuit-breaker.js';
|
|
5
|
+
export type { TestResult, TestSuite, TestSuiteSummary, TestFunction, TestDefinition, } from './testing/index.js';
|
|
6
|
+
export { runTest, runSuite, formatResults, formatResultsJson, assert, assertEqual, assertThrows, } from './testing/index.js';
|
|
4
7
|
export { EXPERT_WORKERS, matchTaskToWorker } from './helpers/workers.js';
|
|
5
8
|
export { parseJsonArray, getDispatchableTasks, getExecutionContext, resolveTaskDependencies, createTask, listTasks, getTask, claimTask, releaseTask, updateTaskStatus, addTaskContext, getTaskSummary, heartbeatTask } from './helpers/tasks.js';
|
|
6
9
|
export { injectContext } from './helpers/api-client.js';
|
|
7
10
|
export { recordToolCall, getToolCallWarnings, getSessionStats, clearSessionState, recordContextReference, calculateEngagementScore, getEngagementStats } from './helpers/session-validation.js';
|
|
8
11
|
export { createDeduplicationTracker, InMemoryDeduplicationTracker } from './context/index.js';
|
|
12
|
+
export { recordWorkerFailure, isCircuitOpen, getCircuitStatus, resetCircuit, resetAllCircuits } from './helpers/circuit-breaker.js';
|
|
9
13
|
export declare function startServer(): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -11,12 +11,14 @@ import { registerToolHandlers } from './handlers/tool-handlers.js';
|
|
|
11
11
|
// Resource and prompt handlers
|
|
12
12
|
import { registerResourceHandlers } from './resources.js';
|
|
13
13
|
import { registerPromptHandlers } from './prompts.js';
|
|
14
|
+
export { runTest, runSuite, formatResults, formatResultsJson, assert, assertEqual, assertThrows, } from './testing/index.js';
|
|
14
15
|
// Re-export functions that are used by handlers
|
|
15
16
|
export { EXPERT_WORKERS, matchTaskToWorker } from './helpers/workers.js';
|
|
16
17
|
export { parseJsonArray, getDispatchableTasks, getExecutionContext, resolveTaskDependencies, createTask, listTasks, getTask, claimTask, releaseTask, updateTaskStatus, addTaskContext, getTaskSummary, heartbeatTask } from './helpers/tasks.js';
|
|
17
18
|
export { injectContext } from './helpers/api-client.js';
|
|
18
19
|
export { recordToolCall, getToolCallWarnings, getSessionStats, clearSessionState, recordContextReference, calculateEngagementScore, getEngagementStats } from './helpers/session-validation.js';
|
|
19
20
|
export { createDeduplicationTracker, InMemoryDeduplicationTracker } from './context/index.js';
|
|
21
|
+
export { recordWorkerFailure, isCircuitOpen, getCircuitStatus, resetCircuit, resetAllCircuits } from './helpers/circuit-breaker.js';
|
|
20
22
|
// =============================================================================
|
|
21
23
|
// Server Initialization
|
|
22
24
|
// =============================================================================
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface TestResult {
|
|
2
|
+
name: string;
|
|
3
|
+
status: 'pass' | 'fail' | 'error';
|
|
4
|
+
duration: number;
|
|
5
|
+
error?: string;
|
|
6
|
+
details?: any;
|
|
7
|
+
}
|
|
8
|
+
export interface TestSuiteResult {
|
|
9
|
+
totalTests: number;
|
|
10
|
+
passed: number;
|
|
11
|
+
failed: number;
|
|
12
|
+
errors: number;
|
|
13
|
+
duration: number;
|
|
14
|
+
tests: TestResult[];
|
|
15
|
+
}
|
|
16
|
+
export interface TestOptions {
|
|
17
|
+
verbose?: boolean;
|
|
18
|
+
json?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Run all integration tests in the suite.
|
|
22
|
+
*
|
|
23
|
+
* This is the main entry point that imports and runs all test files:
|
|
24
|
+
* - Store-Retrieve-Verify test
|
|
25
|
+
* - Context Injection test
|
|
26
|
+
* - Task Lifecycle test
|
|
27
|
+
* - Cross-Project Isolation test
|
|
28
|
+
*/
|
|
29
|
+
export declare function runSuite(): Promise<TestSuiteResult>;
|
|
30
|
+
/**
|
|
31
|
+
* Run tests from CLI with options.
|
|
32
|
+
*/
|
|
33
|
+
export declare function runTests(options?: TestOptions): Promise<void>;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Integration Test Runner
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// Runs the full integration test suite and outputs results
|
|
5
|
+
// Supports --verbose and --json output formats
|
|
6
|
+
// -----------------------------------------------------------------------------
|
|
7
|
+
// Test Suite Runner
|
|
8
|
+
// -----------------------------------------------------------------------------
|
|
9
|
+
/**
|
|
10
|
+
* Run all integration tests in the suite.
|
|
11
|
+
*
|
|
12
|
+
* This is the main entry point that imports and runs all test files:
|
|
13
|
+
* - Store-Retrieve-Verify test
|
|
14
|
+
* - Context Injection test
|
|
15
|
+
* - Task Lifecycle test
|
|
16
|
+
* - Cross-Project Isolation test
|
|
17
|
+
*/
|
|
18
|
+
export async function runSuite() {
|
|
19
|
+
const startTime = Date.now();
|
|
20
|
+
const results = [];
|
|
21
|
+
// TODO: Import and run actual test files once they're implemented
|
|
22
|
+
// Example structure:
|
|
23
|
+
//
|
|
24
|
+
// import { runStoreRetrieveTest } from './__tests__/store-retrieve-verify.js';
|
|
25
|
+
// import { runContextInjectionTest } from './__tests__/context-injection.js';
|
|
26
|
+
// import { runTaskLifecycleTest } from './__tests__/task-lifecycle.js';
|
|
27
|
+
// import { runCrossProjectTest } from './__tests__/cross-project-isolation.js';
|
|
28
|
+
//
|
|
29
|
+
// const tests = [
|
|
30
|
+
// { name: 'Store-Retrieve-Verify', fn: runStoreRetrieveTest },
|
|
31
|
+
// { name: 'Context Injection', fn: runContextInjectionTest },
|
|
32
|
+
// { name: 'Task Lifecycle', fn: runTaskLifecycleTest },
|
|
33
|
+
// { name: 'Cross-Project Isolation', fn: runCrossProjectTest },
|
|
34
|
+
// ];
|
|
35
|
+
//
|
|
36
|
+
// for (const test of tests) {
|
|
37
|
+
// const result = await runTest(test.name, test.fn);
|
|
38
|
+
// results.push(result);
|
|
39
|
+
// }
|
|
40
|
+
// Placeholder: Add a simple test to demonstrate structure
|
|
41
|
+
results.push({
|
|
42
|
+
name: 'Placeholder Test',
|
|
43
|
+
status: 'pass',
|
|
44
|
+
duration: 0,
|
|
45
|
+
details: { message: 'Test harness ready - add actual tests' }
|
|
46
|
+
});
|
|
47
|
+
const duration = Date.now() - startTime;
|
|
48
|
+
const passed = results.filter(r => r.status === 'pass').length;
|
|
49
|
+
const failed = results.filter(r => r.status === 'fail').length;
|
|
50
|
+
const errors = results.filter(r => r.status === 'error').length;
|
|
51
|
+
return {
|
|
52
|
+
totalTests: results.length,
|
|
53
|
+
passed,
|
|
54
|
+
failed,
|
|
55
|
+
errors,
|
|
56
|
+
duration,
|
|
57
|
+
tests: results,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Run a single test and capture result.
|
|
62
|
+
*/
|
|
63
|
+
async function runTest(name, testFn) {
|
|
64
|
+
const startTime = Date.now();
|
|
65
|
+
try {
|
|
66
|
+
await testFn();
|
|
67
|
+
return {
|
|
68
|
+
name,
|
|
69
|
+
status: 'pass',
|
|
70
|
+
duration: Date.now() - startTime,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
const isAssertionError = error instanceof Error && error.message.includes('AssertionError');
|
|
75
|
+
return {
|
|
76
|
+
name,
|
|
77
|
+
status: isAssertionError ? 'fail' : 'error',
|
|
78
|
+
duration: Date.now() - startTime,
|
|
79
|
+
error: error instanceof Error ? error.message : String(error),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// -----------------------------------------------------------------------------
|
|
84
|
+
// Output Formatting
|
|
85
|
+
// -----------------------------------------------------------------------------
|
|
86
|
+
/**
|
|
87
|
+
* Format results as human-readable text.
|
|
88
|
+
*/
|
|
89
|
+
function formatHumanReadable(result, verbose) {
|
|
90
|
+
const lines = [];
|
|
91
|
+
lines.push('');
|
|
92
|
+
lines.push('Integration Test Suite Results');
|
|
93
|
+
lines.push('================================');
|
|
94
|
+
lines.push('');
|
|
95
|
+
lines.push(`Total Tests: ${result.totalTests}`);
|
|
96
|
+
lines.push(`Passed: ${result.passed}`);
|
|
97
|
+
lines.push(`Failed: ${result.failed}`);
|
|
98
|
+
lines.push(`Errors: ${result.errors}`);
|
|
99
|
+
lines.push(`Duration: ${result.duration}ms`);
|
|
100
|
+
lines.push('');
|
|
101
|
+
if (verbose || result.failed > 0 || result.errors > 0) {
|
|
102
|
+
lines.push('Test Details:');
|
|
103
|
+
lines.push('');
|
|
104
|
+
for (const test of result.tests) {
|
|
105
|
+
const statusIcon = test.status === 'pass' ? '✓' : '✗';
|
|
106
|
+
lines.push(`${statusIcon} ${test.name} (${test.duration}ms)`);
|
|
107
|
+
if (test.error) {
|
|
108
|
+
lines.push(` Error: ${test.error}`);
|
|
109
|
+
}
|
|
110
|
+
if (verbose && test.details) {
|
|
111
|
+
lines.push(` Details: ${JSON.stringify(test.details, null, 2)}`);
|
|
112
|
+
}
|
|
113
|
+
lines.push('');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return lines.join('\n');
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Format results as JSON.
|
|
120
|
+
*/
|
|
121
|
+
function formatJSON(result) {
|
|
122
|
+
return JSON.stringify(result, null, 2);
|
|
123
|
+
}
|
|
124
|
+
// -----------------------------------------------------------------------------
|
|
125
|
+
// CLI Entry Point
|
|
126
|
+
// -----------------------------------------------------------------------------
|
|
127
|
+
/**
|
|
128
|
+
* Run tests from CLI with options.
|
|
129
|
+
*/
|
|
130
|
+
export async function runTests(options = {}) {
|
|
131
|
+
const { verbose = false, json = false } = options;
|
|
132
|
+
try {
|
|
133
|
+
console.error('Running integration tests...\n');
|
|
134
|
+
const result = await runSuite();
|
|
135
|
+
if (json) {
|
|
136
|
+
console.log(formatJSON(result));
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
console.log(formatHumanReadable(result, verbose));
|
|
140
|
+
}
|
|
141
|
+
// Exit with code 0 if all pass, 1 if any fail
|
|
142
|
+
const exitCode = result.failed + result.errors > 0 ? 1 : 0;
|
|
143
|
+
process.exit(exitCode);
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
console.error('Fatal error running test suite:', error);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Test Runner Example Usage
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// This file demonstrates how to use the test harness for integration testing.
|
|
6
|
+
// Run with: tsx src/testing/example.ts
|
|
7
|
+
//
|
|
8
|
+
import { runSuite, formatResults, formatResultsJson, assert, assertEqual, assertThrows, } from './test-runner.js';
|
|
9
|
+
// Example: Memory integration tests
|
|
10
|
+
async function exampleUsage() {
|
|
11
|
+
const suite = await runSuite('Memory Integration Example', [
|
|
12
|
+
{
|
|
13
|
+
name: 'should pass basic assertion',
|
|
14
|
+
fn: async () => {
|
|
15
|
+
const value = 42;
|
|
16
|
+
assert(value === 42, 'Value should be 42');
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'should pass equality check',
|
|
21
|
+
fn: async () => {
|
|
22
|
+
const obj = { id: '123', name: 'Test' };
|
|
23
|
+
assertEqual(obj, { id: '123', name: 'Test' });
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'should catch expected errors',
|
|
28
|
+
fn: async () => {
|
|
29
|
+
await assertThrows(async () => {
|
|
30
|
+
throw new Error('Invalid input');
|
|
31
|
+
}, 'Invalid input');
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'should fail on purpose',
|
|
36
|
+
fn: async () => {
|
|
37
|
+
// This test intentionally fails to demonstrate error handling
|
|
38
|
+
assert(false, 'This is a deliberate failure');
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'should handle async operations',
|
|
43
|
+
fn: async () => {
|
|
44
|
+
// Simulate async API call
|
|
45
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
46
|
+
const result = { success: true };
|
|
47
|
+
assert(result.success === true);
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
]);
|
|
51
|
+
// Print human-readable results
|
|
52
|
+
console.log(formatResults(suite));
|
|
53
|
+
// Example: Save JSON results to file
|
|
54
|
+
console.log('\n--- JSON Output ---\n');
|
|
55
|
+
console.log(formatResultsJson(suite));
|
|
56
|
+
// Exit with appropriate code
|
|
57
|
+
process.exit(suite.summary.failed > 0 ? 1 : 0);
|
|
58
|
+
}
|
|
59
|
+
// Run example
|
|
60
|
+
exampleUsage().catch(error => {
|
|
61
|
+
console.error('Fatal error:', error);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { TestResult, TestSuite, TestSuiteSummary, TestFunction, TestDefinition, runTest, runSuite, formatResults, formatResultsJson, assert, assertEqual, assertThrows, } from './test-runner.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Agent-Based Integration Testing
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Re-exports for test runner and utilities
|
|
6
|
+
//
|
|
7
|
+
export {
|
|
8
|
+
// Core test runner functions
|
|
9
|
+
runTest, runSuite,
|
|
10
|
+
// Formatting
|
|
11
|
+
formatResults, formatResultsJson,
|
|
12
|
+
// Assertion helpers
|
|
13
|
+
assert, assertEqual, assertThrows, } from './test-runner.js';
|