@vellumai/assistant 0.12.0-staging.1 → 0.12.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/Dockerfile +5 -0
- package/docs/architecture/turn-actor.md +9 -0
- package/knip.json +1 -0
- package/node_modules/@vellumai/app-icons/package.json +18 -0
- package/node_modules/@vellumai/app-icons/src/index.test.ts +85 -0
- package/node_modules/@vellumai/app-icons/src/index.ts +387 -0
- package/node_modules/@vellumai/app-icons/tsconfig.json +20 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +31 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +28 -3
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +31 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +28 -3
- package/node_modules/@vellumai/gateway-client/src/__tests__/plugin-admission-denied-contract.test.ts +10 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/src/plugin-admission-denied-contract.ts +15 -2
- package/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +31 -0
- package/node_modules/@vellumai/service-contracts/src/channels.ts +28 -3
- package/openapi.yaml +628 -0
- package/package.json +3 -1
- package/scripts/smoke-container-workspace-dependencies.ts +19 -0
- package/src/__tests__/app-builder-icon-names.test.ts +29 -0
- package/src/__tests__/assistant-attachment-directive.test.ts +4 -0
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop.test.ts +2 -0
- package/src/__tests__/conversation-attachments.test.ts +142 -0
- package/src/__tests__/conversation-delete-activation-progress.test.ts +145 -0
- package/src/__tests__/conversation-event-sink.test.ts +50 -0
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +10 -2
- package/src/__tests__/conversation-queue.test.ts +297 -0
- package/src/__tests__/credential-routes.test.ts +185 -6
- package/src/__tests__/drain-kick-guard.test.ts +2 -0
- package/src/__tests__/drain-requeue-on-contention.test.ts +6 -0
- package/src/__tests__/messaging-send-tool.test.ts +42 -0
- package/src/__tests__/oauth-commands-routes.test.ts +47 -0
- package/src/__tests__/subagent-manager-notify.test.ts +25 -4
- package/src/activation/progress-store.test.ts +1564 -0
- package/src/activation/progress-store.ts +1296 -0
- package/src/activation/turn-hooks.test.ts +246 -0
- package/src/activation/turn-hooks.ts +126 -0
- package/src/api/events/subagent-status-changed.ts +7 -5
- package/src/api/responses/activation.ts +136 -0
- package/src/apps/app-store.ts +8 -0
- package/src/cli/__tests__/catalog-search-help.test.ts +7 -5
- package/src/cli/commands/__tests__/cli-test-harness.ts +12 -3
- package/src/cli/commands/channels/__tests__/channels.test.ts +2 -0
- package/src/cli/commands/channels/__tests__/request.test.ts +191 -0
- package/src/cli/commands/channels/index.help.ts +70 -18
- package/src/cli/commands/channels/index.ts +17 -6
- package/src/cli/commands/channels/request.ts +66 -0
- package/src/cli/commands/oauth/request.test.ts +2 -0
- package/src/cli/commands/oauth/request.ts +227 -186
- package/src/config/bundled-skills/app-builder/SKILL.md +3 -1
- package/src/config/bundled-skills/app-builder/TOOLS.json +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +8 -4
- package/src/config/feature-flag-registry.json +35 -5
- package/src/daemon/assistant-attachments.ts +11 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +5 -0
- package/src/daemon/conversation-agent-loop.ts +71 -3
- package/src/daemon/conversation-attachments.ts +42 -1
- package/src/daemon/conversation-event-sink.ts +25 -0
- package/src/daemon/conversation-process.ts +69 -21
- package/src/daemon/conversation-store.ts +4 -3
- package/src/daemon/conversation-surfaces.ts +19 -4
- package/src/daemon/message-types/sync.ts +2 -0
- package/src/ipc/assistant-server.ts +2 -0
- package/src/ipc/routes/__tests__/activation-sync-ipc-routes.test.ts +51 -0
- package/src/ipc/routes/activation-sync-ipc-routes.ts +42 -0
- package/src/notifications/AGENTS.md +1 -1
- package/src/notifications/__tests__/proactive-home-thread.test.ts +111 -0
- package/src/notifications/conversation-pairing.ts +47 -5
- package/src/notifications/delivered-post-record.ts +3 -0
- package/src/persistence/__tests__/slack-thread-root-evidence.test.ts +102 -0
- package/src/persistence/conversation-crud.ts +39 -0
- package/src/persistence/delivery-crud.ts +13 -0
- package/src/plugins/AGENTS.md +1 -0
- package/src/runtime/auth/__tests__/route-policy.test.ts +37 -0
- package/src/runtime/routes/__tests__/user-routes-notices.test.ts +248 -0
- package/src/runtime/routes/activation-routes.test.ts +415 -0
- package/src/runtime/routes/activation-routes.ts +172 -0
- package/src/runtime/routes/credential-routes.ts +46 -3
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/oauth-commands-routes.ts +21 -8
- package/src/runtime/routes/platform-managed-credentials.ts +48 -0
- package/src/runtime/routes/secret-routes.ts +3 -12
- package/src/runtime/routes/user-route-resolution.ts +21 -0
- package/src/runtime/routes/user-routes.ts +112 -9
- package/src/runtime/sync/activation-sidecar-publish.test.ts +78 -0
- package/src/runtime/sync/documents-sidecar-publish.test.ts +3 -0
- package/src/runtime/sync/resource-sync-events.ts +23 -0
- package/src/runtime/sync/worker-daemon-notify.test.ts +36 -0
- package/src/runtime/sync/worker-daemon-notify.ts +39 -1
- package/src/tools/apps/executors.ts +8 -8
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The app-builder skill lists the icon names a model may put in
|
|
3
|
+
* `preview.icon`, and `@vellumai/app-icons` is what the executor accepts.
|
|
4
|
+
* The skill is prose, so nothing types it against the package; this pins
|
|
5
|
+
* the two lists to each other.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { describe, expect, test } from "bun:test";
|
|
11
|
+
|
|
12
|
+
import { APP_ICON_NAMES } from "@vellumai/app-icons";
|
|
13
|
+
|
|
14
|
+
const SKILL_PATH = join(
|
|
15
|
+
import.meta.dir,
|
|
16
|
+
"../config/bundled-skills/app-builder/SKILL.md",
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
describe("app-builder skill icon names", () => {
|
|
20
|
+
test("lists exactly the registry's names, in its order", () => {
|
|
21
|
+
const skill = readFileSync(SKILL_PATH, "utf8");
|
|
22
|
+
const line = skill
|
|
23
|
+
.split("\n")
|
|
24
|
+
.find((l) => l.startsWith("**App icon names**"));
|
|
25
|
+
expect(line).toBeDefined();
|
|
26
|
+
const listed = [...line!.matchAll(/`([a-z0-9-]+)`/g)].map((m) => m[1]);
|
|
27
|
+
expect(listed).toEqual([...APP_ICON_NAMES]);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -218,6 +218,9 @@ describe("resolveSandboxDirective", () => {
|
|
|
218
218
|
expect(result.draft!.mimeType).toBe("text/plain");
|
|
219
219
|
expect(result.draft!.sizeBytes).toBe(11);
|
|
220
220
|
expect(result.draft!.kind).toBe("document");
|
|
221
|
+
// The resolved path, not the named one: callers that surface the file
|
|
222
|
+
// need to know where it actually is.
|
|
223
|
+
expect(result.draft!.sourcePath).toBe(filePath);
|
|
221
224
|
});
|
|
222
225
|
|
|
223
226
|
test("uses directive filename override when provided", () => {
|
|
@@ -360,6 +363,7 @@ describe("resolveHostDirective", () => {
|
|
|
360
363
|
expect(result.draft!.filename).toBe("doc.txt");
|
|
361
364
|
expect(result.draft!.mimeType).toBe("text/plain");
|
|
362
365
|
expect(result.draft!.sizeBytes).toBe(12);
|
|
366
|
+
expect(result.draft!.sourcePath).toBe(filePath);
|
|
363
367
|
});
|
|
364
368
|
|
|
365
369
|
test("skips when user denies", async () => {
|
|
@@ -230,6 +230,7 @@ mock.module("../daemon/conversation-attachments.js", () => ({
|
|
|
230
230
|
assistantAttachments: [],
|
|
231
231
|
emittedAttachments: [],
|
|
232
232
|
directiveWarnings: [],
|
|
233
|
+
persistedFiles: [],
|
|
233
234
|
}),
|
|
234
235
|
approveHostAttachmentRead: async () => true,
|
|
235
236
|
formatAttachmentWarnings: () => "",
|
|
@@ -339,6 +339,7 @@ const resolveAssistantAttachmentsMock = mock(async () => ({
|
|
|
339
339
|
assistantAttachments: [],
|
|
340
340
|
emittedAttachments: [],
|
|
341
341
|
directiveWarnings: [],
|
|
342
|
+
persistedFiles: [],
|
|
342
343
|
}));
|
|
343
344
|
mock.module("../daemon/conversation-attachments.js", () => ({
|
|
344
345
|
resolveAssistantAttachments: resolveAssistantAttachmentsMock,
|
|
@@ -584,6 +584,7 @@ const resolveAssistantAttachmentsMock = mock(async () => ({
|
|
|
584
584
|
assistantAttachments: [],
|
|
585
585
|
emittedAttachments: [],
|
|
586
586
|
directiveWarnings: [],
|
|
587
|
+
persistedFiles: [],
|
|
587
588
|
}));
|
|
588
589
|
mock.module("../daemon/conversation-attachments.js", () => ({
|
|
589
590
|
resolveAssistantAttachments: resolveAssistantAttachmentsMock,
|
|
@@ -1036,6 +1037,7 @@ beforeEach(() => {
|
|
|
1036
1037
|
assistantAttachments: [],
|
|
1037
1038
|
emittedAttachments: [],
|
|
1038
1039
|
directiveWarnings: [],
|
|
1040
|
+
persistedFiles: [],
|
|
1039
1041
|
}));
|
|
1040
1042
|
mockMessageById = null;
|
|
1041
1043
|
resetConversationNoticesForTests();
|
|
@@ -187,6 +187,148 @@ describe("resolveAssistantAttachments", () => {
|
|
|
187
187
|
// fileBacked flag is always true now
|
|
188
188
|
expect(emitted.fileBacked).toBe(true);
|
|
189
189
|
});
|
|
190
|
+
|
|
191
|
+
test("persistedFiles carries only directives that resolved and persisted", async () => {
|
|
192
|
+
const conv = createConversation("test-conv-persisted");
|
|
193
|
+
const msg = await addMessage(conv.id, "assistant", "hello");
|
|
194
|
+
|
|
195
|
+
const accepted: AssistantAttachmentDraft = {
|
|
196
|
+
sourceType: "sandbox_file",
|
|
197
|
+
filename: "plan.md",
|
|
198
|
+
mimeType: "text/markdown",
|
|
199
|
+
dataBase64: makeBase64(64),
|
|
200
|
+
sizeBytes: 64,
|
|
201
|
+
kind: "document",
|
|
202
|
+
sourcePath: "notes/plan.md",
|
|
203
|
+
};
|
|
204
|
+
const oversized: AssistantAttachmentDraft = {
|
|
205
|
+
sourceType: "sandbox_file",
|
|
206
|
+
filename: "huge.bin",
|
|
207
|
+
mimeType: "application/octet-stream",
|
|
208
|
+
dataBase64: makeBase64(64),
|
|
209
|
+
sizeBytes: 64,
|
|
210
|
+
kind: "document",
|
|
211
|
+
sourcePath: "notes/huge.bin",
|
|
212
|
+
};
|
|
213
|
+
const toolBlock: AssistantAttachmentDraft = {
|
|
214
|
+
sourceType: "tool_block",
|
|
215
|
+
filename: "tool-output.png",
|
|
216
|
+
mimeType: "image/png",
|
|
217
|
+
dataBase64: makeBase64(64),
|
|
218
|
+
sizeBytes: 64,
|
|
219
|
+
kind: "image",
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
mock.module("../daemon/assistant-attachments.js", () => ({
|
|
223
|
+
// The missing file's directive produces a warning and no draft.
|
|
224
|
+
resolveDirectives: () =>
|
|
225
|
+
Promise.resolve({
|
|
226
|
+
drafts: [accepted, oversized],
|
|
227
|
+
warnings: [
|
|
228
|
+
'Skipped sandbox attachment "notes/missing.md": file not found.',
|
|
229
|
+
],
|
|
230
|
+
}),
|
|
231
|
+
contentBlocksToDrafts: () => [toolBlock],
|
|
232
|
+
deduplicateDrafts: (d: AssistantAttachmentDraft[]) => d,
|
|
233
|
+
validateDrafts: (d: AssistantAttachmentDraft[]) => ({
|
|
234
|
+
accepted: d.filter((draft) => draft.filename !== "huge.bin"),
|
|
235
|
+
warnings: ['Skipped attachment "huge.bin": too large.'],
|
|
236
|
+
}),
|
|
237
|
+
}));
|
|
238
|
+
|
|
239
|
+
const { resolveAssistantAttachments: resolve } =
|
|
240
|
+
await import("../daemon/conversation-attachments.js");
|
|
241
|
+
|
|
242
|
+
const result = await resolve(
|
|
243
|
+
[
|
|
244
|
+
{
|
|
245
|
+
source: "sandbox" as const,
|
|
246
|
+
path: "notes/plan.md",
|
|
247
|
+
filename: "plan.md",
|
|
248
|
+
mimeType: "text/markdown",
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
source: "sandbox" as const,
|
|
252
|
+
path: "notes/missing.md",
|
|
253
|
+
filename: undefined,
|
|
254
|
+
mimeType: undefined,
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
source: "sandbox" as const,
|
|
258
|
+
path: "notes/huge.bin",
|
|
259
|
+
filename: undefined,
|
|
260
|
+
mimeType: undefined,
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
[
|
|
264
|
+
{
|
|
265
|
+
type: "image",
|
|
266
|
+
source: { type: "base64", media_type: "image/png", data: "" },
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
[],
|
|
270
|
+
"/tmp",
|
|
271
|
+
async () => true,
|
|
272
|
+
msg.id,
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
// The rejected directive, the failed validation, and the tool block
|
|
276
|
+
// (which has no file of its own) are all absent.
|
|
277
|
+
expect(result.persistedFiles).toEqual([
|
|
278
|
+
{
|
|
279
|
+
sourcePath: "notes/plan.md",
|
|
280
|
+
displayName: "plan.md",
|
|
281
|
+
sourceType: "sandbox_file",
|
|
282
|
+
},
|
|
283
|
+
]);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("persistedFiles is empty when there is no message to persist against", async () => {
|
|
287
|
+
const accepted: AssistantAttachmentDraft = {
|
|
288
|
+
sourceType: "sandbox_file",
|
|
289
|
+
filename: "plan.md",
|
|
290
|
+
mimeType: "text/markdown",
|
|
291
|
+
dataBase64: makeBase64(64),
|
|
292
|
+
sizeBytes: 64,
|
|
293
|
+
kind: "document",
|
|
294
|
+
sourcePath: "notes/plan.md",
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
mock.module("../daemon/assistant-attachments.js", () => ({
|
|
298
|
+
resolveDirectives: () =>
|
|
299
|
+
Promise.resolve({ drafts: [accepted], warnings: [] }),
|
|
300
|
+
contentBlocksToDrafts: () => [],
|
|
301
|
+
deduplicateDrafts: (d: AssistantAttachmentDraft[]) => d,
|
|
302
|
+
validateDrafts: (d: AssistantAttachmentDraft[]) => ({
|
|
303
|
+
accepted: d,
|
|
304
|
+
warnings: [],
|
|
305
|
+
}),
|
|
306
|
+
}));
|
|
307
|
+
|
|
308
|
+
const { resolveAssistantAttachments: resolve } =
|
|
309
|
+
await import("../daemon/conversation-attachments.js");
|
|
310
|
+
|
|
311
|
+
// No assistant message id: the draft is emitted for this turn only and
|
|
312
|
+
// nothing is stored, so it is not a persisted file.
|
|
313
|
+
const result = await resolve(
|
|
314
|
+
[
|
|
315
|
+
{
|
|
316
|
+
source: "sandbox" as const,
|
|
317
|
+
path: "notes/plan.md",
|
|
318
|
+
filename: "plan.md",
|
|
319
|
+
mimeType: "text/markdown",
|
|
320
|
+
},
|
|
321
|
+
],
|
|
322
|
+
[],
|
|
323
|
+
[],
|
|
324
|
+
"/tmp",
|
|
325
|
+
async () => true,
|
|
326
|
+
undefined,
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
expect(result.emittedAttachments).toHaveLength(1);
|
|
330
|
+
expect(result.persistedFiles).toEqual([]);
|
|
331
|
+
});
|
|
190
332
|
});
|
|
191
333
|
|
|
192
334
|
describe("approveHostAttachmentRead", () => {
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The activation checklist records which task was launched into which
|
|
3
|
+
* conversation in a workspace file no database cascade reaches. A delete that
|
|
4
|
+
* skipped it would leave the task's row stuck on Working, pointing at a
|
|
5
|
+
* conversation the user can no longer open and offering no way to launch the
|
|
6
|
+
* task again.
|
|
7
|
+
*
|
|
8
|
+
* The cleanup hangs off the shared delete primitives rather than the route, so
|
|
9
|
+
* every caller (route, retrospective GC, cleanup jobs) cleans up, and it is
|
|
10
|
+
* best-effort like the rest of the post-transaction cleanup: these tests wait
|
|
11
|
+
* for it rather than awaiting it.
|
|
12
|
+
*/
|
|
13
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
14
|
+
|
|
15
|
+
// Keep the rest of the module real; only the Qdrant collection drop is
|
|
16
|
+
// replaced, so `clearAll` does not reach for a lexical index that is not
|
|
17
|
+
// running here.
|
|
18
|
+
const actualLexical =
|
|
19
|
+
await import("../persistence/job-handlers/message-lexical.js");
|
|
20
|
+
mock.module("../persistence/job-handlers/message-lexical.js", () => ({
|
|
21
|
+
...actualLexical,
|
|
22
|
+
clearMessagesLexicalIndex: async () => {},
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
markActivationTurnComplete,
|
|
27
|
+
readActivationProgress,
|
|
28
|
+
startActivationTask,
|
|
29
|
+
} from "../activation/progress-store.js";
|
|
30
|
+
import type { ActivationTaskProgress } from "../api/responses/activation.js";
|
|
31
|
+
import {
|
|
32
|
+
clearAll,
|
|
33
|
+
createConversation,
|
|
34
|
+
deleteConversation,
|
|
35
|
+
deleteConversationGently,
|
|
36
|
+
} from "../persistence/conversation-crud.js";
|
|
37
|
+
import { initializeDb } from "../persistence/db-init.js";
|
|
38
|
+
|
|
39
|
+
await initializeDb();
|
|
40
|
+
|
|
41
|
+
function taskRecord(taskId: string): ActivationTaskProgress | undefined {
|
|
42
|
+
return readActivationProgress().tasks[taskId];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function waitFor(predicate: () => boolean): Promise<void> {
|
|
46
|
+
const deadline = Date.now() + 5_000;
|
|
47
|
+
while (Date.now() < deadline) {
|
|
48
|
+
if (predicate()) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
await Bun.sleep(10);
|
|
52
|
+
}
|
|
53
|
+
throw new Error("Timed out waiting for the activation checklist to settle");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A conversation with a `started` checklist task pointing at it. */
|
|
57
|
+
async function seedStartedTask(taskId: string): Promise<string> {
|
|
58
|
+
const conversation = createConversation(`activation-${taskId}`);
|
|
59
|
+
await startActivationTask({ taskId, conversationId: conversation.id });
|
|
60
|
+
expect(taskRecord(taskId)).toMatchObject({
|
|
61
|
+
status: "started",
|
|
62
|
+
conversationId: conversation.id,
|
|
63
|
+
});
|
|
64
|
+
return conversation.id;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A conversation whose checklist task already finished in it. */
|
|
68
|
+
async function seedDoneTask(taskId: string): Promise<string> {
|
|
69
|
+
const conversationId = await seedStartedTask(taskId);
|
|
70
|
+
await markActivationTurnComplete({
|
|
71
|
+
conversationId,
|
|
72
|
+
toolCallCount: 2,
|
|
73
|
+
endedAwaitingUser: false,
|
|
74
|
+
artifacts: [],
|
|
75
|
+
});
|
|
76
|
+
expect(taskRecord(taskId)).toMatchObject({ status: "done", stepCount: 2 });
|
|
77
|
+
return conversationId;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
describe("deleteConversation releases the checklist task", () => {
|
|
81
|
+
test("a started task returns to Todo and can be launched again", async () => {
|
|
82
|
+
const taskId = "delete-started";
|
|
83
|
+
const conversationId = await seedStartedTask(taskId);
|
|
84
|
+
const other = await seedStartedTask("delete-started-other");
|
|
85
|
+
|
|
86
|
+
deleteConversation(conversationId);
|
|
87
|
+
await waitFor(() => taskRecord(taskId) === undefined);
|
|
88
|
+
|
|
89
|
+
// Scoped to the conversation being deleted.
|
|
90
|
+
expect(taskRecord("delete-started-other")).toMatchObject({
|
|
91
|
+
status: "started",
|
|
92
|
+
conversationId: other,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Todo again, so the row is launchable.
|
|
96
|
+
const relaunched = createConversation("activation-relaunch");
|
|
97
|
+
await startActivationTask({ taskId, conversationId: relaunched.id });
|
|
98
|
+
expect(taskRecord(taskId)).toMatchObject({
|
|
99
|
+
status: "started",
|
|
100
|
+
conversationId: relaunched.id,
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("a finished task keeps its history and loses only the dead link", async () => {
|
|
105
|
+
const taskId = "delete-done";
|
|
106
|
+
const conversationId = await seedDoneTask(taskId);
|
|
107
|
+
|
|
108
|
+
deleteConversation(conversationId);
|
|
109
|
+
await waitFor(() => taskRecord(taskId)?.conversationId === "");
|
|
110
|
+
|
|
111
|
+
expect(taskRecord(taskId)).toMatchObject({
|
|
112
|
+
status: "done",
|
|
113
|
+
stepCount: 2,
|
|
114
|
+
conversationId: "",
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("deleteConversationGently releases the checklist task", () => {
|
|
120
|
+
test("the off-loop path clears the same state as the synchronous one", async () => {
|
|
121
|
+
const taskId = "delete-gently";
|
|
122
|
+
const conversationId = await seedStartedTask(taskId);
|
|
123
|
+
|
|
124
|
+
await deleteConversationGently(conversationId);
|
|
125
|
+
await waitFor(() => taskRecord(taskId) === undefined);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("clearAll releases every checklist task", () => {
|
|
130
|
+
test("started tasks go, finished ones keep their history", async () => {
|
|
131
|
+
await seedStartedTask("clear-all-started");
|
|
132
|
+
await seedDoneTask("clear-all-done");
|
|
133
|
+
|
|
134
|
+
await clearAll();
|
|
135
|
+
await waitFor(
|
|
136
|
+
() => readActivationProgress().tasks["clear-all-started"] === undefined,
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
expect(taskRecord("clear-all-done")).toMatchObject({
|
|
140
|
+
status: "done",
|
|
141
|
+
stepCount: 2,
|
|
142
|
+
conversationId: "",
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
const published: Array<{ type: string; conversationId: string | undefined }> =
|
|
4
|
+
[];
|
|
5
|
+
mock.module("../runtime/assistant-event-hub.js", () => ({
|
|
6
|
+
broadcastMessage: (msg: { type: string }, conversationId?: string) => {
|
|
7
|
+
published.push({ type: msg.type, conversationId });
|
|
8
|
+
},
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
const { conversationEventSink } =
|
|
12
|
+
await import("../daemon/conversation-event-sink.js");
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The scoping guarantee the hub depends on: attribution comes from the
|
|
16
|
+
* emitting conversation, so an event carrying no conversation of its own is
|
|
17
|
+
* still filtered, seq-stamped and replayed like every other one.
|
|
18
|
+
*/
|
|
19
|
+
describe("conversationEventSink", () => {
|
|
20
|
+
test("scopes an event whose payload names no conversation", () => {
|
|
21
|
+
published.length = 0;
|
|
22
|
+
|
|
23
|
+
conversationEventSink("conv-parent")({
|
|
24
|
+
type: "subagent_status_changed",
|
|
25
|
+
subagentId: "sa-1",
|
|
26
|
+
status: "running",
|
|
27
|
+
} as never);
|
|
28
|
+
|
|
29
|
+
expect(published).toEqual([
|
|
30
|
+
{ type: "subagent_status_changed", conversationId: "conv-parent" },
|
|
31
|
+
]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("scopes every event to the emitting conversation, not the payload's", () => {
|
|
35
|
+
published.length = 0;
|
|
36
|
+
const sink = conversationEventSink("conv-parent");
|
|
37
|
+
|
|
38
|
+
sink({ type: "subagent_spawned", subagentId: "sa-1" } as never);
|
|
39
|
+
sink({
|
|
40
|
+
type: "subagent_event",
|
|
41
|
+
subagentId: "sa-1",
|
|
42
|
+
conversationId: "conv-parent",
|
|
43
|
+
} as never);
|
|
44
|
+
|
|
45
|
+
expect(published.map((p) => p.conversationId)).toEqual([
|
|
46
|
+
"conv-parent",
|
|
47
|
+
"conv-parent",
|
|
48
|
+
]);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -161,6 +161,12 @@ function makeFakeContext(opts: {
|
|
|
161
161
|
trustClass: "guardian" as const,
|
|
162
162
|
guardianPrincipalId: "user-1",
|
|
163
163
|
},
|
|
164
|
+
setTrustContext(
|
|
165
|
+
this: { trustContext?: TrustContext },
|
|
166
|
+
trustContext: TrustContext | null,
|
|
167
|
+
) {
|
|
168
|
+
this.trustContext = trustContext ?? undefined;
|
|
169
|
+
},
|
|
164
170
|
setTransportHints() {},
|
|
165
171
|
applyHostEnvFromTransport() {},
|
|
166
172
|
ensureHostProxiesForTurn() {},
|
|
@@ -409,8 +415,10 @@ describe("drainQueue preactivation re-add for host-proxy interfaces", () => {
|
|
|
409
415
|
"U-contact",
|
|
410
416
|
);
|
|
411
417
|
expect(ctx.currentTurnTrustContext?.sourceChannel).toBe("slack");
|
|
412
|
-
// The
|
|
413
|
-
|
|
418
|
+
// The drain is where a queued message commits to a run, so the resting
|
|
419
|
+
// slot names the sender too: history is scoped from it, and a slot still
|
|
420
|
+
// naming the guardian would hand the contact's turn the guardian's rows.
|
|
421
|
+
expect(ctx.trustContext).toBe(contactTrust);
|
|
414
422
|
});
|
|
415
423
|
|
|
416
424
|
test("buildPassthroughBatch refuses to coalesce two channel senders", async () => {
|