@desplega.ai/agent-swarm 2.5.2 → 2.5.4
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/dist/{acp-adapter-csh0zz3b.js → acp-adapter-ek38cwsv.js} +5 -5
- package/dist/{actions-6xx079d1.js → actions-92mepq03.js} +19 -19
- package/dist/{amp-adapter-8n6f0hz8.js → amp-adapter-9gaxg015.js} +4 -4
- package/dist/{app-ft6r6v23.js → app-ctn8gsgx.js} +3 -3
- package/dist/{assistant-vt2cyfzd.js → assistant-zyr5kz47.js} +21 -21
- package/dist/{boot-reembed-tgrfy27t.js → boot-reembed-dwzfttdr.js} +1 -1
- package/dist/{boot-reembed-r3e808ky.js → boot-reembed-w75maga2.js} +1 -1
- package/dist/{boot-scrub-sweep-masdwxkm.js → boot-scrub-sweep-e9kq43kf.js} +3 -3
- package/dist/{boot-seeds-0n82qc7q.js → boot-seeds-x28v3fhb.js} +4 -4
- package/dist/{claude-adapter-s7j47k06.js → claude-adapter-f0afqnne.js} +5 -5
- package/dist/{claude-managed-adapter-gt6pa35d.js → claude-managed-adapter-x81a7r65.js} +3 -3
- package/dist/{claude-managed-setup-3hzrgrnk.js → claude-managed-setup-bqzwjd9x.js} +2 -2
- package/dist/{claude-sdk-session-0zrdzyx2.js → claude-sdk-session-7b7whkaq.js} +9307 -9254
- package/dist/{cli-a5ebc95n.js → cli-0shx9j93.js} +16 -13
- package/dist/{cli-4qkg9968.js → cli-19202kew.js} +4 -4
- package/dist/{cli-cyfgxnvb.js → cli-1r5ad7j5.js} +19 -17
- package/dist/{cli-jmcjbjkt.js → cli-25n3mv5d.js} +9 -7
- package/dist/cli-2655k16q.js +140 -0
- package/dist/{cli-wadzapwb.js → cli-277c31dw.js} +1 -1
- package/dist/{cli-n6kmqngw.js → cli-4b2z6n0m.js} +29 -7
- package/dist/{cli-7ccf7akh.js → cli-5bnsc0e1.js} +9 -7
- package/dist/{cli-qrjv7t18.js → cli-621ce7kw.js} +1 -1
- package/dist/{cli-xzge46sj.js → cli-64mh9mf3.js} +20 -6
- package/dist/{cli-b6tnjdvx.js → cli-6a1xg1p7.js} +6 -2
- package/dist/{cli-ydg6ep51.js → cli-6n8axjv1.js} +1 -1
- package/dist/{cli-e91vshb1.js → cli-7p4jxgtt.js} +5 -5
- package/dist/{cli-f3pfn4x3.js → cli-9j129brq.js} +9 -3
- package/dist/{cli-xw9q173h.js → cli-9s05422w.js} +1 -1
- package/dist/{cli-15xd0j7w.js → cli-abs8ndtr.js} +12 -12
- package/dist/{cli-h5d22pfs.js → cli-asq5ydm4.js} +219 -309
- package/dist/{cli-w9etnnxk.js → cli-bf8ma3f1.js} +120 -50
- package/dist/{cli-0vgy5wfw.js → cli-bvpgr9ny.js} +2 -2
- package/dist/{cli-3e9vs93g.js → cli-c61mbdds.js} +4 -4
- package/dist/{cli-e5bxnjps.js → cli-c952c7ac.js} +5 -5
- package/dist/{cli-p859hcc8.js → cli-dew6x255.js} +1 -1
- package/dist/{cli-tx00c0h6.js → cli-dqygeeh5.js} +17 -1
- package/dist/{cli-gpv60278.js → cli-e42vss7b.js} +2 -2
- package/dist/{cli-b63d08rs.js → cli-eftzyh9y.js} +1 -1
- package/dist/{cli-fgn7yg5r.js → cli-ethgy7ha.js} +1 -1
- package/dist/{cli-86zpfje5.js → cli-ff1tm9n9.js} +1 -1
- package/dist/{cli-cf63dnxj.js → cli-g9vd9ece.js} +37 -12
- package/dist/{cli-j3dwd4kz.js → cli-gbfxp48k.js} +770 -126
- package/dist/{cli-n2918sed.js → cli-gn5af0x5.js} +21 -21
- package/dist/{cli-d172svcw.js → cli-gwhjmh8g.js} +4 -4
- package/dist/{cli-stxdrqs2.js → cli-hh5we352.js} +24 -5
- package/dist/{cli-0ebey9hg.js → cli-j6rktc91.js} +1 -1
- package/dist/{cli-csh8fnbg.js → cli-jb30dzvk.js} +473 -58
- package/dist/{cli-vaxw3bg0.js → cli-jbnn4vxb.js} +3 -3
- package/dist/{cli-ha22h0eg.js → cli-k4tjsdx9.js} +45 -40
- package/dist/{cli-f55b6ky0.js → cli-m4ecdxvz.js} +40 -13
- package/dist/{cli-mntxtkhn.js → cli-mz9afxxf.js} +2 -1
- package/dist/{cli-ta8bd7th.js → cli-pzatvw94.js} +1 -1
- package/dist/{cli-3qv8naq8.js → cli-rr7zx19j.js} +1591 -353
- package/dist/{cli-b1tv22wr.js → cli-s682zgqz.js} +1 -1
- package/dist/{cli-bpbcpd8b.js → cli-sbwsxvtd.js} +1 -1
- package/dist/{cli-0xk505e8.js → cli-sxgtf5fw.js} +1 -1
- package/dist/{cli-6y43v79h.js → cli-tsma7p8v.js} +1 -1
- package/dist/{cli-edtm741g.js → cli-wghjkr0e.js} +5 -5
- package/dist/{cli-aaqnqpyw.js → cli-wx2nj19m.js} +30 -11
- package/dist/{cli-y3dzwyy3.js → cli-x0f2v69c.js} +180 -60
- package/dist/{cli-kcnwn85g.js → cli-xtrz6bzs.js} +7 -7
- package/dist/{cli-14v1sxxy.js → cli-xzfq4at3.js} +7 -7
- package/dist/cli.js +20 -20
- package/dist/{codex-adapter-brh2tcxr.js → codex-adapter-wch12q99.js} +3 -3
- package/dist/{codex-hook-e2he7aca.js → codex-hook-brpnycn5.js} +1 -1
- package/dist/{codex-session-runner-0dnj21qs.js → codex-session-runner-41pfcr95.js} +2 -2
- package/dist/{commands-1cnvseqx.js → commands-9849dkj6.js} +2 -2
- package/dist/{controller-kc7s9xhn.js → controller-ja0sd95p.js} +29 -3
- package/dist/{controllers-connect-jzx7t4bn.js → controllers-connect-xp05dj6x.js} +20 -20
- package/dist/{cursor-adapter-qpwp5sfg.js → cursor-adapter-623qxqew.js} +1 -2
- package/dist/{db-4vq88jkc.js → db-47vj40dy.js} +3 -3
- package/dist/{db-v5ytg5pw.js → db-w759qmx0.js} +2 -2
- package/dist/{dispatcher-trtcrqae.js → dispatcher-7b8e4z2j.js} +17 -17
- package/dist/{docker-nfy2gc8t.js → docker-s84hjs91.js} +2 -2
- package/dist/{dsh-adapter-f20xwn1z.js → dsh-adapter-2ezw3xea.js} +2 -3
- package/dist/{external-channel-n9bymc2n.js → external-channel-e2s028da.js} +2 -2
- package/dist/{grok-adapter-4kpmmnr6.js → grok-adapter-ys7gaqee.js} +6 -6
- package/dist/{handlers-ptsraqa8.js → handlers-wy1dr6ja.js} +20 -20
- package/dist/{heartbeat-60qtxtjf.js → heartbeat-w3mntghj.js} +20 -20
- package/dist/{hook-rbktcstg.js → hook-3jjywnpb.js} +9 -3
- package/dist/{hook-ngtvh729.js → hook-g7kvv6x9.js} +3 -2
- package/dist/{http-z8xy72gp.js → http-tkp7d4kb.js} +2984 -2483
- package/dist/{index-r70q0xjx.js → index-1ef1myq2.js} +17 -17
- package/dist/{index-a9nsavhx.js → index-3n8m9ea2.js} +1 -1
- package/dist/{index-cbxkmfc5.js → index-9pn1pfkf.js} +1 -1
- package/dist/{index-s193j5fh.js → index-nmm79qgr.js} +20 -20
- package/dist/{index-6srwvd5b.js → index-p4xfz37z.js} +1 -1
- package/dist/{index-06qkmcx6.js → index-q7m26kvs.js} +33 -23
- package/dist/{index-1cscynkb.js → index-xdaahthb.js} +17 -17
- package/dist/{keepalive-a9srcphj.js → keepalive-v4ta8wgg.js} +3 -3
- package/dist/{lead-3devneqy.js → lead-sawnyqxk.js} +35 -35
- package/dist/{maintenance-0er16h2y.js → maintenance-84n1ex21.js} +1 -1
- package/dist/{oauth-refresh-sweep-b65jcyw2.js → oauth-refresh-sweep-vjnnandw.js} +2 -2
- package/dist/{onboard-wqywz5py.js → onboard-sfmpe4np.js} +2 -2
- package/dist/{opencode-adapter-e56086m1.js → opencode-adapter-jmpst4q4.js} +4 -4
- package/dist/{otel-impl-vk32s884.js → otel-impl-v6cc0zve.js} +1 -1
- package/dist/{pi-mono-adapter-tzps8f38.js → pi-mono-adapter-fk8fkcf6.js} +33 -6
- package/dist/{pricing-refresh-t27anv4z.js → pricing-refresh-fsk1e1zc.js} +3 -3
- package/dist/{rbac-roles-452665y0.js → rbac-roles-prmkdnkw.js} +2 -2
- package/dist/{rbac-roles-bmcgsxjn.js → rbac-roles-zxmxqa39.js} +1 -1
- package/dist/{render-v2-0ttsk2j4.js → render-v2-pjwxvnkw.js} +3 -3
- package/dist/{resume-a26kvr41.js → resume-frdtgfa0.js} +9 -5
- package/dist/{session-access-dgh9nkwk.js → session-access-fpez4y5q.js} +4 -4
- package/dist/{setup-9vc9qahm.js → setup-mq6m3m8q.js} +1 -1
- package/dist/{work-objects-vn8nkxwn.js → work-objects-kfahhjsq.js} +20 -20
- package/dist/{worker-1x1h8vnm.js → worker-v5e5rxj3.js} +35 -35
- package/openapi.json +3078 -2424
- package/package.json +7 -7
- package/plugin/commands/create-pr.md +2 -2
- package/plugin/pi-skills/create-pr/SKILL.md +2 -2
- package/src/be/approval-config-question.ts +389 -0
- package/src/be/automation-preflight.ts +37 -3
- package/src/be/db/approvals.ts +48 -0
- package/src/be/db/session-tree.ts +73 -15
- package/src/be/db/skill-search.ts +87 -0
- package/src/be/db/tasks/read.ts +29 -2
- package/src/be/db/tasks/write.ts +15 -5
- package/src/be/db/workflow-idempotency.ts +183 -0
- package/src/be/db/workflow-runs.ts +366 -8
- package/src/be/db.ts +92 -22
- package/src/be/migrations/239_skills_fts.sql +40 -0
- package/src/be/migrations/240_workflow_run_parent_step_attempt.sql +37 -0
- package/src/be/migrations/241_skills_meta_fts.sql +39 -0
- package/src/be/migrations/242_workflow_idempotency_keys.sql +40 -0
- package/src/be/sealed-json.ts +1 -0
- package/src/be/seed-scripts/catalog/compound-insights.inline.ts +2 -2
- package/src/be/seed-scripts/catalog/compound-insights.ts +2 -2
- package/src/be/seed-skills/bundled-files.generated.json +1 -1
- package/src/be/sql-connections.ts +7 -1
- package/src/be/swarm-config-guard.ts +25 -0
- package/src/be/users.ts +9 -0
- package/src/commands/controller.ts +33 -0
- package/src/commands/runner.ts +6 -2
- package/src/extensions/apply-task-create.ts +57 -11
- package/src/extensions/catalog.generated.json +4 -4
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +25 -0
- package/src/extensions/dispatcher.ts +4 -0
- package/src/extensions/post-bridge.ts +8 -0
- package/src/hooks/context-bound-note.ts +196 -0
- package/src/hooks/hook.ts +12 -0
- package/src/http/approval-requests.ts +209 -38
- package/src/http/config.ts +70 -53
- package/src/http/idempotency-key.ts +117 -0
- package/src/http/sessions.ts +27 -2
- package/src/http/workflows.ts +129 -24
- package/src/integrations/chat-sdk/inbound.ts +5 -4
- package/src/prompts/context-bound-note.ts +48 -0
- package/src/prompts/session-templates.ts +25 -1
- package/src/providers/amp-adapter.ts +30 -8
- package/src/providers/claude-adapter.ts +29 -4
- package/src/providers/claude-managed-adapter.ts +26 -0
- package/src/providers/claude-sdk-session.ts +10 -0
- package/src/providers/claude-session-events.ts +13 -0
- package/src/providers/codex-adapter.ts +12 -0
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/providers/opencode-adapter.ts +18 -0
- package/src/providers/pi-mono-adapter.ts +35 -0
- package/src/runtime-providers/docker.ts +6 -2
- package/src/scripts-runtime/types/swarm-extension.d.ts +35 -0
- package/src/tests/amp-adapter.test.ts +97 -0
- package/src/tests/approval-config-question.test.ts +702 -0
- package/src/tests/approval-requests.test.ts +10 -7
- package/src/tests/approval-sweeps.test.ts +12 -6
- package/src/tests/base-prompt.test.ts +1 -1
- package/src/tests/chat-sdk-inbound.test.ts +20 -0
- package/src/tests/claude-hook-output.test.ts +79 -1
- package/src/tests/claude-managed-adapter.test.ts +275 -0
- package/src/tests/codex-adapter.test.ts +163 -0
- package/src/tests/context-bound-note.test.ts +269 -0
- package/src/tests/dsh-adapter.test.ts +1 -1
- package/src/tests/entrypoint-gitlab-credential-helper.test.ts +273 -0
- package/src/tests/extensions-example-comms-adherence-gate.test.ts +321 -12
- package/src/tests/extensions-pre-task.test.ts +273 -0
- package/src/tests/fetch-resolved-env.test.ts +17 -0
- package/src/tests/fixtures/extensions/record-blocks/hooks.ts +10 -0
- package/src/tests/fixtures/extensions/record-blocks/manifest.json +7 -0
- package/src/tests/fixtures/fake-amp.ts +22 -8
- package/src/tests/fixtures/held-template-render.ts +42 -0
- package/src/tests/fixtures/workflow-idempotency-trigger.child.ts +73 -0
- package/src/tests/list-endpoint-slimming.test.ts +33 -0
- package/src/tests/migration-240-workflow-run-parent-step-attempt.test.ts +55 -0
- package/src/tests/openapi-route-headers.test.ts +2 -0
- package/src/tests/opencode-adapter.test.ts +134 -0
- package/src/tests/pi-mono-adapter.test.ts +105 -0
- package/src/tests/prompt-template-session.test.ts +4 -3
- package/src/tests/runner-push-delivery.test.ts +25 -0
- package/src/tests/runtime-controller-liveness.test.ts +133 -0
- package/src/tests/runtime-providers-docker.test.ts +2 -0
- package/src/tests/script-connections.test.ts +9 -0
- package/src/tests/script-run-config-scope.test.ts +91 -3
- package/src/tests/session-logs.test.ts +70 -0
- package/src/tests/sessions-tree.test.ts +205 -1
- package/src/tests/skill-search-long-content.test.ts +193 -0
- package/src/tests/skill-search.test.ts +173 -22
- package/src/tests/workflow-cascade-cancel.test.ts +1169 -0
- package/src/tests/workflow-foreach-supersede-retry.test.ts +488 -0
- package/src/tests/workflow-idempotency.test.ts +915 -0
- package/src/tests/workflow-sub-workflow.test.ts +538 -1
- package/src/tools/request-human-input.ts +27 -5
- package/src/tools/workflows/cancel-workflow-run.ts +1 -1
- package/src/types.ts +7 -0
- package/src/utils/context-window.ts +8 -6
- package/src/workflows/approval-cancel.ts +15 -13
- package/src/workflows/approval-notifications.ts +15 -4
- package/src/workflows/checkpoint.ts +72 -35
- package/src/workflows/engine.ts +262 -93
- package/src/workflows/executors/base.ts +6 -0
- package/src/workflows/executors/foreach.ts +2 -2
- package/src/workflows/executors/human-in-the-loop.ts +25 -1
- package/src/workflows/executors/sub-workflow.ts +45 -16
- package/src/workflows/foreach-driver.ts +10 -3
- package/src/workflows/foreach-join.ts +29 -6
- package/src/workflows/recovery.ts +68 -1
- package/src/workflows/resume.ts +228 -77
- package/src/workflows/retry-poller.ts +74 -7
- package/src/workflows/task-step-routing.ts +29 -7
- package/templates/ai-toolbox.manifest.json +8 -8
- package/templates/community/open-prompt-showreel/PLAYBOOK.md +5 -6
- package/templates/community/open-prompt-showreel/lead-prompt.md +1 -1
- package/templates/community/open-prompt-showreel/skills/open-prompt-showreel/SKILL.md +4 -4
- package/templates/extensions/comms-adherence-gate/README.md +38 -13
- package/templates/extensions/comms-adherence-gate/hooks.ts +272 -69
- package/templates/extensions/comms-adherence-gate/manifest.json +2 -2
- package/templates/skills/agent-browser/SKILL.md +4 -4
- package/templates/skills/agent-browser/content.md +4 -4
- package/templates/skills/code-quality/config.json +1 -1
- package/templates/skills/code-quality/content.md +2 -2
- package/templates/skills/delegate-work/content.md +29 -25
- package/templates/skills/delegate-work/files/scripts/codex-exec.sh +5 -5
- package/templates/skills/qa/content.md +2 -2
- package/templates/skills/researching/content.md +2 -0
- package/templates/skills/turso-interaction/content.md +1 -1
- package/templates/skills/work-on-task/SKILL.md +1 -1
- package/templates/skills/work-on-task/content.md +1 -1
- package/templates/skills/workflow-iterate/content.md +8 -1
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
closeDb,
|
|
6
6
|
createSessionLogs,
|
|
7
7
|
createTaskExtended,
|
|
8
|
+
getDbClient,
|
|
8
9
|
getSessionLogsBySession,
|
|
9
10
|
getSessionLogsByTaskId,
|
|
10
11
|
getTaskById,
|
|
@@ -226,6 +227,75 @@ describe("Session Logs API", () => {
|
|
|
226
227
|
expect(logs[3]?.iteration).toBe(2);
|
|
227
228
|
expect(logs[3]?.lineNumber).toBe(1);
|
|
228
229
|
});
|
|
230
|
+
|
|
231
|
+
test("keeps write order across many flushes of one iteration", async () => {
|
|
232
|
+
const task = await createTaskExtended("Task for multi-flush ordering");
|
|
233
|
+
// The runner flushes every 50 lines or 5s, so one iteration arrives as
|
|
234
|
+
// many POSTs. Each POST used to restart lineNumber at 0.
|
|
235
|
+
const written: string[] = [];
|
|
236
|
+
for (let flush = 0; flush < 3; flush++) {
|
|
237
|
+
const lines = [0, 1, 2, 3].map((i) => `flush${flush}-line${i}`);
|
|
238
|
+
written.push(...lines);
|
|
239
|
+
await createSessionLogs({
|
|
240
|
+
taskId: task.id,
|
|
241
|
+
sessionId: "multi-flush-session",
|
|
242
|
+
iteration: 1,
|
|
243
|
+
cli: "claude",
|
|
244
|
+
lines,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const all = await getSessionLogsByTaskId(task.id);
|
|
249
|
+
expect(all.map((l) => l.content)).toEqual(written);
|
|
250
|
+
expect(all.map((l) => l.lineNumber)).toEqual(written.map((_, i) => i));
|
|
251
|
+
|
|
252
|
+
const tail = await getSessionLogsByTaskId(task.id, 5);
|
|
253
|
+
expect(tail.map((l) => l.content)).toEqual(written.slice(-5));
|
|
254
|
+
|
|
255
|
+
const bySession = await getSessionLogsBySession("multi-flush-session", 1);
|
|
256
|
+
expect(bySession.map((l) => l.content)).toEqual(written);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("reads rows stored with per-flush lineNumbers in write order", async () => {
|
|
260
|
+
const task = await createTaskExtended("Task with legacy session log rows");
|
|
261
|
+
// Rows written before the fix: lineNumber restarts per POST, and
|
|
262
|
+
// createdAt ties inside one POST.
|
|
263
|
+
const legacy = [
|
|
264
|
+
["a0", 0],
|
|
265
|
+
["a1", 1],
|
|
266
|
+
["a2", 2],
|
|
267
|
+
["b0", 0],
|
|
268
|
+
["b1", 1],
|
|
269
|
+
["c0", 0],
|
|
270
|
+
] as const;
|
|
271
|
+
await getDbClient().transaction(async (tx) => {
|
|
272
|
+
for (const [content, lineNumber] of legacy) {
|
|
273
|
+
await tx.run(
|
|
274
|
+
`INSERT INTO session_logs (id, taskId, sessionId, iteration, cli, content, lineNumber, createdAt)
|
|
275
|
+
VALUES (?, ?, 'legacy-session', 1, 'claude', ?, ?, '2026-10-01T00:00:00.000Z')`,
|
|
276
|
+
[crypto.randomUUID(), task.id, content, lineNumber],
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
// A flush after deploy lands behind the legacy rows.
|
|
281
|
+
await createSessionLogs({
|
|
282
|
+
taskId: task.id,
|
|
283
|
+
sessionId: "legacy-session",
|
|
284
|
+
iteration: 1,
|
|
285
|
+
cli: "claude",
|
|
286
|
+
lines: ["d0", "d1"],
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const written = ["a0", "a1", "a2", "b0", "b1", "c0", "d0", "d1"];
|
|
290
|
+
const all = await getSessionLogsByTaskId(task.id);
|
|
291
|
+
expect(all.map((l) => l.content)).toEqual(written);
|
|
292
|
+
|
|
293
|
+
const tail = await getSessionLogsByTaskId(task.id, 4);
|
|
294
|
+
expect(tail.map((l) => l.content)).toEqual(["b1", "c0", "d0", "d1"]);
|
|
295
|
+
|
|
296
|
+
const bySession = await getSessionLogsBySession("legacy-session", 1);
|
|
297
|
+
expect(bySession.map((l) => l.content)).toEqual(written);
|
|
298
|
+
});
|
|
229
299
|
});
|
|
230
300
|
|
|
231
301
|
describe("POST /api/session-logs", () => {
|
|
@@ -4,6 +4,9 @@ import { keyPrefixRange, sessionGroupKey } from "../assets/key";
|
|
|
4
4
|
import {
|
|
5
5
|
closeDb,
|
|
6
6
|
countSessions,
|
|
7
|
+
createAgent,
|
|
8
|
+
createSessionCost,
|
|
9
|
+
createSteeringMessage,
|
|
7
10
|
createTaskExtended,
|
|
8
11
|
createUser,
|
|
9
12
|
getDbClient,
|
|
@@ -58,6 +61,7 @@ beforeAll(async () => {
|
|
|
58
61
|
});
|
|
59
62
|
beforeEach(async () => {
|
|
60
63
|
await getDbClient().run("DELETE FROM approval_requests");
|
|
64
|
+
await getDbClient().run("DELETE FROM task_steering_messages");
|
|
61
65
|
await getDbClient().run("DELETE FROM inbox_item_state");
|
|
62
66
|
await getDbClient().run("DELETE FROM session_shares");
|
|
63
67
|
await getDbClient().run("DELETE FROM agent_tasks");
|
|
@@ -195,7 +199,8 @@ describe("chain-wide read state", () => {
|
|
|
195
199
|
{ activity: "2026-10-14T12:00:00.000Z", expected: "read" },
|
|
196
200
|
{ status: "dismissed", updated: OLD, expected: "done" },
|
|
197
201
|
{ status: "done", updated: NOW, expected: "done" },
|
|
198
|
-
|
|
202
|
+
// Agent activity after the Done mark keeps it Done.
|
|
203
|
+
{ status: "done", updated: OLD, expected: "done" },
|
|
199
204
|
{ status: "snoozed", snoozeUntil: "2026-10-22T12:00:00.000Z", expected: "later" },
|
|
200
205
|
{ status: "snoozed", snoozeUntil: NOW, expected: "unread" },
|
|
201
206
|
{ status: "snoozed", expected: "unread" },
|
|
@@ -259,6 +264,145 @@ describe("chain-wide read state", () => {
|
|
|
259
264
|
});
|
|
260
265
|
});
|
|
261
266
|
|
|
267
|
+
describe("Done holds until the session needs the user", () => {
|
|
268
|
+
const DONE_AT = "2026-10-21T11:00:00.000Z";
|
|
269
|
+
const BEFORE = "2026-10-21T10:30:00.000Z";
|
|
270
|
+
const AFTER = "2026-10-21T11:30:00.000Z";
|
|
271
|
+
const readState = async (id: string) =>
|
|
272
|
+
(await listRecentSessions(filters({ limit: 100 }))).find((s) => s.root.id === id)?.readState;
|
|
273
|
+
const doneSession = async () => {
|
|
274
|
+
const root = await task("shared/", { source: "ui" }, BEFORE);
|
|
275
|
+
await getDbClient().run(
|
|
276
|
+
"UPDATE agent_tasks SET createdAt = ?, status = 'in_progress' WHERE id = ?",
|
|
277
|
+
[BEFORE, root.id],
|
|
278
|
+
);
|
|
279
|
+
await state(root.id, "done", DONE_AT);
|
|
280
|
+
expect(await readState(root.id)).toBe("done");
|
|
281
|
+
return root;
|
|
282
|
+
};
|
|
283
|
+
const child = async (
|
|
284
|
+
parentTaskId: string,
|
|
285
|
+
extra: Parameters<typeof createTaskExtended>[1],
|
|
286
|
+
createdAt = AFTER,
|
|
287
|
+
) => {
|
|
288
|
+
const row = await createTaskExtended("Child", { parentTaskId, key: "shared/", ...extra });
|
|
289
|
+
await getDbClient().run(
|
|
290
|
+
"UPDATE agent_tasks SET createdAt = ?, lastUpdatedAt = ?, status = 'completed' WHERE id = ?",
|
|
291
|
+
[createdAt, createdAt, row.id],
|
|
292
|
+
);
|
|
293
|
+
return row;
|
|
294
|
+
};
|
|
295
|
+
const approval = async (taskId: string, createdAt: string, status = "pending") =>
|
|
296
|
+
getDbClient().run(
|
|
297
|
+
`INSERT INTO approval_requests (id, sourceTaskId, title, questions, approvers, status, createdAt)
|
|
298
|
+
VALUES (?, ?, 'Approve deploy', '[]', '{}', ?, ?)`,
|
|
299
|
+
[crypto.randomUUID(), taskId, status, createdAt],
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
test("agent progress and a status change keep it Done", async () => {
|
|
303
|
+
const root = await doneSession();
|
|
304
|
+
await getDbClient().run(
|
|
305
|
+
"UPDATE agent_tasks SET lastUpdatedAt = ?, status = 'completed' WHERE id = ?",
|
|
306
|
+
[AFTER, root.id],
|
|
307
|
+
);
|
|
308
|
+
expect(await readState(root.id)).toBe("done");
|
|
309
|
+
const tree = await getSessionTree(actor(), NOW);
|
|
310
|
+
expect(tree.done.total).toBe(1);
|
|
311
|
+
expect(tree.inbox.total).toBe(0);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test("agent-created child and delegated tasks keep it Done", async () => {
|
|
315
|
+
const root = await doneSession();
|
|
316
|
+
// Inherits the requester from its parent.
|
|
317
|
+
const inherited = await child(root.id, { source: "mcp" });
|
|
318
|
+
// Lead delegation, a deferred wake-up and a follow-up name the requester explicitly.
|
|
319
|
+
await child(inherited.id, { source: "mcp", requestedByUserId: owner.id });
|
|
320
|
+
await child(root.id, { source: "schedule", requestedByUserId: owner.id });
|
|
321
|
+
await child(root.id, { source: "system", requestedByUserId: owner.id });
|
|
322
|
+
await child(root.id, { source: "workflow", requestedByUserId: owner.id });
|
|
323
|
+
expect(await readState(root.id)).toBe("done");
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("a pending approval created after Done returns it to the Inbox", async () => {
|
|
327
|
+
const before = await doneSession();
|
|
328
|
+
await approval(before.id, BEFORE);
|
|
329
|
+
const resolved = await doneSession();
|
|
330
|
+
await approval(resolved.id, AFTER, "approved");
|
|
331
|
+
expect(await readState(before.id)).toBe("done");
|
|
332
|
+
expect(await readState(resolved.id)).toBe("done");
|
|
333
|
+
|
|
334
|
+
const root = await doneSession();
|
|
335
|
+
const agent = await child(root.id, { source: "mcp" });
|
|
336
|
+
await approval(agent.id, AFTER);
|
|
337
|
+
expect(await readState(root.id)).toBe("unread");
|
|
338
|
+
expect(await ids({ triage: "inbox" })).toEqual([root.id]);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test("a human follow-up after Done returns it to the Inbox", async () => {
|
|
342
|
+
const earlier = await doneSession();
|
|
343
|
+
await child(earlier.id, { source: "ui", requestedByUserId: owner.id }, BEFORE);
|
|
344
|
+
expect(await readState(earlier.id)).toBe("done");
|
|
345
|
+
|
|
346
|
+
const root = await doneSession();
|
|
347
|
+
await child(root.id, { source: "ui", requestedByUserId: owner.id });
|
|
348
|
+
expect(await readState(root.id)).toBe("unread");
|
|
349
|
+
const slack = await doneSession();
|
|
350
|
+
await child(slack.id, { source: "slack", requestedByUserId: other.id });
|
|
351
|
+
expect(await readState(slack.id)).toBe("unread");
|
|
352
|
+
const tree = await getSessionTree(actor(), NOW);
|
|
353
|
+
expect(tree.inbox.total).toBe(2);
|
|
354
|
+
expect(tree.done.total).toBe(1);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("a person's steer or queued message after Done returns it to the Inbox", async () => {
|
|
358
|
+
// The real writer stamps created_at with the column default, after DONE_AT.
|
|
359
|
+
const steered = await doneSession();
|
|
360
|
+
await createSteeringMessage({
|
|
361
|
+
taskId: steered.id,
|
|
362
|
+
body: "Change course",
|
|
363
|
+
mode: "steer",
|
|
364
|
+
source: "ui",
|
|
365
|
+
createdByKind: "user",
|
|
366
|
+
createdByUserId: owner.id,
|
|
367
|
+
});
|
|
368
|
+
expect(await readState(steered.id)).toBe("unread");
|
|
369
|
+
|
|
370
|
+
const queued = await doneSession();
|
|
371
|
+
const agent = await child(queued.id, { source: "mcp" });
|
|
372
|
+
await createSteeringMessage({
|
|
373
|
+
taskId: agent.id,
|
|
374
|
+
body: "Also check this",
|
|
375
|
+
mode: "queue",
|
|
376
|
+
source: "slack",
|
|
377
|
+
createdByKind: "user",
|
|
378
|
+
createdByUserId: owner.id,
|
|
379
|
+
});
|
|
380
|
+
expect(await readState(queued.id)).toBe("unread");
|
|
381
|
+
expect((await ids({ triage: "inbox" })).sort()).toEqual([steered.id, queued.id].sort());
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("agent, system, cancelled and earlier steering keep it Done", async () => {
|
|
385
|
+
const steer = async (
|
|
386
|
+
taskId: string,
|
|
387
|
+
createdByKind: "user" | "agent" | "system",
|
|
388
|
+
createdAt = AFTER,
|
|
389
|
+
status = "pending",
|
|
390
|
+
) =>
|
|
391
|
+
getDbClient().run(
|
|
392
|
+
`INSERT INTO task_steering_messages
|
|
393
|
+
(id, task_id, body, mode, status, source, created_by_kind, created_at)
|
|
394
|
+
VALUES (?, ?, 'Steer', 'queue', ?, 'mcp', ?, ?)`,
|
|
395
|
+
[crypto.randomUUID(), taskId, status, createdByKind, createdAt],
|
|
396
|
+
);
|
|
397
|
+
const root = await doneSession();
|
|
398
|
+
await steer(root.id, "agent");
|
|
399
|
+
await steer(root.id, "system");
|
|
400
|
+
await steer(root.id, "user", AFTER, "cancelled");
|
|
401
|
+
await steer(root.id, "user", BEFORE, "handled");
|
|
402
|
+
expect(await readState(root.id)).toBe("done");
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
262
406
|
test("needs attention uses the latest failed status and pending approvals anywhere in the chain", async () => {
|
|
263
407
|
const failed = await task();
|
|
264
408
|
const child = await task("shared/a/", { parentTaskId: failed.id }, NOW);
|
|
@@ -463,6 +607,41 @@ test("session detail carries the viewer's read state with the list rule", async
|
|
|
463
607
|
expect(bypass.lastActivityAt).toBeUndefined();
|
|
464
608
|
});
|
|
465
609
|
|
|
610
|
+
test("session detail carries each chain task's cost, so the header can total the whole session", async () => {
|
|
611
|
+
const agent = await createAgent({ name: "cost-agent", isLead: false, status: "idle" });
|
|
612
|
+
const root = await task();
|
|
613
|
+
const child = await task("shared/a/", { parentTaskId: root.id });
|
|
614
|
+
const grandchild = await task("shared/a/", { parentTaskId: child.id });
|
|
615
|
+
const followUp = await task("shared/a/", { parentTaskId: root.id });
|
|
616
|
+
const noCost = await task("shared/a/", { parentTaskId: root.id });
|
|
617
|
+
const spend = async (taskId: string, totalCostUsd: number) =>
|
|
618
|
+
createSessionCost({
|
|
619
|
+
sessionId: crypto.randomUUID(),
|
|
620
|
+
taskId,
|
|
621
|
+
agentId: agent.id,
|
|
622
|
+
totalCostUsd,
|
|
623
|
+
durationMs: 1_000,
|
|
624
|
+
numTurns: 1,
|
|
625
|
+
model: "test-model",
|
|
626
|
+
});
|
|
627
|
+
await spend(root.id, 0.2);
|
|
628
|
+
await spend(root.id, 0.0051);
|
|
629
|
+
await spend(child.id, 0.5);
|
|
630
|
+
await spend(grandchild.id, 0.25);
|
|
631
|
+
await spend(followUp.id, 0.335);
|
|
632
|
+
|
|
633
|
+
const res = await fetch(`${baseUrl}/api/sessions/${root.id}`);
|
|
634
|
+
const body = (await res.json()) as { chain: { id: string; totalCostUsd?: number }[] };
|
|
635
|
+
const costs = Object.fromEntries(body.chain.map((t) => [t.id, t.totalCostUsd]));
|
|
636
|
+
expect(costs[root.id]).toBeCloseTo(0.2051, 6);
|
|
637
|
+
expect(costs[child.id]).toBeCloseTo(0.5, 6);
|
|
638
|
+
expect(costs[grandchild.id]).toBeCloseTo(0.25, 6);
|
|
639
|
+
expect(costs[followUp.id]).toBeCloseTo(0.335, 6);
|
|
640
|
+
expect(costs[noCost.id]).toBeUndefined();
|
|
641
|
+
const total = body.chain.reduce((sum, t) => sum + (t.totalCostUsd ?? 0), 0);
|
|
642
|
+
expect(total).toBeCloseTo(1.2901, 6);
|
|
643
|
+
});
|
|
644
|
+
|
|
466
645
|
test("limit clamps to 200 and count without chain filters does not recurse", async () => {
|
|
467
646
|
await getDbClient().transaction(async () => {
|
|
468
647
|
for (let i = 0; i < 205; i++)
|
|
@@ -591,6 +770,31 @@ describe("source filter and list triage", () => {
|
|
|
591
770
|
);
|
|
592
771
|
});
|
|
593
772
|
|
|
773
|
+
test("showSource keeps only the listed sources, and an empty list keeps none", async () => {
|
|
774
|
+
await task("shared/", { source: "slack" });
|
|
775
|
+
const ui = await task("shared/a/", { source: "ui" });
|
|
776
|
+
await task("shared/", { source: "github" });
|
|
777
|
+
|
|
778
|
+
const full = await treeOf();
|
|
779
|
+
const shown = await treeOf("showSource=ui");
|
|
780
|
+
expect(shown.inbox.total).toBe(1);
|
|
781
|
+
expect(shown.groups.map((g) => g.key)).toEqual(["shared/a/"]);
|
|
782
|
+
expect(shown.sources).toEqual(full.sources);
|
|
783
|
+
const inbox = await listOf("triage=inbox&showSource=ui");
|
|
784
|
+
expect(inbox.total).toBe(1);
|
|
785
|
+
expect(inbox.sessions.map((s) => s.root.id)).toEqual([ui.id]);
|
|
786
|
+
|
|
787
|
+
const none = await treeOf("showSource=");
|
|
788
|
+
expect(none.inbox.total).toBe(0);
|
|
789
|
+
expect(none.sources).toEqual(full.sources);
|
|
790
|
+
expect((await listOf("triage=inbox&showSource=")).total).toBe(0);
|
|
791
|
+
|
|
792
|
+
expect((await triage({ action: "done", triage: "inbox", showSource: ["ui"] })).status).toBe(
|
|
793
|
+
200,
|
|
794
|
+
);
|
|
795
|
+
expect((await listOf("triage=done")).sessions.map((s) => s.root.id)).toEqual([ui.id]);
|
|
796
|
+
});
|
|
797
|
+
|
|
594
798
|
test("triage by list writes every matching session, honoring the list and the filters", async () => {
|
|
595
799
|
const a = await task("shared/", { source: "ui" });
|
|
596
800
|
const b = await task("shared/", { source: "ui" }, OLD);
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
3
|
+
import { unlink } from "node:fs/promises";
|
|
4
|
+
import {
|
|
5
|
+
closeDb,
|
|
6
|
+
createSkill,
|
|
7
|
+
deleteSkill,
|
|
8
|
+
getDb,
|
|
9
|
+
initDb,
|
|
10
|
+
searchSkills,
|
|
11
|
+
updateSkill,
|
|
12
|
+
} from "../be/db";
|
|
13
|
+
|
|
14
|
+
// Natural-language probes that missed on the live catalog: the obvious skill
|
|
15
|
+
// has a long SKILL.md body, and a shorter skill repeats the query words in
|
|
16
|
+
// its content. bm25 over the whole row buried the name/description hit.
|
|
17
|
+
const TEST_DB_PATH = `./test-skill-search-long-content-${process.pid}.sqlite`;
|
|
18
|
+
|
|
19
|
+
async function removeDbFiles(path: string): Promise<void> {
|
|
20
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
21
|
+
await unlink(path + suffix).catch(() => {});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function topNames(query: string): Promise<string[]> {
|
|
26
|
+
return (await searchSkills(query, 10, false)).map((skill) => skill.name);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// A long body of words that no probe uses, like a full SKILL.md.
|
|
30
|
+
const longBody = (topic: string) =>
|
|
31
|
+
`${topic}\n${"Each step lists its inputs, outputs and the checks to run afterwards. ".repeat(60)}`;
|
|
32
|
+
|
|
33
|
+
describe("searchSkills on long skill bodies", () => {
|
|
34
|
+
beforeAll(async () => {
|
|
35
|
+
await removeDbFiles(TEST_DB_PATH);
|
|
36
|
+
initDb(TEST_DB_PATH);
|
|
37
|
+
|
|
38
|
+
const fixture = [
|
|
39
|
+
{
|
|
40
|
+
name: "pages",
|
|
41
|
+
description: "Publish hosted HTML pages with share links for outside readers.",
|
|
42
|
+
content: longBody("Hosted artifacts."),
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "newsletter-sources",
|
|
46
|
+
description: "Weekly AI newsletter source list and reading picks.",
|
|
47
|
+
content: "Publish a short note for a page you read, then publish the page list. ".repeat(6),
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "browser-interaction",
|
|
51
|
+
description: "Drive a headless browser.",
|
|
52
|
+
content: "Open a page, wait for a page to load, then read a page. ".repeat(6),
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "scheduling",
|
|
56
|
+
description: "Recurring jobs with cron expressions: create, pause or resume a schedule.",
|
|
57
|
+
content: longBody("Cron jobs."),
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "social-content",
|
|
61
|
+
description: "Draft social posts for the brand accounts.",
|
|
62
|
+
content: "How do I create a post? Create a draft, then schedule a post for a slot. ".repeat(
|
|
63
|
+
6,
|
|
64
|
+
),
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "code-quality",
|
|
68
|
+
description: "Checks to run before you push or open a pull request, and how to review one.",
|
|
69
|
+
content: longBody("Pre-push gate."),
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "customer-ops",
|
|
73
|
+
description: "Operations runbook for one customer deployment.",
|
|
74
|
+
content:
|
|
75
|
+
"Open a pull request, ask a reviewer, review a pull request, request a fix. ".repeat(6),
|
|
76
|
+
},
|
|
77
|
+
// "get" is a stopword, and "aaa-get-helper" sorts first by name.
|
|
78
|
+
{ name: "get", description: "Fetch API resources.", content: "fetch resources" },
|
|
79
|
+
{ name: "aaa-get-helper", description: "Get helper instructions", content: "get content" },
|
|
80
|
+
{ name: "what is it", description: "Release notes digest.", content: "notes" },
|
|
81
|
+
{ name: "aaa-what-is-it", description: "What is it for, in short.", content: "notes" },
|
|
82
|
+
];
|
|
83
|
+
for (const skill of fixture) await createSkill({ ...skill, scope: "swarm" });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
afterAll(async () => {
|
|
87
|
+
closeDb();
|
|
88
|
+
await removeDbFiles(TEST_DB_PATH);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test.each([
|
|
92
|
+
["publish a page", "pages"],
|
|
93
|
+
["how do I create a schedule", "scheduling"],
|
|
94
|
+
["how do I review a pull request", "code-quality"],
|
|
95
|
+
])("%p ranks %p first over content that repeats the query words", async (query, expected) => {
|
|
96
|
+
expect((await topNames(query))[0]).toBe(expected);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("stopwords do not match every skill", async () => {
|
|
100
|
+
// Every fixture body contains "a"; only skills about pages may match.
|
|
101
|
+
expect(await topNames("publish a page")).not.toContain("code-quality");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("a query of only stopwords falls back to the substring match", async () => {
|
|
105
|
+
const names = await topNames("how do I");
|
|
106
|
+
expect(names).toContain("social-content");
|
|
107
|
+
expect(names).not.toContain("pages");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test.each([
|
|
111
|
+
"get",
|
|
112
|
+
"GET",
|
|
113
|
+
" Get ",
|
|
114
|
+
])("an exact-name query of only stopwords (%p) ranks that skill first", async (query) => {
|
|
115
|
+
expect((await searchSkills(query, 1, false)).map((skill) => skill.name)).toEqual(["get"]);
|
|
116
|
+
expect((await topNames(query)).slice(0, 2)).toEqual(["get", "aaa-get-helper"]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("a multi-word exact name of only stopwords ranks first", async () => {
|
|
120
|
+
expect((await searchSkills("What Is It", 1, false)).map((skill) => skill.name)).toEqual([
|
|
121
|
+
"what is it",
|
|
122
|
+
]);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("keeps the name/description index in sync on update and delete", async () => {
|
|
126
|
+
const skill = await createSkill({
|
|
127
|
+
name: "meta-sync-probe",
|
|
128
|
+
description: "Starts with gammaword.",
|
|
129
|
+
content: "",
|
|
130
|
+
scope: "swarm",
|
|
131
|
+
});
|
|
132
|
+
const metaRows = (term: string) =>
|
|
133
|
+
getDb()
|
|
134
|
+
.query<{ skill_id: string }, [string]>(
|
|
135
|
+
"SELECT skill_id FROM skills_meta_fts WHERE skills_meta_fts MATCH ?",
|
|
136
|
+
)
|
|
137
|
+
.all(`"${term}"`)
|
|
138
|
+
.map((row) => row.skill_id);
|
|
139
|
+
expect(metaRows("gammaword")).toEqual([skill.id]);
|
|
140
|
+
|
|
141
|
+
await updateSkill(skill.id, { description: "Now about deltaword." });
|
|
142
|
+
expect(metaRows("gammaword")).toEqual([]);
|
|
143
|
+
expect(metaRows("deltaword")).toEqual([skill.id]);
|
|
144
|
+
|
|
145
|
+
await deleteSkill(skill.id);
|
|
146
|
+
expect(metaRows("deltaword")).toEqual([]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("migrated schema carries the name/description sync triggers", () => {
|
|
150
|
+
const triggers = getDb()
|
|
151
|
+
.query<{ name: string }, []>(
|
|
152
|
+
"SELECT name FROM sqlite_master WHERE type = 'trigger' AND tbl_name = 'skills' AND name LIKE 'skills_meta_fts_%' ORDER BY name",
|
|
153
|
+
)
|
|
154
|
+
.all()
|
|
155
|
+
.map((row) => row.name);
|
|
156
|
+
expect(triggers).toEqual([
|
|
157
|
+
"skills_meta_fts_delete",
|
|
158
|
+
"skills_meta_fts_insert",
|
|
159
|
+
"skills_meta_fts_update",
|
|
160
|
+
]);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("migration 241 backfills skills that existed before it ran", async () => {
|
|
165
|
+
const db = new Database(":memory:");
|
|
166
|
+
try {
|
|
167
|
+
db.exec(await Bun.file("src/be/migrations/019_skills.sql").text());
|
|
168
|
+
db.run(
|
|
169
|
+
"INSERT INTO skills (id, name, description, content, type, scope, createdAt, lastUpdatedAt) VALUES ('s1', 'legacy-skill', 'Old description', 'old body', 'remote', 'swarm', '2026-01-01', '2026-01-01')",
|
|
170
|
+
);
|
|
171
|
+
const migration = await Bun.file("src/be/migrations/241_skills_meta_fts.sql").text();
|
|
172
|
+
db.exec(migration);
|
|
173
|
+
|
|
174
|
+
const match = (term: string) =>
|
|
175
|
+
db
|
|
176
|
+
.query<{ skill_id: string }, [string]>(
|
|
177
|
+
"SELECT skill_id FROM skills_meta_fts WHERE skills_meta_fts MATCH ?",
|
|
178
|
+
)
|
|
179
|
+
.all(term)
|
|
180
|
+
.map((row) => row.skill_id);
|
|
181
|
+
expect(match('"description"')).toEqual(["s1"]);
|
|
182
|
+
// Content is not indexed here.
|
|
183
|
+
expect(match('"body"')).toEqual([]);
|
|
184
|
+
|
|
185
|
+
// Re-running the migration does not duplicate backfilled rows.
|
|
186
|
+
db.exec(migration);
|
|
187
|
+
expect(db.query<{ n: number }, []>("SELECT count(*) AS n FROM skills_meta_fts").get()?.n).toBe(
|
|
188
|
+
1,
|
|
189
|
+
);
|
|
190
|
+
} finally {
|
|
191
|
+
db.close();
|
|
192
|
+
}
|
|
193
|
+
});
|