@desplega.ai/agent-swarm 2.5.1 → 2.5.3
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-7g6bpwbk.js → acp-adapter-gr2fcyyj.js} +3 -3
- package/dist/{actions-w8xd014k.js → actions-wv766ghf.js} +18 -18
- package/dist/{amp-adapter-z4qvhacj.js → amp-adapter-speb906t.js} +3 -3
- package/dist/{app-nndcvftq.js → app-ag29n14f.js} +3 -3
- package/dist/{assistant-hjmqyscx.js → assistant-3x8bz3eg.js} +19 -19
- package/dist/{boot-reembed-r8rhf9h8.js → boot-reembed-5b3d2eea.js} +1 -1
- package/dist/{boot-reembed-vwcj2y5d.js → boot-reembed-jsmfrpzf.js} +1 -1
- package/dist/{boot-scrub-sweep-fr9q36ne.js → boot-scrub-sweep-s73gerxy.js} +2 -2
- package/dist/{boot-seeds-9jqwnegq.js → boot-seeds-5zmax2wr.js} +4 -4
- package/dist/{claude-adapter-2f8b85df.js → claude-adapter-hswdnm3t.js} +3 -3
- package/dist/{claude-managed-adapter-gt6pa35d.js → claude-managed-adapter-cvbzj1h6.js} +2 -2
- package/dist/{claude-managed-setup-c3hp36ph.js → claude-managed-setup-3k8m3hhc.js} +2 -2
- package/dist/{claude-sdk-session-05kat8rv.js → claude-sdk-session-fkbyk66x.js} +6905 -6855
- package/dist/{cli-y3zyaee5.js → cli-1fc85mvt.js} +5 -5
- package/dist/{cli-v2g93k4v.js → cli-210xd1f4.js} +4 -4
- package/dist/{cli-sn1vbtg2.js → cli-25j01dpj.js} +3 -3
- package/dist/{cli-n6kmqngw.js → cli-35gtk1p6.js} +1 -1
- package/dist/{cli-r9fy0ddc.js → cli-3ngdk5te.js} +1 -1
- package/dist/{cli-z63a269p.js → cli-40jsnrjm.js} +2 -2
- package/dist/{cli-ba8ne1es.js → cli-4rvr7zfx.js} +10 -0
- package/dist/{cli-qfm6my86.js → cli-502eqv93.js} +1 -1
- package/dist/{cli-xd4eqtr2.js → cli-6n8axjv1.js} +1 -1
- package/dist/{cli-hadv0725.js → cli-8s5wx3nz.js} +2 -2
- package/dist/{cli-z2dv25mf.js → cli-99xgw26r.js} +116 -5
- package/dist/{cli-jpxsrxcb.js → cli-9s05422w.js} +1 -1
- package/dist/{cli-kcsd1ez8.js → cli-aecdcr77.js} +343 -325
- package/dist/{cli-w9etnnxk.js → cli-bf8ma3f1.js} +120 -50
- package/dist/{cli-e9btqw4y.js → cli-ce0ak9ms.js} +18 -13
- package/dist/{cli-nj12tn2c.js → cli-e2ar4bp1.js} +71 -20
- package/dist/{cli-gwcf52gz.js → cli-e8jyq7d8.js} +11 -7
- package/dist/{cli-rngde2jv.js → cli-egjggehm.js} +1 -1
- package/dist/{cli-qfjm4gvt.js → cli-ewm3pwbq.js} +2 -2
- package/dist/{cli-9mm5ade4.js → cli-fsdprwrg.js} +1 -1
- package/dist/{cli-3ee5z0q0.js → cli-fzcn3d87.js} +1 -1
- package/dist/{cli-eym3jg7w.js → cli-gpsz7g88.js} +6 -6
- package/dist/{cli-cwyafhw1.js → cli-hcpj2xqz.js} +1 -1
- package/dist/{cli-j1eghqfm.js → cli-hhxjnjzs.js} +1 -1
- package/dist/{cli-pyy4c9ch.js → cli-jjznk3f7.js} +881 -232
- package/dist/{cli-q9pzwxvk.js → cli-kxawjrxg.js} +222 -37
- package/dist/{cli-rh5smt8t.js → cli-mz9afxxf.js} +14 -3
- package/dist/{cli-mz2ygeyq.js → cli-n45mr7zh.js} +2 -2
- package/dist/{cli-fes51ya2.js → cli-p0rzjkya.js} +2 -2
- package/dist/{cli-3qv8naq8.js → cli-p6zs9h4y.js} +827 -107
- package/dist/{cli-mq7kg1b1.js → cli-p9rafdrb.js} +41 -6
- package/dist/{cli-cah5detr.js → cli-qgf8emrn.js} +5 -5
- package/dist/{cli-2wba1ac9.js → cli-qzme5949.js} +26 -26
- package/dist/{cli-znvq0k3t.js → cli-s49hpn37.js} +2 -2
- package/dist/{cli-xc65x52q.js → cli-s53vp709.js} +1 -1
- package/dist/{cli-0jy85zz4.js → cli-svrj71pg.js} +18 -16
- package/dist/{cli-1dp1zj04.js → cli-vwn81jbs.js} +3 -3
- package/dist/{cli-ajqhxtnz.js → cli-w1zwctj2.js} +1 -1
- package/dist/{cli-22rmcjkg.js → cli-y561h5a9.js} +7 -6
- package/dist/{cli-2jg61wab.js → cli-z21whrrg.js} +15 -15
- package/dist/cli.js +19 -18
- package/dist/{codex-adapter-p4ptwar5.js → codex-adapter-n8ka66vf.js} +2 -2
- package/dist/{codex-session-runner-964c7dnk.js → codex-session-runner-fsebts37.js} +1 -1
- package/dist/{commands-mbvhbrcy.js → commands-t04rt2pc.js} +2 -2
- package/dist/{controller-gnq1k1mf.js → controller-rgna04r2.js} +2 -2
- package/dist/{controllers-connect-6dkm71tg.js → controllers-connect-bqm1rtft.js} +20 -20
- package/dist/{cursor-adapter-yq5xgfcg.js → cursor-adapter-swr2k95y.js} +1 -1
- package/dist/{db-88wcymxt.js → db-24ck09xy.js} +1 -1
- package/dist/{db-af9y8nkf.js → db-a4m7haw0.js} +3 -3
- package/dist/{dispatcher-606ydx5v.js → dispatcher-nwf9ss0q.js} +17 -17
- package/dist/{dsh-adapter-tvhc4k2d.js → dsh-adapter-kpdnhjst.js} +2 -2
- package/dist/{external-channel-y8q5z8z2.js → external-channel-j1e2p34p.js} +2 -2
- package/dist/{grok-adapter-39aha52t.js → grok-adapter-qtjz0whk.js} +4 -4
- package/dist/{handlers-rk9dqech.js → handlers-19xr6dnz.js} +20 -20
- package/dist/{heartbeat-jfe9zfjr.js → heartbeat-vtesda5d.js} +20 -20
- package/dist/{hook-b1mez1m8.js → hook-8wqaszda.js} +1 -1
- package/dist/{hook-ffevzed1.js → hook-b2hhj6rd.js} +1 -1
- package/dist/{http-d5b44f1r.js → http-mxcqhhcs.js} +3020 -2463
- package/dist/{index-5en23s5z.js → index-1r8wfpzs.js} +1 -1
- package/dist/{index-cbxkmfc5.js → index-9pn1pfkf.js} +1 -1
- package/dist/{index-mj46rk9k.js → index-kw2mmat7.js} +20 -20
- package/dist/{index-nq8hhv28.js → index-n1vbvta9.js} +19 -10
- package/dist/{index-6srwvd5b.js → index-p4xfz37z.js} +1 -1
- package/dist/{index-d2qeem97.js → index-spwxjgay.js} +17 -17
- package/dist/{index-4q3vj1yr.js → index-srg522m6.js} +17 -17
- package/dist/{keepalive-wsby1btq.js → keepalive-dsd8rtgx.js} +3 -3
- package/dist/{lead-paa141j6.js → lead-zyerk1x2.js} +30 -30
- package/dist/{maintenance-vnshcyv9.js → maintenance-jqmqwnet.js} +1 -1
- package/dist/{oauth-refresh-sweep-d6sqn5np.js → oauth-refresh-sweep-q2c3bnvm.js} +2 -2
- package/dist/{onboard-nr4xhpzy.js → onboard-w4yd7s8t.js} +2 -2
- package/dist/{opencode-adapter-e56086m1.js → opencode-adapter-9tppvxar.js} +2 -2
- package/dist/{otel-impl-ytn9wyv9.js → otel-impl-f36byzbd.js} +1 -1
- package/dist/{pricing-refresh-pva04zee.js → pricing-refresh-xvak5p4v.js} +3 -3
- package/dist/{rbac-roles-7rc1q3g4.js → rbac-roles-682k2mt5.js} +12 -2
- package/dist/rbac-roles-gfn30fb8.js +26 -0
- package/dist/{render-v2-pyfsg9zk.js → render-v2-hgytz8nn.js} +3 -3
- package/dist/{resume-phj8xhqr.js → resume-2dvq348g.js} +7 -5
- package/dist/{session-access-jzx2z9h5.js → session-access-7f21ekq9.js} +4 -4
- package/dist/{setup-rfg0f0sg.js → setup-epffxtbk.js} +1 -1
- package/dist/{work-objects-44z2yjg8.js → work-objects-xz1a5rqk.js} +20 -20
- package/dist/{worker-zsfqzxhm.js → worker-42t8emjt.js} +30 -30
- package/openapi.json +3163 -2114
- package/package.json +7 -6
- package/src/be/approval-config-question.ts +389 -0
- package/src/be/automation-preflight.ts +37 -3
- package/src/be/db/session-tree.ts +158 -13
- package/src/be/db/session-visibility.ts +31 -0
- package/src/be/db/skill-search.ts +87 -0
- package/src/be/db/tasks/read.ts +47 -7
- package/src/be/db/workflow-runs.ts +54 -7
- package/src/be/db.ts +27 -5
- package/src/be/migrations/233_script_run_session_tokens.sql +8 -0
- package/src/be/migrations/235_chat_relay_defer.sql +86 -0
- package/src/be/migrations/237_session_inbox_cleanup.sql +75 -0
- 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/rbac-roles.ts +138 -0
- package/src/be/scripts/run-credential.ts +64 -0
- package/src/be/scripts/run-saved.ts +3 -2
- package/src/be/swarm-config-guard.ts +23 -0
- package/src/be/users.ts +28 -4
- package/src/extensions/catalog.generated.json +4 -4
- package/src/http/all-routes.ts +1 -0
- package/src/http/approval-requests.ts +254 -43
- package/src/http/config.ts +22 -37
- package/src/http/index.ts +2 -0
- package/src/http/schedules.ts +26 -0
- package/src/http/scripts.ts +2 -2
- package/src/http/sessions.ts +118 -2
- package/src/http/user-roles.ts +173 -0
- package/src/http/users.ts +9 -0
- package/src/http/x.ts +2 -2
- package/src/integrations/chat-sdk/commands.ts +14 -1
- package/src/integrations/chat-sdk/inbound.ts +42 -6
- package/src/integrations/chat-sdk/outbound.ts +35 -10
- package/src/integrations/chat-sdk/reactions.ts +107 -0
- package/src/integrations/chat-sdk/relay.ts +75 -7
- package/src/integrations/chat-sdk/thread-buffer.ts +2 -0
- package/src/integrations/reaction-events.ts +20 -0
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/rbac/legacy-policy.ts +3 -0
- package/src/rbac/permissions.ts +8 -0
- package/src/scheduler/schedule-task.ts +44 -3
- package/src/scheduler/scheduler.ts +4 -4
- package/src/scripts-runtime/loader.ts +8 -1
- package/src/slack/ack.ts +2 -1
- package/src/slack/reaction-shortcode.ts +4 -7
- package/src/tasks/worker-follow-up.ts +30 -0
- package/src/tests/approval-config-question.test.ts +702 -0
- package/src/tests/approval-requests-notifications.test.ts +189 -0
- package/src/tests/chat-sdk-commands.test.ts +12 -0
- package/src/tests/chat-sdk-inbound.test.ts +183 -9
- package/src/tests/chat-sdk-outbound-files.test.ts +9 -2
- package/src/tests/chat-sdk-relay.test.ts +361 -10
- package/src/tests/chat-sdk-tools.test.ts +104 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- 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/http-users.test.ts +38 -0
- package/src/tests/list-endpoint-slimming.test.ts +33 -0
- package/src/tests/migration-237-session-inbox-cleanup.test.ts +136 -0
- package/src/tests/migration-240-workflow-run-parent-step-attempt.test.ts +55 -0
- package/src/tests/rbac-engine.test.ts +2 -0
- package/src/tests/rbac-roles.test.ts +10 -0
- package/src/tests/schedule-http-triage-tools.test.ts +44 -0
- package/src/tests/script-api-credential.test.ts +173 -0
- package/src/tests/script-connections.test.ts +10 -0
- package/src/tests/script-run-config-scope.test.ts +120 -0
- package/src/tests/script-run-token.test.ts +75 -0
- package/src/tests/scripts-http.test.ts +19 -0
- package/src/tests/sessions-tree.test.ts +410 -1
- package/src/tests/skill-search-long-content.test.ts +193 -0
- package/src/tests/skill-search.test.ts +173 -22
- package/src/tests/user-roles-http.test.ts +204 -0
- package/src/tests/workflow-hitl-dynamic-questions.test.ts +1 -1
- package/src/tests/workflow-sub-workflow.test.ts +538 -1
- package/src/tools/chat-reply.ts +2 -0
- package/src/tools/request-human-input.ts +27 -5
- package/src/tools/schedules/patch-schedule.ts +18 -0
- package/src/tools/schedules/update-schedule.ts +18 -0
- package/src/types.ts +23 -0
- package/src/workflows/approval-notifications.ts +221 -0
- package/src/workflows/checkpoint.ts +69 -35
- package/src/workflows/engine.ts +234 -87
- package/src/workflows/executors/base.ts +6 -0
- package/src/workflows/executors/human-in-the-loop.ts +36 -173
- package/src/workflows/executors/sub-workflow.ts +45 -16
- package/src/workflows/executors/swarm-script.ts +2 -2
- package/src/workflows/recovery.ts +63 -1
- package/src/workflows/resume.ts +60 -21
- package/src/workflows/retry-poller.ts +25 -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/schedules/weekly-harness-upgrade-check/content.md +1 -1
- package/templates/skills/code-quality/content.md +1 -1
- package/templates/skills/turso-interaction/content.md +1 -1
- package/templates/skills/workflow-iterate/content.md +8 -1
- package/dist/rbac-roles-ff1ffmec.js +0 -12
- /package/dist/{cli-stxdrqs2.js → cli-avdvgcay.js} +0 -0
- /package/dist/{pi-mono-adapter-tzps8f38.js → pi-mono-adapter-tqsge1ay.js} +0 -0
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
closeDb,
|
|
6
6
|
completeTask,
|
|
7
7
|
createAgent,
|
|
8
|
+
createScheduledTask,
|
|
8
9
|
createTaskExtended,
|
|
9
10
|
failTask,
|
|
10
11
|
getDbClient,
|
|
@@ -19,13 +20,16 @@ import { cleanExtensionTmpRoot, loadExtension } from "../extensions/loader";
|
|
|
19
20
|
import { initExtensionPostBridge, teardownExtensionPostBridge } from "../extensions/post-bridge";
|
|
20
21
|
import { splitChatMessage } from "../integrations/chat-sdk/limits";
|
|
21
22
|
import {
|
|
23
|
+
formatChatRelayMessages,
|
|
22
24
|
formatChatRelayText,
|
|
23
25
|
processPendingChatRelays,
|
|
24
26
|
RELAY_GIVE_UP_AFTER_MS,
|
|
25
27
|
stopChatRelayWatcher,
|
|
28
|
+
TELEGRAM_FOLD_CHARS,
|
|
26
29
|
} from "../integrations/chat-sdk/relay";
|
|
30
|
+
import { createStandaloneScheduleTask } from "../scheduler/schedule-task";
|
|
27
31
|
import { chatSdkContextKey } from "../tasks/context-key";
|
|
28
|
-
import { createResumeFollowUp } from "../tasks/worker-follow-up";
|
|
32
|
+
import { createResumeFollowUp, createWorkerTaskFollowUp } from "../tasks/worker-follow-up";
|
|
29
33
|
import type { Agent, AgentTask } from "../types";
|
|
30
34
|
import { startTelegramHarness, type TelegramHarness } from "./chat-sdk-harness";
|
|
31
35
|
import { loadBundleFixture } from "./fixtures/extensions/load";
|
|
@@ -82,13 +86,19 @@ function sentText(body: Record<string, unknown>): string {
|
|
|
82
86
|
|
|
83
87
|
async function createChatTask(
|
|
84
88
|
chatId: number,
|
|
85
|
-
options: { source?: "chat" | "system"; taskType?: string } = {},
|
|
89
|
+
options: { source?: "chat" | "system"; taskType?: string; askMessageId?: number } = {},
|
|
86
90
|
): Promise<AgentTask> {
|
|
87
91
|
return createTaskExtended(`chat task for ${chatId}`, {
|
|
88
92
|
agentId: lead.id,
|
|
89
93
|
source: options.source ?? "chat",
|
|
90
94
|
taskType: options.taskType,
|
|
91
95
|
contextKey: chatSdkContextKey(`telegram:${chatId}`),
|
|
96
|
+
...(options.askMessageId !== undefined
|
|
97
|
+
? {
|
|
98
|
+
mentionMessageId: `${chatId}:${options.askMessageId}`,
|
|
99
|
+
mentionChannelId: `telegram:${chatId}`,
|
|
100
|
+
}
|
|
101
|
+
: {}),
|
|
92
102
|
});
|
|
93
103
|
}
|
|
94
104
|
|
|
@@ -122,14 +132,45 @@ beforeEach(() => {
|
|
|
122
132
|
});
|
|
123
133
|
|
|
124
134
|
describe("chat relay text", () => {
|
|
125
|
-
test("status
|
|
126
|
-
const base = {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
135
|
+
test("task tag first, status line for failed and cancelled, default for an empty completion", () => {
|
|
136
|
+
const base = {
|
|
137
|
+
id: "a3f2b1c9-0000-4000-8000-000000000000",
|
|
138
|
+
output: undefined,
|
|
139
|
+
failureReason: undefined,
|
|
140
|
+
} as unknown as AgentTask;
|
|
141
|
+
const tag = "#a3f2b1c9 · Coder";
|
|
142
|
+
expect(formatChatRelayText({ ...base, status: "completed" }, "Coder")).toBe(
|
|
143
|
+
`${tag}\n\nTask completed.`,
|
|
144
|
+
);
|
|
145
|
+
expect(formatChatRelayText({ ...base, status: "completed", output: "done" }, "Coder")).toBe(
|
|
146
|
+
`${tag}\n\ndone`,
|
|
147
|
+
);
|
|
148
|
+
expect(formatChatRelayText({ ...base, status: "failed", failureReason: "boom" }, "Coder")).toBe(
|
|
149
|
+
`${tag}\n**Task failed.**\n\nboom`,
|
|
150
|
+
);
|
|
151
|
+
expect(formatChatRelayText({ ...base, status: "cancelled" })).toBe(
|
|
152
|
+
"#a3f2b1c9\n**Task cancelled.**",
|
|
131
153
|
);
|
|
132
|
-
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("a long Telegram answer folds into <details>; other adapters and short answers do not", () => {
|
|
157
|
+
const base = { id: "a3f2b1c9-0000-4000-8000-000000000000", status: "completed" };
|
|
158
|
+
const long = `The short answer is yes.\n\n${"detail line\n".repeat(200)}`.trim();
|
|
159
|
+
const task = { ...base, output: long } as unknown as AgentTask;
|
|
160
|
+
expect(long.length).toBeGreaterThan(TELEGRAM_FOLD_CHARS);
|
|
161
|
+
|
|
162
|
+
const [message, ...more] = formatChatRelayMessages(task, "telegram", "Coder");
|
|
163
|
+
expect(more).toHaveLength(0);
|
|
164
|
+
// Tag and first paragraph stay visible; the rest is collapsed.
|
|
165
|
+
expect(message).toStartWith("#a3f2b1c9 · Coder\n\nThe short answer is yes.\n\n<details>");
|
|
166
|
+
expect(message).toContain("<summary>Rest of the answer</summary>");
|
|
167
|
+
expect(message).toEndWith("</details>");
|
|
168
|
+
|
|
169
|
+
expect(formatChatRelayMessages(task, "slack", "Coder")).toEqual([
|
|
170
|
+
formatChatRelayText(task, "Coder"),
|
|
171
|
+
]);
|
|
172
|
+
const short = { ...base, output: "fine" } as unknown as AgentTask;
|
|
173
|
+
expect(formatChatRelayMessages(short, "telegram")).toEqual(["#a3f2b1c9\n\nfine"]);
|
|
133
174
|
});
|
|
134
175
|
|
|
135
176
|
test("splitChatMessage keeps every chunk within the limit and loses no text", () => {
|
|
@@ -289,7 +330,7 @@ describe("chat relay watcher", () => {
|
|
|
289
330
|
adapter: "telegram",
|
|
290
331
|
threadId: `telegram:${chatId}`,
|
|
291
332
|
direction: "outbound",
|
|
292
|
-
text:
|
|
333
|
+
text: `#${task.id.slice(0, 8)} · ChatRelayLead\n\nobserved by an extension`,
|
|
293
334
|
taskId: task.id,
|
|
294
335
|
});
|
|
295
336
|
});
|
|
@@ -408,6 +449,316 @@ describe("chat relay watcher", () => {
|
|
|
408
449
|
expect((await obligation(resume.id))?.delivered_at).not.toBeNull();
|
|
409
450
|
});
|
|
410
451
|
|
|
452
|
+
test("the answer replies to the ask and starts with the task tag", async () => {
|
|
453
|
+
const chatId = nextChatId;
|
|
454
|
+
const task = await createChatTask(chatId, { askMessageId: 41 });
|
|
455
|
+
await completeTask(task.id, "Here is your answer.");
|
|
456
|
+
await processPendingChatRelays();
|
|
457
|
+
|
|
458
|
+
const [send, ...rest] = sendsTo(chatId);
|
|
459
|
+
expect(rest).toHaveLength(0);
|
|
460
|
+
expect(send?.reply_parameters).toEqual({ message_id: 41, allow_sending_without_reply: true });
|
|
461
|
+
// The line-leading tag is escaped so Telegram does not render it as a heading.
|
|
462
|
+
expect(sentText(send ?? {})).toStartWith(`\\#${task.id.slice(0, 8)} · ChatRelayLead`);
|
|
463
|
+
expect(sentText(send ?? {})).toContain("Here is your answer.");
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test("the answer turns the ask's reaction into 👍, or 👎 for a failed task", async () => {
|
|
467
|
+
const reactionsTo = (chatId: number) =>
|
|
468
|
+
harness.mock.calls
|
|
469
|
+
.filter((call) => call.method === "setMessageReaction")
|
|
470
|
+
.map((call) => call.body as Record<string, unknown>)
|
|
471
|
+
.filter((body) => String(body.chat_id) === String(chatId));
|
|
472
|
+
const doneChat = nextChatId;
|
|
473
|
+
const done = await createChatTask(doneChat, { askMessageId: 51 });
|
|
474
|
+
await completeTask(done.id, "Done.");
|
|
475
|
+
const failedChat = nextChatId + 2_000;
|
|
476
|
+
const failed = await createChatTask(failedChat, { askMessageId: 52 });
|
|
477
|
+
await failTask(failed.id, "the build broke");
|
|
478
|
+
await processPendingChatRelays();
|
|
479
|
+
|
|
480
|
+
expect(reactionsTo(doneChat)).toEqual([
|
|
481
|
+
expect.objectContaining({ message_id: 51, reaction: [{ type: "emoji", emoji: "👍" }] }),
|
|
482
|
+
]);
|
|
483
|
+
expect(reactionsTo(failedChat)).toEqual([
|
|
484
|
+
expect.objectContaining({ message_id: 52, reaction: [{ type: "emoji", emoji: "👎" }] }),
|
|
485
|
+
]);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
test("a REACTION_INVALID answer never fails the delivery", async () => {
|
|
489
|
+
const chatId = nextChatId;
|
|
490
|
+
const task = await createChatTask(chatId, { askMessageId: 61 });
|
|
491
|
+
await completeTask(task.id, "Still delivered.");
|
|
492
|
+
harness.mock.failReactions(1);
|
|
493
|
+
try {
|
|
494
|
+
await processPendingChatRelays();
|
|
495
|
+
} finally {
|
|
496
|
+
harness.mock.failReactions(0);
|
|
497
|
+
}
|
|
498
|
+
expect(sentText(sendsTo(chatId)[0] ?? {})).toContain("Still delivered.");
|
|
499
|
+
const row = await obligation(task.id);
|
|
500
|
+
expect(row?.delivered_at).not.toBeNull();
|
|
501
|
+
expect(row?.last_error).toBeNull();
|
|
502
|
+
expect(
|
|
503
|
+
harness.mock.calls.some(
|
|
504
|
+
(call) =>
|
|
505
|
+
call.method === "setMessageReaction" &&
|
|
506
|
+
String((call.body as Record<string, unknown>).chat_id) === String(chatId),
|
|
507
|
+
),
|
|
508
|
+
).toBe(true);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
test("a chunked answer anchors only its first message", async () => {
|
|
512
|
+
const chatId = nextChatId;
|
|
513
|
+
const task = await createChatTask(chatId, { askMessageId: 42 });
|
|
514
|
+
const output = Array.from({ length: 1000 }, (_, i) => `line${String(i).padStart(4, "0")}`).join(
|
|
515
|
+
"\n",
|
|
516
|
+
);
|
|
517
|
+
await completeTask(task.id, output);
|
|
518
|
+
await processPendingChatRelays();
|
|
519
|
+
|
|
520
|
+
const sends = sendsTo(chatId);
|
|
521
|
+
expect(sends.length).toBeGreaterThan(1);
|
|
522
|
+
expect(sends[0]?.reply_parameters).toEqual({
|
|
523
|
+
message_id: 42,
|
|
524
|
+
allow_sending_without_reply: true,
|
|
525
|
+
});
|
|
526
|
+
for (const later of sends.slice(1)) expect(later.reply_parameters).toBeUndefined();
|
|
527
|
+
// Every message carries a whole collapsed block.
|
|
528
|
+
for (const body of sends) {
|
|
529
|
+
expect(sentText(body)).toContain("<details>");
|
|
530
|
+
expect(sentText(body)).toEndWith("</details>");
|
|
531
|
+
expect(sentText(body).length).toBeLessThanOrEqual(4096);
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
test("a deferred task posts nothing; its wake-up answers the original ask", async () => {
|
|
536
|
+
const chatId = nextChatId;
|
|
537
|
+
const deferred = await createChatTask(chatId, { askMessageId: 43 });
|
|
538
|
+
await completeTask(deferred.id, "Deferred until 18:00.", {
|
|
539
|
+
addTags: ["deferred"],
|
|
540
|
+
deferredAt: new Date().toISOString(),
|
|
541
|
+
});
|
|
542
|
+
expect(await obligation(deferred.id)).toBeNull();
|
|
543
|
+
|
|
544
|
+
// What defer-task books; the scheduler fires it later.
|
|
545
|
+
const schedule = await createScheduledTask({
|
|
546
|
+
name: `deferred-${deferred.id.slice(0, 8)}-${crypto.randomUUID()}`,
|
|
547
|
+
taskTemplate: "Check the deploy and answer.",
|
|
548
|
+
targetType: "agent-task",
|
|
549
|
+
scheduleType: "one_time",
|
|
550
|
+
nextRunAt: new Date().toISOString(),
|
|
551
|
+
targetAgentId: lead.id,
|
|
552
|
+
createdByAgentId: lead.id,
|
|
553
|
+
taskType: "deferred",
|
|
554
|
+
tags: ["deferred"],
|
|
555
|
+
parentTaskId: deferred.id,
|
|
556
|
+
});
|
|
557
|
+
const wakeUp = await createStandaloneScheduleTask(schedule);
|
|
558
|
+
expect(wakeUp.contextKey).toBe(deferred.contextKey);
|
|
559
|
+
expect(wakeUp.mentionMessageId).toBe(`${chatId}:43`);
|
|
560
|
+
await completeTask(wakeUp.id, "The deploy is live.");
|
|
561
|
+
|
|
562
|
+
await processPendingChatRelays();
|
|
563
|
+
const sends = sendsTo(chatId);
|
|
564
|
+
expect(sends).toHaveLength(1);
|
|
565
|
+
expect(sentText(sends[0] ?? {})).toContain("The deploy is live.");
|
|
566
|
+
expect(sentText(sends[0] ?? {})).not.toContain("Deferred until");
|
|
567
|
+
expect(sends[0]?.reply_parameters).toEqual({
|
|
568
|
+
message_id: 43,
|
|
569
|
+
allow_sending_without_reply: true,
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
test("a wake-up of a task outside chat keeps its schedule contextKey", async () => {
|
|
574
|
+
const parent = await createTaskExtended("not a chat task", { agentId: lead.id });
|
|
575
|
+
const schedule = await createScheduledTask({
|
|
576
|
+
name: `deferred-${parent.id.slice(0, 8)}-${crypto.randomUUID()}`,
|
|
577
|
+
taskTemplate: "wake up",
|
|
578
|
+
targetType: "agent-task",
|
|
579
|
+
scheduleType: "one_time",
|
|
580
|
+
nextRunAt: new Date().toISOString(),
|
|
581
|
+
targetAgentId: lead.id,
|
|
582
|
+
createdByAgentId: lead.id,
|
|
583
|
+
taskType: "deferred",
|
|
584
|
+
parentTaskId: parent.id,
|
|
585
|
+
});
|
|
586
|
+
const wakeUp = await createStandaloneScheduleTask(schedule);
|
|
587
|
+
expect(wakeUp.contextKey).toBe(`task:schedule:${schedule.id}`);
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
describe("a delegated worker's deferral", () => {
|
|
591
|
+
let worker: Agent;
|
|
592
|
+
let otherWorker: Agent;
|
|
593
|
+
|
|
594
|
+
beforeAll(async () => {
|
|
595
|
+
worker = await createAgent({
|
|
596
|
+
id: crypto.randomUUID(),
|
|
597
|
+
name: "ChatRelayDeferringWorker",
|
|
598
|
+
isLead: false,
|
|
599
|
+
status: "idle",
|
|
600
|
+
maxTasks: 5,
|
|
601
|
+
});
|
|
602
|
+
otherWorker = await createAgent({
|
|
603
|
+
id: crypto.randomUUID(),
|
|
604
|
+
name: "ChatRelayRetargetWorker",
|
|
605
|
+
isLead: false,
|
|
606
|
+
status: "idle",
|
|
607
|
+
maxTasks: 5,
|
|
608
|
+
});
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
/** A Lead chat ask (anchored on message 43) delegated to `worker`. */
|
|
612
|
+
async function delegate(
|
|
613
|
+
chatId: number,
|
|
614
|
+
): Promise<{ leadTask: AgentTask; delegated: AgentTask }> {
|
|
615
|
+
const leadTask = await createChatTask(chatId, { askMessageId: 43 });
|
|
616
|
+
const delegated = await createTaskExtended("worker part", {
|
|
617
|
+
agentId: worker.id,
|
|
618
|
+
creatorAgentId: lead.id,
|
|
619
|
+
source: "mcp",
|
|
620
|
+
parentTaskId: leadTask.id,
|
|
621
|
+
});
|
|
622
|
+
expect(delegated.contextKey).toBe(leadTask.contextKey);
|
|
623
|
+
return { leadTask, delegated };
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/** What defer-task books for `task`, fired by the scheduler. */
|
|
627
|
+
async function deferAndWake(
|
|
628
|
+
task: AgentTask,
|
|
629
|
+
options: { targetAgentId?: string } = {},
|
|
630
|
+
): Promise<AgentTask> {
|
|
631
|
+
await completeTask(task.id, "Checking back later.", {
|
|
632
|
+
deferredAt: new Date().toISOString(),
|
|
633
|
+
});
|
|
634
|
+
const schedule = await createScheduledTask({
|
|
635
|
+
name: `deferred-${task.id.slice(0, 8)}-${crypto.randomUUID()}`,
|
|
636
|
+
taskTemplate: "resume the worker part",
|
|
637
|
+
targetType: "agent-task",
|
|
638
|
+
scheduleType: "one_time",
|
|
639
|
+
nextRunAt: new Date().toISOString(),
|
|
640
|
+
targetAgentId: options.targetAgentId ?? task.agentId ?? undefined,
|
|
641
|
+
createdByAgentId: task.agentId ?? undefined,
|
|
642
|
+
taskType: "deferred",
|
|
643
|
+
parentTaskId: task.id,
|
|
644
|
+
});
|
|
645
|
+
const wakeUp = await createStandaloneScheduleTask(schedule);
|
|
646
|
+
// The worker reports through the Lead, as its delegated task did.
|
|
647
|
+
expect(wakeUp.contextKey).toBe(`task:schedule:${schedule.id}`);
|
|
648
|
+
return wakeUp;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Completes the worker's `task` with raw output, then runs its Lead
|
|
653
|
+
* follow-up to a reviewed answer. Returns what reached the chat.
|
|
654
|
+
*/
|
|
655
|
+
async function reportThroughLead(
|
|
656
|
+
task: AgentTask,
|
|
657
|
+
chatId: number,
|
|
658
|
+
leadTask: AgentTask,
|
|
659
|
+
): Promise<Array<Record<string, unknown>>> {
|
|
660
|
+
const completed = await completeTask(task.id, "RAW PRIVATE WORKER RESULT");
|
|
661
|
+
expect(completed).not.toBeNull();
|
|
662
|
+
expect(await obligation(task.id)).toBeNull();
|
|
663
|
+
const followUp = await createWorkerTaskFollowUp({
|
|
664
|
+
task: completed!,
|
|
665
|
+
status: "completed",
|
|
666
|
+
output: completed!.output,
|
|
667
|
+
});
|
|
668
|
+
await processPendingChatRelays();
|
|
669
|
+
expect(sendsTo(chatId)).toHaveLength(0);
|
|
670
|
+
|
|
671
|
+
expect(followUp?.agentId).toBe(lead.id);
|
|
672
|
+
expect(followUp?.contextKey).toBe(leadTask.contextKey);
|
|
673
|
+
expect(followUp?.mentionMessageId).toBe(`${chatId}:43`);
|
|
674
|
+
await completeTask(followUp!.id, "REVIEWED LEAD ANSWER");
|
|
675
|
+
await processPendingChatRelays();
|
|
676
|
+
return sendsTo(chatId);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function expectOneReviewedAnswer(sends: Array<Record<string, unknown>>): void {
|
|
680
|
+
expect(sends).toHaveLength(1);
|
|
681
|
+
expect(sentText(sends[0] ?? {})).toContain("REVIEWED LEAD ANSWER");
|
|
682
|
+
expect(sentText(sends[0] ?? {})).not.toContain("RAW PRIVATE WORKER RESULT");
|
|
683
|
+
expect(sends[0]?.reply_parameters).toEqual({
|
|
684
|
+
message_id: 43,
|
|
685
|
+
allow_sending_without_reply: true,
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
test("control: an undeferred worker reports one reviewed answer", async () => {
|
|
690
|
+
const chatId = nextChatId;
|
|
691
|
+
const { leadTask, delegated } = await delegate(chatId);
|
|
692
|
+
expectOneReviewedAnswer(await reportThroughLead(delegated, chatId, leadTask));
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
test("wakes up outside the chat, stays silent and reports through the Lead", async () => {
|
|
696
|
+
const chatId = nextChatId;
|
|
697
|
+
const { leadTask, delegated } = await delegate(chatId);
|
|
698
|
+
const wakeUp = await deferAndWake(delegated);
|
|
699
|
+
expectOneReviewedAnswer(await reportThroughLead(wakeUp, chatId, leadTask));
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
test("a repeated deferral still reports through the Lead", async () => {
|
|
703
|
+
const chatId = nextChatId;
|
|
704
|
+
const { leadTask, delegated } = await delegate(chatId);
|
|
705
|
+
const firstWake = await deferAndWake(delegated);
|
|
706
|
+
const secondWake = await deferAndWake(firstWake);
|
|
707
|
+
expect(await obligation(firstWake.id)).toBeNull();
|
|
708
|
+
expectOneReviewedAnswer(await reportThroughLead(secondWake, chatId, leadTask));
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
test("a resume of the wake-up still reports through the Lead", async () => {
|
|
712
|
+
const chatId = nextChatId;
|
|
713
|
+
const { leadTask, delegated } = await delegate(chatId);
|
|
714
|
+
const wakeUp = await deferAndWake(delegated);
|
|
715
|
+
await startTask(wakeUp.id);
|
|
716
|
+
await supersedeTask(wakeUp.id, { reason: "graceful_shutdown", resumeTaskId: null });
|
|
717
|
+
const result = await createResumeFollowUp({
|
|
718
|
+
parentId: wakeUp.id,
|
|
719
|
+
reason: "graceful_shutdown",
|
|
720
|
+
});
|
|
721
|
+
expect(result.kind).toBe("created");
|
|
722
|
+
if (result.kind !== "created") return;
|
|
723
|
+
expect(result.task.agentId).toBe(worker.id);
|
|
724
|
+
expect(result.task.contextKey).toBe(wakeUp.contextKey);
|
|
725
|
+
expectOneReviewedAnswer(await reportThroughLead(result.task, chatId, leadTask));
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
test("a wake-up re-targeted to another worker reports through the Lead", async () => {
|
|
729
|
+
const chatId = nextChatId;
|
|
730
|
+
const { leadTask, delegated } = await delegate(chatId);
|
|
731
|
+
const wakeUp = await deferAndWake(delegated, { targetAgentId: otherWorker.id });
|
|
732
|
+
expect(wakeUp.agentId).toBe(otherWorker.id);
|
|
733
|
+
expectOneReviewedAnswer(await reportThroughLead(wakeUp, chatId, leadTask));
|
|
734
|
+
});
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
test("a deferred wake-up that runs as another agent than its parent is not relayed", async () => {
|
|
738
|
+
const other = await createAgent({
|
|
739
|
+
id: crypto.randomUUID(),
|
|
740
|
+
name: "ChatRelayOtherAgent",
|
|
741
|
+
isLead: false,
|
|
742
|
+
status: "idle",
|
|
743
|
+
});
|
|
744
|
+
const chatId = nextChatId;
|
|
745
|
+
const parent = await createChatTask(chatId);
|
|
746
|
+
await completeTask(parent.id, "Deferred.", { deferredAt: new Date().toISOString() });
|
|
747
|
+
// Bypasses the scheduler's check: the triggers hold the same line.
|
|
748
|
+
const wakeUp = await createTaskExtended("not the deferring agent", {
|
|
749
|
+
agentId: other.id,
|
|
750
|
+
source: "schedule",
|
|
751
|
+
taskType: "deferred",
|
|
752
|
+
parentTaskId: parent.id,
|
|
753
|
+
contextKey: parent.contextKey,
|
|
754
|
+
});
|
|
755
|
+
await completeTask(wakeUp.id, "OTHER AGENT OUTPUT");
|
|
756
|
+
|
|
757
|
+
expect(await obligation(wakeUp.id)).toBeNull();
|
|
758
|
+
await processPendingChatRelays();
|
|
759
|
+
expect(sendsTo(chatId)).toHaveLength(0);
|
|
760
|
+
});
|
|
761
|
+
|
|
411
762
|
test("delegated worker tasks and their resumes stay silent", async () => {
|
|
412
763
|
const worker = await createAgent({
|
|
413
764
|
id: crypto.randomUUID(),
|
|
@@ -3,14 +3,18 @@ import { unlink } from "node:fs/promises";
|
|
|
3
3
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
4
|
import {
|
|
5
5
|
closeDb,
|
|
6
|
+
completeTask,
|
|
6
7
|
createAgent,
|
|
8
|
+
createScheduledTask,
|
|
7
9
|
createTaskExtended,
|
|
8
10
|
getDbClient,
|
|
9
11
|
getKv,
|
|
12
|
+
getScheduledTaskById,
|
|
10
13
|
getTaskById,
|
|
11
14
|
initDb,
|
|
12
15
|
} from "../be/db";
|
|
13
16
|
import { CHAT_CHANNELS_NAMESPACE } from "../integrations/chat-sdk/router";
|
|
17
|
+
import { createStandaloneScheduleTask } from "../scheduler/schedule-task";
|
|
14
18
|
import { createServer } from "../server";
|
|
15
19
|
import { chatSdkContextKey } from "../tasks/context-key";
|
|
16
20
|
import { registerChatOpenDmTool } from "../tools/chat-open-dm";
|
|
@@ -21,6 +25,8 @@ import {
|
|
|
21
25
|
registerRegisterChatChannelTool,
|
|
22
26
|
registerUnregisterChatChannelTool,
|
|
23
27
|
} from "../tools/register-chat-channel";
|
|
28
|
+
import { registerPatchScheduleTool } from "../tools/schedules/patch-schedule";
|
|
29
|
+
import { registerUpdateScheduleTool } from "../tools/schedules/update-schedule";
|
|
24
30
|
import { registerSendTaskTool } from "../tools/send-task";
|
|
25
31
|
import type { Agent } from "../types";
|
|
26
32
|
import { startTelegramHarness, type TelegramHarness } from "./chat-sdk-harness";
|
|
@@ -108,6 +114,8 @@ beforeAll(async () => {
|
|
|
108
114
|
registerRegisterChatChannelTool(server);
|
|
109
115
|
registerUnregisterChatChannelTool(server);
|
|
110
116
|
registerSendTaskTool(server);
|
|
117
|
+
registerPatchScheduleTool(server);
|
|
118
|
+
registerUpdateScheduleTool(server);
|
|
111
119
|
});
|
|
112
120
|
|
|
113
121
|
afterAll(async () => {
|
|
@@ -194,6 +202,8 @@ describe("chat-reply", () => {
|
|
|
194
202
|
agentId: worker.id,
|
|
195
203
|
source: "chat",
|
|
196
204
|
contextKey: chatSdkContextKey("telegram:991002"),
|
|
205
|
+
mentionMessageId: "991002:77",
|
|
206
|
+
mentionChannelId: "telegram:991002",
|
|
197
207
|
});
|
|
198
208
|
const result = await callTool(
|
|
199
209
|
"chat-reply",
|
|
@@ -212,6 +222,11 @@ describe("chat-reply", () => {
|
|
|
212
222
|
String((call.body as { chat_id?: unknown }).chat_id) === "991002",
|
|
213
223
|
);
|
|
214
224
|
expect(sends).toHaveLength(1);
|
|
225
|
+
// Anchored like the final answer: a reply to the message that started the task.
|
|
226
|
+
expect(sends[0]?.body.reply_parameters).toEqual({
|
|
227
|
+
message_id: 77,
|
|
228
|
+
allow_sending_without_reply: true,
|
|
229
|
+
});
|
|
215
230
|
});
|
|
216
231
|
});
|
|
217
232
|
|
|
@@ -518,3 +533,92 @@ describe("chat thread binding through send-task", () => {
|
|
|
518
533
|
expect(child?.contextKey).toBe(chatSdkContextKey("telegram:444444"));
|
|
519
534
|
});
|
|
520
535
|
});
|
|
536
|
+
|
|
537
|
+
describe("defer-task wake-ups and chat thread access", () => {
|
|
538
|
+
let harness: TelegramHarness;
|
|
539
|
+
|
|
540
|
+
beforeAll(async () => {
|
|
541
|
+
harness = await startTelegramHarness();
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
afterAll(async () => {
|
|
545
|
+
await harness.stop();
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
/** A lead chat task deferred with defer-task: the task is done, its wake-up booked. */
|
|
549
|
+
async function leadDeferral(chatId: string) {
|
|
550
|
+
const parent = await createTaskExtended("human ask", {
|
|
551
|
+
agentId: lead.id,
|
|
552
|
+
source: "chat",
|
|
553
|
+
contextKey: chatSdkContextKey(`telegram:${chatId}`),
|
|
554
|
+
mentionMessageId: `${chatId}:55`,
|
|
555
|
+
});
|
|
556
|
+
await completeTask(parent.id, "Deferred.", { deferredAt: new Date().toISOString() });
|
|
557
|
+
const schedule = await createScheduledTask({
|
|
558
|
+
name: `deferred-${parent.id.slice(0, 8)}-${crypto.randomUUID()}`,
|
|
559
|
+
taskTemplate: "finish the original ask",
|
|
560
|
+
targetType: "agent-task",
|
|
561
|
+
scheduleType: "one_time",
|
|
562
|
+
nextRunAt: new Date(Date.now() + 60_000).toISOString(),
|
|
563
|
+
targetAgentId: lead.id,
|
|
564
|
+
createdByAgentId: lead.id,
|
|
565
|
+
taskType: "deferred",
|
|
566
|
+
parentTaskId: parent.id,
|
|
567
|
+
});
|
|
568
|
+
return { parent, schedule };
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
test("another worker cannot change a lead's deferral", async () => {
|
|
572
|
+
const { schedule } = await leadDeferral("992881");
|
|
573
|
+
for (const tool of ["patch-schedule", "update-schedule"]) {
|
|
574
|
+
const result = await callTool(
|
|
575
|
+
tool,
|
|
576
|
+
{ "x-agent-id": worker.id },
|
|
577
|
+
{ scheduleId: schedule.id, targetAgentId: worker.id, taskTemplate: "emit ATTACKER OUTPUT" },
|
|
578
|
+
);
|
|
579
|
+
expect(result.structuredContent.success).toBe(false);
|
|
580
|
+
}
|
|
581
|
+
const after = await getScheduledTaskById(schedule.id);
|
|
582
|
+
expect(after?.targetAgentId).toBe(lead.id);
|
|
583
|
+
expect(after?.taskTemplate).toBe("finish the original ask");
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
test("a deferral re-targeted to another agent wakes up outside the chat thread", async () => {
|
|
587
|
+
const chatId = "992882";
|
|
588
|
+
const { schedule } = await leadDeferral(chatId);
|
|
589
|
+
// A lead may re-target it; the new assignee still gets no thread access.
|
|
590
|
+
const patched = await callTool(
|
|
591
|
+
"patch-schedule",
|
|
592
|
+
{ "x-agent-id": lead.id },
|
|
593
|
+
{ scheduleId: schedule.id, targetAgentId: worker.id },
|
|
594
|
+
);
|
|
595
|
+
expect(patched.structuredContent.success).toBe(true);
|
|
596
|
+
const retargeted = await getScheduledTaskById(schedule.id);
|
|
597
|
+
if (!retargeted) throw new Error("schedule vanished");
|
|
598
|
+
|
|
599
|
+
const wakeUp = await createStandaloneScheduleTask(retargeted);
|
|
600
|
+
expect(wakeUp.agentId).toBe(worker.id);
|
|
601
|
+
expect(wakeUp.contextKey).toBe(`task:schedule:${schedule.id}`);
|
|
602
|
+
|
|
603
|
+
const reply = await callTool(
|
|
604
|
+
"chat-reply",
|
|
605
|
+
{ "x-agent-id": worker.id, "x-source-task-id": wakeUp.id },
|
|
606
|
+
{ text: "ATTACKER DIRECT REPLY" },
|
|
607
|
+
);
|
|
608
|
+
expect(reply.structuredContent.success).toBe(false);
|
|
609
|
+
|
|
610
|
+
await completeTask(wakeUp.id, "ATTACKER OUTPUT");
|
|
611
|
+
const obligation = await getDbClient().get(
|
|
612
|
+
"SELECT task_id FROM chat_relay_obligations WHERE task_id = ?",
|
|
613
|
+
[wakeUp.id],
|
|
614
|
+
);
|
|
615
|
+
expect(obligation).toBeNull();
|
|
616
|
+
expect(
|
|
617
|
+
harness.mock.calls.some(
|
|
618
|
+
(call) =>
|
|
619
|
+
/^send(Rich)?Message$/.test(call.method) &&
|
|
620
|
+
String((call.body as { chat_id?: unknown }).chat_id) === chatId,
|
|
621
|
+
),
|
|
622
|
+
).toBe(false);
|
|
623
|
+
});
|
|
624
|
+
});
|
|
@@ -78,7 +78,7 @@ describe("docs API index footprint", () => {
|
|
|
78
78
|
expect(tasks).toContain('"path":"/api/task-templates","method":"get"');
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
-
test("OpenAPI and generated pages keep all
|
|
81
|
+
test("OpenAPI and generated pages keep all 449 operations", async () => {
|
|
82
82
|
const spec = await Bun.file(resolve(repoRoot, "openapi.json")).json();
|
|
83
83
|
const operationKeys = [];
|
|
84
84
|
const tags = new Set();
|
|
@@ -103,8 +103,8 @@ describe("docs API index footprint", () => {
|
|
|
103
103
|
expect(spec.paths["/api/agents/{id}/runtime"]?.get).toBeDefined();
|
|
104
104
|
|
|
105
105
|
const generatedKeys = await generatedOperationKeys();
|
|
106
|
-
expect(operationKeys).toHaveLength(
|
|
107
|
-
expect(generatedKeys).toHaveLength(
|
|
106
|
+
expect(operationKeys).toHaveLength(449);
|
|
107
|
+
expect(generatedKeys).toHaveLength(449);
|
|
108
108
|
expect(new Set(generatedKeys)).toEqual(new Set(operationKeys));
|
|
109
109
|
});
|
|
110
110
|
|
|
@@ -376,7 +376,7 @@ describe("dsh harness", () => {
|
|
|
376
376
|
await rename(join(config.cwd, "dsh"), join(config.cwd, "npx"));
|
|
377
377
|
config.env = { ...config.env, DSH_BINARY: "", PATH: config.cwd };
|
|
378
378
|
await expect(new DshAdapter().createSession(config)).rejects.toThrow(
|
|
379
|
-
"dsh CLI not found. Install @deepseek-ai/dsh@0.2.1-rc.
|
|
379
|
+
"dsh CLI not found. Install @deepseek-ai/dsh@0.2.1-rc.3",
|
|
380
380
|
);
|
|
381
381
|
expect(await Bun.file(join(config.cwd, "invocation.json")).exists()).toBe(false);
|
|
382
382
|
});
|