@cleocode/core 2026.3.58 → 2026.3.60
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/agents/agent-registry.d.ts +206 -0
- package/dist/agents/agent-registry.d.ts.map +1 -0
- package/dist/agents/agent-registry.js +288 -0
- package/dist/agents/agent-registry.js.map +1 -0
- package/dist/agents/agent-schema.js +5 -0
- package/dist/agents/agent-schema.js.map +1 -1
- package/dist/agents/execution-learning.js +474 -0
- package/dist/agents/execution-learning.js.map +1 -0
- package/dist/agents/health-monitor.d.ts +161 -0
- package/dist/agents/health-monitor.d.ts.map +1 -0
- package/dist/agents/health-monitor.js +217 -0
- package/dist/agents/health-monitor.js.map +1 -0
- package/dist/agents/index.d.ts +3 -1
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +9 -1
- package/dist/agents/index.js.map +1 -1
- package/dist/agents/retry.d.ts +57 -4
- package/dist/agents/retry.d.ts.map +1 -1
- package/dist/agents/retry.js +57 -4
- package/dist/agents/retry.js.map +1 -1
- package/dist/backfill/index.d.ts +27 -0
- package/dist/backfill/index.d.ts.map +1 -1
- package/dist/backfill/index.js +229 -0
- package/dist/backfill/index.js.map +1 -0
- package/dist/bootstrap.d.ts +2 -1
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +135 -28
- package/dist/bootstrap.js.map +1 -1
- package/dist/cleo.d.ts +40 -0
- package/dist/cleo.d.ts.map +1 -1
- package/dist/config.js +83 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1036 -536
- package/dist/index.js.map +4 -4
- package/dist/intelligence/adaptive-validation.js +497 -0
- package/dist/intelligence/adaptive-validation.js.map +1 -0
- package/dist/intelligence/impact.d.ts +34 -1
- package/dist/intelligence/impact.d.ts.map +1 -1
- package/dist/intelligence/impact.js +176 -0
- package/dist/intelligence/impact.js.map +1 -1
- package/dist/intelligence/index.d.ts +2 -2
- package/dist/intelligence/index.d.ts.map +1 -1
- package/dist/intelligence/index.js +6 -1
- package/dist/intelligence/index.js.map +1 -1
- package/dist/intelligence/types.d.ts +60 -0
- package/dist/intelligence/types.d.ts.map +1 -1
- package/dist/internal.d.ts +5 -4
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +11 -2
- package/dist/internal.js.map +1 -1
- package/dist/lib/index.d.ts +10 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +10 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/retry.d.ts +128 -0
- package/dist/lib/retry.d.ts.map +1 -0
- package/dist/lib/retry.js +152 -0
- package/dist/lib/retry.js.map +1 -0
- package/dist/nexus/sharing/index.d.ts +48 -2
- package/dist/nexus/sharing/index.d.ts.map +1 -1
- package/dist/nexus/sharing/index.js +110 -1
- package/dist/nexus/sharing/index.js.map +1 -1
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +22 -2
- package/dist/scaffold.js.map +1 -1
- package/dist/sessions/session-enforcement.js +4 -0
- package/dist/sessions/session-enforcement.js.map +1 -1
- package/dist/stats/index.js +2 -0
- package/dist/stats/index.js.map +1 -1
- package/dist/stats/workflow-telemetry.d.ts +15 -0
- package/dist/stats/workflow-telemetry.d.ts.map +1 -1
- package/dist/stats/workflow-telemetry.js +400 -0
- package/dist/stats/workflow-telemetry.js.map +1 -0
- package/dist/store/brain-schema.js +4 -1
- package/dist/store/brain-schema.js.map +1 -1
- package/dist/store/converters.js +2 -0
- package/dist/store/converters.js.map +1 -1
- package/dist/store/cross-db-cleanup.d.ts +35 -0
- package/dist/store/cross-db-cleanup.d.ts.map +1 -1
- package/dist/store/cross-db-cleanup.js +169 -0
- package/dist/store/cross-db-cleanup.js.map +1 -0
- package/dist/store/db-helpers.js +2 -0
- package/dist/store/db-helpers.js.map +1 -1
- package/dist/store/migration-sqlite.js +5 -0
- package/dist/store/migration-sqlite.js.map +1 -1
- package/dist/store/sqlite-data-accessor.js +20 -28
- package/dist/store/sqlite-data-accessor.js.map +1 -1
- package/dist/store/sqlite.js +13 -2
- package/dist/store/sqlite.js.map +1 -1
- package/dist/store/task-store.js +4 -0
- package/dist/store/task-store.js.map +1 -1
- package/dist/store/tasks-schema.js +50 -20
- package/dist/store/tasks-schema.js.map +1 -1
- package/dist/tasks/add.js +87 -3
- package/dist/tasks/add.js.map +1 -1
- package/dist/tasks/complete.d.ts.map +1 -1
- package/dist/tasks/complete.js +15 -4
- package/dist/tasks/complete.js.map +1 -1
- package/dist/tasks/enforcement.d.ts.map +1 -1
- package/dist/tasks/enforcement.js +8 -1
- package/dist/tasks/enforcement.js.map +1 -1
- package/dist/tasks/epic-enforcement.d.ts +61 -0
- package/dist/tasks/epic-enforcement.d.ts.map +1 -1
- package/dist/tasks/epic-enforcement.js +294 -0
- package/dist/tasks/epic-enforcement.js.map +1 -0
- package/dist/tasks/index.js +1 -1
- package/dist/tasks/index.js.map +1 -1
- package/dist/tasks/pipeline-stage.d.ts +70 -1
- package/dist/tasks/pipeline-stage.d.ts.map +1 -1
- package/dist/tasks/pipeline-stage.js +248 -0
- package/dist/tasks/pipeline-stage.js.map +1 -0
- package/dist/tasks/update.js +28 -0
- package/dist/tasks/update.js.map +1 -1
- package/package.json +5 -5
- package/schemas/config.schema.json +37 -1547
- package/src/__tests__/sharing.test.ts +24 -0
- package/src/agents/__tests__/agent-registry.test.ts +351 -0
- package/src/agents/__tests__/health-monitor.test.ts +332 -0
- package/src/agents/agent-registry.ts +394 -0
- package/src/agents/health-monitor.ts +279 -0
- package/src/agents/index.ts +24 -1
- package/src/agents/retry.ts +57 -4
- package/src/backfill/index.ts +27 -0
- package/src/bootstrap.ts +171 -30
- package/src/cleo.ts +103 -2
- package/src/config.ts +3 -3
- package/src/index.ts +1 -0
- package/src/intelligence/__tests__/impact.test.ts +165 -1
- package/src/intelligence/impact.ts +203 -0
- package/src/intelligence/index.ts +3 -0
- package/src/intelligence/types.ts +76 -0
- package/src/internal.ts +20 -0
- package/src/lib/__tests__/retry.test.ts +321 -0
- package/src/lib/index.ts +16 -0
- package/src/lib/retry.ts +224 -0
- package/src/nexus/sharing/index.ts +142 -2
- package/src/scaffold.ts +24 -2
- package/src/stats/workflow-telemetry.ts +15 -0
- package/src/store/__tests__/session-store.test.ts +43 -7
- package/src/store/__tests__/task-store.test.ts +1 -1
- package/src/store/__tests__/test-db-helper.ts +7 -3
- package/src/store/cross-db-cleanup.ts +35 -0
- package/src/tasks/__tests__/epic-enforcement.test.ts +9 -4
- package/src/tasks/__tests__/minimal-test.test.ts +2 -2
- package/src/tasks/__tests__/update.test.ts +25 -25
- package/src/tasks/complete.ts +11 -6
- package/src/tasks/enforcement.ts +6 -3
- package/src/tasks/epic-enforcement.ts +61 -0
- package/src/tasks/pipeline-stage.ts +70 -1
- package/templates/config.template.json +5 -116
- package/templates/global-config.template.json +2 -44
|
@@ -153,5 +153,29 @@ describe('sharing', () => {
|
|
|
153
153
|
expect(status.tracked).toContain('adrs/ADR-001.md');
|
|
154
154
|
expect(status.ignored).toContain('tasks.db');
|
|
155
155
|
});
|
|
156
|
+
|
|
157
|
+
it('returns safe defaults for git sync fields when .cleo/.git does not exist', async () => {
|
|
158
|
+
const status = await getSharingStatus(tempDir);
|
|
159
|
+
|
|
160
|
+
expect(status.hasGit).toBe(false);
|
|
161
|
+
expect(status.remotes).toEqual([]);
|
|
162
|
+
expect(status.pendingChanges).toBe(false);
|
|
163
|
+
expect(status.lastSync).toBeNull();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('reports hasGit=true when .cleo/.git/HEAD exists', async () => {
|
|
167
|
+
// Simulate an initialized .cleo/.git repo (HEAD file is the sentinel)
|
|
168
|
+
const cleoDir = join(tempDir, '.cleo');
|
|
169
|
+
await mkdir(join(cleoDir, '.git'), { recursive: true });
|
|
170
|
+
await writeFile(join(cleoDir, '.git', 'HEAD'), 'ref: refs/heads/main\n');
|
|
171
|
+
|
|
172
|
+
const status = await getSharingStatus(tempDir);
|
|
173
|
+
|
|
174
|
+
expect(status.hasGit).toBe(true);
|
|
175
|
+
// With no actual git repo content, git commands will fail gracefully
|
|
176
|
+
expect(Array.isArray(status.remotes)).toBe(true);
|
|
177
|
+
expect(typeof status.pendingChanges).toBe('boolean');
|
|
178
|
+
expect(status.lastSync === null || typeof status.lastSync === 'string').toBe(true);
|
|
179
|
+
});
|
|
156
180
|
});
|
|
157
181
|
});
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the agent registry with capacity tracking.
|
|
3
|
+
*
|
|
4
|
+
* Covers: task-count-based capacity, specialization read/write,
|
|
5
|
+
* performance recording delegation, and sorted agent queries.
|
|
6
|
+
*
|
|
7
|
+
* @module agents/__tests__/agent-registry.test
|
|
8
|
+
* @task T041
|
|
9
|
+
* @epic T038
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { mkdir, mkdtemp, rm } from 'node:fs/promises';
|
|
13
|
+
import { tmpdir } from 'node:os';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
getAgentCapacity,
|
|
19
|
+
getAgentSpecializations,
|
|
20
|
+
getAgentsByCapacity,
|
|
21
|
+
MAX_TASKS_PER_AGENT,
|
|
22
|
+
recordAgentPerformance,
|
|
23
|
+
updateAgentSpecializations,
|
|
24
|
+
} from '../agent-registry.js';
|
|
25
|
+
import { registerAgent, updateAgentStatus } from '../registry.js';
|
|
26
|
+
|
|
27
|
+
describe('Agent Registry (T041)', () => {
|
|
28
|
+
let tempDir: string;
|
|
29
|
+
|
|
30
|
+
beforeEach(async () => {
|
|
31
|
+
tempDir = await mkdtemp(join(tmpdir(), 'cleo-agent-registry-test-'));
|
|
32
|
+
await mkdir(join(tempDir, '.cleo'), { recursive: true });
|
|
33
|
+
await mkdir(join(tempDir, '.cleo', 'backups', 'operational'), { recursive: true });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(async () => {
|
|
37
|
+
try {
|
|
38
|
+
const { closeAllDatabases } = await import('../../store/sqlite.js');
|
|
39
|
+
await closeAllDatabases();
|
|
40
|
+
} catch {
|
|
41
|
+
/* module may not be loaded */
|
|
42
|
+
}
|
|
43
|
+
await Promise.race([
|
|
44
|
+
rm(tempDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 300 }).catch(() => {}),
|
|
45
|
+
new Promise<void>((resolve) => setTimeout(resolve, 8_000)),
|
|
46
|
+
]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// ==========================================================================
|
|
50
|
+
// MAX_TASKS_PER_AGENT constant
|
|
51
|
+
// ==========================================================================
|
|
52
|
+
|
|
53
|
+
describe('MAX_TASKS_PER_AGENT', () => {
|
|
54
|
+
it('is 5', () => {
|
|
55
|
+
expect(MAX_TASKS_PER_AGENT).toBe(5);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// ==========================================================================
|
|
60
|
+
// getAgentCapacity
|
|
61
|
+
// ==========================================================================
|
|
62
|
+
|
|
63
|
+
describe('getAgentCapacity', () => {
|
|
64
|
+
it('returns null for a non-existent agent', async () => {
|
|
65
|
+
const result = await getAgentCapacity('agt_nonexistent_abc123', tempDir);
|
|
66
|
+
expect(result).toBeNull();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('returns full capacity for a newly registered idle agent', async () => {
|
|
70
|
+
const agent = await registerAgent({ agentType: 'executor' }, tempDir);
|
|
71
|
+
await updateAgentStatus(agent.id, { status: 'active' }, tempDir);
|
|
72
|
+
|
|
73
|
+
const cap = await getAgentCapacity(agent.id, tempDir);
|
|
74
|
+
expect(cap).not.toBeNull();
|
|
75
|
+
expect(cap!.agentId).toBe(agent.id);
|
|
76
|
+
expect(cap!.maxCapacity).toBe(MAX_TASKS_PER_AGENT);
|
|
77
|
+
expect(cap!.activeTasks).toBe(0);
|
|
78
|
+
expect(cap!.remainingCapacity).toBe(MAX_TASKS_PER_AGENT);
|
|
79
|
+
expect(cap!.available).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('counts the agent own task_id as 1 active task', async () => {
|
|
83
|
+
// We need a task row to satisfy the FK; insert one directly.
|
|
84
|
+
const { getDb } = await import('../../store/sqlite.js');
|
|
85
|
+
const { tasks: tasksTable } = await import('../../store/tasks-schema.js');
|
|
86
|
+
const db = await getDb(tempDir);
|
|
87
|
+
db.insert(tasksTable)
|
|
88
|
+
.values({
|
|
89
|
+
id: 'T-cap-001',
|
|
90
|
+
title: 'Capacity test task',
|
|
91
|
+
status: 'active',
|
|
92
|
+
priority: 'medium',
|
|
93
|
+
createdAt: new Date().toISOString(),
|
|
94
|
+
})
|
|
95
|
+
.run();
|
|
96
|
+
|
|
97
|
+
const agent = await registerAgent({ agentType: 'executor', taskId: 'T-cap-001' }, tempDir);
|
|
98
|
+
await updateAgentStatus(agent.id, { status: 'active' }, tempDir);
|
|
99
|
+
|
|
100
|
+
const cap = await getAgentCapacity(agent.id, tempDir);
|
|
101
|
+
expect(cap).not.toBeNull();
|
|
102
|
+
expect(cap!.activeTasks).toBe(1);
|
|
103
|
+
expect(cap!.remainingCapacity).toBe(MAX_TASKS_PER_AGENT - 1);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('counts active child agents toward capacity', async () => {
|
|
107
|
+
const parent = await registerAgent({ agentType: 'orchestrator' }, tempDir);
|
|
108
|
+
await updateAgentStatus(parent.id, { status: 'active' }, tempDir);
|
|
109
|
+
|
|
110
|
+
// Register 3 child agents
|
|
111
|
+
for (let i = 0; i < 3; i++) {
|
|
112
|
+
const child = await registerAgent(
|
|
113
|
+
{ agentType: 'executor', parentAgentId: parent.id },
|
|
114
|
+
tempDir,
|
|
115
|
+
);
|
|
116
|
+
await updateAgentStatus(child.id, { status: 'active' }, tempDir);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const cap = await getAgentCapacity(parent.id, tempDir);
|
|
120
|
+
expect(cap).not.toBeNull();
|
|
121
|
+
expect(cap!.activeTasks).toBe(3); // no own taskId + 3 children
|
|
122
|
+
expect(cap!.remainingCapacity).toBe(MAX_TASKS_PER_AGENT - 3);
|
|
123
|
+
expect(cap!.available).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('returns zero remaining capacity for stopped agents', async () => {
|
|
127
|
+
const agent = await registerAgent({ agentType: 'executor' }, tempDir);
|
|
128
|
+
await updateAgentStatus(agent.id, { status: 'stopped' }, tempDir);
|
|
129
|
+
|
|
130
|
+
const cap = await getAgentCapacity(agent.id, tempDir);
|
|
131
|
+
expect(cap).not.toBeNull();
|
|
132
|
+
expect(cap!.remainingCapacity).toBe(0);
|
|
133
|
+
expect(cap!.available).toBe(false);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('returns zero remaining capacity for crashed agents', async () => {
|
|
137
|
+
const agent = await registerAgent({ agentType: 'executor' }, tempDir);
|
|
138
|
+
await updateAgentStatus(agent.id, { status: 'crashed' }, tempDir);
|
|
139
|
+
|
|
140
|
+
const cap = await getAgentCapacity(agent.id, tempDir);
|
|
141
|
+
expect(cap).not.toBeNull();
|
|
142
|
+
expect(cap!.remainingCapacity).toBe(0);
|
|
143
|
+
expect(cap!.available).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('excludes stopped children from active count', async () => {
|
|
147
|
+
const parent = await registerAgent({ agentType: 'orchestrator' }, tempDir);
|
|
148
|
+
await updateAgentStatus(parent.id, { status: 'active' }, tempDir);
|
|
149
|
+
|
|
150
|
+
const child = await registerAgent(
|
|
151
|
+
{ agentType: 'executor', parentAgentId: parent.id },
|
|
152
|
+
tempDir,
|
|
153
|
+
);
|
|
154
|
+
// Immediately stop the child — should not count
|
|
155
|
+
await updateAgentStatus(child.id, { status: 'stopped' }, tempDir);
|
|
156
|
+
|
|
157
|
+
const cap = await getAgentCapacity(parent.id, tempDir);
|
|
158
|
+
expect(cap!.activeTasks).toBe(0);
|
|
159
|
+
expect(cap!.remainingCapacity).toBe(MAX_TASKS_PER_AGENT);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// ==========================================================================
|
|
164
|
+
// getAgentsByCapacity
|
|
165
|
+
// ==========================================================================
|
|
166
|
+
|
|
167
|
+
describe('getAgentsByCapacity', () => {
|
|
168
|
+
it('returns empty array when no active agents', async () => {
|
|
169
|
+
const result = await getAgentsByCapacity(undefined, tempDir);
|
|
170
|
+
expect(result).toEqual([]);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('sorts agents by remaining capacity descending', async () => {
|
|
174
|
+
const a1 = await registerAgent({ agentType: 'executor' }, tempDir);
|
|
175
|
+
const a2 = await registerAgent({ agentType: 'executor' }, tempDir);
|
|
176
|
+
const a3 = await registerAgent({ agentType: 'executor' }, tempDir);
|
|
177
|
+
await updateAgentStatus(a1.id, { status: 'active' }, tempDir);
|
|
178
|
+
await updateAgentStatus(a2.id, { status: 'active' }, tempDir);
|
|
179
|
+
await updateAgentStatus(a3.id, { status: 'active' }, tempDir);
|
|
180
|
+
|
|
181
|
+
// Add 2 children to a1 — so a1 has less capacity
|
|
182
|
+
for (let i = 0; i < 2; i++) {
|
|
183
|
+
const child = await registerAgent({ agentType: 'executor', parentAgentId: a1.id }, tempDir);
|
|
184
|
+
await updateAgentStatus(child.id, { status: 'active' }, tempDir);
|
|
185
|
+
}
|
|
186
|
+
// Add 1 child to a2
|
|
187
|
+
const child2 = await registerAgent({ agentType: 'executor', parentAgentId: a2.id }, tempDir);
|
|
188
|
+
await updateAgentStatus(child2.id, { status: 'active' }, tempDir);
|
|
189
|
+
// a3 has 0 children — most capacity
|
|
190
|
+
|
|
191
|
+
const result = await getAgentsByCapacity(undefined, tempDir);
|
|
192
|
+
|
|
193
|
+
// Only parent agents (a1, a2, a3) should appear (children are 'active' too
|
|
194
|
+
// but they have no children themselves so they appear with full capacity).
|
|
195
|
+
// Filter to just a1/a2/a3 for ordering assertions:
|
|
196
|
+
const parents = result.filter((c) => [a1.id, a2.id, a3.id].includes(c.agentId));
|
|
197
|
+
|
|
198
|
+
// a3 (5 remaining) >= a2 (4 remaining) >= a1 (3 remaining)
|
|
199
|
+
expect(parents[0]!.remainingCapacity).toBeGreaterThanOrEqual(parents[1]!.remainingCapacity);
|
|
200
|
+
expect(parents[1]!.remainingCapacity).toBeGreaterThanOrEqual(parents[2]!.remainingCapacity);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('filters by agent type', async () => {
|
|
204
|
+
await registerAgent({ agentType: 'executor' }, tempDir).then((a) =>
|
|
205
|
+
updateAgentStatus(a.id, { status: 'active' }, tempDir),
|
|
206
|
+
);
|
|
207
|
+
await registerAgent({ agentType: 'researcher' }, tempDir).then((a) =>
|
|
208
|
+
updateAgentStatus(a.id, { status: 'active' }, tempDir),
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const executors = await getAgentsByCapacity('executor', tempDir);
|
|
212
|
+
expect(executors.every((c) => c.agentType === 'executor')).toBe(true);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('excludes non-active agents', async () => {
|
|
216
|
+
const stopped = await registerAgent({ agentType: 'executor' }, tempDir);
|
|
217
|
+
await updateAgentStatus(stopped.id, { status: 'stopped' }, tempDir);
|
|
218
|
+
|
|
219
|
+
const result = await getAgentsByCapacity(undefined, tempDir);
|
|
220
|
+
expect(result.some((c) => c.agentId === stopped.id)).toBe(false);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// ==========================================================================
|
|
225
|
+
// getAgentSpecializations / updateAgentSpecializations
|
|
226
|
+
// ==========================================================================
|
|
227
|
+
|
|
228
|
+
describe('getAgentSpecializations', () => {
|
|
229
|
+
it('returns empty array for agent with no specializations', async () => {
|
|
230
|
+
const agent = await registerAgent({ agentType: 'executor' }, tempDir);
|
|
231
|
+
const specs = await getAgentSpecializations(agent.id, tempDir);
|
|
232
|
+
expect(specs).toEqual([]);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('returns empty array for non-existent agent', async () => {
|
|
236
|
+
const specs = await getAgentSpecializations('agt_nonexistent_abc123', tempDir);
|
|
237
|
+
expect(specs).toEqual([]);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe('updateAgentSpecializations', () => {
|
|
242
|
+
it('stores and retrieves specializations', async () => {
|
|
243
|
+
const agent = await registerAgent({ agentType: 'architect' }, tempDir);
|
|
244
|
+
await updateAgentSpecializations(agent.id, ['typescript', 'drizzle-orm', 'sqlite'], tempDir);
|
|
245
|
+
|
|
246
|
+
const specs = await getAgentSpecializations(agent.id, tempDir);
|
|
247
|
+
expect(specs).toEqual(['typescript', 'drizzle-orm', 'sqlite']);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('replaces existing specializations', async () => {
|
|
251
|
+
const agent = await registerAgent({ agentType: 'executor' }, tempDir);
|
|
252
|
+
await updateAgentSpecializations(agent.id, ['python'], tempDir);
|
|
253
|
+
await updateAgentSpecializations(agent.id, ['typescript', 'testing'], tempDir);
|
|
254
|
+
|
|
255
|
+
const specs = await getAgentSpecializations(agent.id, tempDir);
|
|
256
|
+
expect(specs).toEqual(['typescript', 'testing']);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('preserves other metadata keys', async () => {
|
|
260
|
+
const agent = await registerAgent(
|
|
261
|
+
{ agentType: 'executor', metadata: { model: 'opus-4', region: 'us-east' } },
|
|
262
|
+
tempDir,
|
|
263
|
+
);
|
|
264
|
+
await updateAgentSpecializations(agent.id, ['research'], tempDir);
|
|
265
|
+
|
|
266
|
+
// Pull raw metadata to verify other keys survive
|
|
267
|
+
const { getDb } = await import('../../store/sqlite.js');
|
|
268
|
+
const { agentInstances: table } = await import('../agent-schema.js');
|
|
269
|
+
const { eq } = await import('drizzle-orm');
|
|
270
|
+
const db = await getDb(tempDir);
|
|
271
|
+
const row = await db
|
|
272
|
+
.select({ metadataJson: table.metadataJson })
|
|
273
|
+
.from(table)
|
|
274
|
+
.where(eq(table.id, agent.id))
|
|
275
|
+
.get();
|
|
276
|
+
|
|
277
|
+
const parsed = JSON.parse(row!.metadataJson ?? '{}') as Record<string, unknown>;
|
|
278
|
+
expect(parsed.model).toBe('opus-4');
|
|
279
|
+
expect(parsed.region).toBe('us-east');
|
|
280
|
+
expect(parsed.specializations).toEqual(['research']);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('returns null for non-existent agent', async () => {
|
|
284
|
+
const result = await updateAgentSpecializations(
|
|
285
|
+
'agt_nonexistent_abc123',
|
|
286
|
+
['typescript'],
|
|
287
|
+
tempDir,
|
|
288
|
+
);
|
|
289
|
+
expect(result).toBeNull();
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// ==========================================================================
|
|
294
|
+
// recordAgentPerformance
|
|
295
|
+
// ==========================================================================
|
|
296
|
+
|
|
297
|
+
describe('recordAgentPerformance', () => {
|
|
298
|
+
it('returns null for non-existent agent', async () => {
|
|
299
|
+
const result = await recordAgentPerformance(
|
|
300
|
+
'agt_nonexistent_abc123',
|
|
301
|
+
{ taskId: 'T001', taskType: 'task', outcome: 'success' },
|
|
302
|
+
tempDir,
|
|
303
|
+
);
|
|
304
|
+
expect(result).toBeNull();
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('records performance and returns a decision ID', async () => {
|
|
308
|
+
const agent = await registerAgent({ agentType: 'executor' }, tempDir);
|
|
309
|
+
await updateAgentStatus(agent.id, { status: 'active' }, tempDir);
|
|
310
|
+
|
|
311
|
+
const decisionId = await recordAgentPerformance(
|
|
312
|
+
agent.id,
|
|
313
|
+
{
|
|
314
|
+
taskId: 'T041',
|
|
315
|
+
taskType: 'task',
|
|
316
|
+
outcome: 'success',
|
|
317
|
+
durationMs: 3000,
|
|
318
|
+
taskLabels: ['implementation'],
|
|
319
|
+
},
|
|
320
|
+
tempDir,
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
// brain.db may not be initialised in the test tmpDir, so we accept
|
|
324
|
+
// either a string ID or null (best-effort recording)
|
|
325
|
+
if (decisionId !== null) {
|
|
326
|
+
expect(typeof decisionId).toBe('string');
|
|
327
|
+
expect(decisionId.length).toBeGreaterThan(0);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('records failure with error metadata', async () => {
|
|
332
|
+
const agent = await registerAgent({ agentType: 'executor', sessionId: undefined }, tempDir);
|
|
333
|
+
await updateAgentStatus(agent.id, { status: 'active' }, tempDir);
|
|
334
|
+
|
|
335
|
+
// Should not throw regardless of brain.db availability
|
|
336
|
+
await expect(
|
|
337
|
+
recordAgentPerformance(
|
|
338
|
+
agent.id,
|
|
339
|
+
{
|
|
340
|
+
taskId: 'T999',
|
|
341
|
+
taskType: 'task',
|
|
342
|
+
outcome: 'failure',
|
|
343
|
+
errorMessage: 'ECONNREFUSED',
|
|
344
|
+
errorType: 'retriable',
|
|
345
|
+
},
|
|
346
|
+
tempDir,
|
|
347
|
+
),
|
|
348
|
+
).resolves.not.toThrow();
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
});
|