@desplega.ai/agent-swarm 1.131.1 → 1.133.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 +2 -2
- package/dist/{actions-jgcy477k.js → actions-cskpayd7.js} +11 -9
- package/dist/{anthropic-messages-rjmd2e7n.js → anthropic-messages-tpxtbx7b.js} +28 -12
- package/dist/{app-5da78bd4.js → app-ang77nbb.js} +8 -6
- package/dist/{assistant-f2vkk1hj.js → assistant-b4exebdw.js} +16 -13
- package/dist/{azure-openai-responses-c0yjmbfd.js → azure-openai-responses-m3921t4y.js} +8 -8
- package/dist/{boot-reembed-q7mp8hcr.js → boot-reembed-ps2aftpb.js} +9 -7
- package/dist/{boot-reembed-xmw8wcx6.js → boot-reembed-znyk3nxb.js} +10 -8
- package/dist/{boot-scrub-logs-dsqrv79n.js → boot-scrub-logs-2ax3xgz1.js} +7 -5
- package/dist/{claude-adapter-q6x7yspx.js → claude-adapter-3ct8kgxw.js} +13 -9
- package/dist/{claude-managed-adapter-wsg37bt8.js → claude-managed-adapter-akx4ntf2.js} +2 -2
- package/dist/{cli-114fvtmj.js → cli-13w2hwkh.js} +1 -1
- package/dist/{cli-4rkfg1wx.js → cli-1532szb1.js} +8 -6
- package/dist/{cli-g74vpjp2.js → cli-23d3rseb.js} +33 -17
- package/dist/{cli-3ty0t082.js → cli-2a75z9c5.js} +196 -7
- package/dist/{cli-pxfxkx5m.js → cli-2t30rj0d.js} +1 -1
- package/dist/{cli-rgrfzg8z.js → cli-3c3tdgab.js} +10 -10
- package/dist/{cli-6dp3ydxa.js → cli-3zzxa9jz.js} +15 -11
- package/dist/{cli-2f07m0xr.js → cli-4k6233gc.js} +1 -1
- package/dist/{cli-t8d349cy.js → cli-5p2tz05r.js} +2 -2
- package/dist/{cli-w2wxd56v.js → cli-5z2yhzvd.js} +127 -49
- package/dist/{cli-kd1ck4rx.js → cli-6zfq31nj.js} +9 -3
- package/dist/{cli-er3a4bb0.js → cli-8m9de10a.js} +6 -1
- package/dist/{cli-gm9cbhbq.js → cli-8t14vg9w.js} +27 -8
- package/dist/{cli-ccf9vy7q.js → cli-9nfc2es8.js} +6 -6
- package/dist/{cli-ycrnynpw.js → cli-9ta3pwms.js} +11 -4
- package/dist/{cli-7dm2zqy2.js → cli-avr0ns7z.js} +2 -2
- package/dist/{cli-n6h8qt8n.js → cli-c897wyhw.js} +12 -7
- package/dist/{cli-gecy3z2s.js → cli-dgk20ft7.js} +4 -4
- package/dist/cli-et1bbwqf.js +13 -0
- package/dist/{cli-bqw0mt1n.js → cli-f5w27pj3.js} +3 -3
- package/dist/{cli-2wadx7p2.js → cli-f8dre4bz.js} +2 -2
- package/dist/{cli-6pcz9h15.js → cli-ftwpv451.js} +170 -387
- package/dist/{cli-nrkkknat.js → cli-g854s2dw.js} +1 -1
- package/dist/{cli-793jvbsh.js → cli-ggmdxeqa.js} +13 -8
- package/dist/cli-hkwfe3qj.js +530 -0
- package/dist/{cli-5dsbcjjw.js → cli-jqwnqx9m.js} +1 -1
- package/dist/{cli-9dr5qzc0.js → cli-ny122vh4.js} +21 -10
- package/dist/{cli-sy7jk3tx.js → cli-pvzqhbs9.js} +2 -2
- package/dist/cli-pzq5rjym.js +15 -0
- package/dist/{cli-xpxde7rm.js → cli-qes8ef67.js} +48 -30
- package/dist/{cli-sajgpkqg.js → cli-qs9hhsvb.js} +1723 -325
- package/dist/cli-qys3rhhz.js +27 -0
- package/dist/{cli-se2p6rkb.js → cli-r6tpm37h.js} +2 -2
- package/dist/{cli-g1re8r05.js → cli-rch80c76.js} +2 -2
- package/dist/{cli-0258203w.js → cli-rpp0x4zy.js} +137 -17
- package/dist/{cli-5n6e5yfn.js → cli-s1k0wxhe.js} +12 -7
- package/dist/{cli-aca5pz9f.js → cli-sfd9v7sj.js} +1 -1
- package/dist/{cli-2rk0cx7s.js → cli-sh3ytsyj.js} +49 -39
- package/dist/{cli-zsspvvm2.js → cli-w1mp9jy7.js} +1 -1
- package/dist/{cli-6dpytdr2.js → cli-xdb4mpnp.js} +2 -2
- package/dist/{cli-57khth04.js → cli-xmyqv00z.js} +120 -4
- package/dist/{cli-jbcva0xj.js → cli-xv8t8p3c.js} +56 -31
- package/dist/{cli-f1am9szv.js → cli-xxv5pn4f.js} +1 -1
- package/dist/{cli-jevgrb4w.js → cli-y08jvxzx.js} +601 -140
- package/dist/{cli-50vbt00w.js → cli-zfbf3srj.js} +650 -245
- package/dist/{cli-ygrbmjja.js → cli-zmq89k34.js} +6 -6
- package/dist/cli.js +22 -22
- package/dist/{codex-adapter-htjkn22b.js → codex-adapter-bef93pq4.js} +9 -7
- package/dist/{codex-hook-hq54t9ft.js → codex-hook-e8dba65p.js} +3 -2
- package/dist/{codex-session-runner-wjfc7smz.js → codex-session-runner-5pkxys7x.js} +9 -7
- package/dist/{commands-xhrfwqsk.js → commands-e23s1bff.js} +9 -7
- package/dist/{db-anvwb22f.js → db-m3nabatn.js} +17 -5
- package/dist/{google-generative-ai-hysrd8qx.js → google-generative-ai-8maw55xh.js} +11 -8
- package/dist/{google-vertex-p5arrga0.js → google-vertex-8vjqmx8g.js} +11 -8
- package/dist/{handlers-b9kbc621.js → handlers-akazedvr.js} +16 -13
- package/dist/{hook-xkvn6c5h.js → hook-ze4cpf0v.js} +55 -41
- package/dist/{hook-cgk4wxmx.js → hook-zfqyyenw.js} +10 -7
- package/dist/{http-9xh6ta4r.js → http-sja0n6kw.js} +608 -147
- package/dist/index-0gr8vd6k.js +39 -0
- package/dist/{index-eaxyqcmj.js → index-354vkhdv.js} +154 -74
- package/dist/{index-070v4eyb.js → index-5qthek5t.js} +2 -3
- package/dist/{index-25wjsdv2.js → index-95t3cqq5.js} +16 -14
- package/dist/{index-9znhar3x.js → index-sea66619.js} +3 -4
- package/dist/{index-ywds4812.js → index-vpn36mma.js} +15 -13
- package/dist/{keepalive-vc7s2aa6.js → keepalive-3sp3meqj.js} +16 -10
- package/dist/{lead-r2yqfmx7.js → lead-mefeewkf.js} +39 -36
- package/dist/{maintenance-mw70k468.js → maintenance-8tbrbjfg.js} +10 -8
- package/dist/mistral-conversations-00zrp7g4.js +717 -0
- package/dist/{oauth-refresh-sweep-yecgjpk4.js → oauth-refresh-sweep-srjwjz6m.js} +8 -6
- package/dist/{onboard-xj0f73f9.js → onboard-mt2tgyjg.js} +7 -7
- package/dist/{openai-codex-responses-5g27pd6f.js → openai-codex-responses-map32h2s.js} +18 -18
- package/dist/{openai-completions-3vkmfz2a.js → openai-completions-w871b1cq.js} +12 -11
- package/dist/{openai-responses-dg4t7j7a.js → openai-responses-b0j7xnw8.js} +15 -12
- package/dist/{opencode-adapter-1gm38aba.js → opencode-adapter-b39jefcj.js} +5 -3
- package/dist/{openrouter-images-g3gm8931.js → openrouter-images-srfkywy0.js} +4 -4
- package/dist/{otel-impl-nxg0n0b3.js → otel-impl-z6pxt3j9.js} +224 -9
- package/dist/{pi-mono-adapter-4g5kby31.js → pi-mono-adapter-btvqmj58.js} +1138 -408
- package/dist/{pricing-refresh-2cn6effr.js → pricing-refresh-kyq8cezh.js} +9 -7
- package/dist/{rbac-roles-1hcxdtg7.js → rbac-roles-3ryfrfna.js} +8 -6
- package/dist/{rbac-roles-vynday5g.js → rbac-roles-6qfvbee1.js} +9 -7
- package/dist/{render-v2-tc0bh8kk.js → render-v2-p61ppjey.js} +8 -6
- package/dist/{seed-pricing-8kceb43f.js → seed-pricing-j56bbj4y.js} +8 -6
- package/dist/{setup-46xtgq3c.js → setup-tpr6c1kn.js} +2 -2
- package/dist/{worker-qrbz6hmv.js → worker-9w1v0mbb.js} +39 -36
- package/openapi.json +542 -16
- package/package.json +6 -6
- package/src/be/budget-refusal-notify.ts +14 -7
- package/src/be/db-queries/task-artifacts.ts +182 -0
- package/src/be/db.ts +783 -169
- package/src/be/events.ts +5 -0
- package/src/be/migrations/131_session_logs_created_at_index.sql +2 -0
- package/src/be/migrations/132_multi_runtime_instances.sql +44 -0
- package/src/be/migrations/133_workflow_run_trigger_type.sql +12 -0
- package/src/be/migrations/135_backfill_task_pull_request_attachments.sql +228 -0
- package/src/be/migrations/136_task_requester_provenance.sql +37 -0
- package/src/be/migrations/137_memory_retrieval_composite_index.sql +1 -0
- package/src/be/multi-runtime.ts +430 -0
- package/src/be/scripts/extract-schema.ts +3 -1
- package/src/be/scripts/typecheck.ts +3 -2
- package/src/be/seed-skills/index.ts +7 -0
- package/src/be/steering.ts +1 -1
- package/src/be/swarm-config-guard.ts +18 -0
- package/src/commands/credential-wait.ts +54 -0
- package/src/commands/onboard/steps/core-credentials.tsx +1 -1
- package/src/commands/onboard/steps/custom-templates.tsx +1 -1
- package/src/commands/onboard/steps/harness-credentials.tsx +1 -1
- package/src/commands/onboard/steps/health-check.tsx +1 -1
- package/src/commands/onboard/steps/start.tsx +1 -1
- package/src/commands/profile-sync-audit.ts +56 -0
- package/src/commands/profile-sync.ts +287 -15
- package/src/commands/provider-credentials.ts +46 -19
- package/src/commands/runner.ts +196 -33
- package/src/commands/templates.ts +22 -0
- package/src/github/handlers.ts +55 -9
- package/src/heartbeat/heartbeat.ts +71 -12
- package/src/hooks/hook.ts +49 -33
- package/src/http/active-sessions.ts +11 -1
- package/src/http/agents.ts +261 -30
- package/src/http/all-routes.ts +1 -0
- package/src/http/config.ts +13 -2
- package/src/http/core.ts +102 -4
- package/src/http/db-query-bounded.ts +313 -0
- package/src/http/db-query-shared.ts +249 -0
- package/src/http/db-query.ts +54 -42
- package/src/http/events.ts +2 -0
- package/src/http/index.ts +22 -2
- package/src/http/mcp-bridge.ts +37 -3
- package/src/http/mcp.ts +12 -0
- package/src/http/memory.ts +13 -2
- package/src/http/openapi.ts +1 -0
- package/src/http/poll.ts +70 -27
- package/src/http/route-def.ts +43 -9
- package/src/http/schedules.ts +2 -2
- package/src/http/scripts.ts +10 -1
- package/src/http/session-data.ts +44 -0
- package/src/http/tasks.ts +2 -2
- package/src/http/users.ts +74 -52
- package/src/http/webhooks.ts +5 -2
- package/src/http/workflows.ts +1 -4
- package/src/integrations/kapso/inbound.ts +1 -1
- package/src/jira/outbound.ts +39 -8
- package/src/jira/webhook-lifecycle.ts +13 -2
- package/src/linear/outbound.ts +43 -10
- package/src/oauth/keepalive.ts +13 -2
- package/src/prompts/base-prompt.ts +1 -1
- package/src/prompts/session-templates.ts +45 -7
- package/src/providers/claude-adapter.ts +6 -0
- package/src/providers/codex-adapter.ts +6 -0
- package/src/providers/opencode-adapter.ts +6 -0
- package/src/providers/pi-mono-adapter.ts +8 -0
- package/src/providers/pricing-sources.md +24 -0
- package/src/queue-stall-alarm.ts +216 -0
- package/src/scheduler/scheduler.ts +7 -0
- package/src/script-workflows/executor.ts +2 -1
- package/src/scripts-runtime/executors/native.ts +2 -1
- package/src/scripts-runtime/executors/types.ts +5 -0
- package/src/scripts-runtime/loader.ts +5 -0
- package/src/scripts-runtime/swarm-config.ts +8 -0
- package/src/scripts-runtime/swarm-sdk.ts +7 -0
- package/src/scripts-runtime/types/stdlib.d.ts +1 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
- package/src/tests/budget-claim-gate.test.ts +20 -0
- package/src/tests/claude-adapter.test.ts +32 -0
- package/src/tests/codex-adapter.test.ts +30 -0
- package/src/tests/create-task-validation.test.ts +128 -0
- package/src/tests/credential-wait.test.ts +67 -1
- package/src/tests/db-query.test.ts +711 -2
- package/src/tests/http-api-integration.test.ts +81 -0
- package/src/tests/http-users.test.ts +166 -1
- package/src/tests/identity-field-budget.test.ts +176 -0
- package/src/tests/mcp-tools-user.test.ts +34 -0
- package/src/tests/mcp-user-route.test.ts +17 -0
- package/src/tests/metrics-http.test.ts +44 -0
- package/src/tests/multi-runtime-registration.test.ts +2703 -0
- package/src/tests/openapi-response-contract.test.ts +24 -0
- package/src/tests/openapi-route-headers.test.ts +83 -0
- package/src/tests/opencode-adapter.test.ts +40 -0
- package/src/tests/outbound-listener-rejection.test.ts +138 -0
- package/src/tests/pi-mono-adapter.test.ts +98 -0
- package/src/tests/profile-sync-warning.test.ts +277 -0
- package/src/tests/profile-sync.test.ts +232 -6
- package/src/tests/prompt-template-session.test.ts +32 -3
- package/src/tests/queue-stall-alarm.test.ts +226 -0
- package/src/tests/requester-comms.test.ts +32 -0
- package/src/tests/runner-requester-profile.test.ts +38 -0
- package/src/tests/schedule-target-type.test.ts +38 -0
- package/src/tests/script-connections.test.ts +13 -1
- package/src/tests/scripts-runtime-identity.test.ts +375 -0
- package/src/tests/scripts-runtime.test.ts +4 -3
- package/src/tests/sdk-allowlist.test.ts +47 -0
- package/src/tests/send-task-requested-by.test.ts +9 -0
- package/src/tests/session-costs.test.ts +506 -1
- package/src/tests/shutdown-error-scrubbing.test.ts +92 -0
- package/src/tests/steering-core.test.ts +28 -0
- package/src/tests/task-artifacts.test.ts +386 -0
- package/src/tests/task-attachments-schema.test.ts +17 -1
- package/src/tests/task-completion-idempotency.test.ts +57 -0
- package/src/tests/task-pull-request-backfill-migration.test.ts +321 -0
- package/src/tests/update-profile-api.test.ts +67 -39
- package/src/tests/update-profile-auth.test.ts +48 -4
- package/src/tests/user-attribution-migration.test.ts +54 -0
- package/src/tests/user-identity.test.ts +72 -0
- package/src/tests/workflow-async-v2.test.ts +39 -1
- package/src/tests/workflow-http-v2.test.ts +3 -2
- package/src/tests/workflow-mcp-trigger-schema.test.ts +52 -3
- package/src/tests/workflow-retry-v2.test.ts +45 -1
- package/src/tools/db-query.ts +18 -8
- package/src/tools/get-task-details.ts +24 -3
- package/src/tools/manage-user.ts +36 -3
- package/src/tools/poll-task.ts +93 -14
- package/src/tools/script-common.ts +5 -0
- package/src/tools/send-task.ts +1 -4
- package/src/tools/store-progress.ts +13 -2
- package/src/tools/swarm-config/delete-config.ts +9 -0
- package/src/tools/swarm-config/set-config.ts +3 -2
- package/src/tools/task-action.ts +30 -0
- package/src/tools/task-tool-ctx.ts +8 -1
- package/src/tools/update-profile.ts +16 -21
- package/src/tools/utils.ts +5 -0
- package/src/tools/workflows/trigger-workflow.ts +7 -1
- package/src/types.ts +165 -1
- package/src/utils/github-pull-request.ts +76 -0
- package/src/utils/identity-field-budget.ts +73 -0
- package/src/utils/multi-runtime.ts +20 -0
- package/src/utils/requester-comms.ts +24 -0
- package/src/workflows/engine.ts +22 -2
- package/src/workflows/resume.ts +19 -11
- package/src/workflows/retry-poller.ts +1 -0
- package/src/workflows/triggers.ts +4 -0
- package/templates/skills/db-query-guidance/config.json +14 -0
- package/templates/skills/db-query-guidance/content.md +57 -0
- package/dist/cli-3te4jdkm.js +0 -225
- package/dist/cli-bmze986d.js +0 -7
- package/dist/cli-kh3ba2zf.js +0 -607
- package/dist/index-vg4snay4.js +0 -36
- package/dist/mistral-conversations-81fwav0j.js +0 -30578
- package/dist/otel-nqa12wf3.js +0 -3149
- package/dist/{cli-wz9115x0.js → cli-zr9q011v.js} +6 -6
- package/dist/{pi-messages-tqj8q6b9.js → pi-messages-sssy32x4.js} +3 -3
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { describe, expect, spyOn, test } from "bun:test";
|
|
2
2
|
import {
|
|
3
3
|
buildIdentityPayload,
|
|
4
|
+
buildIndependentIdentityPayloads,
|
|
4
5
|
CLAUDE_MD_PATH,
|
|
5
6
|
collectProfilePayloads,
|
|
6
7
|
contentSha256,
|
|
7
8
|
extractSetupScriptContent,
|
|
8
9
|
type FileReader,
|
|
10
|
+
findProfileDivergences,
|
|
11
|
+
HEARTBEAT_MD_PATH,
|
|
9
12
|
IDENTITY_BASELINES_PATH,
|
|
10
13
|
IDENTITY_MD_PATH,
|
|
11
14
|
type IdentityBaselines,
|
|
@@ -17,7 +20,9 @@ import {
|
|
|
17
20
|
TOOLS_MD_PATH,
|
|
18
21
|
WORKSPACE_CLAUDE_MD_PATH,
|
|
19
22
|
} from "../commands/profile-sync";
|
|
23
|
+
import { profileSyncAuditExitCode, runProfileSyncAudit } from "../commands/profile-sync-audit";
|
|
20
24
|
import { MAX_PROFILE_FILE_LENGTH } from "../utils/constants";
|
|
25
|
+
import { IDENTITY_FIELD_BUDGETS } from "../utils/identity-field-budget";
|
|
21
26
|
|
|
22
27
|
const MARKER_START = "# === Agent-managed setup (from DB) ===";
|
|
23
28
|
const MARKER_END = "# === End agent-managed setup ===";
|
|
@@ -25,6 +30,182 @@ const MARKER_END = "# === End agent-managed setup ===";
|
|
|
25
30
|
// A SOUL/IDENTITY body long enough to clear the 500-char min-length guard.
|
|
26
31
|
const LONG = "x".repeat(600);
|
|
27
32
|
|
|
33
|
+
describe("profile divergence audit", () => {
|
|
34
|
+
test("compares all four budgeted fields plus ungated heartbeatMd", () => {
|
|
35
|
+
const divergences = findProfileDivergences(
|
|
36
|
+
{
|
|
37
|
+
soulMd: "disk soul",
|
|
38
|
+
identityMd: "disk identity",
|
|
39
|
+
claudeMd: "disk claude",
|
|
40
|
+
toolsMd: "disk tools",
|
|
41
|
+
heartbeatMd: "disk heartbeat",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
soulMd: "db soul",
|
|
45
|
+
identityMd: "db identity",
|
|
46
|
+
claudeMd: "db claude",
|
|
47
|
+
toolsMd: "db tools",
|
|
48
|
+
heartbeatMd: "db heartbeat",
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
expect(divergences.map((entry) => entry.field)).toEqual([
|
|
53
|
+
"soulMd",
|
|
54
|
+
"identityMd",
|
|
55
|
+
"claudeMd",
|
|
56
|
+
"toolsMd",
|
|
57
|
+
"heartbeatMd",
|
|
58
|
+
]);
|
|
59
|
+
expect(divergences[0]?.budget).toBe(IDENTITY_FIELD_BUDGETS.soulMd);
|
|
60
|
+
expect(divergences[2]?.budget).toBe(IDENTITY_FIELD_BUDGETS.claudeMd);
|
|
61
|
+
expect(divergences[4]?.budget).toBeNull();
|
|
62
|
+
expect(divergences[4]?.diskSize).toBe("disk heartbeat".length);
|
|
63
|
+
expect(divergences[4]?.dbSize).toBe("db heartbeat".length);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("fetches the DB profile and reads the canonical local paths", async () => {
|
|
67
|
+
const local = {
|
|
68
|
+
[SOUL_MD_PATH]: "same soul",
|
|
69
|
+
[IDENTITY_MD_PATH]: "disk identity",
|
|
70
|
+
[WORKSPACE_CLAUDE_MD_PATH]: "same claude",
|
|
71
|
+
[TOOLS_MD_PATH]: "same tools",
|
|
72
|
+
[HEARTBEAT_MD_PATH]: "disk heartbeat",
|
|
73
|
+
};
|
|
74
|
+
const readFile: FileReader = async (path) => local[path as keyof typeof local];
|
|
75
|
+
let request: Request | null = null;
|
|
76
|
+
const fetchImpl = (async (input: string | URL | Request, init?: RequestInit) => {
|
|
77
|
+
request = new Request(input, init);
|
|
78
|
+
return new Response(
|
|
79
|
+
JSON.stringify({
|
|
80
|
+
soulMd: "same soul",
|
|
81
|
+
identityMd: "db identity",
|
|
82
|
+
claudeMd: "same claude",
|
|
83
|
+
toolsMd: "same tools",
|
|
84
|
+
heartbeatMd: "db heartbeat",
|
|
85
|
+
}),
|
|
86
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
87
|
+
);
|
|
88
|
+
}) as typeof fetch;
|
|
89
|
+
|
|
90
|
+
const result = await runProfileSyncAudit(
|
|
91
|
+
{
|
|
92
|
+
agentId: "agent-1",
|
|
93
|
+
apiUrl: "https://api.example.test",
|
|
94
|
+
apiKey: "secret-key",
|
|
95
|
+
claudeMdPath: WORKSPACE_CLAUDE_MD_PATH,
|
|
96
|
+
},
|
|
97
|
+
{ fetchImpl, readFile },
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
expect(request?.url).toBe("https://api.example.test/me");
|
|
101
|
+
expect(request?.headers.get("X-Agent-ID")).toBe("agent-1");
|
|
102
|
+
expect(result.divergences.map((entry) => entry.field)).toEqual(["identityMd", "heartbeatMd"]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("uses the registered effective provider to select the Claude profile path", async () => {
|
|
106
|
+
const readPaths: string[] = [];
|
|
107
|
+
const readFile: FileReader = async (path) => {
|
|
108
|
+
readPaths.push(path);
|
|
109
|
+
return "same";
|
|
110
|
+
};
|
|
111
|
+
const fetchImpl = (async () =>
|
|
112
|
+
new Response(
|
|
113
|
+
JSON.stringify({
|
|
114
|
+
harnessProvider: "codex",
|
|
115
|
+
soulMd: "same",
|
|
116
|
+
identityMd: "same",
|
|
117
|
+
claudeMd: "same",
|
|
118
|
+
toolsMd: "same",
|
|
119
|
+
heartbeatMd: "same",
|
|
120
|
+
}),
|
|
121
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
122
|
+
)) as typeof fetch;
|
|
123
|
+
|
|
124
|
+
const result = await runProfileSyncAudit(
|
|
125
|
+
{
|
|
126
|
+
agentId: "agent-codex",
|
|
127
|
+
apiUrl: "https://api.example.test",
|
|
128
|
+
apiKey: "secret-key",
|
|
129
|
+
},
|
|
130
|
+
{ fetchImpl, readFile },
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
expect(result.divergences).toEqual([]);
|
|
134
|
+
expect(readPaths).toContain(WORKSPACE_CLAUDE_MD_PATH);
|
|
135
|
+
expect(readPaths).not.toContain(CLAUDE_MD_PATH);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("refuses to guess the audit path when no effective provider is registered", async () => {
|
|
139
|
+
const fetchImpl = (async () =>
|
|
140
|
+
new Response(JSON.stringify({}), {
|
|
141
|
+
status: 200,
|
|
142
|
+
headers: { "Content-Type": "application/json" },
|
|
143
|
+
})) as typeof fetch;
|
|
144
|
+
|
|
145
|
+
expect(
|
|
146
|
+
runProfileSyncAudit(
|
|
147
|
+
{
|
|
148
|
+
agentId: "agent-unregistered",
|
|
149
|
+
apiUrl: "https://api.example.test",
|
|
150
|
+
apiKey: "secret-key",
|
|
151
|
+
},
|
|
152
|
+
{ fetchImpl, readFile: async () => undefined },
|
|
153
|
+
),
|
|
154
|
+
).rejects.toThrow("registered harness provider");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("treats profile fields absent on disk and empty in the DB as equal", async () => {
|
|
158
|
+
const readFile: FileReader = async () => undefined;
|
|
159
|
+
const fetchImpl = (async () =>
|
|
160
|
+
new Response(JSON.stringify({ soulMd: "" }), {
|
|
161
|
+
status: 200,
|
|
162
|
+
headers: { "Content-Type": "application/json" },
|
|
163
|
+
})) as typeof fetch;
|
|
164
|
+
|
|
165
|
+
const result = await runProfileSyncAudit(
|
|
166
|
+
{
|
|
167
|
+
agentId: "agent-unset",
|
|
168
|
+
apiUrl: "https://api.example.test",
|
|
169
|
+
apiKey: "secret-key",
|
|
170
|
+
claudeMdPath: WORKSPACE_CLAUDE_MD_PATH,
|
|
171
|
+
},
|
|
172
|
+
{ fetchImpl, readFile },
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
expect(result.divergences).toEqual([]);
|
|
176
|
+
expect(profileSyncAuditExitCode(result.divergences)).toBe(0);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("reports a missing local file when the DB value is non-empty", () => {
|
|
180
|
+
const divergences = findProfileDivergences(
|
|
181
|
+
{
|
|
182
|
+
soulMd: undefined,
|
|
183
|
+
identityMd: "same identity",
|
|
184
|
+
claudeMd: "same claude",
|
|
185
|
+
toolsMd: "same tools",
|
|
186
|
+
heartbeatMd: "same heartbeat",
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
soulMd: "stored soul",
|
|
190
|
+
identityMd: "same identity",
|
|
191
|
+
claudeMd: "same claude",
|
|
192
|
+
toolsMd: "same tools",
|
|
193
|
+
heartbeatMd: "same heartbeat",
|
|
194
|
+
},
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
expect(divergences).toHaveLength(1);
|
|
198
|
+
expect(divergences[0]).toMatchObject({
|
|
199
|
+
field: "soulMd",
|
|
200
|
+
diskMissing: true,
|
|
201
|
+
diskSize: null,
|
|
202
|
+
diskHash: null,
|
|
203
|
+
dbSize: "stored soul".length,
|
|
204
|
+
delta: null,
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
28
209
|
describe("extractSetupScriptContent (marker extraction)", () => {
|
|
29
210
|
test("extracts ONLY the content between the agent-managed markers", () => {
|
|
30
211
|
const raw = [
|
|
@@ -97,6 +278,19 @@ describe("buildIdentityPayload (min-length guard)", () => {
|
|
|
97
278
|
expect(payload.heartbeatMd).toBe("");
|
|
98
279
|
});
|
|
99
280
|
|
|
281
|
+
test("forwards oversized gated identity fields so the server ratchet can decide", () => {
|
|
282
|
+
const oversized = "x".repeat(MAX_PROFILE_FILE_LENGTH + 1);
|
|
283
|
+
const payload = buildIdentityPayload({
|
|
284
|
+
soulMd: oversized,
|
|
285
|
+
identityMd: oversized,
|
|
286
|
+
toolsMd: oversized,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
expect(payload.soulMd).toBe(oversized);
|
|
290
|
+
expect(payload.identityMd).toBe(oversized);
|
|
291
|
+
expect(payload.toolsMd).toBe(oversized);
|
|
292
|
+
});
|
|
293
|
+
|
|
100
294
|
test("logs the agent, field, actual length, and cap when a file is too large", () => {
|
|
101
295
|
const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
|
102
296
|
try {
|
|
@@ -142,7 +336,13 @@ describe("collectProfilePayloads (field gate)", () => {
|
|
|
142
336
|
"session_sync",
|
|
143
337
|
files,
|
|
144
338
|
);
|
|
145
|
-
expect(all.map((p) => p.label).sort()).toEqual([
|
|
339
|
+
expect(all.map((p) => p.label).sort()).toEqual([
|
|
340
|
+
"claude",
|
|
341
|
+
"identity.identityMd",
|
|
342
|
+
"identity.soulMd",
|
|
343
|
+
"identity.toolsMd",
|
|
344
|
+
"setup",
|
|
345
|
+
]);
|
|
146
346
|
});
|
|
147
347
|
|
|
148
348
|
test("a missing file yields no payload for that group (no empty POST)", async () => {
|
|
@@ -413,11 +613,13 @@ describe("collectProfilePayloads (baseline integration)", () => {
|
|
|
413
613
|
});
|
|
414
614
|
|
|
415
615
|
const payloads = await collectProfilePayloads(["identity"], "self_edit", files);
|
|
416
|
-
expect(payloads).toHaveLength(
|
|
417
|
-
// self_edit should include ALL files regardless of baselines
|
|
418
|
-
expect(payloads
|
|
419
|
-
|
|
420
|
-
|
|
616
|
+
expect(payloads).toHaveLength(3);
|
|
617
|
+
// self_edit should include ALL files regardless of baselines, independently.
|
|
618
|
+
expect(payloads.map((payload) => payload.body)).toEqual([
|
|
619
|
+
{ soulMd: identityContent, changeSource: "self_edit" },
|
|
620
|
+
{ identityMd: identityContent, changeSource: "self_edit" },
|
|
621
|
+
{ toolsMd: "tools", changeSource: "self_edit" },
|
|
622
|
+
]);
|
|
421
623
|
});
|
|
422
624
|
|
|
423
625
|
test("session_sync skips unchanged CLAUDE.md when baseline matches", async () => {
|
|
@@ -482,3 +684,27 @@ describe("collectProfilePayloads (baseline integration)", () => {
|
|
|
482
684
|
expect(payloads).toEqual([]);
|
|
483
685
|
});
|
|
484
686
|
});
|
|
687
|
+
|
|
688
|
+
describe("buildIndependentIdentityPayloads", () => {
|
|
689
|
+
test("splits fields so one rejected write cannot discard valid siblings", () => {
|
|
690
|
+
expect(
|
|
691
|
+
buildIndependentIdentityPayloads(
|
|
692
|
+
{ soulMd: "rejected growth", toolsMd: "valid edit", heartbeatMd: "ungated edit" },
|
|
693
|
+
"session_sync",
|
|
694
|
+
),
|
|
695
|
+
).toEqual([
|
|
696
|
+
{
|
|
697
|
+
label: "identity.soulMd",
|
|
698
|
+
body: { soulMd: "rejected growth", changeSource: "session_sync" },
|
|
699
|
+
},
|
|
700
|
+
{
|
|
701
|
+
label: "identity.toolsMd",
|
|
702
|
+
body: { toolsMd: "valid edit", changeSource: "session_sync" },
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
label: "identity.heartbeatMd",
|
|
706
|
+
body: { heartbeatMd: "ungated edit", changeSource: "session_sync" },
|
|
707
|
+
},
|
|
708
|
+
]);
|
|
709
|
+
});
|
|
710
|
+
});
|
|
@@ -68,7 +68,7 @@ describe("Session templates — registration", () => {
|
|
|
68
68
|
await ensureTemplatesRegistered();
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
-
test("all
|
|
71
|
+
test("all 19 system templates are registered", () => {
|
|
72
72
|
const systemTemplates = [
|
|
73
73
|
"system.agent.role",
|
|
74
74
|
"system.agent.register",
|
|
@@ -89,6 +89,7 @@ describe("Session templates — registration", () => {
|
|
|
89
89
|
"system.agent.services",
|
|
90
90
|
"system.agent.artifacts",
|
|
91
91
|
"system.agent.share_urls",
|
|
92
|
+
"system.agent.communication_style",
|
|
92
93
|
];
|
|
93
94
|
|
|
94
95
|
for (const eventType of systemTemplates) {
|
|
@@ -113,7 +114,7 @@ describe("Session templates — registration", () => {
|
|
|
113
114
|
}
|
|
114
115
|
});
|
|
115
116
|
|
|
116
|
-
test("total of
|
|
117
|
+
test("total of 32 session/system templates registered", () => {
|
|
117
118
|
const all = getAllTemplateDefinitions();
|
|
118
119
|
const sessionSystem = all.filter((d) => d.category === "system" || d.category === "session");
|
|
119
120
|
// 26 = the original 19 + `system.session.worker.pi` + `system.agent.seed_scripts`
|
|
@@ -124,7 +125,8 @@ describe("Session templates — registration", () => {
|
|
|
124
125
|
// 30 = 29 + `system.agent.scheduling` (split out of context_mode so the
|
|
125
126
|
// pi composites keep the scheduling rules without the ctx_* tool block).
|
|
126
127
|
// 31 = 30 + `system.agent.apps`.
|
|
127
|
-
|
|
128
|
+
// 32 = 31 + `system.agent.communication_style`.
|
|
129
|
+
expect(sessionSystem.length).toBe(32);
|
|
128
130
|
});
|
|
129
131
|
});
|
|
130
132
|
|
|
@@ -332,6 +334,9 @@ describe("Session templates — composite resolution", () => {
|
|
|
332
334
|
// Contains register section
|
|
333
335
|
expect(result.text).toContain("join-swarm");
|
|
334
336
|
|
|
337
|
+
// Lead is hinted to persist learned requester comms preferences
|
|
338
|
+
expect(result.text).toContain("persist them with its `comms` field");
|
|
339
|
+
|
|
335
340
|
// Contains lead-specific section (not worker)
|
|
336
341
|
expect(result.text).toContain("CRITICAL: You are a coordinator");
|
|
337
342
|
expect(result.text).toContain("coordinator");
|
|
@@ -351,6 +356,9 @@ describe("Session templates — composite resolution", () => {
|
|
|
351
356
|
|
|
352
357
|
// Contains system
|
|
353
358
|
expect(result.text).toContain("System packages available");
|
|
359
|
+
|
|
360
|
+
// Contains communication style
|
|
361
|
+
expect(result.text).toContain("### Communication Style");
|
|
354
362
|
});
|
|
355
363
|
|
|
356
364
|
test("system.session.worker resolves all template references", () => {
|
|
@@ -387,6 +395,27 @@ describe("Session templates — composite resolution", () => {
|
|
|
387
395
|
|
|
388
396
|
// Contains system
|
|
389
397
|
expect(result.text).toContain("System packages available");
|
|
398
|
+
|
|
399
|
+
// Contains communication style
|
|
400
|
+
expect(result.text).toContain("### Communication Style");
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test("communication style block resolves in every session composite", () => {
|
|
404
|
+
const composites = [
|
|
405
|
+
"system.session.lead",
|
|
406
|
+
"system.session.worker",
|
|
407
|
+
"system.session.worker.pi",
|
|
408
|
+
"system.session.lead.pi",
|
|
409
|
+
"system.session.worker.remote",
|
|
410
|
+
];
|
|
411
|
+
|
|
412
|
+
for (const eventType of composites) {
|
|
413
|
+
const result = resolveTemplate(eventType, { role: "worker", agentId: "agent-001" });
|
|
414
|
+
expect(result.skipped).toBe(false);
|
|
415
|
+
expect(result.text).toContain("### Communication Style");
|
|
416
|
+
expect(result.text).toContain("Mirror the requester");
|
|
417
|
+
expect(result.text).toContain("Never use em dashes");
|
|
418
|
+
}
|
|
390
419
|
});
|
|
391
420
|
|
|
392
421
|
test("composite does NOT include conditional sections (agent_fs, services, artifacts)", () => {
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import { closeDb, createAgent, createTaskExtended, getDb, initDb, updateTaskVcs } from "../be/db";
|
|
4
|
+
import {
|
|
5
|
+
_test,
|
|
6
|
+
checkQueueStall,
|
|
7
|
+
getQueueStallSnapshot,
|
|
8
|
+
isQueueStalled,
|
|
9
|
+
QUEUE_STALL_THRESHOLD_MS,
|
|
10
|
+
} from "../queue-stall-alarm";
|
|
11
|
+
|
|
12
|
+
const TEST_DB_PATH = "./test-queue-stall-alarm.sqlite";
|
|
13
|
+
const NOW = new Date("2026-08-17T17:00:00.000Z");
|
|
14
|
+
|
|
15
|
+
beforeAll(() => {
|
|
16
|
+
closeDb();
|
|
17
|
+
initDb(TEST_DB_PATH);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
_test.resetState();
|
|
22
|
+
getDb().run("DELETE FROM agent_log");
|
|
23
|
+
getDb().run("DELETE FROM agent_tasks");
|
|
24
|
+
getDb().run("DELETE FROM agents");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterAll(async () => {
|
|
28
|
+
_test.resetState();
|
|
29
|
+
closeDb();
|
|
30
|
+
await unlink(TEST_DB_PATH).catch(() => {});
|
|
31
|
+
await unlink(`${TEST_DB_PATH}-wal`).catch(() => {});
|
|
32
|
+
await unlink(`${TEST_DB_PATH}-shm`).catch(() => {});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
function setCreatedAt(taskId: string, createdAt: string): void {
|
|
36
|
+
getDb().run("UPDATE agent_tasks SET createdAt = ?, lastUpdatedAt = ? WHERE id = ?", [
|
|
37
|
+
createdAt,
|
|
38
|
+
createdAt,
|
|
39
|
+
taskId,
|
|
40
|
+
]);
|
|
41
|
+
getDb().run(
|
|
42
|
+
"UPDATE agent_log SET createdAt = ? WHERE taskId = ? AND eventType = 'task_created'",
|
|
43
|
+
[createdAt, taskId],
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe("queue stall alarm", () => {
|
|
48
|
+
test("positive control: alerts when a claimable task is older than 30 minutes", async () => {
|
|
49
|
+
const worker = createAgent({ name: "worker", isLead: false, status: "idle" });
|
|
50
|
+
const task = createTaskExtended("Old claimable task", { agentId: worker.id });
|
|
51
|
+
setCreatedAt(task.id, "2026-08-17T16:29:59.000Z");
|
|
52
|
+
const notify = mock(async (_message: string) => {});
|
|
53
|
+
|
|
54
|
+
const snapshot = await checkQueueStall(NOW, notify);
|
|
55
|
+
|
|
56
|
+
expect(isQueueStalled(snapshot)).toBe(true);
|
|
57
|
+
expect(snapshot.claimableCount).toBe(1);
|
|
58
|
+
expect(snapshot.oldestTaskId).toBe(task.id);
|
|
59
|
+
expect(snapshot.oldestAgeMs).toBeGreaterThan(QUEUE_STALL_THRESHOLD_MS);
|
|
60
|
+
expect(notify).toHaveBeenCalledTimes(1);
|
|
61
|
+
expect(notify.mock.calls[0]?.[0]).toContain("queue pickup stalled");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("negative control: an empty queue never alerts", async () => {
|
|
65
|
+
const notify = mock(async (_message: string) => {});
|
|
66
|
+
|
|
67
|
+
const snapshot = await checkQueueStall(NOW, notify);
|
|
68
|
+
|
|
69
|
+
expect(snapshot.claimableCount).toBe(0);
|
|
70
|
+
expect(isQueueStalled(snapshot)).toBe(false);
|
|
71
|
+
expect(notify).not.toHaveBeenCalled();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("does not alert for a non-empty but fresh queue", async () => {
|
|
75
|
+
const task = createTaskExtended("Fresh pool task");
|
|
76
|
+
setCreatedAt(task.id, "2026-08-17T16:45:00.000Z");
|
|
77
|
+
const notify = mock(async (_message: string) => {});
|
|
78
|
+
|
|
79
|
+
const snapshot = await checkQueueStall(NOW, notify);
|
|
80
|
+
|
|
81
|
+
expect(snapshot.claimableCount).toBe(1);
|
|
82
|
+
expect(isQueueStalled(snapshot)).toBe(false);
|
|
83
|
+
expect(notify).not.toHaveBeenCalled();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("measures queue age from a backlog task becoming claimable", async () => {
|
|
87
|
+
const task = createTaskExtended("Released backlog task");
|
|
88
|
+
setCreatedAt(task.id, "2026-08-17T12:00:00.000Z");
|
|
89
|
+
getDb().run("UPDATE agent_tasks SET status = 'backlog' WHERE id = ?", [task.id]);
|
|
90
|
+
getDb().run("UPDATE agent_tasks SET status = 'unassigned', lastUpdatedAt = ? WHERE id = ?", [
|
|
91
|
+
"2026-08-17T16:59:00.000Z",
|
|
92
|
+
task.id,
|
|
93
|
+
]);
|
|
94
|
+
getDb().run(
|
|
95
|
+
`INSERT INTO agent_log (id, eventType, taskId, oldValue, newValue, createdAt)
|
|
96
|
+
VALUES (?, 'task_status_change', ?, 'backlog', 'unassigned', ?)`,
|
|
97
|
+
[crypto.randomUUID(), task.id, "2026-08-17T16:59:00.000Z"],
|
|
98
|
+
);
|
|
99
|
+
const notify = mock(async (_message: string) => {});
|
|
100
|
+
|
|
101
|
+
const snapshot = await checkQueueStall(NOW, notify);
|
|
102
|
+
|
|
103
|
+
expect(snapshot.oldestAgeMs).toBe(60_000);
|
|
104
|
+
expect(isQueueStalled(snapshot)).toBe(false);
|
|
105
|
+
expect(notify).not.toHaveBeenCalled();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("measures queue age from the final dependency completing", async () => {
|
|
109
|
+
const worker = createAgent({ name: "worker", isLead: false, status: "idle" });
|
|
110
|
+
const prerequisite = createTaskExtended("Prerequisite", { agentId: worker.id });
|
|
111
|
+
const blocked = createTaskExtended("Newly unblocked", {
|
|
112
|
+
agentId: worker.id,
|
|
113
|
+
dependsOn: [prerequisite.id],
|
|
114
|
+
});
|
|
115
|
+
setCreatedAt(blocked.id, "2026-08-17T12:00:00.000Z");
|
|
116
|
+
getDb().run(
|
|
117
|
+
"UPDATE agent_tasks SET status = 'completed', finishedAt = ?, lastUpdatedAt = ? WHERE id = ?",
|
|
118
|
+
["2026-08-17T16:59:00.000Z", "2026-08-17T16:59:00.000Z", prerequisite.id],
|
|
119
|
+
);
|
|
120
|
+
const notify = mock(async (_message: string) => {});
|
|
121
|
+
|
|
122
|
+
const snapshot = await checkQueueStall(NOW, notify);
|
|
123
|
+
|
|
124
|
+
expect(snapshot.oldestTaskId).toBe(blocked.id);
|
|
125
|
+
expect(snapshot.oldestAgeMs).toBe(60_000);
|
|
126
|
+
expect(isQueueStalled(snapshot)).toBe(false);
|
|
127
|
+
expect(notify).not.toHaveBeenCalled();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("does not reset queue age when VCS metadata updates a claimable task", () => {
|
|
131
|
+
const task = createTaskExtended("Old task with new VCS metadata");
|
|
132
|
+
setCreatedAt(task.id, "2026-08-17T16:00:00.000Z");
|
|
133
|
+
|
|
134
|
+
updateTaskVcs(task.id, {
|
|
135
|
+
vcsProvider: "github",
|
|
136
|
+
vcsRepo: "desplega-ai/agent-swarm",
|
|
137
|
+
vcsNumber: 1177,
|
|
138
|
+
vcsUrl: "https://github.com/desplega-ai/agent-swarm/pull/1177",
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const snapshot = getQueueStallSnapshot(NOW);
|
|
142
|
+
expect(snapshot.oldestAgeMs).toBe(60 * 60 * 1000);
|
|
143
|
+
expect(isQueueStalled(snapshot)).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("does not reset claimable age when VCS metadata updates a completed dependency", () => {
|
|
147
|
+
const worker = createAgent({ name: "worker", isLead: false, status: "idle" });
|
|
148
|
+
const prerequisite = createTaskExtended("Completed prerequisite", { agentId: worker.id });
|
|
149
|
+
const blocked = createTaskExtended("Waiting after prerequisite", {
|
|
150
|
+
agentId: worker.id,
|
|
151
|
+
dependsOn: [prerequisite.id],
|
|
152
|
+
});
|
|
153
|
+
setCreatedAt(blocked.id, "2026-08-17T12:00:00.000Z");
|
|
154
|
+
getDb().run(
|
|
155
|
+
"UPDATE agent_tasks SET status = 'completed', finishedAt = ?, lastUpdatedAt = ? WHERE id = ?",
|
|
156
|
+
["2026-08-17T16:00:00.000Z", "2026-08-17T16:00:00.000Z", prerequisite.id],
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
updateTaskVcs(prerequisite.id, {
|
|
160
|
+
vcsProvider: "github",
|
|
161
|
+
vcsRepo: "desplega-ai/agent-swarm",
|
|
162
|
+
vcsNumber: 1177,
|
|
163
|
+
vcsUrl: "https://github.com/desplega-ai/agent-swarm/pull/1177",
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const snapshot = getQueueStallSnapshot(NOW);
|
|
167
|
+
expect(snapshot.oldestTaskId).toBe(blocked.id);
|
|
168
|
+
expect(snapshot.oldestAgeMs).toBe(60 * 60 * 1000);
|
|
169
|
+
expect(isQueueStalled(snapshot)).toBe(true);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("excludes pending tasks whose dependencies are not complete", () => {
|
|
173
|
+
const worker = createAgent({ name: "worker", isLead: false, status: "idle" });
|
|
174
|
+
const prerequisite = createTaskExtended("Prerequisite", { agentId: worker.id });
|
|
175
|
+
const blocked = createTaskExtended("Blocked", {
|
|
176
|
+
agentId: worker.id,
|
|
177
|
+
dependsOn: [prerequisite.id],
|
|
178
|
+
});
|
|
179
|
+
setCreatedAt(blocked.id, "2026-08-17T12:00:00.000Z");
|
|
180
|
+
|
|
181
|
+
const snapshot = getQueueStallSnapshot(NOW);
|
|
182
|
+
|
|
183
|
+
expect(snapshot.claimableCount).toBe(1);
|
|
184
|
+
expect(snapshot.oldestTaskId).toBe(prerequisite.id);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("reports direct and pool pickup transitions as diagnostic context", () => {
|
|
188
|
+
const worker = createAgent({ name: "worker", isLead: false, status: "idle" });
|
|
189
|
+
const task = createTaskExtended("Queued", { agentId: worker.id });
|
|
190
|
+
getDb().run(
|
|
191
|
+
`INSERT INTO agent_log (id, eventType, taskId, agentId, oldValue, newValue, createdAt)
|
|
192
|
+
VALUES (?, 'task_status_change', ?, ?, 'pending', 'in_progress', ?)`,
|
|
193
|
+
[crypto.randomUUID(), task.id, worker.id, "2026-08-17T16:50:00.000Z"],
|
|
194
|
+
);
|
|
195
|
+
getDb().run(
|
|
196
|
+
`INSERT INTO agent_log (id, eventType, taskId, agentId, oldValue, newValue, createdAt)
|
|
197
|
+
VALUES (?, 'task_claimed', ?, ?, 'unassigned', 'in_progress', ?)`,
|
|
198
|
+
[crypto.randomUUID(), task.id, worker.id, "2026-08-17T16:55:00.000Z"],
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
expect(getQueueStallSnapshot(NOW).recentPickupCount).toBe(2);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("scrubs secrets from notification failures before logging", () => {
|
|
205
|
+
const token = "ghp_1234567890abcdefABCDEF1234567890ABCD";
|
|
206
|
+
|
|
207
|
+
const output = _test.formatCheckFailure(new Error(`Slack rejected token ${token}`));
|
|
208
|
+
|
|
209
|
+
expect(output).not.toContain(token);
|
|
210
|
+
expect(output).toContain("[REDACTED:github_token]");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("deduplicates an active alarm and sends recovery", async () => {
|
|
214
|
+
const task = createTaskExtended("Old pool task");
|
|
215
|
+
setCreatedAt(task.id, "2026-08-17T16:00:00.000Z");
|
|
216
|
+
const notify = mock(async (_message: string) => {});
|
|
217
|
+
|
|
218
|
+
await checkQueueStall(NOW, notify);
|
|
219
|
+
await checkQueueStall(NOW, notify);
|
|
220
|
+
getDb().run("UPDATE agent_tasks SET status = 'completed' WHERE id = ?", [task.id]);
|
|
221
|
+
await checkQueueStall(NOW, notify);
|
|
222
|
+
|
|
223
|
+
expect(notify).toHaveBeenCalledTimes(2);
|
|
224
|
+
expect(notify.mock.calls[1]?.[0]).toContain("queue pickup recovered");
|
|
225
|
+
});
|
|
226
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { getUserCommsPrefs } from "../utils/requester-comms";
|
|
3
|
+
|
|
4
|
+
describe("getUserCommsPrefs", () => {
|
|
5
|
+
test("returns undefined for missing user or metadata", () => {
|
|
6
|
+
expect(getUserCommsPrefs(undefined)).toBeUndefined();
|
|
7
|
+
expect(getUserCommsPrefs({ metadata: undefined })).toBeUndefined();
|
|
8
|
+
expect(getUserCommsPrefs({ metadata: {} })).toBeUndefined();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("returns undefined when comms is not an object", () => {
|
|
12
|
+
expect(getUserCommsPrefs({ metadata: { comms: "casual" } })).toBeUndefined();
|
|
13
|
+
expect(getUserCommsPrefs({ metadata: { comms: ["casual"] } })).toBeUndefined();
|
|
14
|
+
expect(getUserCommsPrefs({ metadata: { comms: null } })).toBeUndefined();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("picks trimmed string fields and drops the rest", () => {
|
|
18
|
+
expect(
|
|
19
|
+
getUserCommsPrefs({
|
|
20
|
+
metadata: {
|
|
21
|
+
comms: { tone: " casual ", language: "uk", verbosity: 3, extra: "ignored" },
|
|
22
|
+
},
|
|
23
|
+
}),
|
|
24
|
+
).toEqual({ tone: "casual", language: "uk", verbosity: undefined });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("returns undefined when all comms fields are empty or non-string", () => {
|
|
28
|
+
expect(
|
|
29
|
+
getUserCommsPrefs({ metadata: { comms: { tone: " ", language: 7 } } }),
|
|
30
|
+
).toBeUndefined();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -22,4 +22,42 @@ describe("runner requester profile prompt", () => {
|
|
|
22
22
|
expect(prompt).toContain("Lead with the answer; keep updates terse.");
|
|
23
23
|
expect(prompt).toContain("where it doesn't conflict with correctness or your operating rules");
|
|
24
24
|
});
|
|
25
|
+
|
|
26
|
+
test("renders structured comms preferences alongside role and notes", async () => {
|
|
27
|
+
const prompt = await buildRequesterProfilePrompt({
|
|
28
|
+
name: "Taras",
|
|
29
|
+
role: "CEO",
|
|
30
|
+
notes: "Keep updates terse.",
|
|
31
|
+
comms: { tone: "casual", language: "Ukrainian", verbosity: "terse" },
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
expect(prompt).toContain(
|
|
35
|
+
"Their communication preferences: tone: casual, language: Ukrainian, verbosity: terse.",
|
|
36
|
+
);
|
|
37
|
+
expect(prompt).toContain("Their stated notes for how you should respond and act:");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("fires when only comms preferences are set", async () => {
|
|
41
|
+
const prompt = await buildRequesterProfilePrompt({
|
|
42
|
+
name: "Taras",
|
|
43
|
+
comms: { language: "Ukrainian" },
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(prompt).toContain("## Requester Profile");
|
|
47
|
+
expect(prompt).toContain("This task was requested by Taras.");
|
|
48
|
+
expect(prompt).toContain("Their communication preferences: language: Ukrainian.");
|
|
49
|
+
expect(prompt).not.toContain("Their stated notes");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("hints that learned preferences should be persisted via manage-user", async () => {
|
|
53
|
+
const prompt = await buildRequesterProfilePrompt({ name: "Taras", role: "CEO" });
|
|
54
|
+
|
|
55
|
+
expect(prompt).toContain(
|
|
56
|
+
"the lead updates `comms` (tone, language, verbosity) via `manage-user`",
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("ignores empty comms object", async () => {
|
|
61
|
+
await expect(buildRequesterProfilePrompt({ name: "Taras", comms: {} })).resolves.toBe("");
|
|
62
|
+
});
|
|
25
63
|
});
|