@desplega.ai/agent-swarm 1.146.0 → 1.148.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 +12 -1
- package/dist/{acp-adapter-jnq4612m.js → acp-adapter-grez7xhd.js} +42 -4
- package/dist/{actions-s41xcn3f.js → actions-kweznh5a.js} +9 -8
- package/dist/{app-j8bfw3f6.js → app-5fvvjv9m.js} +6 -5
- package/dist/assistant-40m4w50s.js +236 -0
- package/dist/{boot-reembed-x1476agm.js → boot-reembed-9rxmb26n.js} +8 -7
- package/dist/{boot-reembed-jxdz46jp.js → boot-reembed-cn357y5n.js} +7 -6
- package/dist/{boot-scrub-logs-rab9h5bt.js → boot-scrub-logs-5tpp0x3y.js} +4 -4
- package/dist/{claude-adapter-ep6qekbm.js → claude-adapter-9pz5q2pb.js} +8 -6
- package/dist/{claude-managed-setup-w619jncn.js → claude-managed-setup-f5pmsyrp.js} +24 -3
- package/dist/{claude-sdk-session-0wp3qrcy.js → claude-sdk-session-v8w0s7qm.js} +8 -6
- package/dist/{cli-yqeb27t1.js → cli-0amc6myn.js} +13 -10
- package/dist/{cli-bafd6xnf.js → cli-0b6y7kxm.js} +1 -1
- package/dist/{cli-68h2mfrn.js → cli-0er5m69z.js} +3 -3
- package/dist/{cli-qar1f940.js → cli-3n7cbfjn.js} +5 -3
- package/dist/{cli-zww3vcnm.js → cli-56y3hch2.js} +7 -6
- package/dist/{cli-pzfc7j7g.js → cli-5khqvvnq.js} +2 -2
- package/dist/{cli-d4xqde2b.js → cli-6s2y152y.js} +2780 -2587
- package/dist/{cli-m4xsy03s.js → cli-7gn7c2af.js} +1 -1
- package/dist/{cli-ah5hjk49.js → cli-9216xm24.js} +94 -372
- package/dist/cli-9d8ntetj.js +42 -0
- package/dist/{cli-yxwe913z.js → cli-anedxf1w.js} +3 -1
- package/dist/{cli-zn65x0w8.js → cli-brxyat7h.js} +21 -47
- package/dist/{cli-jyy3e193.js → cli-cdjezzc7.js} +4 -4
- package/dist/cli-d8brzxjb.js +190 -0
- package/dist/{cli-4secrq32.js → cli-dcem3pr1.js} +1 -1
- package/dist/{cli-46yck6nj.js → cli-dk3me3qs.js} +14 -31
- package/dist/cli-ep19k3mb.js +19 -0
- package/dist/{cli-3k2g3kg2.js → cli-f8j56xv2.js} +9 -9
- package/dist/{cli-a9bn9svs.js → cli-hp2est46.js} +2 -2
- package/dist/cli-hz8xhefy.js +823 -0
- package/dist/{cli-506z6jqa.js → cli-hzhhrt7b.js} +1 -1
- package/dist/{cli-fhsz3twr.js → cli-jsp14b6c.js} +171 -1
- package/dist/{cli-2zvtzevh.js → cli-kpv03zhj.js} +13 -11
- package/dist/{cli-pmwbdw36.js → cli-kzvhgvk3.js} +2 -2
- package/dist/{cli-rymrjneb.js → cli-m1p5cysp.js} +2 -2
- package/dist/cli-m9sxbkhm.js +779 -0
- package/dist/{cli-s8sbe32s.js → cli-n2cwscdd.js} +2 -2
- package/dist/{cli-c8q7292f.js → cli-n9grk3yd.js} +5 -4
- package/dist/{cli-dhg6j8h3.js → cli-p73drk6p.js} +886 -756
- package/dist/{cli-d9pnxgth.js → cli-qy6h009w.js} +7 -50
- package/dist/{cli-a960nkfb.js → cli-tebsjpk7.js} +2 -2
- package/dist/{cli-q3qr220d.js → cli-v3e51x7w.js} +1 -1
- package/dist/{cli-q0qq1sxw.js → cli-xqcpg1st.js} +37 -2
- package/dist/{cli-skd674mt.js → cli-y6b6fb76.js} +25 -3
- package/dist/{cli-6xwhtc64.js → cli-ztn0ga15.js} +1 -1
- package/dist/cli.js +15 -13
- package/dist/{codex-adapter-d6qajxj3.js → codex-adapter-ehhj2qwc.js} +2 -2
- package/dist/{codex-hook-n7m5k7cr.js → codex-hook-bw8p5nh4.js} +2 -2
- package/dist/{codex-session-runner-v9krqe4x.js → codex-session-runner-5hne566k.js} +2 -2
- package/dist/{commands-923wsas7.js → commands-xnvh951h.js} +4 -4
- package/dist/{db-m1hy4zh5.js → db-ycr5zfr4.js} +6 -4
- package/dist/handlers-1vt77kgq.js +603 -0
- package/dist/{hook-bxe3bkdy.js → hook-91ntpmff.js} +24 -49
- package/dist/{hook-ee4hff56.js → hook-qfwjydvn.js} +7 -5
- package/dist/{http-a8rx91me.js → http-h6g7bv5b.js} +223 -92
- package/dist/{index-697zfzv1.js → index-0ez2aack.js} +15 -14
- package/dist/{index-1j9va48r.js → index-47ghesbn.js} +371 -110
- package/dist/{index-jj4q3x59.js → index-antf9t4p.js} +14 -13
- package/dist/{index-3x9g3nbg.js → index-d2mytx95.js} +13 -12
- package/dist/{keepalive-sds4e4ce.js → keepalive-c4sp7yvd.js} +6 -6
- package/dist/{lead-ggq6szk8.js → lead-4r7z9e8v.js} +29 -27
- package/dist/{maintenance-093p41d4.js → maintenance-yqjfdjas.js} +8 -7
- package/dist/{oauth-refresh-sweep-dvzq4ckj.js → oauth-refresh-sweep-wtqgnh3c.js} +5 -5
- package/dist/{onboard-ydr9tzw5.js → onboard-a4krmh46.js} +2 -2
- package/dist/{otel-impl-zx3tdf3j.js → otel-impl-725kq0vp.js} +1 -1
- package/dist/{pi-mono-adapter-dtkz4e7j.js → pi-mono-adapter-2has4gnp.js} +12 -2
- package/dist/{pricing-refresh-7e6pqnzp.js → pricing-refresh-kqxd7w9h.js} +6 -6
- package/dist/{rbac-roles-0dv1wt0w.js → rbac-roles-gws5xej6.js} +4 -4
- package/dist/{rbac-roles-p2rh6d0r.js → rbac-roles-t3wygpth.js} +5 -5
- package/dist/{registry-9kdck5ye.js → registry-v5wytg0s.js} +1 -2
- package/dist/{render-v2-4k3jt4r2.js → render-v2-x1wxvn51.js} +6 -5
- package/dist/{seed-pricing-2cc1qmcd.js → seed-pricing-t6y8d0vf.js} +7 -5
- package/dist/{setup-c5yss30k.js → setup-x50py6xg.js} +2 -2
- package/dist/{worker-7srgeqcr.js → worker-zsz0qfxk.js} +29 -27
- package/openapi.json +263 -12
- package/package.json +3 -1
- package/plugin/commands/start-worker.md +4 -9
- package/plugin/pi-skills/start-worker/SKILL.md +4 -9
- package/src/be/db/agents.ts +556 -0
- package/src/be/db/context-versions.ts +117 -0
- package/src/be/db/runtime.ts +406 -0
- package/src/be/db/tasks/read.ts +1167 -0
- package/src/be/db/tasks/write.ts +953 -0
- package/src/be/db.ts +583 -3428
- package/src/be/migrations/147_scheduled_tasks_parent_task.sql +7 -0
- package/src/be/migrations/148_pages_svg.sql +123 -0
- package/src/be/migrations/149_session_tokens.sql +24 -0
- package/src/be/scripts/typecheck.ts +1 -0
- package/src/be/seed-pricing.ts +13 -8
- package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +1 -1
- package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +1 -1
- package/src/be/seed-scripts/catalog/star-history-refresh.ts +101 -0
- package/src/be/seed-scripts/catalog/star-history-renderer.ts +119 -0
- package/src/be/seed-scripts/index.ts +17 -0
- package/src/be/seed-skills/index.ts +14 -0
- package/src/be/swarm-config-guard.ts +1 -1
- package/src/be/task-attachment-store.ts +70 -0
- package/src/be/users.ts +77 -0
- package/src/commands/attachments-section.ts +37 -0
- package/src/commands/claude-managed-setup.ts +45 -3
- package/src/commands/claude-md-session.ts +408 -0
- package/src/commands/context-preamble.ts +15 -3
- package/src/commands/profile-sync.ts +205 -5
- package/src/commands/provider-credentials.ts +60 -27
- package/src/commands/runner.ts +63 -58
- package/src/commands/templates.ts +25 -2
- package/src/hooks/hook.ts +26 -67
- package/src/http/agents.ts +40 -1
- package/src/http/auth.ts +13 -1
- package/src/http/fs.ts +21 -51
- package/src/http/mcp.ts +32 -0
- package/src/http/pages-public.ts +20 -4
- package/src/http/poll.ts +4 -0
- package/src/http/repos.ts +3 -3
- package/src/http/sessions.ts +82 -0
- package/src/http/tasks.ts +10 -2
- package/src/prompts/session-templates.ts +4 -2
- package/src/providers/acp-adapter.ts +42 -6
- package/src/providers/pi-mono-adapter.ts +23 -6
- package/src/scheduler/scheduler.test.ts +49 -2
- package/src/scheduler/scheduler.ts +4 -0
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +8 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +8 -0
- package/src/server.ts +2 -0
- package/src/slack/assistant.ts +87 -46
- package/src/slack/handlers.test.ts +1 -3
- package/src/slack/handlers.ts +102 -87
- package/src/slack/inbound-files.ts +466 -0
- package/src/slack/templates.ts +14 -0
- package/src/tasks/task-terminal-effects.ts +158 -0
- package/src/tests/acp-adapter.test.ts +327 -192
- package/src/tests/budget-claim-gate.test.ts +1 -0
- package/src/tests/build-pi-skills.test.ts +5 -7
- package/src/tests/claude-managed-setup.test.ts +52 -4
- package/src/tests/claude-md-session.test.ts +603 -0
- package/src/tests/codex-skill-resolver.test.ts +19 -0
- package/src/tests/context-versioning.test.ts +107 -0
- package/src/tests/credential-check.test.ts +157 -14
- package/src/tests/db-agents.test.ts +91 -0
- package/src/tests/db-context-versions.test.ts +37 -0
- package/src/tests/db-extraction-contract.test.ts +45 -0
- package/src/tests/db-runtime.test.ts +70 -0
- package/src/tests/db-task-read-contract.test.ts +624 -0
- package/src/tests/db-task-write-contract.test.ts +653 -0
- package/src/tests/db-tasks-read.test.ts +183 -0
- package/src/tests/db-tasks-write.test.ts +230 -0
- package/src/tests/defer-task.test.ts +389 -0
- package/src/tests/docs-index-footprint.test.js +2 -2
- package/src/tests/e2e-visuals-stage.test.ts +151 -0
- package/src/tests/file-lock.test.ts +144 -0
- package/src/tests/fixtures/file-lock-holder.ts +13 -0
- package/src/tests/fixtures/hold-file-lock.ts +25 -0
- package/src/tests/heartbeat-expiry.test.ts +188 -0
- package/src/tests/http-api-integration.test.ts +78 -1
- package/src/tests/mcp-input-ergonomics.test.ts +386 -0
- package/src/tests/pages-public-html.test.ts +43 -0
- package/src/tests/pages-svg-migration.test.ts +42 -0
- package/src/tests/preload.ts +4 -1
- package/src/tests/profile-sync-warning.test.ts +16 -0
- package/src/tests/profile-sync.test.ts +126 -0
- package/src/tests/prompt-template-session.test.ts +20 -4
- package/src/tests/provider-registration.test.ts +93 -0
- package/src/tests/rbac-e2e-helpers.test.ts +53 -0
- package/src/tests/rbac-e2e-helpers.ts +19 -2
- package/src/tests/routing-affinity-inventory.test.ts +1 -1
- package/src/tests/runner-attachments-section.test.ts +22 -2
- package/src/tests/runner-context-preamble.test.ts +30 -0
- package/src/tests/runner-deployment-resume.test.ts +107 -0
- package/src/tests/runner-repo-autostash.test.ts +12 -0
- package/src/tests/script-connections.test.ts +4 -0
- package/src/tests/seed-scripts.test.ts +2 -2
- package/src/tests/session-tokens.test.ts +279 -0
- package/src/tests/slack-file-tools-attachments.test.ts +408 -0
- package/src/tests/slack-inbound-files.test.ts +847 -0
- package/src/tests/star-history-refresh.test.ts +102 -0
- package/src/tests/swarm-repos.test.ts +20 -0
- package/src/tests/tool-annotations.test.ts +4 -3
- package/src/tests/ui-logs-parser.test.ts +176 -0
- package/src/tools/accept-steer.ts +0 -1
- package/src/tools/create-page.ts +4 -2
- package/src/tools/defer-task.ts +239 -0
- package/src/tools/memory-get.ts +10 -3
- package/src/tools/memory-rate.ts +19 -7
- package/src/tools/memory-search.ts +2 -1
- package/src/tools/memory-store.ts +25 -5
- package/src/tools/repos/update-repo.ts +2 -2
- package/src/tools/schedules/create-schedule.ts +1 -0
- package/src/tools/schedules/list-schedules.ts +1 -0
- package/src/tools/schedules/patch-schedule.ts +1 -0
- package/src/tools/schedules/update-schedule.ts +1 -0
- package/src/tools/send-task.ts +9 -2
- package/src/tools/slack-download-file.ts +60 -29
- package/src/tools/slack-read.ts +73 -32
- package/src/tools/store-progress.ts +51 -150
- package/src/tools/tool-config.ts +1 -0
- package/src/tools/utils.ts +21 -0
- package/src/types.ts +37 -4
- package/src/utils/acp-session-token.ts +56 -0
- package/src/utils/file-lock.ts +111 -0
- package/src/utils/heartbeat-expiry.ts +78 -0
- package/src/utils/request-auth-context.ts +2 -0
- package/src/utils/task-attachment-links.ts +22 -0
- package/templates/skills/code-quality/content.md +8 -5
- package/templates/skills/download-task-attachment/content.md +2 -1
- package/{plugin/pi-skills → templates/skills}/review-offered-task/SKILL.md +5 -5
- package/templates/skills/review-offered-task/config.json +14 -0
- package/{plugin/commands/review-offered-task.md → templates/skills/review-offered-task/content.md} +3 -8
- package/templates/skills/scheduling/content.md +4 -0
- package/templates/skills/slack-interaction/content.md +2 -2
- package/templates/skills/work-on-task/SKILL.md +16 -0
- package/templates/skills/work-on-task/config.json +14 -0
- package/templates/skills/work-on-task/content.md +11 -0
- package/dist/assistant-dx3ew4d8.js +0 -212
- package/dist/cli-00kyrp1a.js +0 -317
- package/dist/cli-n1q8q14d.js +0 -177
- package/dist/cli-ss6vybmt.js +0 -269
- package/dist/handlers-e9vp2fr8.js +0 -592
- package/plugin/commands/work-on-task.md +0 -39
- package/plugin/pi-skills/work-on-task/SKILL.md +0 -39
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Holds a `withFileLock` lock from a separate process: prints "locked" once it
|
|
2
|
+
// has it, then keeps it for `holdMs` (or until killed).
|
|
3
|
+
import { withFileLock } from "../../utils/file-lock";
|
|
4
|
+
|
|
5
|
+
const [lockPath, holdMs] = process.argv.slice(2);
|
|
6
|
+
const result = await withFileLock(lockPath as string, async () => {
|
|
7
|
+
console.log("locked");
|
|
8
|
+
await Bun.sleep(Number(holdMs));
|
|
9
|
+
});
|
|
10
|
+
if (!result.acquired) {
|
|
11
|
+
console.log(`not-acquired:${result.reason}`);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { withFileLock } from "../../utils/file-lock";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hold `lockPath` from this process (a live holder) until the returned release
|
|
5
|
+
* function is awaited. Resolves once the lock is actually held.
|
|
6
|
+
*/
|
|
7
|
+
export async function holdFileLock(lockPath: string): Promise<() => Promise<void>> {
|
|
8
|
+
let release!: () => void;
|
|
9
|
+
const released = new Promise<void>((resolve) => {
|
|
10
|
+
release = resolve;
|
|
11
|
+
});
|
|
12
|
+
let held!: () => void;
|
|
13
|
+
const acquired = new Promise<void>((resolve) => {
|
|
14
|
+
held = resolve;
|
|
15
|
+
});
|
|
16
|
+
const holding = withFileLock(lockPath, async () => {
|
|
17
|
+
held();
|
|
18
|
+
await released;
|
|
19
|
+
});
|
|
20
|
+
await acquired;
|
|
21
|
+
return async () => {
|
|
22
|
+
release();
|
|
23
|
+
await holding;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import {
|
|
4
|
+
closeDb,
|
|
5
|
+
createAgent,
|
|
6
|
+
getAgentById,
|
|
7
|
+
getDbClient,
|
|
8
|
+
initDb,
|
|
9
|
+
updateAgentProfile,
|
|
10
|
+
} from "../be/db";
|
|
11
|
+
import { handleAgentsRest } from "../http/agents";
|
|
12
|
+
import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
13
|
+
import { HeartbeatExpiryError, validateHeartbeatExpiry } from "../utils/heartbeat-expiry";
|
|
14
|
+
import { listenOnFreePort } from "./test-net";
|
|
15
|
+
|
|
16
|
+
const section = (...lines: string[]) => `## Tracked Items\n${lines.join("\n")}\n`;
|
|
17
|
+
|
|
18
|
+
describe("heartbeat expiry validation", () => {
|
|
19
|
+
test.each(["DELETE 09-30", "delete on 09-30", "DeLeTe ON 09-30"])("accepts %s", (expiry) => {
|
|
20
|
+
expect(() => validateHeartbeatExpiry("", section(`- New item. ${expiry}`))).not.toThrow();
|
|
21
|
+
});
|
|
22
|
+
test("rejects a new undated item and names it", () => {
|
|
23
|
+
expect(() => validateHeartbeatExpiry("", section("- Check the queue"))).toThrow(
|
|
24
|
+
"new tracked item requires DELETE <MM-DD> (or DELETE on <MM-DD>): Check the queue",
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
test.each([
|
|
28
|
+
"📌 Permanent rule",
|
|
29
|
+
"- 📌 Permanent rule",
|
|
30
|
+
"- [ ] 📌 Permanent rule",
|
|
31
|
+
])("exempts pin durables: %s", (line) => {
|
|
32
|
+
expect(() => validateHeartbeatExpiry("", section(line))).not.toThrow();
|
|
33
|
+
});
|
|
34
|
+
test("does not exempt a pin in the middle of an item", () => {
|
|
35
|
+
expect(() => validateHeartbeatExpiry("", section("- Check 📌 status"))).toThrow(
|
|
36
|
+
HeartbeatExpiryError,
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
test("allows unchanged, shortened, and edited legacy items", () => {
|
|
40
|
+
const old = section(
|
|
41
|
+
"- **Queue:** Check the queue every morning and evening",
|
|
42
|
+
"- Keep this item",
|
|
43
|
+
);
|
|
44
|
+
for (const line of [
|
|
45
|
+
"- **Queue:** Check the queue every morning and evening",
|
|
46
|
+
"- **Queue:** Check the queue",
|
|
47
|
+
"- **Queue:** Inspect pending work weekly",
|
|
48
|
+
]) {
|
|
49
|
+
expect(() => validateHeartbeatExpiry(old, section(line, "- Keep this item"))).not.toThrow();
|
|
50
|
+
}
|
|
51
|
+
expect(() =>
|
|
52
|
+
validateHeartbeatExpiry(section("- Check the queue daily"), section("- Check the queue")),
|
|
53
|
+
).not.toThrow();
|
|
54
|
+
});
|
|
55
|
+
test("allows pure deletion, including the entire section", () => {
|
|
56
|
+
expect(() => validateHeartbeatExpiry(section("- Old undated item"), section())).not.toThrow();
|
|
57
|
+
expect(() => validateHeartbeatExpiry(section("- Old undated item"), "")).not.toThrow();
|
|
58
|
+
});
|
|
59
|
+
test("uses the last marker and excludes the marker line and later sections", () => {
|
|
60
|
+
const next =
|
|
61
|
+
"## Tracked Items\n- Earlier example\n## Tracked Items (current)\n- New. DELETE 09-30\n## Notes\nUndated notes";
|
|
62
|
+
expect(() => validateHeartbeatExpiry("", next)).not.toThrow();
|
|
63
|
+
expect(() => validateHeartbeatExpiry("", "Mention ## Tracked Items here\n- Undated")).toThrow();
|
|
64
|
+
expect(() => validateHeartbeatExpiry("", "## Tracked Items")).not.toThrow();
|
|
65
|
+
expect(() => validateHeartbeatExpiry("", "## Other\n- Undated")).not.toThrow();
|
|
66
|
+
});
|
|
67
|
+
test("does not use a token on the marker or another line", () => {
|
|
68
|
+
expect(() => validateHeartbeatExpiry("", "## Tracked Items DELETE 09-30\n- Undated")).toThrow();
|
|
69
|
+
expect(() =>
|
|
70
|
+
validateHeartbeatExpiry("", section("- Dated DELETE 09-30", "- Undated")),
|
|
71
|
+
).toThrow();
|
|
72
|
+
});
|
|
73
|
+
test("legacy matches are one-to-one, even for reordered lines", () => {
|
|
74
|
+
const old = section("- **Old:** Keep", "- **Other:** Keep");
|
|
75
|
+
expect(() =>
|
|
76
|
+
validateHeartbeatExpiry(old, section("- **Other:** Keep", "- **Old:** Updated")),
|
|
77
|
+
).not.toThrow();
|
|
78
|
+
expect(() =>
|
|
79
|
+
validateHeartbeatExpiry(old, section("- **Old:** Keep", "- **Old:** New")),
|
|
80
|
+
).toThrow();
|
|
81
|
+
expect(() => validateHeartbeatExpiry(old, section("- **Unrelated:** New"))).toThrow();
|
|
82
|
+
});
|
|
83
|
+
test("condensing the middle of a legacy line passes", () => {
|
|
84
|
+
expect(() =>
|
|
85
|
+
validateHeartbeatExpiry(
|
|
86
|
+
section("- Check the large queue daily"),
|
|
87
|
+
section("- Check the queue"),
|
|
88
|
+
),
|
|
89
|
+
).not.toThrow();
|
|
90
|
+
});
|
|
91
|
+
test("dating a legacy item cannot authorize a second undated copy", () => {
|
|
92
|
+
for (const lines of [
|
|
93
|
+
["- **Old:** New", "- **Old:** Updated DELETE 09-30"],
|
|
94
|
+
["- **Old:** Updated DELETE 09-30", "- **Old:** New"],
|
|
95
|
+
["- Legacy text DELETE 09-30", "- Legacy"],
|
|
96
|
+
]) {
|
|
97
|
+
expect(() =>
|
|
98
|
+
validateHeartbeatExpiry(section("- **Old:** Original", "- Legacy text"), section(...lines)),
|
|
99
|
+
).toThrow();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
test("does not grandfather removal of a date or pin", () => {
|
|
103
|
+
expect(() =>
|
|
104
|
+
validateHeartbeatExpiry(section("- **Old:** DELETE 09-30"), section("- **Old:** Undated")),
|
|
105
|
+
).toThrow();
|
|
106
|
+
expect(() => validateHeartbeatExpiry(section("📌 Permanent"), section("Permanent"))).toThrow();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const dbPath = "./test-heartbeat-expiry.sqlite";
|
|
111
|
+
describe("profile persistence heartbeat gate", () => {
|
|
112
|
+
const agentId = crypto.randomUUID();
|
|
113
|
+
beforeAll(async () => {
|
|
114
|
+
initDb(dbPath);
|
|
115
|
+
await createAgent({ id: agentId, name: "Expiry test", isLead: false, status: "idle" });
|
|
116
|
+
// Seed pre-gate legacy state directly in this isolated test database.
|
|
117
|
+
await getDbClient().run("UPDATE agents SET heartbeatMd = ? WHERE id = ?", [
|
|
118
|
+
section("- **Legacy:** Old text"),
|
|
119
|
+
agentId,
|
|
120
|
+
]);
|
|
121
|
+
});
|
|
122
|
+
afterAll(async () => {
|
|
123
|
+
closeDb();
|
|
124
|
+
for (const suffix of ["", "-wal", "-shm"])
|
|
125
|
+
await Bun.file(dbPath + suffix)
|
|
126
|
+
.delete()
|
|
127
|
+
.catch(() => {});
|
|
128
|
+
});
|
|
129
|
+
test("rejects atomically before profile or version writes", async () => {
|
|
130
|
+
const before = await getAgentById(agentId);
|
|
131
|
+
const versionsBefore = await getDbClient().query(
|
|
132
|
+
"SELECT * FROM context_versions WHERE agentId = ?",
|
|
133
|
+
[agentId],
|
|
134
|
+
);
|
|
135
|
+
await expect(
|
|
136
|
+
updateAgentProfile(agentId, {
|
|
137
|
+
description: "Must not land",
|
|
138
|
+
heartbeatMd: section("- **Legacy:** Old text", "- New undated item"),
|
|
139
|
+
}),
|
|
140
|
+
).rejects.toThrow(HeartbeatExpiryError);
|
|
141
|
+
expect(await getAgentById(agentId)).toEqual(before);
|
|
142
|
+
expect(
|
|
143
|
+
await getDbClient().query("SELECT * FROM context_versions WHERE agentId = ?", [agentId]),
|
|
144
|
+
).toEqual(versionsBefore);
|
|
145
|
+
});
|
|
146
|
+
test("HTTP returns 400 with the offending line", async () => {
|
|
147
|
+
const server = createServer(async (req, res) => {
|
|
148
|
+
try {
|
|
149
|
+
await handleAgentsRest(
|
|
150
|
+
req,
|
|
151
|
+
res,
|
|
152
|
+
getPathSegments(req.url ?? "/"),
|
|
153
|
+
parseQueryParams(req.url ?? "/"),
|
|
154
|
+
agentId,
|
|
155
|
+
);
|
|
156
|
+
} catch {
|
|
157
|
+
res.writeHead(500).end();
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
const port = await listenOnFreePort(server);
|
|
161
|
+
try {
|
|
162
|
+
const response = await fetch(`http://localhost:${port}/api/agents/${agentId}/profile`, {
|
|
163
|
+
method: "PUT",
|
|
164
|
+
headers: { "Content-Type": "application/json" },
|
|
165
|
+
body: JSON.stringify({ heartbeatMd: section("- New undated HTTP item") }),
|
|
166
|
+
});
|
|
167
|
+
expect(response.status).toBe(400);
|
|
168
|
+
expect((await response.json()).error).toContain("New undated HTTP item");
|
|
169
|
+
} finally {
|
|
170
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
test("persists edits, dated additions, pin durables, and deletions", async () => {
|
|
174
|
+
const next = section("- **Legacy:** Revised", "- Dated DELETE on 09-30", "📌 Durable");
|
|
175
|
+
expect((await updateAgentProfile(agentId, { heartbeatMd: next }))?.heartbeatMd).toBe(next);
|
|
176
|
+
expect((await updateAgentProfile(agentId, { heartbeatMd: "" }))?.heartbeatMd).toBe("");
|
|
177
|
+
});
|
|
178
|
+
test("drops a stale heartbeat before validating, preserving other updates", async () => {
|
|
179
|
+
const result = await updateAgentProfile(
|
|
180
|
+
agentId,
|
|
181
|
+
{ heartbeatMd: section("- Invalid stale item"), description: "Fresh metadata" },
|
|
182
|
+
undefined,
|
|
183
|
+
{ expectedHashes: { heartbeatMd: "stale" } },
|
|
184
|
+
);
|
|
185
|
+
expect(result?.heartbeatMd).toBe("");
|
|
186
|
+
expect(result?.description).toBe("Fresh metadata");
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* and tests every API endpoint for correct behavior.
|
|
6
6
|
*/
|
|
7
7
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
8
|
-
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
9
9
|
import { rm, unlink } from "node:fs/promises";
|
|
10
10
|
import type { Subprocess } from "bun";
|
|
11
11
|
import { Webhook } from "svix";
|
|
@@ -388,6 +388,83 @@ describe("Agents", () => {
|
|
|
388
388
|
expect(unrelated.body.events).toHaveLength(0);
|
|
389
389
|
});
|
|
390
390
|
|
|
391
|
+
test("PUT /api/agents/:id/profile — expectedHashes drops a stale field, keeps the rest", async () => {
|
|
392
|
+
const sha = (v: string) => createHash("sha256").update(v).digest("hex");
|
|
393
|
+
const base = "# CLAUDE.md\n\nbase the session materialized";
|
|
394
|
+
const seeded = await put(`/api/agents/${ids.workerAgent}/profile`, {
|
|
395
|
+
body: { claudeMd: base, changeSource: "self_edit" },
|
|
396
|
+
agentId: ids.workerAgent,
|
|
397
|
+
});
|
|
398
|
+
expect(seeded.status).toBe(200);
|
|
399
|
+
|
|
400
|
+
const reconciledClaudeMd = async () =>
|
|
401
|
+
(
|
|
402
|
+
await get(
|
|
403
|
+
`/api/events?${new URLSearchParams({
|
|
404
|
+
event: "system.profile_sync_reconciled",
|
|
405
|
+
agentId: ids.workerAgent,
|
|
406
|
+
dataField: "claudeMd",
|
|
407
|
+
limit: "100",
|
|
408
|
+
})}`,
|
|
409
|
+
{ agentId: ids.workerAgent },
|
|
410
|
+
)
|
|
411
|
+
).body.events.length as number;
|
|
412
|
+
const before = await reconciledClaudeMd();
|
|
413
|
+
|
|
414
|
+
// A copy based on something the DB already moved past: dropped, 200, and
|
|
415
|
+
// the other field of the same sync still lands.
|
|
416
|
+
const stale = await put(`/api/agents/${ids.workerAgent}/profile`, {
|
|
417
|
+
body: {
|
|
418
|
+
claudeMd: "stale copy",
|
|
419
|
+
toolsMd: "fresh tools",
|
|
420
|
+
changeSource: "session_sync",
|
|
421
|
+
expectedHashes: { claudeMd: sha("an older materialization") },
|
|
422
|
+
},
|
|
423
|
+
agentId: ids.workerAgent,
|
|
424
|
+
});
|
|
425
|
+
expect(stale.status).toBe(200);
|
|
426
|
+
expect(stale.body.claudeMd).toBe(base);
|
|
427
|
+
expect(stale.body.toolsMd).toBe("fresh tools");
|
|
428
|
+
expect(await reconciledClaudeMd()).toBe(before); // not written → not reconciled
|
|
429
|
+
const conflictEvents = await get(
|
|
430
|
+
`/api/events?${new URLSearchParams({
|
|
431
|
+
event: "system.profile_sync_conflict",
|
|
432
|
+
agentId: ids.workerAgent,
|
|
433
|
+
dataField: "claudeMd",
|
|
434
|
+
limit: "1",
|
|
435
|
+
})}`,
|
|
436
|
+
{ agentId: ids.workerAgent },
|
|
437
|
+
);
|
|
438
|
+
expect(conflictEvents.body.events[0]).toMatchObject({
|
|
439
|
+
event: "system.profile_sync_conflict",
|
|
440
|
+
status: "skipped",
|
|
441
|
+
data: {
|
|
442
|
+
field: "claudeMd",
|
|
443
|
+
expectedHash: sha("an older materialization"),
|
|
444
|
+
currentHash: sha(base),
|
|
445
|
+
changeSource: "session_sync",
|
|
446
|
+
},
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// An edit on top of the current value applies.
|
|
450
|
+
const edited = await put(`/api/agents/${ids.workerAgent}/profile`, {
|
|
451
|
+
body: {
|
|
452
|
+
claudeMd: "edited in session",
|
|
453
|
+
changeSource: "session_sync",
|
|
454
|
+
expectedHashes: { claudeMd: sha(base) },
|
|
455
|
+
},
|
|
456
|
+
agentId: ids.workerAgent,
|
|
457
|
+
});
|
|
458
|
+
expect(edited.status).toBe(200);
|
|
459
|
+
expect(edited.body.claudeMd).toBe("edited in session");
|
|
460
|
+
|
|
461
|
+
const malformed = await put(`/api/agents/${ids.workerAgent}/profile`, {
|
|
462
|
+
body: { claudeMd: "x", expectedHashes: { claudeMd: "not-a-sha256" } },
|
|
463
|
+
agentId: ids.workerAgent,
|
|
464
|
+
});
|
|
465
|
+
expect(malformed.status).toBe(400);
|
|
466
|
+
});
|
|
467
|
+
|
|
391
468
|
test("PUT /api/agents/:id/profile — non-existent returns 404", async () => {
|
|
392
469
|
const { status } = await put(`/api/agents/${randomUUID()}/profile`, {
|
|
393
470
|
body: { role: "ghost" },
|