@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.
@@ -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 = 30) {
55
- return apiRequest(`/api/v1/tasks/${userId}/${projectId}/${taskId}/claim`, 'POST', {
56
- agent_id: agentId,
57
- lock_duration_minutes: lockDurationMinutes,
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
- // Configuration: Default maximum parallel tasks
94
- export const DEFAULT_MAX_PARALLEL = 5;
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 count of currently active tasks
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 max parallel limit by considering currently active tasks
159
+ * - Respects effort-weighted capacity budget
134
160
  */
135
- export async function getDispatchableTasks(userId, projectId, epicId, maxParallel = DEFAULT_MAX_PARALLEL) {
136
- // Get current active task count
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
- const remainingCapacity = maxParallel - activeCount.total;
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
- // Only dispatch tasks that fit within remaining capacity
199
- if (dispatchable.length >= remainingCapacity) {
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';