@desplega.ai/agent-swarm 1.91.0 → 1.92.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/README.md +3 -2
- package/openapi.json +1005 -152
- package/package.json +6 -6
- package/plugin/skills/pages/SKILL.md +5 -2
- package/src/be/db.ts +662 -19
- package/src/be/memory/constants.ts +2 -1
- package/src/be/memory/providers/openai-embedding.ts +2 -5
- package/src/be/memory/providers/sqlite-store.ts +293 -76
- package/src/be/memory/types.ts +35 -0
- package/src/be/migrations/083_script_workflows.sql +51 -0
- package/src/be/migrations/084_script_run_journal_duration.sql +5 -0
- package/src/be/migrations/085_script_runs_kind.sql +9 -0
- package/src/be/migrations/086_pages_default_authed.sql +64 -0
- package/src/be/migrations/087_skill_files.sql +19 -0
- package/src/be/modelsdev-cache.json +42310 -38617
- package/src/be/scripts/typecheck.ts +49 -0
- package/src/be/seed-scripts/catalog/boot-triage.ts +221 -0
- package/src/be/seed-scripts/catalog/catalog-report.ts +457 -0
- package/src/be/seed-scripts/catalog/compound-insights.ts +310 -6
- package/src/be/seed-scripts/catalog/gh-pr-snapshot.ts +1 -1
- package/src/be/seed-scripts/catalog/memory-eval.ts +1059 -0
- package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +506 -0
- package/src/be/seed-scripts/catalog/schedule-health.ts +78 -2
- package/src/be/seed-scripts/catalog/task-context-gathering.ts +92 -0
- package/src/be/seed-scripts/catalog/task-failure-audit.ts +48 -1
- package/src/be/seed-scripts/catalog/tool-usage.ts +6 -3
- package/src/be/seed-scripts/index.ts +51 -5
- package/src/be/seed-skills/index.ts +3 -3
- package/src/be/skill-sync.ts +91 -7
- package/src/be/swarm-config-guard.ts +17 -0
- package/src/commands/runner.ts +49 -4
- package/src/heartbeat/templates.ts +20 -16
- package/src/http/db-query.ts +20 -5
- package/src/http/index.ts +51 -7
- package/src/http/mcp-user.ts +23 -0
- package/src/http/mcp.ts +58 -0
- package/src/http/memory.ts +58 -0
- package/src/http/pages.ts +1 -1
- package/src/http/script-runs.ts +557 -0
- package/src/http/scripts.ts +39 -2
- package/src/http/skills.ts +225 -0
- package/src/prompts/session-templates.ts +24 -4
- package/src/providers/claude-adapter.ts +107 -28
- package/src/script-workflows/executor.ts +110 -0
- package/src/script-workflows/harness.ts +73 -0
- package/src/script-workflows/label-lint.ts +51 -0
- package/src/script-workflows/limits.ts +22 -0
- package/src/script-workflows/supervisor.ts +139 -0
- package/src/script-workflows/workflow-ctx.ts +209 -0
- package/src/scripts-runtime/sdk-allowlist.ts +4 -0
- package/src/scripts-runtime/swarm-sdk.ts +13 -0
- package/src/scripts-runtime/types/stdlib.d.ts +61 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +61 -0
- package/src/server.ts +4 -0
- package/src/slack/handlers.ts +11 -4
- package/src/slack/message-text.ts +98 -0
- package/src/slack/thread-buffer.ts +5 -3
- package/src/tests/claude-adapter-binary.test.ts +271 -74
- package/src/tests/create-page-tool.test.ts +19 -2
- package/src/tests/db-query.test.ts +28 -0
- package/src/tests/error-tracker.test.ts +121 -0
- package/src/tests/harness-provider-resolution.test.ts +33 -0
- package/src/tests/heartbeat-checklist.test.ts +36 -0
- package/src/tests/mcp-tools.test.ts +6 -0
- package/src/tests/mcp-transport-gc.test.ts +58 -0
- package/src/tests/memory-health-endpoint.test.ts +78 -0
- package/src/tests/memory-store.test.ts +221 -1
- package/src/tests/pages-http.test.ts +20 -2
- package/src/tests/pages-storage.test.ts +26 -0
- package/src/tests/prompt-template-session.test.ts +34 -5
- package/src/tests/script-runs-http.test.ts +278 -0
- package/src/tests/script-workflows-label-lint.test.ts +43 -0
- package/src/tests/script-workflows-runtime-e2e.test.ts +170 -0
- package/src/tests/scripts-mcp-e2e.test.ts +102 -2
- package/src/tests/seed-scripts.test.ts +468 -3
- package/src/tests/skill-files-http.test.ts +171 -0
- package/src/tests/skill-files.test.ts +162 -0
- package/src/tests/skill-get-file-tool.test.ts +110 -0
- package/src/tests/skill-sync.test.ts +125 -6
- package/src/tests/slack-message-text.test.ts +250 -0
- package/src/tests/system-default-skills.test.ts +40 -0
- package/src/tools/create-page.ts +2 -2
- package/src/tools/db-query.ts +16 -6
- package/src/tools/script-runs.ts +123 -0
- package/src/tools/skills/index.ts +1 -0
- package/src/tools/skills/skill-get-file.ts +80 -0
- package/src/tools/slack-read.ts +12 -3
- package/src/tools/tool-config.ts +6 -2
- package/src/types.ts +72 -0
- package/src/utils/error-tracker.ts +40 -1
- package/src/utils/internal-ai/complete-structured.ts +10 -4
- package/src/workflows/executors/raw-llm.ts +76 -59
- package/templates/schedules/daily-blocker-digest/content.md +68 -54
- package/templates/schedules/daily-compounding-reflection/content.md +4 -4
- package/templates/schedules/daily-hn-briefing/content.md +5 -5
- package/templates/schedules/daily-workflow-health-audit/content.md +6 -6
- package/templates/schedules/gtm-weekly-review/content.md +9 -9
- package/templates/schedules/weekly-dependabot-triage/content.md +24 -20
- package/templates/skills/agentmail-sending/content.md +6 -7
- package/templates/skills/desloppify/content.md +8 -9
- package/templates/skills/jira-interaction/content.md +25 -33
- package/templates/skills/kapso-whatsapp/content.md +29 -30
- package/templates/skills/linear-interaction/content.md +8 -9
- package/templates/skills/pages/content.md +205 -55
- package/templates/skills/profile-corruption-escalation/content.md +44 -85
- package/templates/skills/script-workflows/config.json +14 -0
- package/templates/skills/script-workflows/content.md +68 -0
- package/templates/skills/sprite-cli/content.md +4 -5
- package/templates/skills/swarm-scripts/content.md +2 -3
- package/templates/skills/turso-interaction/content.md +14 -17
- package/templates/skills/workflow-iterate/content.md +38 -391
- package/templates/skills/x-api-interactions/content.md +4 -6
- package/templates/skills/scheduled-task-resilience/config.json +0 -14
- package/templates/skills/scheduled-task-resilience/content.md +0 -95
|
@@ -63,14 +63,13 @@ describe("create_page MCP tool", () => {
|
|
|
63
63
|
}
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
-
test("first call creates
|
|
66
|
+
test("first call creates an authed row by default + returns shareable URLs", async () => {
|
|
67
67
|
const tool = buildServer();
|
|
68
68
|
const result = (await tool.handler(
|
|
69
69
|
{
|
|
70
70
|
title: "Hello Page",
|
|
71
71
|
body: "<h1>hello</h1>",
|
|
72
72
|
contentType: "text/html",
|
|
73
|
-
authMode: "public",
|
|
74
73
|
},
|
|
75
74
|
fakeMeta,
|
|
76
75
|
)) as {
|
|
@@ -97,6 +96,24 @@ describe("create_page MCP tool", () => {
|
|
|
97
96
|
const row = getPageBySlug(agentId, "hello-page");
|
|
98
97
|
expect(row).not.toBeNull();
|
|
99
98
|
expect(row!.body).toBe("<h1>hello</h1>");
|
|
99
|
+
expect(row!.authMode).toBe("authed");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("explicit public auth mode is preserved", async () => {
|
|
103
|
+
const tool = buildServer();
|
|
104
|
+
await tool.handler(
|
|
105
|
+
{
|
|
106
|
+
title: "Public Page",
|
|
107
|
+
body: "<h1>public</h1>",
|
|
108
|
+
contentType: "text/html",
|
|
109
|
+
authMode: "public",
|
|
110
|
+
},
|
|
111
|
+
fakeMeta,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const row = getPageBySlug(agentId, "public-page");
|
|
115
|
+
expect(row).not.toBeNull();
|
|
116
|
+
expect(row!.authMode).toBe("public");
|
|
100
117
|
});
|
|
101
118
|
|
|
102
119
|
test("re-running with the same slug upserts + bumps edit-counter", async () => {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { DbQueryInputSchema, resolveDbQuerySql } from "../http/db-query";
|
|
3
|
+
|
|
4
|
+
describe("db-query input compatibility", () => {
|
|
5
|
+
test("canonical sql input resolves to sql", () => {
|
|
6
|
+
const parsed = DbQueryInputSchema.parse({ sql: "SELECT 1", params: [] });
|
|
7
|
+
|
|
8
|
+
expect(resolveDbQuerySql(parsed)).toBe("SELECT 1");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("legacy query input remains a runtime alias", () => {
|
|
12
|
+
const parsed = DbQueryInputSchema.parse({ query: "SELECT 2" });
|
|
13
|
+
|
|
14
|
+
expect(resolveDbQuerySql(parsed)).toBe("SELECT 2");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("sql takes precedence when both sql and query are present", () => {
|
|
18
|
+
const parsed = DbQueryInputSchema.parse({ sql: "SELECT 3", query: "SELECT 4" });
|
|
19
|
+
|
|
20
|
+
expect(resolveDbQuerySql(parsed)).toBe("SELECT 3");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("rejects input without sql or query", () => {
|
|
24
|
+
const parsed = DbQueryInputSchema.safeParse({});
|
|
25
|
+
|
|
26
|
+
expect(parsed.success).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import {
|
|
3
|
+
CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS,
|
|
4
|
+
isCodexCreditsExhaustedMessage,
|
|
5
|
+
isRateLimitMessage,
|
|
6
|
+
MAX_RATE_LIMIT_RESET_MS,
|
|
7
|
+
MIN_CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS,
|
|
3
8
|
parseCodexRateLimitResetTime,
|
|
4
9
|
parseRateLimitResetTime,
|
|
5
10
|
parseStderrForErrors,
|
|
11
|
+
resolveCodexCreditsExhaustedCooldownMs,
|
|
6
12
|
SessionErrorTracker,
|
|
7
13
|
trackErrorFromJson,
|
|
8
14
|
} from "../utils/error-tracker";
|
|
@@ -567,6 +573,121 @@ describe("parseRateLimitResetTime", () => {
|
|
|
567
573
|
});
|
|
568
574
|
});
|
|
569
575
|
|
|
576
|
+
describe("isCodexCreditsExhaustedMessage", () => {
|
|
577
|
+
const CANONICAL =
|
|
578
|
+
"Your workspace is out of credits. Ask your workspace owner to refill in order to continue.";
|
|
579
|
+
|
|
580
|
+
test("returns true for the canonical credits-exhausted message", () => {
|
|
581
|
+
expect(isCodexCreditsExhaustedMessage(CANONICAL)).toBe(true);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
test("matches 'out of credits' fragment", () => {
|
|
585
|
+
expect(isCodexCreditsExhaustedMessage("Your workspace is out of credits.")).toBe(true);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
test("matches 'refill in order to continue' fragment", () => {
|
|
589
|
+
expect(isCodexCreditsExhaustedMessage("Please refill in order to continue.")).toBe(true);
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
test("matches 'workspace owner to refill' fragment", () => {
|
|
593
|
+
expect(isCodexCreditsExhaustedMessage("Ask your workspace owner to refill credits.")).toBe(
|
|
594
|
+
true,
|
|
595
|
+
);
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
test("is case-insensitive", () => {
|
|
599
|
+
expect(isCodexCreditsExhaustedMessage("OUT OF CREDITS")).toBe(true);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
test("returns false for unrelated errors", () => {
|
|
603
|
+
expect(isCodexCreditsExhaustedMessage("No conversation found with session ID abc123")).toBe(
|
|
604
|
+
false,
|
|
605
|
+
);
|
|
606
|
+
expect(isCodexCreditsExhaustedMessage("Authentication failed")).toBe(false);
|
|
607
|
+
expect(isCodexCreditsExhaustedMessage("Rate limit exceeded")).toBe(false);
|
|
608
|
+
expect(isCodexCreditsExhaustedMessage("Connection timeout")).toBe(false);
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
test("returns false for bare 'refill' without qualifying context", () => {
|
|
612
|
+
expect(isCodexCreditsExhaustedMessage("Please refill your coffee")).toBe(false);
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
describe("isRateLimitMessage — Codex credits-exhausted integration", () => {
|
|
617
|
+
const CANONICAL =
|
|
618
|
+
"Your workspace is out of credits. Ask your workspace owner to refill in order to continue.";
|
|
619
|
+
|
|
620
|
+
test("returns true for canonical Codex credits-exhausted message", () => {
|
|
621
|
+
expect(isRateLimitMessage(CANONICAL)).toBe(true);
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
test("still returns true for standard rate-limit messages", () => {
|
|
625
|
+
expect(isRateLimitMessage("Rate limit exceeded")).toBe(true);
|
|
626
|
+
expect(isRateLimitMessage("429 Too Many Requests")).toBe(true);
|
|
627
|
+
expect(isRateLimitMessage("You've hit your weekly limit")).toBe(true);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
test("still returns false for unrelated errors", () => {
|
|
631
|
+
expect(isRateLimitMessage("No conversation found with session ID abc123")).toBe(false);
|
|
632
|
+
expect(isRateLimitMessage("Authentication failed")).toBe(false);
|
|
633
|
+
expect(isRateLimitMessage("Server error 500")).toBe(false);
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
describe("resolveCodexCreditsExhaustedCooldownMs", () => {
|
|
638
|
+
test("absent (undefined) → default constant", () => {
|
|
639
|
+
expect(resolveCodexCreditsExhaustedCooldownMs(undefined)).toBe(
|
|
640
|
+
CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS,
|
|
641
|
+
);
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
test("null → default constant", () => {
|
|
645
|
+
expect(resolveCodexCreditsExhaustedCooldownMs(null)).toBe(CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS);
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
test("empty string → default constant", () => {
|
|
649
|
+
expect(resolveCodexCreditsExhaustedCooldownMs("")).toBe(CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS);
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
test("non-numeric string → default constant", () => {
|
|
653
|
+
expect(resolveCodexCreditsExhaustedCooldownMs("abc")).toBe(CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS);
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
test("zero → default constant", () => {
|
|
657
|
+
expect(resolveCodexCreditsExhaustedCooldownMs("0")).toBe(CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS);
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
test("negative → default constant", () => {
|
|
661
|
+
expect(resolveCodexCreditsExhaustedCooldownMs("-5")).toBe(CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS);
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
test("valid in-range string (30m) → parsed value", () => {
|
|
665
|
+
expect(resolveCodexCreditsExhaustedCooldownMs("1800000")).toBe(1_800_000);
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
test("valid in-range number (30m) → parsed value", () => {
|
|
669
|
+
expect(resolveCodexCreditsExhaustedCooldownMs(1_800_000)).toBe(1_800_000);
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
test("below floor → clamped to MIN", () => {
|
|
673
|
+
expect(resolveCodexCreditsExhaustedCooldownMs("1000")).toBe(
|
|
674
|
+
MIN_CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS,
|
|
675
|
+
);
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
test("above ceiling (8d) → clamped to MAX", () => {
|
|
679
|
+
expect(resolveCodexCreditsExhaustedCooldownMs(String(8 * 24 * 60 * 60 * 1000))).toBe(
|
|
680
|
+
MAX_RATE_LIMIT_RESET_MS,
|
|
681
|
+
);
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
test("partial-numeric strings → default constant (not silently truncated)", () => {
|
|
685
|
+
for (const bad of ["60000ms", "1.5", "123abc", "1e5"]) {
|
|
686
|
+
expect(resolveCodexCreditsExhaustedCooldownMs(bad)).toBe(CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS);
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
});
|
|
690
|
+
|
|
570
691
|
describe("Codex/Claude coexistence — single tracker handles both providers", () => {
|
|
571
692
|
test("processRateLimitEvent (Claude) and processCodexUsageLimitMessage (Codex) both stash into getRateLimitResetAt", () => {
|
|
572
693
|
// Claude path: processRateLimitEvent
|
|
@@ -138,6 +138,39 @@ describe("validateConfigValue", () => {
|
|
|
138
138
|
expect(validateConfigValue("HARNESS_PROVIDER", 42)).not.toBeNull();
|
|
139
139
|
expect(validateConfigValue("HARNESS_PROVIDER", null)).not.toBeNull();
|
|
140
140
|
});
|
|
141
|
+
|
|
142
|
+
test("accepts a valid CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS", () => {
|
|
143
|
+
expect(validateConfigValue("CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS", "7200000")).toBeNull();
|
|
144
|
+
expect(validateConfigValue("CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS", "1800000")).toBeNull();
|
|
145
|
+
// case-insensitive key lookup
|
|
146
|
+
expect(validateConfigValue("codex_credits_exhausted_cooldown_ms", "60000")).toBeNull();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("rejects non-positive / non-numeric CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS", () => {
|
|
150
|
+
for (const bad of ["abc", "0", "-5", ""]) {
|
|
151
|
+
const err = validateConfigValue("CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS", bad);
|
|
152
|
+
expect(err).not.toBeNull();
|
|
153
|
+
expect(err).toMatch(/CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS/);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("rejects partial-numeric CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS values", () => {
|
|
158
|
+
for (const bad of ["60000ms", "1.5", "123abc", "1e5", " 60000 ms"]) {
|
|
159
|
+
const err = validateConfigValue("CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS", bad);
|
|
160
|
+
expect(err).not.toBeNull();
|
|
161
|
+
expect(err).toMatch(/CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS/);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("accepts only boolean-like SWARM_USE_CLAUDE_BRIDGE values", () => {
|
|
166
|
+
for (const value of ["true", "false", "1", "0", " TRUE "]) {
|
|
167
|
+
expect(validateConfigValue("SWARM_USE_CLAUDE_BRIDGE", value)).toBeNull();
|
|
168
|
+
}
|
|
169
|
+
expect(validateConfigValue("SWARM_USE_CLAUDE_BRIDGE", "yes")).toMatch(
|
|
170
|
+
/SWARM_USE_CLAUDE_BRIDGE/,
|
|
171
|
+
);
|
|
172
|
+
expect(validateConfigValue("SWARM_USE_CLAUDE_BRIDGE", true)).toMatch(/SWARM_USE_CLAUDE_BRIDGE/);
|
|
173
|
+
});
|
|
141
174
|
});
|
|
142
175
|
|
|
143
176
|
// ─── getResolvedConfig — scope precedence for HARNESS_PROVIDER ───────────────
|
|
@@ -271,6 +271,26 @@ describe("Heartbeat Checklist", () => {
|
|
|
271
271
|
expect(tasks[0]!.task).toContain("Review blocked tasks");
|
|
272
272
|
});
|
|
273
273
|
|
|
274
|
+
test("created task enforces HEARTBEAT tracked-item cap and seeded audit call", async () => {
|
|
275
|
+
const lead = createAgent({ name: "lead", isLead: true, status: "idle" });
|
|
276
|
+
updateAgentProfile(lead.id, {
|
|
277
|
+
heartbeatMd: "- Watch PR #123 until 2026-06-07\n",
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
await checkHeartbeatChecklist();
|
|
281
|
+
|
|
282
|
+
const tasks = getDb()
|
|
283
|
+
.query("SELECT task FROM agent_tasks WHERE taskType = 'heartbeat-checklist'")
|
|
284
|
+
.all() as Array<{ task: string }>;
|
|
285
|
+
expect(tasks.length).toBe(1);
|
|
286
|
+
expect(tasks[0]!.task).toContain("Active Blockers + Watch Items + Open Discussion");
|
|
287
|
+
expect(tasks[0]!.task).toContain("≤10 items");
|
|
288
|
+
expect(tasks[0]!.task).toContain("20 is the absolute max");
|
|
289
|
+
expect(tasks[0]!.task).toContain("script-run");
|
|
290
|
+
expect(tasks[0]!.task).toContain("Heartbeat Audit");
|
|
291
|
+
expect(tasks[0]!.task).toContain("Rule #11");
|
|
292
|
+
});
|
|
293
|
+
|
|
274
294
|
test("created task has correct tags", async () => {
|
|
275
295
|
const lead = createAgent({ name: "lead", isLead: true, status: "idle" });
|
|
276
296
|
updateAgentProfile(lead.id, {
|
|
@@ -369,6 +389,22 @@ describe("Heartbeat Checklist", () => {
|
|
|
369
389
|
expect(tasks[0]!.task).toContain("Check Slack for unaddressed requests");
|
|
370
390
|
});
|
|
371
391
|
|
|
392
|
+
test("boot-triage task enforces HEARTBEAT cap and seeded boot-triage call", async () => {
|
|
393
|
+
createAgent({ name: "lead", isLead: true, status: "idle" });
|
|
394
|
+
|
|
395
|
+
await createBootTriageTask();
|
|
396
|
+
|
|
397
|
+
const tasks = getDb()
|
|
398
|
+
.query("SELECT task FROM agent_tasks WHERE taskType = 'boot-triage'")
|
|
399
|
+
.all() as Array<{ task: string }>;
|
|
400
|
+
expect(tasks[0]!.task).toContain("Active Blockers + Watch Items + Open Discussion");
|
|
401
|
+
expect(tasks[0]!.task).toContain("≤10 items");
|
|
402
|
+
expect(tasks[0]!.task).toContain("20 is the absolute max");
|
|
403
|
+
expect(tasks[0]!.task).toContain("script-run");
|
|
404
|
+
expect(tasks[0]!.task).toContain("boot-triage");
|
|
405
|
+
expect(tasks[0]!.task).toContain("one read-only call");
|
|
406
|
+
});
|
|
407
|
+
|
|
372
408
|
test("dedup: skips when active boot-triage task exists", async () => {
|
|
373
409
|
const lead = createAgent({ name: "lead", isLead: true, status: "idle" });
|
|
374
410
|
updateAgentProfile(lead.id, {
|
|
@@ -55,6 +55,12 @@ describe("script MCP tools", () => {
|
|
|
55
55
|
"Remove a swarm-shared script from the catalog. Versions table preserves history.",
|
|
56
56
|
"script-query-types":
|
|
57
57
|
"Fetch the signature + the auto-generated `swarm-sdk.d.ts` (derived from the live MCP tool registry) + the `stdlib.d.ts` blobs — for IDE-style introspection before authoring or running a script. The same types are used by `script-upsert`'s typecheck pass, so they are authoritative.",
|
|
58
|
+
"launch-script-run":
|
|
59
|
+
"Launch a durable one-off script workflow run. The run executes in the background and can be inspected with get-script-run for terminal status and journal entries.",
|
|
60
|
+
"get-script-run":
|
|
61
|
+
"Get a durable script workflow run by ID, including its journal entries for swarm-script, raw-llm, and agent-task steps.",
|
|
62
|
+
"list-script-runs":
|
|
63
|
+
"List durable script workflow runs, optionally filtered by status or agent ID.",
|
|
58
64
|
};
|
|
59
65
|
|
|
60
66
|
for (const [name, description] of Object.entries(expected)) {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
3
|
+
import { closeIdleMcpTransports } from "../http/mcp";
|
|
4
|
+
import { closeIdleMcpUserTransports } from "../http/mcp-user";
|
|
5
|
+
|
|
6
|
+
function fakeTransport(onClose: () => void): StreamableHTTPServerTransport {
|
|
7
|
+
return {
|
|
8
|
+
close: onClose,
|
|
9
|
+
} as StreamableHTTPServerTransport;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe("MCP transport idle GC", () => {
|
|
13
|
+
test("closes and deletes stale owner transports", () => {
|
|
14
|
+
const closed: string[] = [];
|
|
15
|
+
const transports: Record<string, StreamableHTTPServerTransport> = {
|
|
16
|
+
fresh: fakeTransport(() => closed.push("fresh")),
|
|
17
|
+
stale: fakeTransport(() => closed.push("stale")),
|
|
18
|
+
unknown: fakeTransport(() => closed.push("unknown")),
|
|
19
|
+
};
|
|
20
|
+
const activity = {
|
|
21
|
+
fresh: 9_500,
|
|
22
|
+
stale: 1_000,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const removed = closeIdleMcpTransports(transports, activity, {
|
|
26
|
+
now: 10_000,
|
|
27
|
+
idleTimeoutMs: 1_000,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
expect(removed).toBe(1);
|
|
31
|
+
expect(closed).toEqual(["stale"]);
|
|
32
|
+
expect(transports.stale).toBeUndefined();
|
|
33
|
+
expect(activity.stale).toBeUndefined();
|
|
34
|
+
expect(transports.fresh).toBeDefined();
|
|
35
|
+
expect(transports.unknown).toBeDefined();
|
|
36
|
+
expect(activity.unknown).toBe(10_000);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("deletes user ownership metadata for stale user transports", () => {
|
|
40
|
+
const closed: string[] = [];
|
|
41
|
+
const transports: Record<string, StreamableHTTPServerTransport> = {
|
|
42
|
+
stale: fakeTransport(() => closed.push("stale")),
|
|
43
|
+
};
|
|
44
|
+
const sessionUsers = { stale: "user_1" };
|
|
45
|
+
const activity = { stale: 1_000 };
|
|
46
|
+
|
|
47
|
+
const removed = closeIdleMcpUserTransports(transports, sessionUsers, activity, {
|
|
48
|
+
now: 10_000,
|
|
49
|
+
idleTimeoutMs: 1_000,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(removed).toBe(1);
|
|
53
|
+
expect(closed).toEqual(["stale"]);
|
|
54
|
+
expect(transports.stale).toBeUndefined();
|
|
55
|
+
expect(sessionUsers.stale).toBeUndefined();
|
|
56
|
+
expect(activity.stale).toBeUndefined();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
4
|
+
import { closeDb, initDb } from "../be/db";
|
|
5
|
+
import { EMBEDDING_DIMENSIONS } from "../be/memory/constants";
|
|
6
|
+
import { handleMemory } from "../http/memory";
|
|
7
|
+
import { getPathSegments } from "../http/utils";
|
|
8
|
+
|
|
9
|
+
const TEST_DB_PATH = "./test-memory-health-endpoint.sqlite";
|
|
10
|
+
|
|
11
|
+
function fakeReqRes(path: string) {
|
|
12
|
+
const req = {
|
|
13
|
+
method: "GET",
|
|
14
|
+
url: path,
|
|
15
|
+
headers: {},
|
|
16
|
+
} as unknown as IncomingMessage;
|
|
17
|
+
|
|
18
|
+
const captured = { status: 0, body: "" };
|
|
19
|
+
const res = {
|
|
20
|
+
writeHead(status: number) {
|
|
21
|
+
captured.status = status;
|
|
22
|
+
return this;
|
|
23
|
+
},
|
|
24
|
+
end(chunk?: string) {
|
|
25
|
+
if (chunk) captured.body = chunk;
|
|
26
|
+
return this;
|
|
27
|
+
},
|
|
28
|
+
} as unknown as ServerResponse;
|
|
29
|
+
|
|
30
|
+
return { req, res, captured };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("GET /api/memory/health", () => {
|
|
34
|
+
beforeAll(async () => {
|
|
35
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
36
|
+
try {
|
|
37
|
+
await unlink(TEST_DB_PATH + suffix);
|
|
38
|
+
} catch {}
|
|
39
|
+
}
|
|
40
|
+
initDb(TEST_DB_PATH);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterAll(async () => {
|
|
44
|
+
closeDb();
|
|
45
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
46
|
+
try {
|
|
47
|
+
await unlink(TEST_DB_PATH + suffix);
|
|
48
|
+
} catch {}
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("returns vector index health JSON", async () => {
|
|
53
|
+
const { req, res, captured } = fakeReqRes("/api/memory/health");
|
|
54
|
+
const handled = await handleMemory(req, res, getPathSegments(req.url || ""), undefined);
|
|
55
|
+
|
|
56
|
+
expect(handled).toBe(true);
|
|
57
|
+
expect(captured.status).toBe(200);
|
|
58
|
+
|
|
59
|
+
const body = JSON.parse(captured.body);
|
|
60
|
+
expect(body).toMatchObject({
|
|
61
|
+
sqliteVec: {
|
|
62
|
+
extensionLoaded: expect.any(Boolean),
|
|
63
|
+
tableExists: expect.any(Boolean),
|
|
64
|
+
initialized: expect.any(Boolean),
|
|
65
|
+
vectorDimensions: EMBEDDING_DIMENSIONS,
|
|
66
|
+
distanceMetric: "cosine",
|
|
67
|
+
},
|
|
68
|
+
counts: {
|
|
69
|
+
total: expect.any(Number),
|
|
70
|
+
withEmbedding: expect.any(Number),
|
|
71
|
+
searchable: expect.any(Number),
|
|
72
|
+
memoryVec: expect.any(Number),
|
|
73
|
+
},
|
|
74
|
+
retrievalMode: expect.any(String),
|
|
75
|
+
reasons: expect.any(Array),
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
});
|