@desplega.ai/agent-swarm 1.92.2 → 1.94.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/README.md +2 -2
- package/openapi.json +242 -3
- package/package.json +5 -5
- package/src/be/db.ts +152 -11
- package/src/be/memory/boot-reembed.ts +0 -1
- package/src/be/memory/providers/sqlite-store.ts +42 -25
- package/src/be/memory/raters/llm-client.ts +12 -5
- package/src/be/memory/types.ts +3 -0
- package/src/be/migrations/088_script_runs_list_indexes.sql +10 -0
- package/src/be/migrations/089_harness_variant.sql +2 -0
- package/src/be/migrations/090_model_tiers.sql +2 -0
- package/src/be/migrations/091_seed_swarm_operations_metrics.sql +12 -0
- package/src/be/migrations/092_metrics_dashboard_combobox_filters.sql +68 -0
- package/src/be/migrations/093_slack_message_tracking.sql +6 -0
- package/src/be/migrations/runner.ts +52 -0
- package/src/be/modelsdev-cache.json +3264 -1166
- package/src/be/scripts/boot-reembed.ts +74 -0
- package/src/be/scripts/db.ts +19 -3
- package/src/be/seed/index.ts +1 -1
- package/src/be/seed/registry.ts +2 -2
- package/src/be/seed/runner.ts +5 -5
- package/src/be/seed/types.ts +6 -1
- package/src/be/seed-pricing.ts +2 -0
- package/src/be/seed-scripts/catalog/boot-triage.inline.ts +221 -0
- package/src/be/seed-scripts/catalog/catalog-report.inline.ts +457 -0
- package/src/be/seed-scripts/catalog/compound-insights.inline.ts +863 -0
- package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +506 -0
- package/src/be/seed-scripts/index.ts +8 -7
- package/src/be/skill-sync.ts +28 -179
- package/src/commands/runner.ts +197 -10
- package/src/http/api-keys.ts +42 -0
- package/src/http/index.ts +13 -2
- package/src/http/mcp-bridge.ts +1 -1
- package/src/http/memory.ts +23 -24
- package/src/http/metrics.ts +55 -6
- package/src/http/schedules.ts +16 -15
- package/src/http/script-runs.ts +7 -1
- package/src/http/scripts.ts +147 -1
- package/src/http/tasks.ts +17 -6
- package/src/model-tiers.ts +140 -0
- package/src/providers/claude-adapter.ts +33 -1
- package/src/providers/claude-managed-adapter.ts +3 -0
- package/src/providers/claude-managed-models.ts +16 -0
- package/src/providers/codex-adapter.ts +8 -1
- package/src/providers/codex-models.ts +1 -0
- package/src/providers/codex-oauth/auth-json.ts +1 -0
- package/src/providers/harness-version.ts +7 -0
- package/src/providers/opencode-adapter.ts +12 -4
- package/src/providers/pi-mono-adapter.ts +90 -8
- package/src/providers/types.ts +2 -0
- package/src/scheduler/scheduler.ts +22 -34
- package/src/scripts-runtime/egress-secrets.ts +83 -0
- package/src/scripts-runtime/eval-harness.ts +4 -0
- package/src/scripts-runtime/executors/types.ts +7 -0
- package/src/scripts-runtime/loader.ts +2 -0
- package/src/server-user.ts +8 -2
- package/src/slack/channel-join.ts +41 -0
- package/src/slack/responses.ts +39 -11
- package/src/slack/watcher.ts +121 -8
- package/src/tests/additive-buffer.test.ts +0 -1
- package/src/tests/agents-list-model-display.test.ts +13 -0
- package/src/tests/api-key-tracking.test.ts +113 -0
- package/src/tests/approval-requests.test.ts +0 -6
- package/src/tests/aws-error-classifier.test.ts +148 -0
- package/src/tests/claude-managed-adapter.test.ts +12 -0
- package/src/tests/claude-managed-setup.test.ts +0 -4
- package/src/tests/codex-pool.test.ts +2 -6
- package/src/tests/context-window.test.ts +7 -0
- package/src/tests/http-api-integration.test.ts +23 -6
- package/src/tests/memory-edges.test.ts +0 -2
- package/src/tests/memory-rate-endpoint.test.ts +0 -2
- package/src/tests/memory-rater-e2e.test.ts +0 -2
- package/src/tests/memory-store.test.ts +19 -1
- package/src/tests/memory.test.ts +51 -0
- package/src/tests/metrics-http.test.ts +137 -3
- package/src/tests/migration-046-budgets.test.ts +33 -0
- package/src/tests/migration-runner-regressions.test.ts +69 -0
- package/src/tests/model-control.test.ts +162 -46
- package/src/tests/opencode-adapter.test.ts +9 -0
- package/src/tests/pi-mono-adapter.test.ts +319 -0
- package/src/tests/providers/pi-cost.test.ts +9 -0
- package/src/tests/reload-config.test.ts +33 -17
- package/src/tests/runner-fallback-output.test.ts +50 -0
- package/src/tests/runner-skills-refresh.test.ts +216 -46
- package/src/tests/script-runs-http.test.ts +7 -1
- package/src/tests/scripts-boot-reembed.test.ts +163 -0
- package/src/tests/scripts-embeddings.test.ts +90 -0
- package/src/tests/scripts-runtime-secret-egress.test.ts +129 -0
- package/src/tests/seed-scripts.test.ts +13 -1
- package/src/tests/seed.test.ts +26 -1
- package/src/tests/session-attach.test.ts +6 -6
- package/src/tests/session-costs-model-key-normalize.test.ts +2 -0
- package/src/tests/skill-fs-writer.test.ts +250 -0
- package/src/tests/slack-attachments-block.test.ts +0 -1
- package/src/tests/slack-blocks.test.ts +0 -1
- package/src/tests/slack-channel-join.test.ts +80 -0
- package/src/tests/slack-identity-resolution.test.ts +0 -1
- package/src/tests/slack-watcher.test.ts +66 -0
- package/src/tests/structured-output.test.ts +0 -2
- package/src/tests/use-dismissible-card.test.ts +0 -4
- package/src/tests/workflow-agent-task.test.ts +5 -2
- package/src/tests/workflow-validation-port-routing.test.ts +181 -0
- package/src/tools/memory-get.ts +11 -0
- package/src/tools/memory-search.ts +18 -0
- package/src/tools/schedules/create-schedule.ts +71 -70
- package/src/tools/schedules/update-schedule.ts +43 -31
- package/src/tools/send-task.ts +16 -5
- package/src/tools/slack-post.ts +18 -15
- package/src/tools/slack-read.ts +9 -11
- package/src/tools/slack-reply.ts +18 -15
- package/src/tools/slack-start-thread.ts +17 -14
- package/src/tools/task-action.ts +11 -3
- package/src/types.ts +40 -0
- package/src/utils/aws-error-classifier.ts +97 -0
- package/src/utils/context-window.ts +5 -0
- package/src/utils/credentials.test.ts +68 -0
- package/src/utils/credentials.ts +66 -5
- package/src/utils/pretty-print.ts +25 -10
- package/src/utils/skill-fs-writer.ts +220 -0
- package/src/utils/skills-refresh.ts +123 -40
- package/src/workflows/engine.ts +3 -2
- package/src/workflows/executors/agent-task.ts +3 -1
|
@@ -577,11 +577,8 @@ describe("Approval Requests", () => {
|
|
|
577
577
|
});
|
|
578
578
|
|
|
579
579
|
expect(result.status).toBe("success");
|
|
580
|
-
// biome-ignore lint/suspicious/noExplicitAny: test assertion on untyped executor result
|
|
581
580
|
expect((result as any).async).toBe(true);
|
|
582
|
-
// biome-ignore lint/suspicious/noExplicitAny: test assertion on untyped executor result
|
|
583
581
|
expect((result as any).waitFor).toBe("approval.resolved");
|
|
584
|
-
// biome-ignore lint/suspicious/noExplicitAny: test assertion on untyped executor result
|
|
585
582
|
expect((result as any).correlationId).toBeTruthy();
|
|
586
583
|
|
|
587
584
|
// Verify the request was created in DB
|
|
@@ -616,9 +613,7 @@ describe("Approval Requests", () => {
|
|
|
616
613
|
});
|
|
617
614
|
|
|
618
615
|
expect(result.status).toBe("success");
|
|
619
|
-
// biome-ignore lint/suspicious/noExplicitAny: test assertion on untyped executor result
|
|
620
616
|
expect((result as any).async).toBe(true);
|
|
621
|
-
// biome-ignore lint/suspicious/noExplicitAny: test assertion on untyped executor result
|
|
622
617
|
expect((result as any).correlationId).toBe(existingId);
|
|
623
618
|
});
|
|
624
619
|
|
|
@@ -650,7 +645,6 @@ describe("Approval Requests", () => {
|
|
|
650
645
|
});
|
|
651
646
|
|
|
652
647
|
expect(result.status).toBe("success");
|
|
653
|
-
// biome-ignore lint/suspicious/noExplicitAny: test assertion on untyped executor result
|
|
654
648
|
expect((result as any).async).toBeUndefined();
|
|
655
649
|
expect(result.output).toBeDefined();
|
|
656
650
|
expect(result.output!.requestId).toBe(existingId);
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for `classifyAwsSdkError` in `src/utils/aws-error-classifier.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Exercises all four error categories and the no-match path.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from "bun:test";
|
|
8
|
+
import { classifyAwsSdkError } from "../utils/aws-error-classifier";
|
|
9
|
+
|
|
10
|
+
describe("classifyAwsSdkError — aws-auth", () => {
|
|
11
|
+
test("ExpiredTokenException", () => {
|
|
12
|
+
const r = classifyAwsSdkError(
|
|
13
|
+
"ExpiredTokenException: The security token included in the request is expired",
|
|
14
|
+
);
|
|
15
|
+
expect(r).not.toBeNull();
|
|
16
|
+
expect(r!.category).toBe("aws-auth");
|
|
17
|
+
expect(r!.message).toContain("aws sso login");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("ExpiredToken (without Exception suffix)", () => {
|
|
21
|
+
const r = classifyAwsSdkError("ExpiredToken: token expired");
|
|
22
|
+
expect(r?.category).toBe("aws-auth");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("CredentialsProviderError", () => {
|
|
26
|
+
const r = classifyAwsSdkError("CredentialsProviderError: Could not load credentials");
|
|
27
|
+
expect(r?.category).toBe("aws-auth");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("Unable to locate credentials", () => {
|
|
31
|
+
const r = classifyAwsSdkError(
|
|
32
|
+
'Unable to locate credentials. You can configure credentials by running "aws configure".',
|
|
33
|
+
);
|
|
34
|
+
expect(r?.category).toBe("aws-auth");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("security token ... expired (lower-case)", () => {
|
|
38
|
+
const r = classifyAwsSdkError("The security token included in the request is expired");
|
|
39
|
+
expect(r?.category).toBe("aws-auth");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("InvalidSignatureException", () => {
|
|
43
|
+
const r = classifyAwsSdkError(
|
|
44
|
+
"InvalidSignatureException: The request signature we calculated does not match the signature you provided",
|
|
45
|
+
);
|
|
46
|
+
expect(r?.category).toBe("aws-auth");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("UnrecognizedClientException", () => {
|
|
50
|
+
const r = classifyAwsSdkError(
|
|
51
|
+
"UnrecognizedClientException: The security token included in the request is invalid",
|
|
52
|
+
);
|
|
53
|
+
expect(r?.category).toBe("aws-auth");
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("classifyAwsSdkError — aws-throttle", () => {
|
|
58
|
+
test("ThrottlingException", () => {
|
|
59
|
+
const r = classifyAwsSdkError("ThrottlingException: Rate exceeded");
|
|
60
|
+
expect(r?.category).toBe("aws-throttle");
|
|
61
|
+
expect(r!.message).toContain("quota");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("TooManyRequestsException", () => {
|
|
65
|
+
const r = classifyAwsSdkError("TooManyRequestsException: Too many requests");
|
|
66
|
+
expect(r?.category).toBe("aws-throttle");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("ServiceQuotaExceededException", () => {
|
|
70
|
+
const r = classifyAwsSdkError(
|
|
71
|
+
"ServiceQuotaExceededException: You have exceeded your request quota for this service",
|
|
72
|
+
);
|
|
73
|
+
expect(r?.category).toBe("aws-throttle");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("Rate exceeded (standalone phrase)", () => {
|
|
77
|
+
const r = classifyAwsSdkError("Rate exceeded. Reduce your request rate.");
|
|
78
|
+
expect(r?.category).toBe("aws-throttle");
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("classifyAwsSdkError — aws-access", () => {
|
|
83
|
+
test("AccessDeniedException with bedrock:InvokeModel", () => {
|
|
84
|
+
const r = classifyAwsSdkError(
|
|
85
|
+
"AccessDeniedException: User: arn:aws:iam::123:user/dev is not authorized to perform: bedrock:InvokeModel on resource: arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-v2",
|
|
86
|
+
);
|
|
87
|
+
expect(r?.category).toBe("aws-access");
|
|
88
|
+
expect(r!.message).toContain("bedrock:InvokeModel");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("not authorized to perform (phrase match)", () => {
|
|
92
|
+
const r = classifyAwsSdkError("User is not authorized to perform: bedrock:InvokeModel");
|
|
93
|
+
expect(r?.category).toBe("aws-access");
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("classifyAwsSdkError — aws-model", () => {
|
|
98
|
+
test("ValidationException", () => {
|
|
99
|
+
const r = classifyAwsSdkError(
|
|
100
|
+
"ValidationException: Invocation of model ID anthropic.claude-v99 with on-demand throughput isn't supported",
|
|
101
|
+
);
|
|
102
|
+
expect(r?.category).toBe("aws-model");
|
|
103
|
+
expect(r!.message).toContain("MODEL_OVERRIDE");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("ResourceNotFoundException", () => {
|
|
107
|
+
const r = classifyAwsSdkError("ResourceNotFoundException: Could not find model");
|
|
108
|
+
expect(r?.category).toBe("aws-model");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("ModelTimeoutException", () => {
|
|
112
|
+
const r = classifyAwsSdkError(
|
|
113
|
+
"ModelTimeoutException: The model timed out processing your request",
|
|
114
|
+
);
|
|
115
|
+
expect(r?.category).toBe("aws-model");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("ModelNotReadyException", () => {
|
|
119
|
+
const r = classifyAwsSdkError("ModelNotReadyException: The model is not ready for inference");
|
|
120
|
+
expect(r?.category).toBe("aws-model");
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("classifyAwsSdkError — priority ordering", () => {
|
|
125
|
+
test("aws-auth wins over aws-model when both match (ExpiredToken + ValidationException)", () => {
|
|
126
|
+
// Should not happen in practice, but priority must be deterministic
|
|
127
|
+
const r = classifyAwsSdkError("ExpiredTokenException and also ValidationException");
|
|
128
|
+
expect(r?.category).toBe("aws-auth");
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("classifyAwsSdkError — no-match", () => {
|
|
133
|
+
test("returns null for empty string", () => {
|
|
134
|
+
expect(classifyAwsSdkError("")).toBeNull();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("returns null for unrelated error", () => {
|
|
138
|
+
expect(classifyAwsSdkError("TypeError: Cannot read property 'foo' of undefined")).toBeNull();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("returns null for generic network error", () => {
|
|
142
|
+
expect(classifyAwsSdkError("ECONNREFUSED 127.0.0.1:3013")).toBeNull();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("returns null for Claude API error (not AWS)", () => {
|
|
146
|
+
expect(classifyAwsSdkError("401 Unauthorized: Invalid API key")).toBeNull();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -794,6 +794,18 @@ describe("ClaudeManagedAdapter (Phase 4) — repo provisioning + cost data", ()
|
|
|
794
794
|
});
|
|
795
795
|
|
|
796
796
|
test("CLAUDE_MANAGED_MODEL_PRICING covers sonnet, opus, haiku at minimum", () => {
|
|
797
|
+
expect(CLAUDE_MANAGED_MODEL_PRICING["claude-fable-5"]).toEqual({
|
|
798
|
+
inputPerMillion: 10.0,
|
|
799
|
+
outputPerMillion: 50.0,
|
|
800
|
+
cacheReadPerMillion: 1.0,
|
|
801
|
+
cacheWritePerMillion: 12.5,
|
|
802
|
+
});
|
|
803
|
+
expect(CLAUDE_MANAGED_MODEL_PRICING["claude-mythos-5"]).toEqual({
|
|
804
|
+
inputPerMillion: 10.0,
|
|
805
|
+
outputPerMillion: 50.0,
|
|
806
|
+
cacheReadPerMillion: 1.0,
|
|
807
|
+
cacheWritePerMillion: 12.5,
|
|
808
|
+
});
|
|
797
809
|
expect(CLAUDE_MANAGED_MODEL_PRICING["claude-sonnet-4-6"]).toBeDefined();
|
|
798
810
|
expect(CLAUDE_MANAGED_MODEL_PRICING["claude-opus-4-7"]).toBeDefined();
|
|
799
811
|
expect(CLAUDE_MANAGED_MODEL_PRICING["claude-haiku-4-5"]).toBeDefined();
|
|
@@ -59,7 +59,6 @@ describe("runClaudeManagedSetupFlow — happy path", () => {
|
|
|
59
59
|
const log = mock((_msg: string) => undefined);
|
|
60
60
|
|
|
61
61
|
const result = await runClaudeManagedSetupFlow(baseConfig, {
|
|
62
|
-
// biome-ignore lint/suspicious/noExplicitAny: fake client subset for mock
|
|
63
62
|
client: client as any,
|
|
64
63
|
fetchConfig,
|
|
65
64
|
upsert,
|
|
@@ -132,7 +131,6 @@ describe("runClaudeManagedSetupFlow — happy path", () => {
|
|
|
132
131
|
await runClaudeManagedSetupFlow(
|
|
133
132
|
{ ...baseConfig, mcpBaseUrl: "https://swarm.example.com/" },
|
|
134
133
|
{
|
|
135
|
-
// biome-ignore lint/suspicious/noExplicitAny: fake client subset for mock
|
|
136
134
|
client: client as any,
|
|
137
135
|
fetchConfig: mock(async () => null),
|
|
138
136
|
upsert: mock(async () => undefined),
|
|
@@ -175,7 +173,6 @@ describe("runClaudeManagedSetupFlow — idempotent re-run", () => {
|
|
|
175
173
|
const uploadOne = mock(async () => null);
|
|
176
174
|
|
|
177
175
|
const result = await runClaudeManagedSetupFlow(baseConfig, {
|
|
178
|
-
// biome-ignore lint/suspicious/noExplicitAny: fake client subset for mock
|
|
179
176
|
client: client as any,
|
|
180
177
|
fetchConfig,
|
|
181
178
|
upsert,
|
|
@@ -208,7 +205,6 @@ describe("runClaudeManagedSetupFlow — idempotent re-run", () => {
|
|
|
208
205
|
await runClaudeManagedSetupFlow(
|
|
209
206
|
{ ...baseConfig, force: true },
|
|
210
207
|
{
|
|
211
|
-
// biome-ignore lint/suspicious/noExplicitAny: fake client subset for mock
|
|
212
208
|
client: client as any,
|
|
213
209
|
fetchConfig,
|
|
214
210
|
upsert,
|
|
@@ -24,11 +24,7 @@ import {
|
|
|
24
24
|
credentialsToAuthJson,
|
|
25
25
|
} from "../providers/codex-oauth/auth-json.js";
|
|
26
26
|
import { materializeCodexAuthJson } from "../providers/codex-oauth/auth-json-fs.js";
|
|
27
|
-
import {
|
|
28
|
-
loadAllCodexOAuthSlots,
|
|
29
|
-
persistCodexOAuth,
|
|
30
|
-
storeCodexOAuth,
|
|
31
|
-
} from "../providers/codex-oauth/storage.js";
|
|
27
|
+
import { loadAllCodexOAuthSlots, persistCodexOAuth } from "../providers/codex-oauth/storage.js";
|
|
32
28
|
import type { CodexOAuthCredentials } from "../providers/codex-oauth/types.js";
|
|
33
29
|
|
|
34
30
|
// ─── Fixtures ────────────────────────────────────────────────────────────────
|
|
@@ -117,7 +113,7 @@ afterEach(() => {
|
|
|
117
113
|
describe("Scenario 1 — 3-slot round-trip with availability filter", () => {
|
|
118
114
|
it("selects from available slots [1,2] and materialises the correct creds into auth.json", async () => {
|
|
119
115
|
// Mock API: three slots in config store.
|
|
120
|
-
globalThis.fetch = async (
|
|
116
|
+
globalThis.fetch = async (_url: string | URL | Request) => {
|
|
121
117
|
return makeConfigResponse();
|
|
122
118
|
// (available-indices endpoint not called by loadAllCodexOAuthSlots)
|
|
123
119
|
};
|
|
@@ -8,6 +8,13 @@ import {
|
|
|
8
8
|
} from "../utils/context-window";
|
|
9
9
|
|
|
10
10
|
describe("getContextWindowSize", () => {
|
|
11
|
+
test("returns 1M for fable and mythos models", () => {
|
|
12
|
+
expect(getContextWindowSize("claude-fable-5")).toBe(1_000_000);
|
|
13
|
+
expect(getContextWindowSize("claude-mythos-5")).toBe(1_000_000);
|
|
14
|
+
expect(getContextWindowSize("fable")).toBe(1_000_000);
|
|
15
|
+
expect(getContextWindowSize("mythos")).toBe(1_000_000);
|
|
16
|
+
});
|
|
17
|
+
|
|
11
18
|
test("returns 1M for opus models", () => {
|
|
12
19
|
expect(getContextWindowSize("claude-opus-4-8")).toBe(1_000_000);
|
|
13
20
|
expect(getContextWindowSize("claude-opus-4-7")).toBe(1_000_000);
|
|
@@ -25,7 +25,6 @@ async function api(
|
|
|
25
25
|
method: string,
|
|
26
26
|
path: string,
|
|
27
27
|
opts: { body?: unknown; agentId?: string; headers?: Record<string, string> } = {},
|
|
28
|
-
// biome-ignore lint/suspicious/noExplicitAny: test helper needs flexible body type
|
|
29
28
|
): Promise<{ status: number; body: any; ok: boolean }> {
|
|
30
29
|
const headers: Record<string, string> = {
|
|
31
30
|
"Content-Type": "application/json",
|
|
@@ -41,7 +40,6 @@ async function api(
|
|
|
41
40
|
});
|
|
42
41
|
|
|
43
42
|
const text = await res.text();
|
|
44
|
-
// biome-ignore lint/suspicious/noExplicitAny: body can be parsed JSON or raw text
|
|
45
43
|
let body: any;
|
|
46
44
|
try {
|
|
47
45
|
body = JSON.parse(text);
|
|
@@ -427,9 +425,9 @@ describe("Tasks", () => {
|
|
|
427
425
|
expect(status).toBe(404);
|
|
428
426
|
});
|
|
429
427
|
|
|
430
|
-
test("PUT /api/tasks/:id/
|
|
428
|
+
test("PUT /api/tasks/:id/session — update session ID", async () => {
|
|
431
429
|
const sessionId = randomUUID();
|
|
432
|
-
const { status, body } = await put(`/api/tasks/${ids.task2}/
|
|
430
|
+
const { status, body } = await put(`/api/tasks/${ids.task2}/session`, {
|
|
433
431
|
agentId: ids.workerAgent,
|
|
434
432
|
body: { claudeSessionId: sessionId },
|
|
435
433
|
});
|
|
@@ -437,8 +435,8 @@ describe("Tasks", () => {
|
|
|
437
435
|
expect(body.claudeSessionId).toBe(sessionId);
|
|
438
436
|
});
|
|
439
437
|
|
|
440
|
-
test("PUT /api/tasks/:id/
|
|
441
|
-
const { status } = await put(`/api/tasks/${ids.task2}/
|
|
438
|
+
test("PUT /api/tasks/:id/session — missing fields returns 400", async () => {
|
|
439
|
+
const { status } = await put(`/api/tasks/${ids.task2}/session`, {
|
|
442
440
|
body: {},
|
|
443
441
|
});
|
|
444
442
|
expect(status).toBe(400);
|
|
@@ -1014,6 +1012,25 @@ describe("Schedule CRUD", () => {
|
|
|
1014
1012
|
expect(body.task.id).toBeDefined();
|
|
1015
1013
|
});
|
|
1016
1014
|
|
|
1015
|
+
test("POST /api/schedules/:id/run — propagates modelTier to the created task", async () => {
|
|
1016
|
+
const { body: created } = await post("/api/schedules", {
|
|
1017
|
+
body: {
|
|
1018
|
+
name: "model-tier-manual-run",
|
|
1019
|
+
taskTemplate: "Run model tier integration test",
|
|
1020
|
+
cronExpression: "0 * * * *",
|
|
1021
|
+
modelTier: "smart",
|
|
1022
|
+
},
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
const { status, body } = await post(`/api/schedules/${created.id}/run`);
|
|
1026
|
+
expect(status).toBe(200);
|
|
1027
|
+
expect(body.task).toBeDefined();
|
|
1028
|
+
expect(body.task.model).toBeUndefined();
|
|
1029
|
+
expect(body.task.modelTier).toBe("smart");
|
|
1030
|
+
|
|
1031
|
+
await del(`/api/schedules/${created.id}`);
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1017
1034
|
test("POST /api/schedules/:id/run — disabled schedule returns 400", async () => {
|
|
1018
1035
|
// Disable the schedule first
|
|
1019
1036
|
await put(`/api/schedules/${scheduleId}`, {
|
|
@@ -48,7 +48,6 @@ async function api(
|
|
|
48
48
|
method: string,
|
|
49
49
|
path: string,
|
|
50
50
|
opts: { body?: unknown; agentId?: string } = {},
|
|
51
|
-
// biome-ignore lint/suspicious/noExplicitAny: test helper
|
|
52
51
|
): Promise<{ status: number; body: any }> {
|
|
53
52
|
const headers: Record<string, string> = {
|
|
54
53
|
"Content-Type": "application/json",
|
|
@@ -61,7 +60,6 @@ async function api(
|
|
|
61
60
|
body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
|
|
62
61
|
});
|
|
63
62
|
const text = await res.text();
|
|
64
|
-
// biome-ignore lint/suspicious/noExplicitAny: body may be JSON or text
|
|
65
63
|
let body: any;
|
|
66
64
|
try {
|
|
67
65
|
body = JSON.parse(text);
|
|
@@ -41,7 +41,6 @@ async function api(
|
|
|
41
41
|
method: string,
|
|
42
42
|
path: string,
|
|
43
43
|
opts: { body?: unknown; agentId?: string } = {},
|
|
44
|
-
// biome-ignore lint/suspicious/noExplicitAny: test helper
|
|
45
44
|
): Promise<{ status: number; body: any }> {
|
|
46
45
|
const headers: Record<string, string> = {
|
|
47
46
|
"Content-Type": "application/json",
|
|
@@ -54,7 +53,6 @@ async function api(
|
|
|
54
53
|
body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
|
|
55
54
|
});
|
|
56
55
|
const text = await res.text();
|
|
57
|
-
// biome-ignore lint/suspicious/noExplicitAny: body may be JSON or text
|
|
58
56
|
let body: any;
|
|
59
57
|
try {
|
|
60
58
|
body = JSON.parse(text);
|
|
@@ -64,7 +64,6 @@ async function api(
|
|
|
64
64
|
method: string,
|
|
65
65
|
path: string,
|
|
66
66
|
opts: { body?: unknown; agentId?: string; sourceTaskId?: string } = {},
|
|
67
|
-
// biome-ignore lint/suspicious/noExplicitAny: test helper
|
|
68
67
|
): Promise<{ status: number; body: any }> {
|
|
69
68
|
const headers: Record<string, string> = {
|
|
70
69
|
"Content-Type": "application/json",
|
|
@@ -78,7 +77,6 @@ async function api(
|
|
|
78
77
|
body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
|
|
79
78
|
});
|
|
80
79
|
const text = await res.text();
|
|
81
|
-
// biome-ignore lint/suspicious/noExplicitAny: body may be JSON or text
|
|
82
80
|
let body: any;
|
|
83
81
|
try {
|
|
84
82
|
body = JSON.parse(text);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
2
|
import { unlink } from "node:fs/promises";
|
|
3
|
-
import { closeDb, createAgent, getDb, initDb } from "../be/db";
|
|
3
|
+
import { closeDb, createAgent, getDb, initDb, isSqliteVecAvailable } from "../be/db";
|
|
4
4
|
import { serializeEmbedding } from "../be/embedding";
|
|
5
5
|
import { SqliteMemoryStore } from "../be/memory/providers/sqlite-store";
|
|
6
6
|
|
|
@@ -19,6 +19,16 @@ describe("SqliteMemoryStore", () => {
|
|
|
19
19
|
return embedding;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
function skipVecAssertionsWhenUnavailable(): boolean {
|
|
23
|
+
if (isSqliteVecAvailable()) return false;
|
|
24
|
+
|
|
25
|
+
const health = store.getHealth();
|
|
26
|
+
expect(health.sqliteVec.extensionLoaded).toBe(false);
|
|
27
|
+
expect(health.retrievalMode).toBe("fallback");
|
|
28
|
+
expect(health.reasons).toContain("sqlite_vec_extension_unavailable");
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
22
32
|
beforeAll(async () => {
|
|
23
33
|
for (const suffix of ["", "-wal", "-shm"]) {
|
|
24
34
|
try {
|
|
@@ -221,6 +231,8 @@ describe("SqliteMemoryStore", () => {
|
|
|
221
231
|
});
|
|
222
232
|
|
|
223
233
|
test("uses sqlite-vec for 512d embeddings with scope-filter parity", () => {
|
|
234
|
+
if (skipVecAssertionsWhenUnavailable()) return;
|
|
235
|
+
|
|
224
236
|
for (let i = 0; i < 6; i++) {
|
|
225
237
|
const otherAgent = store.store({
|
|
226
238
|
agentId: agentB,
|
|
@@ -256,6 +268,8 @@ describe("SqliteMemoryStore", () => {
|
|
|
256
268
|
|
|
257
269
|
describe("memory_vec population", () => {
|
|
258
270
|
test("populates existing embeddings on startup and reports health counts", () => {
|
|
271
|
+
if (skipVecAssertionsWhenUnavailable()) return;
|
|
272
|
+
|
|
259
273
|
const raw = store.store({
|
|
260
274
|
agentId: agentA,
|
|
261
275
|
scope: "agent",
|
|
@@ -282,6 +296,8 @@ describe("SqliteMemoryStore", () => {
|
|
|
282
296
|
});
|
|
283
297
|
|
|
284
298
|
test("rebuilds an old non-cosine memory_vec table from agent_memory", () => {
|
|
299
|
+
if (skipVecAssertionsWhenUnavailable()) return;
|
|
300
|
+
|
|
285
301
|
const raw = store.store({
|
|
286
302
|
agentId: agentA,
|
|
287
303
|
scope: "agent",
|
|
@@ -473,6 +489,8 @@ describe("SqliteMemoryStore", () => {
|
|
|
473
489
|
});
|
|
474
490
|
|
|
475
491
|
test("also removes corresponding vec rows", () => {
|
|
492
|
+
if (skipVecAssertionsWhenUnavailable()) return;
|
|
493
|
+
|
|
476
494
|
const db = getDb();
|
|
477
495
|
const emb = vector({ 0: 0.9, 100: 0.1 });
|
|
478
496
|
|
package/src/tests/memory.test.ts
CHANGED
|
@@ -479,6 +479,57 @@ describe("Memory System", () => {
|
|
|
479
479
|
const page2 = store.list(listAgent, { scope: "agent", limit: 3, offset: 3 });
|
|
480
480
|
expect(page2.length).toBe(2);
|
|
481
481
|
});
|
|
482
|
+
|
|
483
|
+
test("counts memories with the same filters used by list", () => {
|
|
484
|
+
const countAgent = "eeee0000-0000-4000-8000-000000000105";
|
|
485
|
+
createAgent({ id: countAgent, name: "Count Agent", isLead: false, status: "idle" });
|
|
486
|
+
|
|
487
|
+
store.store({
|
|
488
|
+
agentId: countAgent,
|
|
489
|
+
scope: "agent",
|
|
490
|
+
name: "Count match 1",
|
|
491
|
+
content: "Content",
|
|
492
|
+
source: "file_index",
|
|
493
|
+
sourcePath: "/workspace/src/MemoryPage.tsx",
|
|
494
|
+
});
|
|
495
|
+
store.store({
|
|
496
|
+
agentId: countAgent,
|
|
497
|
+
scope: "swarm",
|
|
498
|
+
name: "Count match 2",
|
|
499
|
+
content: "Content",
|
|
500
|
+
source: "file_index",
|
|
501
|
+
sourcePath: "/workspace/SRC/memory-page.tsx",
|
|
502
|
+
});
|
|
503
|
+
store.store({
|
|
504
|
+
agentId: countAgent,
|
|
505
|
+
scope: "agent",
|
|
506
|
+
name: "Wrong source",
|
|
507
|
+
content: "Content",
|
|
508
|
+
source: "manual",
|
|
509
|
+
sourcePath: "/workspace/src/MemoryPage.tsx",
|
|
510
|
+
});
|
|
511
|
+
store.store({
|
|
512
|
+
agentId: agentA,
|
|
513
|
+
scope: "agent",
|
|
514
|
+
name: "Wrong owner",
|
|
515
|
+
content: "Content",
|
|
516
|
+
source: "file_index",
|
|
517
|
+
sourcePath: "/workspace/src/MemoryPage.tsx",
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
const filters = {
|
|
521
|
+
scope: "all" as const,
|
|
522
|
+
isLead: true,
|
|
523
|
+
ownerAgentId: countAgent,
|
|
524
|
+
source: "file_index" as const,
|
|
525
|
+
sourcePath: "src/memory",
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
expect(store.count("", filters)).toBe(2);
|
|
529
|
+
expect(store.list("", { ...filters, limit: 1, offset: 0 })).toHaveLength(1);
|
|
530
|
+
expect(store.list("", { ...filters, limit: 1, offset: 1 })).toHaveLength(1);
|
|
531
|
+
expect(store.list("", { ...filters, limit: 1, offset: 2 })).toHaveLength(0);
|
|
532
|
+
});
|
|
482
533
|
});
|
|
483
534
|
|
|
484
535
|
describe("store.delete (deleteMemory)", () => {
|
|
@@ -76,7 +76,60 @@ describe("Metrics HTTP API", () => {
|
|
|
76
76
|
const body = (await res.json()) as { metrics: Metric[]; total: number };
|
|
77
77
|
expect(body.total).toBeGreaterThanOrEqual(1);
|
|
78
78
|
const starter = body.metrics.find((metric) => metric.slug === "swarm-operations-overview");
|
|
79
|
-
expect(starter?.definition.
|
|
79
|
+
expect(starter?.definition.layout?.columns).toBe(3);
|
|
80
|
+
expect(starter?.definition.widgets.map((widget) => widget.id)).toEqual([
|
|
81
|
+
"tasks-created-per-day",
|
|
82
|
+
"usage-by-user",
|
|
83
|
+
"usage-by-model",
|
|
84
|
+
"avg-cost-per-task-by-model",
|
|
85
|
+
"avg-task-time-by-model",
|
|
86
|
+
"cost-per-minute-by-model",
|
|
87
|
+
"cost-per-minute-by-agent",
|
|
88
|
+
"agent-performance",
|
|
89
|
+
"task-outcomes-by-day",
|
|
90
|
+
"recent-task-outcomes",
|
|
91
|
+
]);
|
|
92
|
+
expect(
|
|
93
|
+
starter?.definition.variables?.find((variable) => variable.key === "userFilter"),
|
|
94
|
+
).toMatchObject({
|
|
95
|
+
type: "select",
|
|
96
|
+
defaultValue: "all",
|
|
97
|
+
optionsQuery: { valueKey: "id", labelKey: "label" },
|
|
98
|
+
});
|
|
99
|
+
expect(
|
|
100
|
+
starter?.definition.variables?.find((variable) => variable.key === "agentFilter"),
|
|
101
|
+
).toMatchObject({
|
|
102
|
+
type: "select",
|
|
103
|
+
defaultValue: "all",
|
|
104
|
+
optionsQuery: { valueKey: "id", labelKey: "label" },
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const run = await fetch(`${BASE}/api/metrics/definitions/${starter!.id}/run`, {
|
|
108
|
+
method: "POST",
|
|
109
|
+
headers,
|
|
110
|
+
body: JSON.stringify({ variables: {} }),
|
|
111
|
+
});
|
|
112
|
+
expect(run.status).toBe(200);
|
|
113
|
+
const runBody = (await run.json()) as MetricRunResponse & {
|
|
114
|
+
metric: Metric;
|
|
115
|
+
variables: Record<string, string>;
|
|
116
|
+
};
|
|
117
|
+
expect(runBody.variables.userFilter).toBe("all");
|
|
118
|
+
expect(runBody.variables.agentFilter).toBe("all");
|
|
119
|
+
expect(
|
|
120
|
+
runBody.metric.definition.variables?.find((variable) => variable.key === "userFilter")
|
|
121
|
+
?.options?.[0],
|
|
122
|
+
).toEqual({
|
|
123
|
+
label: "All requesters",
|
|
124
|
+
value: "all",
|
|
125
|
+
});
|
|
126
|
+
expect(
|
|
127
|
+
runBody.metric.definition.variables?.find((variable) => variable.key === "agentFilter")
|
|
128
|
+
?.options?.[0],
|
|
129
|
+
).toEqual({
|
|
130
|
+
label: "All agents",
|
|
131
|
+
value: "all",
|
|
132
|
+
});
|
|
80
133
|
});
|
|
81
134
|
|
|
82
135
|
test("create, run, update snapshots prior definition", async () => {
|
|
@@ -221,8 +274,79 @@ describe("Metrics HTTP API", () => {
|
|
|
221
274
|
expect(runBody.widgets[0]?.result.rows[0]).toHaveProperty("count");
|
|
222
275
|
});
|
|
223
276
|
|
|
277
|
+
test("run resolves dynamic select variable options from read-only SQL", async () => {
|
|
278
|
+
const created = await fetch(`${BASE}/api/metrics/definitions`, {
|
|
279
|
+
method: "POST",
|
|
280
|
+
headers,
|
|
281
|
+
body: JSON.stringify({
|
|
282
|
+
slug: "dynamic-variable-options",
|
|
283
|
+
title: "Dynamic Variable Options",
|
|
284
|
+
definition: {
|
|
285
|
+
version: 1,
|
|
286
|
+
variables: [
|
|
287
|
+
{
|
|
288
|
+
key: "agent",
|
|
289
|
+
label: "Agent",
|
|
290
|
+
type: "select",
|
|
291
|
+
optionsQuery: {
|
|
292
|
+
sql: "SELECT 'agent-a' AS id, 'Agent A' AS name UNION ALL SELECT 'agent-b' AS id, 'Agent B' AS name",
|
|
293
|
+
valueKey: "id",
|
|
294
|
+
labelKey: "name",
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
],
|
|
298
|
+
widgets: [
|
|
299
|
+
{
|
|
300
|
+
id: "selected-agent",
|
|
301
|
+
title: "Selected agent",
|
|
302
|
+
query: {
|
|
303
|
+
sql: "SELECT ? AS agent",
|
|
304
|
+
params: ["{{agent}}"],
|
|
305
|
+
maxRows: 10,
|
|
306
|
+
},
|
|
307
|
+
viz: { type: "table", columns: [{ key: "agent", label: "Agent" }] },
|
|
308
|
+
},
|
|
309
|
+
],
|
|
310
|
+
},
|
|
311
|
+
}),
|
|
312
|
+
});
|
|
313
|
+
expect(created.status).toBe(201);
|
|
314
|
+
const { id } = (await created.json()) as { id: string; version: number };
|
|
315
|
+
|
|
316
|
+
const run = await fetch(`${BASE}/api/metrics/definitions/${id}/run`, {
|
|
317
|
+
method: "POST",
|
|
318
|
+
headers,
|
|
319
|
+
body: JSON.stringify({ variables: { agent: "agent-b" } }),
|
|
320
|
+
});
|
|
321
|
+
expect(run.status).toBe(200);
|
|
322
|
+
const runBody = (await run.json()) as MetricRunResponse & {
|
|
323
|
+
metric: Metric;
|
|
324
|
+
variables: Record<string, string>;
|
|
325
|
+
};
|
|
326
|
+
expect(runBody.variables.agent).toBe("agent-b");
|
|
327
|
+
expect(runBody.metric.definition.variables?.[0]?.options).toEqual([
|
|
328
|
+
{ label: "Agent A", value: "agent-a" },
|
|
329
|
+
{ label: "Agent B", value: "agent-b" },
|
|
330
|
+
]);
|
|
331
|
+
expect(runBody.widgets[0]?.result.rows[0]).toEqual({ agent: "agent-b" });
|
|
332
|
+
|
|
333
|
+
const defaultedRun = await fetch(`${BASE}/api/metrics/definitions/${id}/run`, {
|
|
334
|
+
method: "POST",
|
|
335
|
+
headers,
|
|
336
|
+
body: JSON.stringify({ variables: {} }),
|
|
337
|
+
});
|
|
338
|
+
expect(defaultedRun.status).toBe(200);
|
|
339
|
+
const defaultedBody = (await defaultedRun.json()) as { variables: Record<string, string> };
|
|
340
|
+
expect(defaultedBody.variables.agent).toBe("agent-a");
|
|
341
|
+
});
|
|
342
|
+
|
|
224
343
|
test("saved metric SQL rejects writes and multiple statements", async () => {
|
|
225
|
-
for (const sql of [
|
|
344
|
+
for (const [sql, target] of [
|
|
345
|
+
["DELETE FROM agent_tasks", "widget"],
|
|
346
|
+
["SELECT 1; SELECT 2", "widget"],
|
|
347
|
+
["DELETE FROM agents", "variable"],
|
|
348
|
+
["SELECT 1; SELECT 2", "variable"],
|
|
349
|
+
] as const) {
|
|
226
350
|
const res = await fetch(`${BASE}/api/metrics/definitions`, {
|
|
227
351
|
method: "POST",
|
|
228
352
|
headers,
|
|
@@ -230,11 +354,21 @@ describe("Metrics HTTP API", () => {
|
|
|
230
354
|
title: "Bad Metric",
|
|
231
355
|
definition: {
|
|
232
356
|
version: 1,
|
|
357
|
+
variables:
|
|
358
|
+
target === "variable"
|
|
359
|
+
? [
|
|
360
|
+
{
|
|
361
|
+
key: "agent",
|
|
362
|
+
type: "select",
|
|
363
|
+
optionsQuery: { sql, valueKey: "id" },
|
|
364
|
+
},
|
|
365
|
+
]
|
|
366
|
+
: undefined,
|
|
233
367
|
widgets: [
|
|
234
368
|
{
|
|
235
369
|
id: "bad",
|
|
236
370
|
title: "Bad",
|
|
237
|
-
query: { sql },
|
|
371
|
+
query: { sql: target === "widget" ? sql : "SELECT 1 AS x" },
|
|
238
372
|
viz: { type: "stat", value: "x" },
|
|
239
373
|
},
|
|
240
374
|
],
|