@vellumai/assistant 0.11.10 → 0.11.11-staging.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/plan-credit.test.ts +51 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/plan-credit.ts +66 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/plan-credit.test.ts +51 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/plan-credit.ts +66 -0
- package/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/service-contracts/src/__tests__/plan-credit.test.ts +51 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/plan-credit.ts +66 -0
- package/openapi.yaml +152 -55
- package/package.json +1 -1
- package/src/__tests__/anthropic-provider.test.ts +60 -0
- package/src/__tests__/config-get-vision-flag.test.ts +36 -0
- package/src/__tests__/conversation-error.test.ts +38 -0
- package/src/__tests__/copy-composer-tc-templates.test.ts +2 -2
- package/src/__tests__/guardian-action-service-scope.test.ts +53 -0
- package/src/__tests__/headless-browser-mode.test.ts +63 -13
- package/src/__tests__/heartbeat-disk-pressure.test.ts +2 -0
- package/src/__tests__/heartbeat-service.test.ts +118 -27
- package/src/__tests__/host-app-control-proxy.test.ts +2 -2
- package/src/__tests__/host-app-control-routes.test.ts +41 -0
- package/src/__tests__/host-bash-routes.test.ts +30 -1
- package/src/__tests__/host-browser-routes.test.ts +34 -0
- package/src/__tests__/host-cu-routes-targeted.test.ts +29 -0
- package/src/__tests__/host-file-routes-targeted.test.ts +29 -0
- package/src/__tests__/host-proxy-base.test.ts +1 -0
- package/src/__tests__/host-transfer-routes-targeted.test.ts +86 -0
- package/src/__tests__/llm-schema.test.ts +32 -0
- package/src/__tests__/mcp-config-secret-boundary.test.ts +2 -0
- package/src/__tests__/memory-config-file-parity.test.ts +18 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +15 -0
- package/src/__tests__/messaging-send-tool.test.ts +159 -13
- package/src/__tests__/messaging-shared.test.ts +19 -0
- package/src/__tests__/notification-events-source-context-probe.test.ts +211 -0
- package/src/__tests__/openai-responses-prompt-cache.test.ts +40 -6
- package/src/__tests__/outlook-messaging-provider.test.ts +9 -0
- package/src/__tests__/provider-catalog-visibility.test.ts +2 -2
- package/src/__tests__/schedule-routes.test.ts +30 -1
- package/src/__tests__/scheduler-result-notification.test.ts +209 -0
- package/src/__tests__/skills-install-extract.test.ts +26 -0
- package/src/__tests__/skills-install-staging.test.ts +11 -2
- package/src/api/constants/personality-sliders.test.ts +21 -0
- package/src/api/constants/personality-sliders.ts +64 -0
- package/src/api/index.ts +8 -0
- package/src/archive/ustar-size.test.ts +32 -0
- package/src/archive/ustar-size.ts +23 -0
- package/src/background-wake/next-wake.test.ts +13 -0
- package/src/background-wake/next-wake.ts +4 -2
- package/src/calls/voice-session-bridge.ts +26 -0
- package/src/cli/__tests__/catalog-search-help.test.ts +1 -0
- package/src/cli/commands/__tests__/plugins-search.test.ts +81 -0
- package/src/cli/commands/config.help.ts +3 -1
- package/src/cli/commands/memory/worker.ts +1 -1
- package/src/cli/commands/platform/__tests__/credits-format.test.ts +94 -5
- package/src/cli/commands/platform/__tests__/credits.test.ts +10 -0
- package/src/cli/commands/platform/credits-format.ts +57 -0
- package/src/cli/commands/platform/index.help.ts +19 -3
- package/src/cli/commands/plugins.help.ts +4 -1
- package/src/cli/commands/plugins.ts +2 -1
- package/src/cli/lib/bundled-marketplace.json +1 -1
- package/src/cli/lib/install-from-platform.ts +8 -10
- package/src/config/bundled-skills/messaging/SKILL.md +2 -0
- package/src/config/bundled-skills/messaging/TOOLS.json +4 -4
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +85 -1
- package/src/config/bundled-skills/messaging/tools/shared.ts +6 -0
- package/src/config/bundled-skills/schedule/SKILL.md +9 -1
- package/src/config/bundled-skills/screen-annotation/SKILL.md +87 -20
- package/src/config/bundled-skills/screen-annotation/TOOLS.json +21 -5
- package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +67 -0
- package/src/config/feature-flag-registry.json +17 -1
- package/src/config/input-modalities.test.ts +71 -0
- package/src/config/input-modalities.ts +95 -0
- package/src/config/schemas/heartbeat.ts +3 -3
- package/src/config/schemas/llm.ts +13 -3
- package/src/config/schemas/memory-storage.test.ts +39 -0
- package/src/config/schemas/memory-storage.ts +21 -0
- package/src/context/token-estimator.ts +2 -2
- package/src/daemon/__tests__/embedding-reconcile.test.ts +77 -0
- package/src/daemon/__tests__/lifecycle-embedding-reconcile.test.ts +3 -0
- package/src/daemon/conversation-error.ts +20 -7
- package/src/daemon/embedding-reconcile.ts +64 -1
- package/src/daemon/handlers/config-embeddings.ts +31 -0
- package/src/daemon/host-app-control-proxy.ts +2 -0
- package/src/daemon/host-bash-proxy.ts +1 -0
- package/src/daemon/host-cu-proxy.ts +6 -6
- package/src/daemon/host-file-proxy.ts +6 -6
- package/src/daemon/host-proxy-base.ts +10 -9
- package/src/daemon/host-transfer-proxy.ts +21 -24
- package/src/daemon/mcp-reload-service.ts +6 -0
- package/src/daemon/providers-setup.ts +5 -26
- package/src/heartbeat/heartbeat-service.ts +72 -13
- package/src/live-voice/__tests__/live-voice-photo.test.ts +72 -0
- package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +81 -0
- package/src/live-voice/live-voice-photo.ts +68 -4
- package/src/live-voice/live-voice-session.ts +36 -2
- package/src/live-voice/protocol.ts +98 -1
- package/src/mcp/__tests__/reload-signal-emission.test.ts +54 -0
- package/src/mcp/__tests__/reload-signal.test.ts +57 -0
- package/src/mcp/__tests__/startup.test.ts +150 -0
- package/src/mcp/reload-signal.ts +50 -0
- package/src/mcp/startup.ts +101 -0
- package/src/messaging/providers/outlook/adapter.ts +2 -13
- package/src/messaging/providers/outlook/client.ts +13 -1
- package/src/messaging/providers/outlook/types.ts +3 -0
- package/src/notifications/__tests__/decision-engine.test.ts +79 -6
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +27 -0
- package/src/notifications/__tests__/schedule-result-producer.test.ts +397 -0
- package/src/notifications/copy-composer.ts +33 -11
- package/src/notifications/decision-engine.ts +48 -11
- package/src/notifications/events-store.ts +68 -2
- package/src/notifications/home-feed-side-effect.ts +15 -3
- package/src/notifications/schedule-result-producer.ts +275 -0
- package/src/notifications/signal.ts +5 -0
- package/src/oauth/seed-providers.ts +3 -2
- package/src/persistence/conversation-crud.ts +38 -0
- package/src/persistence/embeddings/embedding-backend.test.ts +162 -0
- package/src/persistence/embeddings/embedding-backend.ts +148 -11
- package/src/persistence/embeddings/embedding-openai.test.ts +113 -0
- package/src/persistence/embeddings/embedding-openai.ts +55 -4
- package/src/persistence/embeddings/embedding-types.test.ts +24 -0
- package/src/persistence/embeddings/embedding-types.ts +25 -1
- package/src/persistence/job-utils.ts +6 -2
- package/src/plugin-api/index.ts +2 -1
- package/src/plugin-api/vision-support.test.ts +81 -0
- package/src/plugin-api/vision-support.ts +57 -10
- package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +8 -0
- package/src/plugins/defaults/memory/jobs/__tests__/embed-concept-page.test.ts +1 -0
- package/src/plugins/defaults/memory/jobs/embed-concept-page.ts +11 -3
- package/src/plugins/defaults/memory/src/__tests__/memory-worker-routes.test.ts +20 -0
- package/src/plugins/defaults/memory/src/memory-worker-routes.ts +3 -1
- package/src/plugins/defaults/memory/substrate/__tests__/reembed-job.test.ts +1 -0
- package/src/plugins/defaults/memory/v2/__tests__/backfill-jobs.test.ts +1 -0
- package/src/plugins/defaults/memory/v3/__tests__/section-dense-store.test.ts +82 -0
- package/src/plugins/defaults/memory/v3/section-dense-store.ts +9 -27
- package/src/providers/__tests__/provider-secret-catalog.test.ts +4 -0
- package/src/providers/__tests__/xml-tool-call-salvage.test.ts +172 -0
- package/src/providers/anthropic/client.ts +9 -4
- package/src/providers/inline-audio-support.test.ts +34 -0
- package/src/providers/inline-audio-support.ts +27 -0
- package/src/providers/model-catalog.ts +22 -2
- package/src/providers/openai/__tests__/api-error-normalization.test.ts +50 -0
- package/src/providers/openai/__tests__/chat-completions-extra-body.test.ts +51 -0
- package/src/providers/openai/__tests__/chat-completions-provider-xml-salvage.test.ts +291 -0
- package/src/providers/openai/api-error-normalization.ts +18 -3
- package/src/providers/openai/chat-completions-provider.ts +113 -13
- package/src/providers/openai/responses-provider.ts +10 -8
- package/src/providers/provider-secret-catalog.ts +11 -1
- package/src/providers/vellum/client.ts +14 -1
- package/src/providers/vellum/personality-directions.test.ts +163 -0
- package/src/providers/vellum/personality-directions.ts +120 -0
- package/src/providers/xml-tool-call-salvage.ts +212 -0
- package/src/runtime/__tests__/background-job-runner.test.ts +42 -4
- package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -6
- package/src/runtime/auth/__tests__/route-policy.test.ts +65 -0
- package/src/runtime/auth/same-actor.ts +29 -5
- package/src/runtime/background-job-runner.ts +38 -29
- package/src/runtime/guardian-action-service.ts +20 -1
- package/src/runtime/migrations/vbundle-validator.ts +3 -3
- package/src/runtime/pending-interactions.ts +4 -5
- package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +24 -0
- package/src/runtime/routes/__tests__/platform-credits-routes.test.ts +99 -5
- package/src/runtime/routes/conversation-query-routes.ts +53 -11
- package/src/runtime/routes/document-comments-routes.ts +8 -2
- package/src/runtime/routes/guardian-action-routes.ts +7 -0
- package/src/runtime/routes/heartbeat-routes.ts +41 -53
- package/src/runtime/routes/host-app-control-routes.ts +10 -36
- package/src/runtime/routes/host-bash-routes.ts +11 -45
- package/src/runtime/routes/host-browser-routes.ts +24 -0
- package/src/runtime/routes/host-cu-routes.ts +11 -45
- package/src/runtime/routes/host-file-routes.ts +11 -44
- package/src/runtime/routes/host-proxy-result-binding.ts +57 -0
- package/src/runtime/routes/host-transfer-routes.ts +33 -25
- package/src/runtime/routes/index.ts +0 -2
- package/src/runtime/routes/integrations/a2a.ts +12 -3
- package/src/runtime/routes/integrations/vercel.ts +13 -3
- package/src/runtime/routes/platform-routes.ts +121 -24
- package/src/runtime/routes/schedule-routes.ts +15 -1
- package/src/schedule/__tests__/schedule-timezone.test.ts +21 -3
- package/src/schedule/__tests__/worker-mcp-tools.test.ts +88 -0
- package/src/schedule/schedule-timezone.ts +25 -0
- package/src/schedule/scheduler.ts +19 -0
- package/src/schedule/worker.ts +119 -2
- package/src/skills/catalog-install.ts +40 -5
- package/src/tools/browser/__tests__/browser-status.test.ts +3 -0
- package/src/tools/browser/browser-execution.ts +117 -53
- package/src/tools/browser/browser-status-constants.ts +6 -6
- package/src/tools/capability-offer.test.ts +87 -0
- package/src/tools/capability-offer.ts +93 -0
- package/src/tools/terminal/__tests__/safe-env.test.ts +17 -0
- package/src/tools/terminal/safe-env.ts +3 -2
- package/src/util/provider-error-patterns.ts +34 -0
- package/src/watcher/__tests__/engine.test.ts +5 -2
- package/src/watcher/engine.ts +7 -4
- package/src/runtime/routes/__tests__/sight-frame-routes.test.ts +0 -487
- package/src/runtime/routes/sight-frame-routes.ts +0 -136
|
@@ -304,9 +304,11 @@ describe("formatModeSelectionFailure", () => {
|
|
|
304
304
|
expect(formatted).toContain(
|
|
305
305
|
"host browser proxy exists but is not connected",
|
|
306
306
|
);
|
|
307
|
-
expect(formatted).toContain("
|
|
308
|
-
expect(formatted).toContain("extension
|
|
307
|
+
expect(formatted).toContain("Setup details:");
|
|
308
|
+
expect(formatted).toContain("Chrome extension");
|
|
309
309
|
expect(formatted).toContain(CHROME_WEB_STORE_INSTALL_URL);
|
|
310
|
+
expect(formatted).toContain("www.vellum.ai/download");
|
|
311
|
+
expect(formatted).toContain("Offer those first");
|
|
310
312
|
});
|
|
311
313
|
|
|
312
314
|
test("renders cdp-inspect transport_error with remediation", () => {
|
|
@@ -330,8 +332,9 @@ describe("formatModeSelectionFailure", () => {
|
|
|
330
332
|
expect(formatted).toContain('Browser mode "cdp-inspect" failed');
|
|
331
333
|
expect(formatted).toContain("cdp-inspect: FAILED at send");
|
|
332
334
|
expect(formatted).toContain("Discovery code: unreachable");
|
|
333
|
-
expect(formatted).toContain("
|
|
335
|
+
expect(formatted).toContain("Setup details:");
|
|
334
336
|
expect(formatted).toContain("--remote-debugging-port");
|
|
337
|
+
expect(formatted).toContain(CHROME_WEB_STORE_INSTALL_URL);
|
|
335
338
|
});
|
|
336
339
|
|
|
337
340
|
test("includes the Chrome Web Store install URL for host-bridge failures", () => {
|
|
@@ -353,8 +356,32 @@ describe("formatModeSelectionFailure", () => {
|
|
|
353
356
|
|
|
354
357
|
const formatted = formatModeSelectionFailure("auto", error);
|
|
355
358
|
|
|
356
|
-
expect(formatted).toContain("
|
|
359
|
+
expect(formatted).toContain("Setup details:");
|
|
360
|
+
expect(formatted).toContain(CHROME_WEB_STORE_INSTALL_URL);
|
|
361
|
+
expect(formatted).toContain("www.vellum.ai/download");
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("phone surfaces do not invent an in-app browser panel", () => {
|
|
365
|
+
const diagnostics: AttemptDiagnostic[] = [
|
|
366
|
+
{
|
|
367
|
+
candidateKind: "extension",
|
|
368
|
+
inclusionReason: "pinned mode: extension",
|
|
369
|
+
stage: "candidate_selection",
|
|
370
|
+
errorCode: "transport_error",
|
|
371
|
+
errorMessage: "not connected",
|
|
372
|
+
},
|
|
373
|
+
];
|
|
374
|
+
const formatted = formatModeSelectionFailure(
|
|
375
|
+
"extension",
|
|
376
|
+
new CdpError("transport_error", "not connected", {
|
|
377
|
+
attemptDiagnostics: diagnostics,
|
|
378
|
+
}),
|
|
379
|
+
{ transportInterface: "ios", clientOs: "ios" },
|
|
380
|
+
);
|
|
381
|
+
expect(formatted).toContain("no in-app browser");
|
|
382
|
+
expect(formatted).toContain("Do not describe a browser panel");
|
|
357
383
|
expect(formatted).toContain(CHROME_WEB_STORE_INSTALL_URL);
|
|
384
|
+
expect(formatted).toContain("www.vellum.ai/download");
|
|
358
385
|
});
|
|
359
386
|
});
|
|
360
387
|
|
|
@@ -372,6 +399,7 @@ describe("browser_mode wiring through tool execution", () => {
|
|
|
372
399
|
);
|
|
373
400
|
expect(result.isError).toBe(true);
|
|
374
401
|
expect(result.content).toContain('Invalid browser_mode "bogus"');
|
|
402
|
+
expect(result.content).not.toContain(CHROME_WEB_STORE_INSTALL_URL);
|
|
375
403
|
expect(cdpSendCalls).toEqual([]);
|
|
376
404
|
});
|
|
377
405
|
|
|
@@ -394,6 +422,28 @@ describe("browser_mode wiring through tool execution", () => {
|
|
|
394
422
|
|
|
395
423
|
// ── Pinned extension with no proxy returns remediation error ───
|
|
396
424
|
|
|
425
|
+
test("navigation timeout offers the desktop app and Chrome extension", async () => {
|
|
426
|
+
cdpSendHandler = (method, params) => {
|
|
427
|
+
if (method === "Page.navigate") {
|
|
428
|
+
throw new Error(
|
|
429
|
+
"Navigation to https://intranet.example.com timed out after 15000ms",
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
return defaultCdpHandler(method, params);
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
const result = await executeBrowserNavigate(
|
|
436
|
+
{ url: "https://intranet.example.com" },
|
|
437
|
+
ctx,
|
|
438
|
+
);
|
|
439
|
+
expect(result.isError).toBe(true);
|
|
440
|
+
expect(result.content).toContain("Navigation failed");
|
|
441
|
+
expect(result.content).toContain("timed out");
|
|
442
|
+
expect(result.content).toContain(CHROME_WEB_STORE_INSTALL_URL);
|
|
443
|
+
expect(result.content).toContain("www.vellum.ai/download");
|
|
444
|
+
expect(result.content).toContain("Offer those first");
|
|
445
|
+
});
|
|
446
|
+
|
|
397
447
|
test("pinned extension with no proxy returns remediation-rich error in navigate", async () => {
|
|
398
448
|
factoryThrowError = new CdpError(
|
|
399
449
|
"transport_error",
|
|
@@ -421,8 +471,8 @@ describe("browser_mode wiring through tool execution", () => {
|
|
|
421
471
|
expect(result.content).toContain(
|
|
422
472
|
"extension: FAILED at candidate_selection",
|
|
423
473
|
);
|
|
424
|
-
expect(result.content).toContain("
|
|
425
|
-
expect(result.content).toContain("extension
|
|
474
|
+
expect(result.content).toContain("Setup details:");
|
|
475
|
+
expect(result.content).toContain("Chrome extension");
|
|
426
476
|
expect(result.content).toContain(CHROME_WEB_STORE_INSTALL_URL);
|
|
427
477
|
// Factory should not have been called for CDP
|
|
428
478
|
expect(cdpSendCalls).toEqual([]);
|
|
@@ -451,7 +501,7 @@ describe("browser_mode wiring through tool execution", () => {
|
|
|
451
501
|
);
|
|
452
502
|
expect(result.isError).toBe(true);
|
|
453
503
|
expect(result.content).toContain('Browser mode "extension" failed');
|
|
454
|
-
expect(result.content).toContain("
|
|
504
|
+
expect(result.content).toContain("Setup details:");
|
|
455
505
|
});
|
|
456
506
|
|
|
457
507
|
// ── cdp-debugger alias normalization ──────────────────────────
|
|
@@ -482,7 +532,7 @@ describe("browser_mode wiring through tool execution", () => {
|
|
|
482
532
|
// The error should reference the canonical cdp-inspect name
|
|
483
533
|
expect(result.content).toContain('Browser mode "cdp-inspect" failed');
|
|
484
534
|
expect(result.content).toContain("Discovery code: unreachable");
|
|
485
|
-
expect(result.content).toContain("
|
|
535
|
+
expect(result.content).toContain("Setup details:");
|
|
486
536
|
expect(result.content).toContain("--remote-debugging-port");
|
|
487
537
|
});
|
|
488
538
|
|
|
@@ -544,7 +594,7 @@ describe("browser_mode wiring through tool execution", () => {
|
|
|
544
594
|
"Reason: HTTP discovery failed (unreachable)",
|
|
545
595
|
);
|
|
546
596
|
expect(result.content).toContain("Discovery code: unreachable");
|
|
547
|
-
expect(result.content).toContain("
|
|
597
|
+
expect(result.content).toContain("Setup details:");
|
|
548
598
|
});
|
|
549
599
|
|
|
550
600
|
test("click tool returns remediation error on pinned mode failure", async () => {
|
|
@@ -576,7 +626,7 @@ describe("browser_mode wiring through tool execution", () => {
|
|
|
576
626
|
expect(result.content).toContain(
|
|
577
627
|
"extension: FAILED at candidate_selection",
|
|
578
628
|
);
|
|
579
|
-
expect(result.content).toContain("
|
|
629
|
+
expect(result.content).toContain("Setup details:");
|
|
580
630
|
});
|
|
581
631
|
|
|
582
632
|
// ── Transport-classified host-browser errors produce failover diagnostics ──
|
|
@@ -609,8 +659,8 @@ describe("browser_mode wiring through tool execution", () => {
|
|
|
609
659
|
expect(result.content).toContain('Browser mode "extension" failed');
|
|
610
660
|
expect(result.content).toContain("extension: FAILED at send");
|
|
611
661
|
expect(result.content).toContain("Reason: Host browser not reachable");
|
|
612
|
-
expect(result.content).toContain("
|
|
613
|
-
expect(result.content).toContain("extension
|
|
662
|
+
expect(result.content).toContain("Setup details:");
|
|
663
|
+
expect(result.content).toContain("Chrome extension");
|
|
614
664
|
expect(result.content).toContain(CHROME_WEB_STORE_INSTALL_URL);
|
|
615
665
|
});
|
|
616
666
|
|
|
@@ -634,7 +684,7 @@ describe("browser_mode wiring through tool execution", () => {
|
|
|
634
684
|
expect(result.isError).toBe(true);
|
|
635
685
|
expect(result.content).toContain('Browser mode "extension" failed');
|
|
636
686
|
expect(result.content).toContain("extension: FAILED at send");
|
|
637
|
-
expect(result.content).toContain("
|
|
687
|
+
expect(result.content).toContain("Setup details:");
|
|
638
688
|
});
|
|
639
689
|
|
|
640
690
|
// ── Per-conversation sticky backend kind ─────────────────────────
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
2
|
|
|
3
|
+
import { hasSetConstructs } from "../schedule/recurrence-engine.js";
|
|
3
4
|
import { setConfig } from "./helpers/set-config.js";
|
|
4
5
|
|
|
5
6
|
// Default the warm-pool gate to OPEN — these tests probe disk-pressure
|
|
@@ -30,6 +31,7 @@ mock.module("../heartbeat/heartbeat-run-store.js", () => ({
|
|
|
30
31
|
}));
|
|
31
32
|
|
|
32
33
|
mock.module("../schedule/recurrence-engine.js", () => ({
|
|
34
|
+
hasSetConstructs,
|
|
33
35
|
computeNextRunAt: () => Date.now() + 60_000,
|
|
34
36
|
}));
|
|
35
37
|
|
|
@@ -2,6 +2,8 @@ import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
4
4
|
|
|
5
|
+
import { hasSetConstructs } from "../schedule/recurrence-engine.js";
|
|
6
|
+
|
|
5
7
|
const testWorkspaceDir = process.env.VELLUM_WORKSPACE_DIR!;
|
|
6
8
|
|
|
7
9
|
// Default the warm-pool gate to OPEN for existing tests — they predate
|
|
@@ -80,11 +82,14 @@ function setHeartbeatConfig(overrides: Partial<HeartbeatSeed> = {}): void {
|
|
|
80
82
|
//
|
|
81
83
|
// HeartbeatService imports computeNextRunAt for cron scheduling.
|
|
82
84
|
// Tests mutate `mockComputeNextRunAt` to control the next cron occurrence.
|
|
85
|
+
// `hasSetConstructs` is re-exported because `schedule-timezone` imports it
|
|
86
|
+
// from the same module and must keep working under this mock.
|
|
83
87
|
let mockComputeNextRunAtResult: number | null = null;
|
|
84
88
|
let mockComputeNextRunAtError: Error | null = null;
|
|
85
89
|
let computeNextRunAtCallCount = 0;
|
|
86
90
|
|
|
87
91
|
mock.module("../schedule/recurrence-engine.js", () => ({
|
|
92
|
+
hasSetConstructs,
|
|
88
93
|
computeNextRunAt: (_spec: {
|
|
89
94
|
syntax: string;
|
|
90
95
|
expression: string;
|
|
@@ -303,8 +308,11 @@ export function setTestProcessMessage(
|
|
|
303
308
|
}
|
|
304
309
|
|
|
305
310
|
// Import after mocks are set up
|
|
306
|
-
const {
|
|
307
|
-
|
|
311
|
+
const {
|
|
312
|
+
HeartbeatService,
|
|
313
|
+
isShallowProfile,
|
|
314
|
+
shouldRescheduleHeartbeatForTimezoneChange,
|
|
315
|
+
} = await import("../heartbeat/heartbeat-service.js");
|
|
308
316
|
|
|
309
317
|
// Read the bundled template files so we can write them into the test workspace
|
|
310
318
|
const templatesDir = join(import.meta.dirname!, "..", "prompts", "templates");
|
|
@@ -410,17 +418,20 @@ describe("HeartbeatService", () => {
|
|
|
410
418
|
|
|
411
419
|
heartbeatConfig = defaultHeartbeatSeed();
|
|
412
420
|
setHeartbeatConfig();
|
|
421
|
+
setConfig("ui", {});
|
|
413
422
|
});
|
|
414
423
|
|
|
415
424
|
function createService(overrides?: {
|
|
416
425
|
processMessage?: (...args: unknown[]) => Promise<{ messageId: string }>;
|
|
417
426
|
getCurrentHour?: () => number;
|
|
427
|
+
now?: () => Date;
|
|
418
428
|
}) {
|
|
419
429
|
if (overrides?.processMessage) {
|
|
420
430
|
setTestProcessMessage(overrides.processMessage);
|
|
421
431
|
}
|
|
422
432
|
return new HeartbeatService({
|
|
423
433
|
getCurrentHour: overrides?.getCurrentHour,
|
|
434
|
+
now: overrides?.now,
|
|
424
435
|
});
|
|
425
436
|
}
|
|
426
437
|
|
|
@@ -546,21 +557,52 @@ describe("HeartbeatService", () => {
|
|
|
546
557
|
expect(processMessageCalls).toHaveLength(1);
|
|
547
558
|
});
|
|
548
559
|
|
|
549
|
-
|
|
550
|
-
|
|
560
|
+
test("active hours handles overnight window", async () => {
|
|
561
|
+
setHeartbeatConfig({ activeHoursStart: 22, activeHoursEnd: 6 });
|
|
551
562
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
563
|
+
// 23:00 should be within the window
|
|
564
|
+
const service = createService({ getCurrentHour: () => 23 });
|
|
565
|
+
await service.runOnce();
|
|
566
|
+
expect(processMessageCalls).toHaveLength(1);
|
|
556
567
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
568
|
+
// 10:00 should be outside the window
|
|
569
|
+
processMessageCalls.length = 0;
|
|
570
|
+
createdConversations.length = 0;
|
|
571
|
+
const service2 = createService({ getCurrentHour: () => 10 });
|
|
572
|
+
await service2.runOnce();
|
|
573
|
+
expect(processMessageCalls).toHaveLength(0);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
test("active hours guard uses user timezone in interval mode", async () => {
|
|
577
|
+
setConfig("ui", { detectedTimezone: "America/Los_Angeles" });
|
|
578
|
+
setHeartbeatConfig({
|
|
579
|
+
cronExpression: null,
|
|
580
|
+
timezone: null,
|
|
581
|
+
activeHoursStart: 8,
|
|
582
|
+
activeHoursEnd: 22,
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
// 23:00 UTC is 16:00 Pacific (inside 8-22) and 23:00 host-local
|
|
586
|
+
// (outside 8-22). If the guard still used the host clock this would skip.
|
|
587
|
+
const inside = createService({
|
|
588
|
+
now: () => new Date("2026-09-08T23:00:00Z"),
|
|
589
|
+
getCurrentHour: () => 23,
|
|
590
|
+
});
|
|
591
|
+
expect(await inside.runOnce()).toBe(true);
|
|
592
|
+
expect(processMessageCalls).toHaveLength(1);
|
|
593
|
+
|
|
594
|
+
processMessageCalls.length = 0;
|
|
595
|
+
createdConversations.length = 0;
|
|
596
|
+
|
|
597
|
+
// 06:00 UTC is 23:00 Pacific, outside 8-22, even though 6 is inside
|
|
598
|
+
// a naive UTC 8-22 window.
|
|
599
|
+
const outside = createService({
|
|
600
|
+
now: () => new Date("2026-09-09T06:00:00Z"),
|
|
601
|
+
getCurrentHour: () => 6,
|
|
602
|
+
});
|
|
603
|
+
expect(await outside.runOnce()).toBe(false);
|
|
604
|
+
expect(processMessageCalls).toHaveLength(0);
|
|
605
|
+
});
|
|
564
606
|
|
|
565
607
|
test("overlap prevention works", async () => {
|
|
566
608
|
let resolveFirst: () => void;
|
|
@@ -1307,26 +1349,25 @@ describe("HeartbeatService", () => {
|
|
|
1307
1349
|
test("active hours guard uses cron timezone when configured", async () => {
|
|
1308
1350
|
setHeartbeatConfig({
|
|
1309
1351
|
cronExpression: "0 9,12,15,18 * * *",
|
|
1310
|
-
timezone: "
|
|
1311
|
-
activeHoursStart:
|
|
1312
|
-
activeHoursEnd:
|
|
1352
|
+
timezone: "America/Los_Angeles",
|
|
1353
|
+
activeHoursStart: 8,
|
|
1354
|
+
activeHoursEnd: 22,
|
|
1313
1355
|
});
|
|
1314
1356
|
mockComputeNextRunAtResult = Date.now() + 3_600_000;
|
|
1315
1357
|
|
|
1316
|
-
|
|
1358
|
+
// 23:00 UTC = 16:00 PDT, inside 8-22 Pacific.
|
|
1359
|
+
const service = createService({
|
|
1360
|
+
now: () => new Date("2026-09-08T23:00:00Z"),
|
|
1361
|
+
getCurrentHour: () => 23,
|
|
1362
|
+
});
|
|
1317
1363
|
service.start();
|
|
1318
|
-
|
|
1319
|
-
// In cron mode with timezone, the hour is computed via Intl.DateTimeFormat
|
|
1320
|
-
// rather than getCurrentHour(). The test verifies the code path runs without
|
|
1321
|
-
// error — the actual hour depends on the system clock and UTC conversion.
|
|
1322
|
-
// We just verify it doesn't throw and returns a boolean result.
|
|
1323
1364
|
const result = await service.runOnce();
|
|
1324
|
-
|
|
1325
|
-
expect(
|
|
1365
|
+
expect(result).toBe(true);
|
|
1366
|
+
expect(processMessageCalls).toHaveLength(1);
|
|
1326
1367
|
service.stop();
|
|
1327
1368
|
});
|
|
1328
1369
|
|
|
1329
|
-
test("active hours guard falls back to getCurrentHour when
|
|
1370
|
+
test("active hours guard falls back to getCurrentHour when no timezone is known", async () => {
|
|
1330
1371
|
setHeartbeatConfig({
|
|
1331
1372
|
cronExpression: "0 9,12,15,18 * * *",
|
|
1332
1373
|
timezone: null,
|
|
@@ -1705,3 +1746,53 @@ describe("HeartbeatService", () => {
|
|
|
1705
1746
|
});
|
|
1706
1747
|
});
|
|
1707
1748
|
});
|
|
1749
|
+
|
|
1750
|
+
describe("shouldRescheduleHeartbeatForTimezoneChange", () => {
|
|
1751
|
+
test("reschedules cron heartbeats when the fallback user timezone changes", () => {
|
|
1752
|
+
expect(
|
|
1753
|
+
shouldRescheduleHeartbeatForTimezoneChange(
|
|
1754
|
+
{
|
|
1755
|
+
ui: { userTimezone: "America/New_York" },
|
|
1756
|
+
heartbeat: { cronExpression: "0 9 * * *" },
|
|
1757
|
+
},
|
|
1758
|
+
{
|
|
1759
|
+
ui: { userTimezone: "America/Los_Angeles" },
|
|
1760
|
+
heartbeat: { cronExpression: "0 9 * * *" },
|
|
1761
|
+
},
|
|
1762
|
+
),
|
|
1763
|
+
).toBe(true);
|
|
1764
|
+
});
|
|
1765
|
+
|
|
1766
|
+
test("ignores ui timezone changes in interval mode", () => {
|
|
1767
|
+
expect(
|
|
1768
|
+
shouldRescheduleHeartbeatForTimezoneChange(
|
|
1769
|
+
{ ui: { userTimezone: "America/New_York" }, heartbeat: {} },
|
|
1770
|
+
{
|
|
1771
|
+
ui: { userTimezone: "America/Los_Angeles" },
|
|
1772
|
+
heartbeat: {},
|
|
1773
|
+
},
|
|
1774
|
+
),
|
|
1775
|
+
).toBe(false);
|
|
1776
|
+
});
|
|
1777
|
+
|
|
1778
|
+
test("ignores ui timezone changes when heartbeat.timezone is set", () => {
|
|
1779
|
+
expect(
|
|
1780
|
+
shouldRescheduleHeartbeatForTimezoneChange(
|
|
1781
|
+
{
|
|
1782
|
+
ui: { userTimezone: "America/New_York" },
|
|
1783
|
+
heartbeat: {
|
|
1784
|
+
cronExpression: "0 9 * * *",
|
|
1785
|
+
timezone: "UTC",
|
|
1786
|
+
},
|
|
1787
|
+
},
|
|
1788
|
+
{
|
|
1789
|
+
ui: { userTimezone: "America/Los_Angeles" },
|
|
1790
|
+
heartbeat: {
|
|
1791
|
+
cronExpression: "0 9 * * *",
|
|
1792
|
+
timezone: "UTC",
|
|
1793
|
+
},
|
|
1794
|
+
},
|
|
1795
|
+
),
|
|
1796
|
+
).toBe(false);
|
|
1797
|
+
});
|
|
1798
|
+
});
|
|
@@ -1398,7 +1398,7 @@ describe("HostAppControlProxy", () => {
|
|
|
1398
1398
|
proxy.dispose();
|
|
1399
1399
|
});
|
|
1400
1400
|
|
|
1401
|
-
test("request() without targetClientId:
|
|
1401
|
+
test("request() without targetClientId: registers the source actor principal", async () => {
|
|
1402
1402
|
const proxy = new HostAppControlProxy("conv-1");
|
|
1403
1403
|
const ctrl = new AbortController();
|
|
1404
1404
|
|
|
@@ -1413,7 +1413,7 @@ describe("HostAppControlProxy", () => {
|
|
|
1413
1413
|
|
|
1414
1414
|
const sent = sentMessages[0] as Record<string, unknown>;
|
|
1415
1415
|
expect(registeredInteractions[0].targetClientId).toBeUndefined();
|
|
1416
|
-
expect(registeredInteractions[0].targetActorPrincipalId).
|
|
1416
|
+
expect(registeredInteractions[0].targetActorPrincipalId).toBe("user-1");
|
|
1417
1417
|
|
|
1418
1418
|
proxy.resolve(sent.requestId as string, payload({ pngBase64: PNG_A }));
|
|
1419
1419
|
await resultPromise;
|
|
@@ -405,6 +405,47 @@ describe("handleHostAppControlResult — same-actor guard", () => {
|
|
|
405
405
|
expect(resolveCalls).toHaveLength(1);
|
|
406
406
|
});
|
|
407
407
|
|
|
408
|
+
test("non-targeted with recorded source actor: rejects a different principal", () => {
|
|
409
|
+
const requestId = "ac-req-untargeted-actor-mismatch";
|
|
410
|
+
const conversationId = "conv-untargeted-actor";
|
|
411
|
+
pending.set(requestId, {
|
|
412
|
+
conversationId,
|
|
413
|
+
kind: "host_app_control",
|
|
414
|
+
targetActorPrincipalId: "user-owner",
|
|
415
|
+
});
|
|
416
|
+
const resolveCalls: Array<{ requestId: string; payload: unknown }> = [];
|
|
417
|
+
setupConversation(conversationId, resolveCalls);
|
|
418
|
+
|
|
419
|
+
expect(() =>
|
|
420
|
+
handleHostAppControlResult({
|
|
421
|
+
body: { requestId, state: "running" },
|
|
422
|
+
headers: { "x-vellum-actor-principal-id": "user-attacker" },
|
|
423
|
+
}),
|
|
424
|
+
).toThrow(ForbiddenError);
|
|
425
|
+
expect(resolveCalls).toHaveLength(0);
|
|
426
|
+
expect(pending.has(requestId)).toBe(true);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test("non-targeted with recorded source actor: accepts the matching principal", async () => {
|
|
430
|
+
const requestId = "ac-req-untargeted-actor-match";
|
|
431
|
+
const conversationId = "conv-untargeted-actor-match";
|
|
432
|
+
pending.set(requestId, {
|
|
433
|
+
conversationId,
|
|
434
|
+
kind: "host_app_control",
|
|
435
|
+
targetActorPrincipalId: "user-owner",
|
|
436
|
+
});
|
|
437
|
+
const resolveCalls: Array<{ requestId: string; payload: unknown }> = [];
|
|
438
|
+
setupConversation(conversationId, resolveCalls);
|
|
439
|
+
|
|
440
|
+
const result = await handleHostAppControlResult({
|
|
441
|
+
body: { requestId, state: "running" },
|
|
442
|
+
headers: { "x-vellum-actor-principal-id": "user-owner" },
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
expect(result).toEqual({ accepted: true });
|
|
446
|
+
expect(resolveCalls).toHaveLength(1);
|
|
447
|
+
});
|
|
448
|
+
|
|
408
449
|
// ── Targeted + correct headers → 200 ─────────────────────────────────
|
|
409
450
|
|
|
410
451
|
test("targeted + matching client id and actor principal: returns 200", async () => {
|
|
@@ -381,7 +381,7 @@ describe("handleHostBashResult", () => {
|
|
|
381
381
|
|
|
382
382
|
// ── Untargeted-request behavior (regression for new check) ─────────
|
|
383
383
|
|
|
384
|
-
describe("untargeted request
|
|
384
|
+
describe("untargeted request: source actor still binds when recorded", () => {
|
|
385
385
|
test("accepts even when submitting actor is absent and no target client is set", async () => {
|
|
386
386
|
const requestId = "req-untargeted-no-actor";
|
|
387
387
|
registerPending(requestId);
|
|
@@ -395,6 +395,35 @@ describe("handleHostBashResult", () => {
|
|
|
395
395
|
expect(resolveSpy).toHaveLength(1);
|
|
396
396
|
expect(resolvedIds).toContain(requestId);
|
|
397
397
|
});
|
|
398
|
+
|
|
399
|
+
test("rejects a different actor when the pending request recorded a source actor", () => {
|
|
400
|
+
const requestId = "req-untargeted-actor-mismatch";
|
|
401
|
+
registerPending(requestId, {
|
|
402
|
+
targetActorPrincipalId: "principal-owner",
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
expect(() =>
|
|
406
|
+
handleHostBashResult({
|
|
407
|
+
body: bashBody(requestId),
|
|
408
|
+
headers: { "x-vellum-actor-principal-id": "principal-attacker" },
|
|
409
|
+
}),
|
|
410
|
+
).toThrow(ForbiddenError);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test("accepts the recorded source actor on an untargeted pending request", async () => {
|
|
414
|
+
const requestId = "req-untargeted-actor-match";
|
|
415
|
+
registerPending(requestId, {
|
|
416
|
+
targetActorPrincipalId: "principal-owner",
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
const result = await handleHostBashResult({
|
|
420
|
+
body: bashBody(requestId),
|
|
421
|
+
headers: { "x-vellum-actor-principal-id": "principal-owner" },
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
expect(result).toEqual({ accepted: true });
|
|
425
|
+
expect(resolveSpy).toHaveLength(1);
|
|
426
|
+
});
|
|
398
427
|
});
|
|
399
428
|
|
|
400
429
|
// ── Error: missing header on targeted request ──────────────────────
|
|
@@ -421,5 +421,39 @@ describe("handleHostBrowserResult — same-actor guard", () => {
|
|
|
421
421
|
expect(result).toEqual({ accepted: true });
|
|
422
422
|
expect(pendingInteractions.get(requestId)).toBeUndefined();
|
|
423
423
|
});
|
|
424
|
+
|
|
425
|
+
test("rejects a different actor when the pending request recorded a source actor", () => {
|
|
426
|
+
const requestId = "browser-req-untargeted-actor-mismatch";
|
|
427
|
+
pendingInteractions.register(requestId, {
|
|
428
|
+
conversationId: "conv-1",
|
|
429
|
+
kind: "host_browser",
|
|
430
|
+
targetActorPrincipalId: "user-owner",
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
expect(() =>
|
|
434
|
+
handleHostBrowserResult({
|
|
435
|
+
body: { requestId, content: "ok", isError: false },
|
|
436
|
+
headers: { "x-vellum-actor-principal-id": "user-attacker" },
|
|
437
|
+
}),
|
|
438
|
+
).toThrow(ForbiddenError);
|
|
439
|
+
expect(pendingInteractions.get(requestId)).toBeDefined();
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test("accepts the recorded source actor on an untargeted pending request", async () => {
|
|
443
|
+
const requestId = "browser-req-untargeted-actor-match";
|
|
444
|
+
pendingInteractions.register(requestId, {
|
|
445
|
+
conversationId: "conv-1",
|
|
446
|
+
kind: "host_browser",
|
|
447
|
+
targetActorPrincipalId: "user-owner",
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
const result = await handleHostBrowserResult({
|
|
451
|
+
body: { requestId, content: "ok", isError: false },
|
|
452
|
+
headers: { "x-vellum-actor-principal-id": "user-owner" },
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
expect(result).toEqual({ accepted: true });
|
|
456
|
+
expect(pendingInteractions.get(requestId)).toBeUndefined();
|
|
457
|
+
});
|
|
424
458
|
});
|
|
425
459
|
});
|
|
@@ -417,6 +417,35 @@ describe("handleHostCuResult — Phase 2 targetClientId guard", () => {
|
|
|
417
417
|
expect(result).toEqual({ accepted: true });
|
|
418
418
|
expect(cuResolveSpy).toHaveLength(1);
|
|
419
419
|
});
|
|
420
|
+
|
|
421
|
+
test("rejects a different actor when the pending request recorded a source actor", () => {
|
|
422
|
+
const requestId = "req-cu-untargeted-actor-mismatch";
|
|
423
|
+
registerPending(requestId, {
|
|
424
|
+
targetActorPrincipalId: "principal-owner",
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
expect(() =>
|
|
428
|
+
handleHostCuResult({
|
|
429
|
+
body: cuBody(requestId),
|
|
430
|
+
headers: { "x-vellum-actor-principal-id": "principal-attacker" },
|
|
431
|
+
}),
|
|
432
|
+
).toThrow(ForbiddenError);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
test("accepts the recorded source actor on an untargeted pending request", async () => {
|
|
436
|
+
const requestId = "req-cu-untargeted-actor-match";
|
|
437
|
+
registerPending(requestId, {
|
|
438
|
+
targetActorPrincipalId: "principal-owner",
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
const result = await handleHostCuResult({
|
|
442
|
+
body: cuBody(requestId),
|
|
443
|
+
headers: { "x-vellum-actor-principal-id": "principal-owner" },
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
expect(result).toEqual({ accepted: true });
|
|
447
|
+
expect(cuResolveSpy).toHaveLength(1);
|
|
448
|
+
});
|
|
420
449
|
});
|
|
421
450
|
});
|
|
422
451
|
|
|
@@ -279,6 +279,35 @@ describe("handleHostFileResult — targetClientId guard", () => {
|
|
|
279
279
|
expect(result).toEqual({ accepted: true });
|
|
280
280
|
expect(resolveSpy).toHaveLength(1);
|
|
281
281
|
});
|
|
282
|
+
|
|
283
|
+
test("rejects a different actor when the pending request recorded a source actor", () => {
|
|
284
|
+
const requestId = "req-file-untargeted-actor-mismatch";
|
|
285
|
+
registerPending(requestId, {
|
|
286
|
+
targetActorPrincipalId: "principal-owner",
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
expect(() =>
|
|
290
|
+
handleHostFileResult({
|
|
291
|
+
body: fileBody(requestId),
|
|
292
|
+
headers: { "x-vellum-actor-principal-id": "principal-attacker" },
|
|
293
|
+
}),
|
|
294
|
+
).toThrow(ForbiddenError);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("accepts the recorded source actor on an untargeted pending request", async () => {
|
|
298
|
+
const requestId = "req-file-untargeted-actor-match";
|
|
299
|
+
registerPending(requestId, {
|
|
300
|
+
targetActorPrincipalId: "principal-owner",
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const result = await handleHostFileResult({
|
|
304
|
+
body: fileBody(requestId),
|
|
305
|
+
headers: { "x-vellum-actor-principal-id": "principal-owner" },
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
expect(result).toEqual({ accepted: true });
|
|
309
|
+
expect(resolveSpy).toHaveLength(1);
|
|
310
|
+
});
|
|
282
311
|
});
|
|
283
312
|
|
|
284
313
|
// ── 5. Targeted + matching client but mismatched actor → 403 ──────────────
|
|
@@ -9,6 +9,7 @@ mock.module("../runtime/assistant-event-hub.js", () => ({
|
|
|
9
9
|
assistantEventHub: {
|
|
10
10
|
getMostRecentClientByCapability: (cap: string) =>
|
|
11
11
|
cap === "host_cu" && mockHasClient ? { id: "mock-client" } : null,
|
|
12
|
+
getActorPrincipalIdForClient: () => undefined,
|
|
12
13
|
},
|
|
13
14
|
}));
|
|
14
15
|
|