@desplega.ai/agent-swarm 1.79.4 → 1.80.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/openapi.json +496 -32
- package/package.json +14 -6
- package/src/artifact-sdk/server.ts +2 -1
- package/src/be/db.ts +102 -31
- package/src/be/migrations/063_cost_context_schema_relax.sql +133 -0
- package/src/be/migrations/064_scripts.sql +39 -0
- package/src/be/migrations/065_script_embeddings.sql +7 -0
- package/src/be/pricing-normalize.ts +81 -0
- package/src/be/scripts/db.ts +391 -0
- package/src/be/scripts/embeddings.ts +231 -0
- package/src/be/scripts/maintenance.ts +9 -0
- package/src/be/scripts/typecheck.ts +193 -0
- package/src/be/seed-pricing.ts +293 -0
- package/src/cli.tsx +22 -5
- package/src/commands/artifact.ts +3 -2
- package/src/commands/claude-managed-setup.ts +21 -4
- package/src/commands/codex-login.ts +5 -3
- package/src/commands/onboard.tsx +2 -1
- package/src/commands/runner.ts +663 -246
- package/src/commands/setup.tsx +5 -3
- package/src/hooks/hook.ts +4 -3
- package/src/http/context.ts +6 -2
- package/src/http/index.ts +126 -68
- package/src/http/memory.ts +28 -0
- package/src/http/openapi.ts +1 -0
- package/src/http/page-proxy.ts +2 -1
- package/src/http/route-def.ts +1 -0
- package/src/http/schedules.ts +37 -0
- package/src/http/scripts.ts +381 -0
- package/src/http/session-data.ts +74 -23
- package/src/linear/outbound.ts +9 -2
- package/src/otel-impl.ts +200 -0
- package/src/otel.ts +132 -0
- package/src/providers/claude-adapter.ts +52 -6
- package/src/providers/claude-managed-adapter.ts +43 -17
- package/src/providers/claude-managed-pricing.ts +34 -0
- package/src/providers/codex-adapter.ts +38 -27
- package/src/providers/codex-models.ts +22 -3
- package/src/providers/devin-adapter.ts +11 -0
- package/src/providers/opencode-adapter.ts +31 -7
- package/src/providers/pi-mono-adapter.ts +39 -7
- package/src/providers/pricing-sources.md +52 -0
- package/src/providers/swarm-events-shared.ts +8 -4
- package/src/providers/types.ts +33 -10
- package/src/scripts-runtime/ctx.ts +23 -0
- package/src/scripts-runtime/eval-harness.ts +39 -0
- package/src/scripts-runtime/executors/native.ts +229 -0
- package/src/scripts-runtime/executors/registry.ts +16 -0
- package/src/scripts-runtime/executors/types.ts +63 -0
- package/src/scripts-runtime/extract-signature.ts +81 -0
- package/src/scripts-runtime/import-allowlist.ts +109 -0
- package/src/scripts-runtime/loader.ts +96 -0
- package/src/scripts-runtime/redacted.ts +48 -0
- package/src/scripts-runtime/sdk-allowlist.ts +29 -0
- package/src/scripts-runtime/stdlib/fetch.ts +46 -0
- package/src/scripts-runtime/stdlib/glob.ts +8 -0
- package/src/scripts-runtime/stdlib/grep.ts +34 -0
- package/src/scripts-runtime/stdlib/index.ts +16 -0
- package/src/scripts-runtime/stdlib/table.ts +17 -0
- package/src/scripts-runtime/swarm-config.ts +35 -0
- package/src/scripts-runtime/swarm-sdk.ts +197 -0
- package/src/scripts-runtime/types/stdlib.d.ts +104 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +86 -0
- package/src/server.ts +18 -0
- package/src/tests/api-key.test.ts +33 -0
- package/src/tests/claude-managed-adapter.test.ts +17 -3
- package/src/tests/claude-managed-setup.test.ts +10 -1
- package/src/tests/codex-adapter.test.ts +20 -19
- package/src/tests/codex-login.test.ts +1 -1
- package/src/tests/context-snapshot.test.ts +2 -2
- package/src/tests/context-window.test.ts +65 -1
- package/src/tests/devin-adapter.test.ts +2 -0
- package/src/tests/http/context-routes.test.ts +161 -0
- package/src/tests/linear-outbound-sync.test.ts +109 -0
- package/src/tests/mcp-tools.test.ts +69 -0
- package/src/tests/migration-063-schema-relax.test.ts +109 -0
- package/src/tests/opencode-adapter.test.ts +146 -1
- package/src/tests/otel-impl-secret-scrubbing.test.ts +33 -0
- package/src/tests/pages-view-count.test.ts +30 -5
- package/src/tests/providers/codex-cost.test.ts +18 -0
- package/src/tests/providers/opencode-cost.test.ts +74 -0
- package/src/tests/providers/pi-cost.test.ts +128 -0
- package/src/tests/redacted.test.ts +29 -0
- package/src/tests/runner-tool-spans.test.ts +268 -0
- package/src/tests/script-executor-conformance.test.ts +142 -0
- package/src/tests/script-executor-registry.test.ts +17 -0
- package/src/tests/scripts-db.test.ts +329 -0
- package/src/tests/scripts-embeddings.test.ts +291 -0
- package/src/tests/scripts-extract-signature.test.ts +47 -0
- package/src/tests/scripts-http.test.ts +350 -0
- package/src/tests/scripts-import-allowlist.test.ts +55 -0
- package/src/tests/scripts-mcp-e2e.test.ts +269 -0
- package/src/tests/scripts-runtime-secret-egress.test.ts +44 -0
- package/src/tests/scripts-runtime.test.ts +289 -0
- package/src/tests/sdk-allowlist.test.ts +59 -0
- package/src/tests/secret-scrubber.test.ts +54 -1
- package/src/tests/session-costs-codex-recompute.test.ts +35 -22
- package/src/tests/session-costs-model-key-normalize.test.ts +271 -0
- package/src/tests/session-costs-recompute-all-providers.test.ts +170 -0
- package/src/tests/store-progress-cost.test.ts +6 -1
- package/src/tests/swarm-config.test.ts +38 -0
- package/src/tests/tool-annotations.test.ts +2 -2
- package/src/tests/tool-call-progress.test.ts +30 -0
- package/src/tests/workflow-e2e.test.ts +218 -0
- package/src/tests/workflow-executors.test.ts +32 -2
- package/src/tests/workflow-input-redaction.test.ts +232 -0
- package/src/tests/workflow-swarm-script.test.ts +273 -0
- package/src/tools/memory-rate.ts +2 -1
- package/src/tools/script-common.ts +88 -0
- package/src/tools/script-delete.ts +35 -0
- package/src/tools/script-query-types.ts +37 -0
- package/src/tools/script-run.ts +43 -0
- package/src/tools/script-search.ts +32 -0
- package/src/tools/script-upsert.ts +43 -0
- package/src/tools/store-progress.ts +16 -60
- package/src/tools/tool-config.ts +7 -0
- package/src/tools/utils.ts +65 -12
- package/src/types.ts +122 -10
- package/src/utils/api-key.ts +28 -0
- package/src/utils/context-window.ts +104 -4
- package/src/utils/page-session.ts +8 -6
- package/src/utils/secret-scrubber.ts +29 -1
- package/src/workflows/engine.ts +12 -4
- package/src/workflows/executors/index.ts +1 -0
- package/src/workflows/executors/registry.ts +2 -0
- package/src/workflows/executors/script.ts +12 -1
- package/src/workflows/executors/swarm-script.ts +170 -0
- package/src/workflows/input.ts +65 -0
- package/src/workflows/recovery.ts +31 -3
- package/src/workflows/resume.ts +43 -5
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// Phase 2 fix — adapter-emitted model ids carry harness-specific routing
|
|
2
|
+
// prefixes (`openrouter/`, `github-copilot/`, …) that the pricing seed does
|
|
3
|
+
// not. Before the fix every opencode + pi-via-copilot run fell through to
|
|
4
|
+
// `costSource='unpriced'` even when a seeded rate row existed. This suite
|
|
5
|
+
// regresses the drift cases observed in real-harness E2E.
|
|
6
|
+
|
|
7
|
+
import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
|
|
8
|
+
import { unlink } from "node:fs/promises";
|
|
9
|
+
import {
|
|
10
|
+
createServer as createHttpServer,
|
|
11
|
+
type IncomingMessage,
|
|
12
|
+
type Server,
|
|
13
|
+
type ServerResponse,
|
|
14
|
+
} from "node:http";
|
|
15
|
+
import { closeDb, createAgent, getDb, initDb, insertPricingRow } from "../be/db";
|
|
16
|
+
import { normalizeModelKey } from "../be/pricing-normalize";
|
|
17
|
+
import { handleCore } from "../http/core";
|
|
18
|
+
import { handleSessionData } from "../http/session-data";
|
|
19
|
+
import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
20
|
+
|
|
21
|
+
const TEST_DB_PATH = "./test-model-key-normalize.sqlite";
|
|
22
|
+
const API_KEY = "test-model-key-normalize";
|
|
23
|
+
|
|
24
|
+
async function removeDbFiles(path: string): Promise<void> {
|
|
25
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
26
|
+
try {
|
|
27
|
+
await unlink(path + suffix);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function listen(server: Server): Promise<number> {
|
|
35
|
+
await new Promise<void>((resolve) => server.listen(0, resolve));
|
|
36
|
+
const addr = server.address();
|
|
37
|
+
if (!addr || typeof addr === "string") throw new Error("no port");
|
|
38
|
+
return addr.port;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function createTestServer(apiKey: string): Server {
|
|
42
|
+
return createHttpServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
43
|
+
const myAgentId = req.headers["x-agent-id"] as string | undefined;
|
|
44
|
+
const handled = await handleCore(req, res, myAgentId, apiKey);
|
|
45
|
+
if (handled) return;
|
|
46
|
+
const pathSegments = getPathSegments(req.url || "");
|
|
47
|
+
const queryParams = parseQueryParams(req.url || "");
|
|
48
|
+
const ok = await handleSessionData(req, res, pathSegments, queryParams, myAgentId);
|
|
49
|
+
if (!ok) {
|
|
50
|
+
res.writeHead(404);
|
|
51
|
+
res.end("Not Found");
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let server: Server;
|
|
57
|
+
let port: number;
|
|
58
|
+
let testAgent: { id: string };
|
|
59
|
+
|
|
60
|
+
beforeAll(async () => {
|
|
61
|
+
await removeDbFiles(TEST_DB_PATH);
|
|
62
|
+
initDb(TEST_DB_PATH);
|
|
63
|
+
testAgent = createAgent({ name: "model-key-normalize-test", isLead: false, status: "idle" });
|
|
64
|
+
server = createTestServer(API_KEY);
|
|
65
|
+
port = await listen(server);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
afterAll(async () => {
|
|
69
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
70
|
+
closeDb();
|
|
71
|
+
await removeDbFiles(TEST_DB_PATH);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
afterEach(() => {
|
|
75
|
+
const db = getDb();
|
|
76
|
+
db.prepare("DELETE FROM session_costs").run();
|
|
77
|
+
db.prepare("DELETE FROM pricing WHERE effective_from > 0").run();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
function authedFetch(path: string, init: RequestInit = {}): Promise<Response> {
|
|
81
|
+
return fetch(`http://localhost:${port}${path}`, {
|
|
82
|
+
...init,
|
|
83
|
+
headers: {
|
|
84
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
85
|
+
"Content-Type": "application/json",
|
|
86
|
+
...(init.headers ?? {}),
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface CostResponse {
|
|
92
|
+
success: boolean;
|
|
93
|
+
cost: {
|
|
94
|
+
totalCostUsd: number;
|
|
95
|
+
model: string;
|
|
96
|
+
costSource: "harness" | "pricing-table" | "unpriced";
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
describe("normalizeModelKey()", () => {
|
|
101
|
+
test("strips opencode routing prefix `openrouter/`", () => {
|
|
102
|
+
expect(normalizeModelKey("opencode", "openrouter/anthropic/claude-sonnet-4.5")).toBe(
|
|
103
|
+
"anthropic/claude-sonnet-4.5",
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("strips pi routing prefix `github-copilot/`", () => {
|
|
108
|
+
expect(normalizeModelKey("pi", "github-copilot/gpt-5.4")).toBe("gpt-5.4");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("strips pi routing prefix `openrouter/`", () => {
|
|
112
|
+
expect(normalizeModelKey("pi", "openrouter/anthropic/claude-sonnet-4.5")).toBe(
|
|
113
|
+
"anthropic/claude-sonnet-4.5",
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("strips pi routing prefix `openrouter/` for deepseek (Phase 3 fix regression)", () => {
|
|
118
|
+
// The exact case from today's E2E (2026-05-18): pi-mono emits
|
|
119
|
+
// `openrouter/deepseek/deepseek-v4-flash`, the pricing seed keys the row
|
|
120
|
+
// under bare `deepseek/deepseek-v4-flash`. Drift collapsed before this
|
|
121
|
+
// assertion exists; keep it as an explicit regression guard.
|
|
122
|
+
expect(normalizeModelKey("pi", "openrouter/deepseek/deepseek-v4-flash")).toBe(
|
|
123
|
+
"deepseek/deepseek-v4-flash",
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("strips opencode routing prefix `openrouter/` for deepseek (Phase 3 fix regression)", () => {
|
|
128
|
+
// Same model, different harness — opencode-adapter wraps the underlying
|
|
129
|
+
// model id under the same `openrouter/` proxy prefix.
|
|
130
|
+
expect(normalizeModelKey("opencode", "openrouter/deepseek/deepseek-v4-flash")).toBe(
|
|
131
|
+
"deepseek/deepseek-v4-flash",
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("is a no-op for canonical claude ids", () => {
|
|
136
|
+
expect(normalizeModelKey("claude", "claude-opus-4-7")).toBe("claude-opus-4-7");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("is idempotent", () => {
|
|
140
|
+
const once = normalizeModelKey("opencode", "openrouter/anthropic/claude-sonnet-4.5");
|
|
141
|
+
const twice = normalizeModelKey("opencode", once);
|
|
142
|
+
expect(twice).toBe(once);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("lowercases mixed-case input", () => {
|
|
146
|
+
expect(normalizeModelKey("opencode", "OpenRouter/Anthropic/Claude-Sonnet-4.5")).toBe(
|
|
147
|
+
"anthropic/claude-sonnet-4.5",
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("Phase 2 fix — POST /api/session-costs normalizes routing prefixes", () => {
|
|
153
|
+
test("opencode `openrouter/anthropic/claude-sonnet-4.5` resolves the seeded `anthropic/claude-sonnet-4.5` row", async () => {
|
|
154
|
+
// Seed mirrors what models.dev → seed-pricing.ts produces for the
|
|
155
|
+
// openrouter section: bare `anthropic/<id>` under the `opencode` provider.
|
|
156
|
+
insertPricingRow({
|
|
157
|
+
provider: "opencode",
|
|
158
|
+
model: "anthropic/claude-sonnet-4.5",
|
|
159
|
+
tokenClass: "input",
|
|
160
|
+
effectiveFrom: 1,
|
|
161
|
+
pricePerMillionUsd: 3,
|
|
162
|
+
});
|
|
163
|
+
insertPricingRow({
|
|
164
|
+
provider: "opencode",
|
|
165
|
+
model: "anthropic/claude-sonnet-4.5",
|
|
166
|
+
tokenClass: "output",
|
|
167
|
+
effectiveFrom: 1,
|
|
168
|
+
pricePerMillionUsd: 15,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const res = await authedFetch(`/api/session-costs`, {
|
|
172
|
+
method: "POST",
|
|
173
|
+
body: JSON.stringify({
|
|
174
|
+
sessionId: "opencode-normalize-1",
|
|
175
|
+
agentId: testAgent.id,
|
|
176
|
+
totalCostUsd: 0.42, // harness-reported, expected to be overwritten
|
|
177
|
+
inputTokens: 1_000_000,
|
|
178
|
+
outputTokens: 100_000,
|
|
179
|
+
// The exact string the opencode adapter emits today.
|
|
180
|
+
model: "openrouter/anthropic/claude-sonnet-4.5",
|
|
181
|
+
provider: "opencode",
|
|
182
|
+
durationMs: 1_000,
|
|
183
|
+
numTurns: 1,
|
|
184
|
+
}),
|
|
185
|
+
});
|
|
186
|
+
expect(res.status).toBe(201);
|
|
187
|
+
const body = (await res.json()) as CostResponse;
|
|
188
|
+
// 1M @ $3 + 100k @ $15 = $3 + $1.50 = $4.50
|
|
189
|
+
expect(body.cost.costSource).toBe("pricing-table");
|
|
190
|
+
expect(body.cost.totalCostUsd).toBeCloseTo(4.5, 5);
|
|
191
|
+
// Original adapter-emitted string is preserved on the row for debugging.
|
|
192
|
+
expect(body.cost.model).toBe("openrouter/anthropic/claude-sonnet-4.5");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("pi `github-copilot/gpt-5.4` resolves the seeded bare `gpt-5.4` row", async () => {
|
|
196
|
+
insertPricingRow({
|
|
197
|
+
provider: "pi",
|
|
198
|
+
model: "gpt-5.4",
|
|
199
|
+
tokenClass: "input",
|
|
200
|
+
effectiveFrom: 1,
|
|
201
|
+
pricePerMillionUsd: 2,
|
|
202
|
+
});
|
|
203
|
+
insertPricingRow({
|
|
204
|
+
provider: "pi",
|
|
205
|
+
model: "gpt-5.4",
|
|
206
|
+
tokenClass: "output",
|
|
207
|
+
effectiveFrom: 1,
|
|
208
|
+
pricePerMillionUsd: 8,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const res = await authedFetch(`/api/session-costs`, {
|
|
212
|
+
method: "POST",
|
|
213
|
+
body: JSON.stringify({
|
|
214
|
+
sessionId: "pi-copilot-normalize-1",
|
|
215
|
+
agentId: testAgent.id,
|
|
216
|
+
totalCostUsd: 9.99,
|
|
217
|
+
inputTokens: 500_000,
|
|
218
|
+
outputTokens: 250_000,
|
|
219
|
+
model: "github-copilot/gpt-5.4",
|
|
220
|
+
provider: "pi",
|
|
221
|
+
durationMs: 1_000,
|
|
222
|
+
numTurns: 1,
|
|
223
|
+
}),
|
|
224
|
+
});
|
|
225
|
+
expect(res.status).toBe(201);
|
|
226
|
+
const body = (await res.json()) as CostResponse;
|
|
227
|
+
// 500k @ $2 + 250k @ $8 = $1 + $2 = $3
|
|
228
|
+
expect(body.cost.costSource).toBe("pricing-table");
|
|
229
|
+
expect(body.cost.totalCostUsd).toBeCloseTo(3.0, 5);
|
|
230
|
+
expect(body.cost.model).toBe("github-copilot/gpt-5.4");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("claude `claude-opus-4-7` (no prefix) still resolves — regression guard", async () => {
|
|
234
|
+
// The bug report flagged claude-adapter as already-working. Make sure
|
|
235
|
+
// we did not regress its bare-id lookup.
|
|
236
|
+
insertPricingRow({
|
|
237
|
+
provider: "claude",
|
|
238
|
+
model: "claude-opus-4-7",
|
|
239
|
+
tokenClass: "input",
|
|
240
|
+
effectiveFrom: 1,
|
|
241
|
+
pricePerMillionUsd: 15,
|
|
242
|
+
});
|
|
243
|
+
insertPricingRow({
|
|
244
|
+
provider: "claude",
|
|
245
|
+
model: "claude-opus-4-7",
|
|
246
|
+
tokenClass: "output",
|
|
247
|
+
effectiveFrom: 1,
|
|
248
|
+
pricePerMillionUsd: 75,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const res = await authedFetch(`/api/session-costs`, {
|
|
252
|
+
method: "POST",
|
|
253
|
+
body: JSON.stringify({
|
|
254
|
+
sessionId: "claude-bare-1",
|
|
255
|
+
agentId: testAgent.id,
|
|
256
|
+
totalCostUsd: 1.23,
|
|
257
|
+
inputTokens: 1_000_000,
|
|
258
|
+
outputTokens: 100_000,
|
|
259
|
+
model: "claude-opus-4-7",
|
|
260
|
+
provider: "claude",
|
|
261
|
+
durationMs: 1_000,
|
|
262
|
+
numTurns: 1,
|
|
263
|
+
}),
|
|
264
|
+
});
|
|
265
|
+
expect(res.status).toBe(201);
|
|
266
|
+
const body = (await res.json()) as CostResponse;
|
|
267
|
+
// 1M @ $15 + 100k @ $75 = $15 + $7.50 = $22.50
|
|
268
|
+
expect(body.cost.costSource).toBe("pricing-table");
|
|
269
|
+
expect(body.cost.totalCostUsd).toBeCloseTo(22.5, 5);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// Phase 2: POST /api/session-costs recompute fires for every provider with
|
|
2
|
+
// seeded pricing rows — not just codex. Unknown (provider, model) pairs are
|
|
3
|
+
// tagged `costSource='unpriced'`.
|
|
4
|
+
|
|
5
|
+
import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
|
|
6
|
+
import { unlink } from "node:fs/promises";
|
|
7
|
+
import {
|
|
8
|
+
createServer as createHttpServer,
|
|
9
|
+
type IncomingMessage,
|
|
10
|
+
type Server,
|
|
11
|
+
type ServerResponse,
|
|
12
|
+
} from "node:http";
|
|
13
|
+
import { closeDb, createAgent, getDb, initDb, insertPricingRow } from "../be/db";
|
|
14
|
+
import { handleCore } from "../http/core";
|
|
15
|
+
import { handleSessionData } from "../http/session-data";
|
|
16
|
+
import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
17
|
+
|
|
18
|
+
const TEST_DB_PATH = "./test-recompute-all-providers.sqlite";
|
|
19
|
+
const API_KEY = "test-recompute-all";
|
|
20
|
+
|
|
21
|
+
async function removeDbFiles(path: string): Promise<void> {
|
|
22
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
23
|
+
try {
|
|
24
|
+
await unlink(path + suffix);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function listen(server: Server): Promise<number> {
|
|
32
|
+
await new Promise<void>((resolve) => server.listen(0, resolve));
|
|
33
|
+
const addr = server.address();
|
|
34
|
+
if (!addr || typeof addr === "string") throw new Error("no port");
|
|
35
|
+
return addr.port;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createTestServer(apiKey: string): Server {
|
|
39
|
+
return createHttpServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
40
|
+
const myAgentId = req.headers["x-agent-id"] as string | undefined;
|
|
41
|
+
const handled = await handleCore(req, res, myAgentId, apiKey);
|
|
42
|
+
if (handled) return;
|
|
43
|
+
const pathSegments = getPathSegments(req.url || "");
|
|
44
|
+
const queryParams = parseQueryParams(req.url || "");
|
|
45
|
+
const ok = await handleSessionData(req, res, pathSegments, queryParams, myAgentId);
|
|
46
|
+
if (!ok) {
|
|
47
|
+
res.writeHead(404);
|
|
48
|
+
res.end("Not Found");
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let server: Server;
|
|
54
|
+
let port: number;
|
|
55
|
+
let testAgent: { id: string };
|
|
56
|
+
|
|
57
|
+
beforeAll(async () => {
|
|
58
|
+
await removeDbFiles(TEST_DB_PATH);
|
|
59
|
+
initDb(TEST_DB_PATH);
|
|
60
|
+
testAgent = createAgent({ name: "recompute-all-test", isLead: false, status: "idle" });
|
|
61
|
+
server = createTestServer(API_KEY);
|
|
62
|
+
port = await listen(server);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
afterAll(async () => {
|
|
66
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
67
|
+
closeDb();
|
|
68
|
+
await removeDbFiles(TEST_DB_PATH);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
afterEach(() => {
|
|
72
|
+
const db = getDb();
|
|
73
|
+
db.prepare("DELETE FROM session_costs").run();
|
|
74
|
+
// Wipe everything we explicitly inserted (effective_from > 0); leave the
|
|
75
|
+
// migration-046 codex seeds alone.
|
|
76
|
+
db.prepare("DELETE FROM pricing WHERE effective_from > 0").run();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
function authedFetch(path: string, init: RequestInit = {}): Promise<Response> {
|
|
80
|
+
return fetch(`http://localhost:${port}${path}`, {
|
|
81
|
+
...init,
|
|
82
|
+
headers: {
|
|
83
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
84
|
+
"Content-Type": "application/json",
|
|
85
|
+
...(init.headers ?? {}),
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface CostResponse {
|
|
91
|
+
success: boolean;
|
|
92
|
+
cost: {
|
|
93
|
+
totalCostUsd: number;
|
|
94
|
+
costSource: "harness" | "pricing-table" | "unpriced";
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function seedTwoClassRates(provider: string, model: string, inputRate = 1, outputRate = 10) {
|
|
99
|
+
insertPricingRow({
|
|
100
|
+
provider: provider as Parameters<typeof insertPricingRow>[0]["provider"],
|
|
101
|
+
model,
|
|
102
|
+
tokenClass: "input",
|
|
103
|
+
effectiveFrom: 1,
|
|
104
|
+
pricePerMillionUsd: inputRate,
|
|
105
|
+
});
|
|
106
|
+
insertPricingRow({
|
|
107
|
+
provider: provider as Parameters<typeof insertPricingRow>[0]["provider"],
|
|
108
|
+
model,
|
|
109
|
+
tokenClass: "output",
|
|
110
|
+
effectiveFrom: 1,
|
|
111
|
+
pricePerMillionUsd: outputRate,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
describe("Phase 2 — POST /api/session-costs recompute fires for every provider", () => {
|
|
116
|
+
for (const provider of [
|
|
117
|
+
"claude",
|
|
118
|
+
"claude-managed",
|
|
119
|
+
"codex",
|
|
120
|
+
"pi",
|
|
121
|
+
"opencode",
|
|
122
|
+
"devin",
|
|
123
|
+
"gemini",
|
|
124
|
+
] as const) {
|
|
125
|
+
test(`provider=${provider} with seeded rows → costSource='pricing-table'`, async () => {
|
|
126
|
+
seedTwoClassRates(provider, `${provider}-test-model`, 2, 10);
|
|
127
|
+
|
|
128
|
+
const res = await authedFetch(`/api/session-costs`, {
|
|
129
|
+
method: "POST",
|
|
130
|
+
body: JSON.stringify({
|
|
131
|
+
sessionId: `${provider}-recompute-1`,
|
|
132
|
+
agentId: testAgent.id,
|
|
133
|
+
totalCostUsd: 999.99, // worker-reported; expected to be overwritten
|
|
134
|
+
inputTokens: 1_000_000, // 1M input
|
|
135
|
+
outputTokens: 500_000, // 500k output
|
|
136
|
+
model: `${provider}-test-model`,
|
|
137
|
+
provider,
|
|
138
|
+
durationMs: 1_000,
|
|
139
|
+
numTurns: 1,
|
|
140
|
+
}),
|
|
141
|
+
});
|
|
142
|
+
expect(res.status).toBe(201);
|
|
143
|
+
const body = (await res.json()) as CostResponse;
|
|
144
|
+
expect(body.cost.costSource).toBe("pricing-table");
|
|
145
|
+
// 1M @ 2 + 0.5M @ 10 = $2 + $5 = $7
|
|
146
|
+
expect(body.cost.totalCostUsd).toBeCloseTo(7.0, 5);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
test("unknown (provider, model) pair → costSource='unpriced', worker value preserved", async () => {
|
|
151
|
+
const res = await authedFetch(`/api/session-costs`, {
|
|
152
|
+
method: "POST",
|
|
153
|
+
body: JSON.stringify({
|
|
154
|
+
sessionId: "unpriced-1",
|
|
155
|
+
agentId: testAgent.id,
|
|
156
|
+
totalCostUsd: 1.23,
|
|
157
|
+
inputTokens: 100,
|
|
158
|
+
outputTokens: 50,
|
|
159
|
+
model: "gpt-future-2027",
|
|
160
|
+
provider: "codex",
|
|
161
|
+
durationMs: 1_000,
|
|
162
|
+
numTurns: 1,
|
|
163
|
+
}),
|
|
164
|
+
});
|
|
165
|
+
expect(res.status).toBe(201);
|
|
166
|
+
const body = (await res.json()) as CostResponse;
|
|
167
|
+
expect(body.cost.costSource).toBe("unpriced");
|
|
168
|
+
expect(body.cost.totalCostUsd).toBe(1.23);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -24,7 +24,12 @@ type TestCostData = {
|
|
|
24
24
|
model?: string;
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
// Phase 11 NOTE: the `store-progress` MCP tool no longer accepts a `costData`
|
|
28
|
+
// field — adapters are the sole writers of `session_costs`. These tests now
|
|
29
|
+
// exercise the lower-level `createSessionCost` API directly, which the runner
|
|
30
|
+
// still calls via `POST /api/session-costs`. They verify the DB write path
|
|
31
|
+
// hasn't regressed, NOT the tool's input schema.
|
|
32
|
+
describe("createSessionCost direct API (was: store-progress with cost data)", () => {
|
|
28
33
|
let agentId: string;
|
|
29
34
|
let taskId: string;
|
|
30
35
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { SwarmConfigPayload } from "../scripts-runtime/executors/types";
|
|
3
|
+
import { Redacted } from "../scripts-runtime/redacted";
|
|
4
|
+
import { SwarmConfig } from "../scripts-runtime/swarm-config";
|
|
5
|
+
|
|
6
|
+
const payload: SwarmConfigPayload = {
|
|
7
|
+
system: {
|
|
8
|
+
apiKey: { value: "test-api-key", isSecret: true },
|
|
9
|
+
agentId: { value: "agent-1", isSecret: false },
|
|
10
|
+
mcpBaseUrl: { value: "http://localhost:3013", isSecret: false },
|
|
11
|
+
},
|
|
12
|
+
user: {
|
|
13
|
+
"user-key": { value: "user-value", isSecret: true },
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
describe("SwarmConfig", () => {
|
|
18
|
+
test("hydrates system values as Redacted values with metadata", () => {
|
|
19
|
+
const config = new SwarmConfig(payload);
|
|
20
|
+
expect(Redacted.value(config.apiKey)).toBe("test-api-key");
|
|
21
|
+
expect(Redacted.meta(config.apiKey)).toEqual({ type: "system", isSecret: true });
|
|
22
|
+
expect(Redacted.value(config.agentId)).toBe("agent-1");
|
|
23
|
+
expect(Redacted.meta(config.mcpBaseUrl)).toEqual({ type: "system", isSecret: false });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("returns user-set config values", () => {
|
|
27
|
+
const config = new SwarmConfig(payload);
|
|
28
|
+
const value = config.get("user-key");
|
|
29
|
+
expect(value).toBeDefined();
|
|
30
|
+
expect(Redacted.value(value!)).toBe("user-value");
|
|
31
|
+
expect(Redacted.meta(value!)).toEqual({ type: "user", isSecret: true });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("missing user keys return undefined", () => {
|
|
35
|
+
const config = new SwarmConfig(payload);
|
|
36
|
+
expect(config.get("missing")).toBeUndefined();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -323,9 +323,9 @@ describe("Tool Annotations & Classification", () => {
|
|
|
323
323
|
test("registered tool count matches expected total", () => {
|
|
324
324
|
const count = Object.keys(tools).length;
|
|
325
325
|
// We expect all tools to be registered when all capabilities are enabled (default)
|
|
326
|
-
// Includes 11 skill tools
|
|
326
|
+
// Includes 11 skill tools, 7 MCP server tools, and reusable script tools
|
|
327
327
|
expect(count).toBeGreaterThanOrEqual(45);
|
|
328
|
-
expect(count).toBeLessThanOrEqual(
|
|
328
|
+
expect(count).toBeLessThanOrEqual(100);
|
|
329
329
|
});
|
|
330
330
|
|
|
331
331
|
test("core tools are fewer than deferred tools", () => {
|
|
@@ -151,6 +151,31 @@ describe("toolCallToProgress", () => {
|
|
|
151
151
|
expect(result).toBe("💬 Posting to Slack");
|
|
152
152
|
});
|
|
153
153
|
|
|
154
|
+
test("agent-swarm:script-search has pretty label", () => {
|
|
155
|
+
const result = toolCallToProgress("mcp__agent-swarm__script-search", {});
|
|
156
|
+
expect(result).toBe("📜 Searching scripts");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("agent-swarm:script-run has pretty label", () => {
|
|
160
|
+
const result = toolCallToProgress("mcp__agent-swarm__script-run", {});
|
|
161
|
+
expect(result).toBe("📜 Running script");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("agent-swarm:script-upsert has pretty label", () => {
|
|
165
|
+
const result = toolCallToProgress("mcp__agent-swarm__script-upsert", {});
|
|
166
|
+
expect(result).toBe("📜 Saving script");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("agent-swarm:script-delete has pretty label", () => {
|
|
170
|
+
const result = toolCallToProgress("mcp__agent-swarm__script-delete", {});
|
|
171
|
+
expect(result).toBe("📜 Deleting script");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("agent-swarm:script-query-types has pretty label", () => {
|
|
175
|
+
const result = toolCallToProgress("mcp__agent-swarm__script-query-types", {});
|
|
176
|
+
expect(result).toBe("📜 Reading script types");
|
|
177
|
+
});
|
|
178
|
+
|
|
154
179
|
// --- Agent-swarm MCP tool NOT in lookup (humanized fallback) ---
|
|
155
180
|
|
|
156
181
|
test("unknown agent-swarm tool gets humanized fallback", () => {
|
|
@@ -191,6 +216,11 @@ describe("toolCallToProgress", () => {
|
|
|
191
216
|
expect(result).toBe("🗃️ Querying database");
|
|
192
217
|
});
|
|
193
218
|
|
|
219
|
+
test("bare agent-swarm script-run has pretty label", () => {
|
|
220
|
+
const result = toolCallToProgress("script-run", {});
|
|
221
|
+
expect(result).toBe("📜 Running script");
|
|
222
|
+
});
|
|
223
|
+
|
|
194
224
|
test("codex MCP args with agent-swarm server use pretty labels", () => {
|
|
195
225
|
const result = toolCallToProgress("send-task", {
|
|
196
226
|
server: "agent-swarm",
|