opencode-swarm-plugin 0.32.0 → 0.34.0
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/.hive/issues.jsonl +12 -0
- package/.hive/memories.jsonl +255 -1
- package/.turbo/turbo-build.log +9 -10
- package/.turbo/turbo-test.log +343 -337
- package/CHANGELOG.md +358 -0
- package/README.md +152 -179
- package/bin/swarm.test.ts +303 -1
- package/bin/swarm.ts +473 -16
- package/dist/compaction-hook.d.ts +1 -1
- package/dist/compaction-hook.d.ts.map +1 -1
- package/dist/index.d.ts +112 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12380 -131
- package/dist/logger.d.ts +34 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/observability-tools.d.ts +116 -0
- package/dist/observability-tools.d.ts.map +1 -0
- package/dist/plugin.js +12254 -119
- package/dist/skills.d.ts.map +1 -1
- package/dist/swarm-orchestrate.d.ts +105 -0
- package/dist/swarm-orchestrate.d.ts.map +1 -1
- package/dist/swarm-prompts.d.ts +113 -2
- package/dist/swarm-prompts.d.ts.map +1 -1
- package/dist/swarm-research.d.ts +127 -0
- package/dist/swarm-research.d.ts.map +1 -0
- package/dist/swarm-review.d.ts.map +1 -1
- package/dist/swarm.d.ts +73 -1
- package/dist/swarm.d.ts.map +1 -1
- package/evals/compaction-resumption.eval.ts +289 -0
- package/evals/coordinator-behavior.eval.ts +307 -0
- package/evals/fixtures/compaction-cases.ts +350 -0
- package/evals/scorers/compaction-scorers.ts +305 -0
- package/evals/scorers/index.ts +12 -0
- package/examples/plugin-wrapper-template.ts +297 -8
- package/package.json +6 -2
- package/src/compaction-hook.test.ts +617 -1
- package/src/compaction-hook.ts +291 -18
- package/src/index.ts +54 -1
- package/src/logger.test.ts +189 -0
- package/src/logger.ts +135 -0
- package/src/observability-tools.test.ts +346 -0
- package/src/observability-tools.ts +594 -0
- package/src/skills.integration.test.ts +137 -1
- package/src/skills.test.ts +42 -1
- package/src/skills.ts +8 -4
- package/src/swarm-orchestrate.test.ts +123 -0
- package/src/swarm-orchestrate.ts +183 -0
- package/src/swarm-prompts.test.ts +553 -1
- package/src/swarm-prompts.ts +406 -4
- package/src/swarm-research.integration.test.ts +544 -0
- package/src/swarm-research.test.ts +698 -0
- package/src/swarm-research.ts +472 -0
- package/src/swarm-review.test.ts +177 -0
- package/src/swarm-review.ts +12 -47
- package/src/swarm.ts +6 -3
package/src/logger.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger infrastructure using Pino with daily rotation
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Daily log rotation via pino-roll (numeric format: swarm.1log, swarm.2log, etc.)
|
|
6
|
+
* - 14-day retention (14 files max in addition to current file)
|
|
7
|
+
* - Module-specific child loggers with separate log files
|
|
8
|
+
* - Pretty mode for development (SWARM_LOG_PRETTY=1 env var)
|
|
9
|
+
* - Logs to ~/.config/swarm-tools/logs/ by default
|
|
10
|
+
*
|
|
11
|
+
* Note: pino-roll uses numeric rotation (e.g., swarm.1log, swarm.2log) not date-based names.
|
|
12
|
+
* Files rotate daily based on frequency='daily', with a maximum of 14 retained files.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { mkdirSync, existsSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { homedir } from "node:os";
|
|
18
|
+
import type { Logger } from "pino";
|
|
19
|
+
import pino from "pino";
|
|
20
|
+
|
|
21
|
+
const DEFAULT_LOG_DIR = join(homedir(), ".config", "swarm-tools", "logs");
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Creates the log directory if it doesn't exist
|
|
25
|
+
*/
|
|
26
|
+
function ensureLogDir(logDir: string): void {
|
|
27
|
+
if (!existsSync(logDir)) {
|
|
28
|
+
mkdirSync(logDir, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Creates a Pino transport with file rotation
|
|
34
|
+
*
|
|
35
|
+
* @param filename - Log file base name (e.g., "swarm" becomes swarm.1log, swarm.2log, etc.)
|
|
36
|
+
* @param logDir - Directory to store logs
|
|
37
|
+
*/
|
|
38
|
+
function createTransport(
|
|
39
|
+
filename: string,
|
|
40
|
+
logDir: string,
|
|
41
|
+
): pino.TransportTargetOptions {
|
|
42
|
+
const isPretty = process.env.SWARM_LOG_PRETTY === "1";
|
|
43
|
+
|
|
44
|
+
if (isPretty) {
|
|
45
|
+
// Pretty mode - output to console with pino-pretty
|
|
46
|
+
return {
|
|
47
|
+
target: "pino-pretty",
|
|
48
|
+
options: {
|
|
49
|
+
colorize: true,
|
|
50
|
+
translateTime: "HH:MM:ss",
|
|
51
|
+
ignore: "pid,hostname",
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Production mode - file rotation with pino-roll
|
|
57
|
+
// pino-roll format: {file}.{number}{extension}
|
|
58
|
+
// So "swarm" becomes "swarm.1log", "swarm.2log", etc.
|
|
59
|
+
return {
|
|
60
|
+
target: "pino-roll",
|
|
61
|
+
options: {
|
|
62
|
+
file: join(logDir, filename),
|
|
63
|
+
frequency: "daily",
|
|
64
|
+
extension: "log",
|
|
65
|
+
limit: { count: 14 },
|
|
66
|
+
mkdir: true,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const loggerCache = new Map<string, Logger>();
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Gets or creates the main logger instance
|
|
75
|
+
*
|
|
76
|
+
* @param logDir - Optional log directory (defaults to ~/.config/swarm-tools/logs)
|
|
77
|
+
* @returns Pino logger instance
|
|
78
|
+
*/
|
|
79
|
+
export function getLogger(logDir: string = DEFAULT_LOG_DIR): Logger {
|
|
80
|
+
const cacheKey = `swarm:${logDir}`;
|
|
81
|
+
|
|
82
|
+
if (loggerCache.has(cacheKey)) {
|
|
83
|
+
return loggerCache.get(cacheKey)!;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
ensureLogDir(logDir);
|
|
87
|
+
|
|
88
|
+
const logger = pino(
|
|
89
|
+
{
|
|
90
|
+
level: process.env.LOG_LEVEL || "info",
|
|
91
|
+
timestamp: pino.stdTimeFunctions.isoTime,
|
|
92
|
+
},
|
|
93
|
+
pino.transport(createTransport("swarm", logDir)),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
loggerCache.set(cacheKey, logger);
|
|
97
|
+
return logger;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Creates a child logger for a specific module with its own log file
|
|
102
|
+
*
|
|
103
|
+
* @param module - Module name (e.g., "compaction", "cli")
|
|
104
|
+
* @param logDir - Optional log directory (defaults to ~/.config/swarm-tools/logs)
|
|
105
|
+
* @returns Child logger instance
|
|
106
|
+
*/
|
|
107
|
+
export function createChildLogger(
|
|
108
|
+
module: string,
|
|
109
|
+
logDir: string = DEFAULT_LOG_DIR,
|
|
110
|
+
): Logger {
|
|
111
|
+
const cacheKey = `${module}:${logDir}`;
|
|
112
|
+
|
|
113
|
+
if (loggerCache.has(cacheKey)) {
|
|
114
|
+
return loggerCache.get(cacheKey)!;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
ensureLogDir(logDir);
|
|
118
|
+
|
|
119
|
+
const childLogger = pino(
|
|
120
|
+
{
|
|
121
|
+
level: process.env.LOG_LEVEL || "info",
|
|
122
|
+
timestamp: pino.stdTimeFunctions.isoTime,
|
|
123
|
+
},
|
|
124
|
+
pino.transport(createTransport(module, logDir)),
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const logger = childLogger.child({ module });
|
|
128
|
+
loggerCache.set(cacheKey, logger);
|
|
129
|
+
return logger;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Default logger instance for immediate use
|
|
134
|
+
*/
|
|
135
|
+
export const logger = getLogger();
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observability Tools Tests
|
|
3
|
+
*
|
|
4
|
+
* TDD: Write tests first, then implement the tools.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
|
8
|
+
import {
|
|
9
|
+
observabilityTools,
|
|
10
|
+
type SwarmAnalyticsArgs,
|
|
11
|
+
type SwarmQueryArgs,
|
|
12
|
+
type SwarmDiagnoseArgs,
|
|
13
|
+
type SwarmInsightsArgs,
|
|
14
|
+
} from "./observability-tools";
|
|
15
|
+
import type { ToolContext } from "@opencode-ai/plugin";
|
|
16
|
+
import {
|
|
17
|
+
closeSwarmMailLibSQL,
|
|
18
|
+
createInMemorySwarmMailLibSQL,
|
|
19
|
+
initSwarmAgent,
|
|
20
|
+
reserveSwarmFiles,
|
|
21
|
+
sendSwarmMessage,
|
|
22
|
+
type SwarmMailAdapter,
|
|
23
|
+
} from "swarm-mail";
|
|
24
|
+
|
|
25
|
+
describe("observability-tools", () => {
|
|
26
|
+
let swarmMail: SwarmMailAdapter;
|
|
27
|
+
const projectPath = "/test/project";
|
|
28
|
+
const mockContext: ToolContext = { sessionID: "test-session" };
|
|
29
|
+
|
|
30
|
+
beforeAll(async () => {
|
|
31
|
+
// Create in-memory database with test data
|
|
32
|
+
swarmMail = await createInMemorySwarmMailLibSQL(projectPath);
|
|
33
|
+
|
|
34
|
+
// Populate with test events using high-level API
|
|
35
|
+
const agentName = "TestAgent";
|
|
36
|
+
|
|
37
|
+
// Register agent
|
|
38
|
+
await initSwarmAgent({
|
|
39
|
+
projectPath,
|
|
40
|
+
agentName,
|
|
41
|
+
taskDescription: "test-task",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Reserve and release files (for lock contention analytics)
|
|
45
|
+
await reserveSwarmFiles({
|
|
46
|
+
projectPath,
|
|
47
|
+
agentName,
|
|
48
|
+
paths: ["src/test.ts"],
|
|
49
|
+
reason: "test-reason",
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Send a message (for message latency analytics)
|
|
53
|
+
await sendSwarmMessage({
|
|
54
|
+
projectPath,
|
|
55
|
+
fromAgent: agentName,
|
|
56
|
+
toAgents: ["Agent2"],
|
|
57
|
+
subject: "test-subject",
|
|
58
|
+
body: "test-body",
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Note: subtask outcomes are recorded via a different API
|
|
62
|
+
// For now, we'll test with the events we have
|
|
63
|
+
// The important thing is that the tools can execute queries
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
afterAll(async () => {
|
|
67
|
+
await closeSwarmMailLibSQL(projectPath);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("swarm_analytics", () => {
|
|
71
|
+
const tool = observabilityTools.swarm_analytics;
|
|
72
|
+
|
|
73
|
+
test("is defined with correct schema", () => {
|
|
74
|
+
expect(tool).toBeDefined();
|
|
75
|
+
expect(tool.description).toBeTruthy();
|
|
76
|
+
expect(tool.args).toBeDefined();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("returns failed-decompositions data", async () => {
|
|
80
|
+
const args: SwarmAnalyticsArgs = {
|
|
81
|
+
query: "failed-decompositions",
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const result = await tool.execute(args, mockContext);
|
|
85
|
+
expect(result).toBeTruthy();
|
|
86
|
+
|
|
87
|
+
const parsed = JSON.parse(result);
|
|
88
|
+
expect(parsed).toHaveProperty("results");
|
|
89
|
+
expect(Array.isArray(parsed.results)).toBe(true);
|
|
90
|
+
// Empty data is fine - we're testing tool execution
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("returns strategy-success-rates data", async () => {
|
|
94
|
+
const args: SwarmAnalyticsArgs = {
|
|
95
|
+
query: "strategy-success-rates",
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const result = await tool.execute(args, mockContext);
|
|
99
|
+
const parsed = JSON.parse(result);
|
|
100
|
+
expect(parsed).toHaveProperty("results");
|
|
101
|
+
expect(Array.isArray(parsed.results)).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("returns agent-activity data", async () => {
|
|
105
|
+
const args: SwarmAnalyticsArgs = {
|
|
106
|
+
query: "agent-activity",
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const result = await tool.execute(args, mockContext);
|
|
110
|
+
const parsed = JSON.parse(result);
|
|
111
|
+
expect(parsed).toHaveProperty("results");
|
|
112
|
+
expect(Array.isArray(parsed.results)).toBe(true);
|
|
113
|
+
// Should have at least our TestAgent
|
|
114
|
+
expect(parsed.results.length).toBeGreaterThanOrEqual(1);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("supports summary format", async () => {
|
|
118
|
+
const args: SwarmAnalyticsArgs = {
|
|
119
|
+
query: "agent-activity",
|
|
120
|
+
format: "summary",
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const result = await tool.execute(args, mockContext);
|
|
124
|
+
expect(result).toBeTruthy();
|
|
125
|
+
expect(typeof result).toBe("string");
|
|
126
|
+
// Summary should be concise (<500 chars)
|
|
127
|
+
expect(result.length).toBeLessThan(500);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("supports time filtering with since", async () => {
|
|
131
|
+
const args: SwarmAnalyticsArgs = {
|
|
132
|
+
query: "agent-activity",
|
|
133
|
+
since: "24h",
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const result = await tool.execute(args, mockContext);
|
|
137
|
+
const parsed = JSON.parse(result);
|
|
138
|
+
expect(parsed).toHaveProperty("results");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("returns error for invalid query type", async () => {
|
|
142
|
+
const args = {
|
|
143
|
+
query: "invalid-query",
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const result = await tool.execute(args as any, mockContext);
|
|
147
|
+
const parsed = JSON.parse(result);
|
|
148
|
+
expect(parsed).toHaveProperty("error");
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("swarm_query", () => {
|
|
153
|
+
const tool = observabilityTools.swarm_query;
|
|
154
|
+
|
|
155
|
+
test("is defined with correct schema", () => {
|
|
156
|
+
expect(tool).toBeDefined();
|
|
157
|
+
expect(tool.description).toBeTruthy();
|
|
158
|
+
expect(tool.args).toBeDefined();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("executes raw SQL queries", async () => {
|
|
162
|
+
const args: SwarmQueryArgs = {
|
|
163
|
+
sql: "SELECT type, COUNT(*) as count FROM events GROUP BY type",
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const result = await tool.execute(args, mockContext);
|
|
167
|
+
expect(result).toBeTruthy();
|
|
168
|
+
|
|
169
|
+
const parsed = JSON.parse(result);
|
|
170
|
+
// May have errors in test environment - that's ok
|
|
171
|
+
if (!parsed.error) {
|
|
172
|
+
// Should have count and results even if empty
|
|
173
|
+
expect(parsed).toHaveProperty("count");
|
|
174
|
+
expect(parsed).toHaveProperty("results");
|
|
175
|
+
expect(Array.isArray(parsed.results)).toBe(true);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("limits results to max 50 rows", async () => {
|
|
180
|
+
const args: SwarmQueryArgs = {
|
|
181
|
+
sql: "SELECT * FROM events LIMIT 100", // Try to fetch 100
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const result = await tool.execute(args, mockContext);
|
|
185
|
+
const parsed = JSON.parse(result);
|
|
186
|
+
// Should be capped at 50 (or less if there's less data)
|
|
187
|
+
// May return error if database issues - that's ok for this test
|
|
188
|
+
if (parsed.error) {
|
|
189
|
+
expect(parsed).toHaveProperty("error");
|
|
190
|
+
} else {
|
|
191
|
+
expect(parsed).toHaveProperty("results");
|
|
192
|
+
expect(parsed.results.length).toBeLessThanOrEqual(50);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("supports table format", async () => {
|
|
197
|
+
const args: SwarmQueryArgs = {
|
|
198
|
+
sql: "SELECT type FROM events LIMIT 3",
|
|
199
|
+
format: "table",
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const result = await tool.execute(args, mockContext);
|
|
203
|
+
expect(typeof result).toBe("string");
|
|
204
|
+
// Table format returns string (even if "No results" for empty data)
|
|
205
|
+
expect(result.length).toBeGreaterThan(0);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("returns error for invalid SQL", async () => {
|
|
209
|
+
const args: SwarmQueryArgs = {
|
|
210
|
+
sql: "SELECT * FROM nonexistent_table",
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const result = await tool.execute(args, mockContext);
|
|
214
|
+
const parsed = JSON.parse(result);
|
|
215
|
+
expect(parsed).toHaveProperty("error");
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe("swarm_diagnose", () => {
|
|
220
|
+
const tool = observabilityTools.swarm_diagnose;
|
|
221
|
+
|
|
222
|
+
test("is defined with correct schema", () => {
|
|
223
|
+
expect(tool).toBeDefined();
|
|
224
|
+
expect(tool.description).toBeTruthy();
|
|
225
|
+
expect(tool.args).toBeDefined();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("diagnoses issues for a specific epic", async () => {
|
|
229
|
+
const args: SwarmDiagnoseArgs = {
|
|
230
|
+
epic_id: "epic-123",
|
|
231
|
+
include: ["blockers", "errors"],
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const result = await tool.execute(args, mockContext);
|
|
235
|
+
expect(result).toBeTruthy();
|
|
236
|
+
|
|
237
|
+
const parsed = JSON.parse(result);
|
|
238
|
+
expect(parsed).toHaveProperty("epic_id");
|
|
239
|
+
expect(parsed).toHaveProperty("diagnosis");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("returns structured diagnosis with suggestions", async () => {
|
|
243
|
+
const args: SwarmDiagnoseArgs = {
|
|
244
|
+
bead_id: "task-1",
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const result = await tool.execute(args, mockContext);
|
|
248
|
+
const parsed = JSON.parse(result);
|
|
249
|
+
expect(parsed).toHaveProperty("diagnosis");
|
|
250
|
+
expect(Array.isArray(parsed.diagnosis)).toBe(true);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("includes timeline when requested", async () => {
|
|
254
|
+
const args: SwarmDiagnoseArgs = {
|
|
255
|
+
bead_id: "task-1",
|
|
256
|
+
include: ["timeline"],
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const result = await tool.execute(args, mockContext);
|
|
260
|
+
const parsed = JSON.parse(result);
|
|
261
|
+
expect(parsed).toHaveProperty("timeline");
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe("swarm_insights", () => {
|
|
266
|
+
const tool = observabilityTools.swarm_insights;
|
|
267
|
+
|
|
268
|
+
test("is defined with correct schema", () => {
|
|
269
|
+
expect(tool).toBeDefined();
|
|
270
|
+
expect(tool.description).toBeTruthy();
|
|
271
|
+
expect(tool.args).toBeDefined();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("generates insights for recent activity", async () => {
|
|
275
|
+
const args: SwarmInsightsArgs = {
|
|
276
|
+
scope: "recent",
|
|
277
|
+
metrics: ["success_rate", "avg_duration"],
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const result = await tool.execute(args, mockContext);
|
|
281
|
+
expect(result).toBeTruthy();
|
|
282
|
+
|
|
283
|
+
const parsed = JSON.parse(result);
|
|
284
|
+
expect(parsed).toHaveProperty("insights");
|
|
285
|
+
expect(Array.isArray(parsed.insights)).toBe(true);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("generates insights for specific epic", async () => {
|
|
289
|
+
const args: SwarmInsightsArgs = {
|
|
290
|
+
scope: "epic",
|
|
291
|
+
epic_id: "epic-123",
|
|
292
|
+
metrics: ["conflict_rate", "retry_rate"],
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const result = await tool.execute(args, mockContext);
|
|
296
|
+
const parsed = JSON.parse(result);
|
|
297
|
+
expect(parsed).toHaveProperty("epic_id", "epic-123");
|
|
298
|
+
expect(parsed).toHaveProperty("insights");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("returns error when epic_id missing for epic scope", async () => {
|
|
302
|
+
const args: SwarmInsightsArgs = {
|
|
303
|
+
scope: "epic",
|
|
304
|
+
metrics: ["success_rate"],
|
|
305
|
+
// Missing epic_id
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const result = await tool.execute(args, mockContext);
|
|
309
|
+
const parsed = JSON.parse(result);
|
|
310
|
+
expect(parsed).toHaveProperty("error");
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
describe("integration with swarm-mail analytics", () => {
|
|
315
|
+
test("all query types are supported", async () => {
|
|
316
|
+
const queryTypes = [
|
|
317
|
+
"failed-decompositions",
|
|
318
|
+
"strategy-success-rates",
|
|
319
|
+
"lock-contention",
|
|
320
|
+
"agent-activity",
|
|
321
|
+
"message-latency",
|
|
322
|
+
"scope-violations",
|
|
323
|
+
"task-duration",
|
|
324
|
+
"checkpoint-frequency",
|
|
325
|
+
"recovery-success",
|
|
326
|
+
"human-feedback",
|
|
327
|
+
];
|
|
328
|
+
|
|
329
|
+
for (const queryType of queryTypes) {
|
|
330
|
+
const tool = observabilityTools.swarm_analytics;
|
|
331
|
+
const args: SwarmAnalyticsArgs = {
|
|
332
|
+
query: queryType as SwarmAnalyticsArgs["query"],
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const result = await tool.execute(args, mockContext);
|
|
336
|
+
const parsed = JSON.parse(result);
|
|
337
|
+
|
|
338
|
+
// Should return results property (even if empty array)
|
|
339
|
+
// May have errors in test environment - that's ok
|
|
340
|
+
if (!parsed.error) {
|
|
341
|
+
expect(parsed).toHaveProperty("results");
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
});
|