@vellumai/assistant 0.7.2 → 0.7.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/ARCHITECTURE.md +16 -1
- package/docs/architecture/memory.md +5 -2
- package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +13 -4
- package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -9
- package/node_modules/@vellumai/slack-text/src/index.test.ts +18 -35
- package/node_modules/@vellumai/slack-text/src/index.ts +2 -48
- package/openapi.yaml +449 -22
- package/package.json +1 -1
- package/src/__tests__/app-control-flow.test.ts +21 -11
- package/src/__tests__/assistant-event-hub.test.ts +48 -0
- package/src/__tests__/assistant-event.test.ts +0 -10
- package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -7
- package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -0
- package/src/__tests__/auto-analysis-end-to-end.test.ts +62 -1
- package/src/__tests__/background-workers-disk-pressure.test.ts +268 -0
- package/src/__tests__/call-conversation-messages.test.ts +8 -2
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +537 -0
- package/src/__tests__/channel-readiness-service.test.ts +4 -2
- package/src/__tests__/config-loader-backfill.test.ts +379 -0
- package/src/__tests__/config-schema.test.ts +1 -0
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +18 -9
- package/src/__tests__/config-watcher.test.ts +140 -69
- package/src/__tests__/context-search-agent-runner.test.ts +61 -3
- package/src/__tests__/context-search-conversations-source.test.ts +0 -24
- package/src/__tests__/context-search-fanout.test.ts +0 -1
- package/src/__tests__/context-search-memory-source.test.ts +3 -7
- package/src/__tests__/context-search-memory-v2-source.test.ts +0 -2
- package/src/__tests__/context-search-pkb-source.test.ts +0 -1
- package/src/__tests__/context-search-workspace-source.test.ts +0 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +6 -0
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +223 -0
- package/src/__tests__/conversation-agent-loop.test.ts +454 -5
- package/src/__tests__/conversation-error.test.ts +150 -3
- package/src/__tests__/conversation-process-callsite.test.ts +43 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +6 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +65 -0
- package/src/__tests__/conversation-slash-unknown.test.ts +6 -0
- package/src/__tests__/conversation-speed-override.test.ts +0 -3
- package/src/__tests__/conversation-store.test.ts +0 -18
- package/src/__tests__/conversation-surfaces-app-control.test.ts +15 -4
- package/src/__tests__/conversation-surfaces-data-persist.test.ts +404 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +2 -5
- package/src/__tests__/conversation-workspace-injection.test.ts +6 -0
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +6 -0
- package/src/__tests__/credentials-cli.test.ts +7 -0
- package/src/__tests__/cu-unified-flow.test.ts +176 -10
- package/src/__tests__/date-context.test.ts +164 -2
- package/src/__tests__/disk-pressure-guard.test.ts +262 -0
- package/src/__tests__/disk-pressure-lifecycle.test.ts +168 -0
- package/src/__tests__/disk-pressure-policy.test.ts +241 -0
- package/src/__tests__/disk-pressure-routes.test.ts +379 -0
- package/src/__tests__/disk-pressure-tools.test.ts +277 -0
- package/src/__tests__/disk-usage.test.ts +150 -0
- package/src/__tests__/events-client-registration.test.ts +52 -0
- package/src/__tests__/events-dev-bypass-actor.test.ts +162 -0
- package/src/__tests__/file-write-tool.test.ts +4 -10
- package/src/__tests__/filing-service.test.ts +3 -4
- package/src/__tests__/heartbeat-disk-pressure.test.ts +183 -0
- package/src/__tests__/heartbeat-service.test.ts +260 -11
- package/src/__tests__/host-app-control-proxy.test.ts +195 -25
- package/src/__tests__/host-bash-proxy.test.ts +227 -34
- package/src/__tests__/host-bash-routes.test.ts +178 -13
- package/src/__tests__/host-cu-proxy.test.ts +210 -3
- package/src/__tests__/host-cu-routes-targeted.test.ts +141 -12
- package/src/__tests__/host-file-proxy-targeted.test.ts +48 -9
- package/src/__tests__/host-file-proxy.test.ts +268 -6
- package/src/__tests__/host-file-routes-targeted.test.ts +175 -17
- package/src/__tests__/host-transfer-proxy-targeted.test.ts +408 -59
- package/src/__tests__/host-transfer-routes-targeted.test.ts +232 -17
- package/src/__tests__/http-user-message-parity.test.ts +107 -1
- package/src/__tests__/injector-chain.test.ts +18 -6
- package/src/__tests__/injector-disk-pressure.test.ts +224 -0
- package/src/__tests__/managed-profile-guard.test.ts +18 -0
- package/src/__tests__/mcp-abort-signal.test.ts +130 -0
- package/src/__tests__/memory-admin-recall.test.ts +3 -11
- package/src/__tests__/memory-retrieval-pipeline.test.ts +22 -1
- package/src/__tests__/normalize-onboarding.test.ts +180 -0
- package/src/__tests__/oauth-connect-routes.test.ts +316 -0
- package/src/__tests__/oauth-provider-seed-logos.test.ts +24 -2
- package/src/__tests__/onboarding-persona-write.test.ts +308 -0
- package/src/__tests__/openai-provider.test.ts +45 -8
- package/src/__tests__/persist-onboarding-artifacts.test.ts +44 -64
- package/src/__tests__/platform-callback-registration.test.ts +21 -4
- package/src/__tests__/platform.test.ts +2 -1
- package/src/__tests__/playbook-execution.test.ts +0 -43
- package/src/__tests__/plugin-tool-contribution.test.ts +47 -0
- package/src/__tests__/prechat-onboarding-contract.test.ts +214 -27
- package/src/__tests__/provider-tool-name.test.ts +23 -0
- package/src/__tests__/relay-server.test.ts +15 -4
- package/src/__tests__/runtime-events-sse.test.ts +4 -8
- package/src/__tests__/scheduler-disk-pressure.test.ts +148 -0
- package/src/__tests__/secret-ingress-http.test.ts +0 -1
- package/src/__tests__/suggestion-routes.test.ts +46 -0
- package/src/__tests__/twilio-validation.test.ts +2 -2
- package/src/__tests__/workspace-migration-065-bump-stale-heartbeat-interval.test.ts +122 -0
- package/src/__tests__/workspace-migration-066-seed-heartbeat-callsite-cost-default.test.ts +285 -0
- package/src/__tests__/workspace-migration-068-release-notes-local-timezone.test.ts +90 -0
- package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +90 -0
- package/src/approvals/guardian-decision-primitive.ts +13 -0
- package/src/approvals/guardian-request-resolvers.ts +16 -17
- package/src/backup/snapshot-lock.ts +2 -27
- package/src/bundler/compiler-tools.ts +3 -2
- package/src/calls/call-conversation-messages.ts +46 -10
- package/src/cli/commands/__tests__/webhooks.test.ts +0 -4
- package/src/cli/commands/bash.ts +35 -108
- package/src/cli/commands/contacts.ts +64 -25
- package/src/cli/commands/credentials.ts +56 -0
- package/src/cli/commands/memory-v2.ts +7 -6
- package/src/cli/commands/oauth/__tests__/connect.test.ts +437 -1
- package/src/cli/commands/oauth/connect.ts +127 -1
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -3
- package/src/cli/commands/platform/__tests__/connect.test.ts +7 -1
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +103 -6
- package/src/cli/commands/platform/index.ts +16 -7
- package/src/cli/commands/status.ts +57 -0
- package/src/cli/program.ts +4 -2
- package/src/config/assistant-feature-flags.ts +13 -3
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -3
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +13 -7
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -2
- package/src/config/env.ts +0 -8
- package/src/config/feature-flag-registry.json +27 -3
- package/src/config/loader.ts +127 -8
- package/src/config/schemas/__tests__/memory-v2.test.ts +10 -5
- package/src/config/schemas/call-site-catalog.ts +14 -0
- package/src/config/schemas/channels.ts +0 -5
- package/src/config/schemas/heartbeat.ts +1 -1
- package/src/config/schemas/llm.ts +2 -0
- package/src/config/schemas/memory-lifecycle.ts +13 -0
- package/src/config/schemas/memory-v2.ts +75 -11
- package/src/config/schemas/platform.ts +43 -3
- package/src/config/schemas/services.ts +28 -0
- package/src/config/seed-inference-profiles.ts +230 -33
- package/src/contacts/contact-store.ts +0 -25
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +86 -25
- package/src/daemon/assistant-attachments.ts +4 -4
- package/src/daemon/config-watcher.ts +85 -57
- package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
- package/src/daemon/conversation-agent-loop.ts +170 -33
- package/src/daemon/conversation-error.ts +87 -15
- package/src/daemon/conversation-lifecycle.ts +1 -3
- package/src/daemon/conversation-process.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +26 -0
- package/src/daemon/conversation-store.ts +2 -2
- package/src/daemon/conversation-surfaces.ts +195 -15
- package/src/daemon/conversation-tool-setup.ts +57 -14
- package/src/daemon/conversation.ts +17 -22
- package/src/daemon/date-context.ts +71 -22
- package/src/daemon/disk-pressure-background-gate.ts +73 -0
- package/src/daemon/disk-pressure-guard.ts +343 -0
- package/src/daemon/disk-pressure-policy.ts +163 -0
- package/src/daemon/handlers/shared.ts +0 -1
- package/src/daemon/handlers/skills.ts +3 -4
- package/src/daemon/host-app-control-proxy.ts +137 -41
- package/src/daemon/host-bash-proxy.ts +46 -21
- package/src/daemon/host-cu-proxy.ts +49 -3
- package/src/daemon/host-file-proxy.ts +43 -7
- package/src/daemon/host-transfer-proxy.ts +95 -4
- package/src/daemon/lifecycle.ts +79 -28
- package/src/daemon/meet-host-supervisor.ts +4 -4
- package/src/daemon/meet-manifest-loader.ts +0 -1
- package/src/daemon/memory-v2-startup.ts +14 -4
- package/src/daemon/message-protocol.ts +3 -0
- package/src/daemon/message-types/conversations.ts +4 -0
- package/src/daemon/message-types/disk-pressure.ts +9 -0
- package/src/daemon/message-types/messages.ts +3 -0
- package/src/daemon/profiler-run-store.ts +5 -5
- package/src/daemon/tool-setup-types.ts +2 -2
- package/src/documents/document-store.ts +85 -0
- package/src/filing/filing-service.ts +30 -5
- package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +9 -16
- package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +36 -0
- package/src/heartbeat/heartbeat-run-store.ts +13 -0
- package/src/heartbeat/heartbeat-service.ts +205 -31
- package/src/home/feed-scheduler.ts +18 -0
- package/src/inbound/platform-callback-registration.ts +8 -15
- package/src/ipc/__tests__/clients-list-ipc.test.ts +169 -0
- package/src/ipc/assistant-server.ts +56 -2
- package/src/ipc/gateway-client.ts +37 -3
- package/src/live-voice/live-voice-archive.ts +4 -4
- package/src/live-voice/protocol.ts +5 -7
- package/src/media/image-service.ts +1 -7
- package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +21 -13
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +52 -22
- package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +0 -6
- package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +272 -0
- package/src/memory/admin.ts +5 -9
- package/src/memory/context-search/agent-runner.ts +19 -2
- package/src/memory/context-search/sources/conversations.ts +2 -11
- package/src/memory/context-search/sources/memory-v2.ts +5 -4
- package/src/memory/context-search/sources/memory.ts +0 -1
- package/src/memory/context-search/types.ts +0 -1
- package/src/memory/conversation-crud.ts +4 -12
- package/src/memory/db-init.ts +2 -0
- package/src/memory/embedding-runtime-manager.ts +119 -5
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +32 -21
- package/src/memory/graph/conversation-graph-memory.ts +42 -54
- package/src/memory/graph/extraction.ts +1 -3
- package/src/memory/graph/graph-search.test.ts +10 -67
- package/src/memory/graph/graph-search.ts +1 -20
- package/src/memory/graph/retriever.test.ts +6 -0
- package/src/memory/graph/retriever.ts +6 -10
- package/src/memory/indexer.ts +54 -45
- package/src/memory/job-handlers/backfill.ts +2 -11
- package/src/memory/job-handlers/cleanup.ts +43 -0
- package/src/memory/job-handlers/embedding.ts +6 -8
- package/src/memory/job-handlers/summarization.ts +2 -7
- package/src/memory/jobs-store.ts +48 -0
- package/src/memory/jobs-worker.ts +81 -43
- package/src/memory/memory-v2-activation-log-store.ts +32 -14
- package/src/memory/memory-v2-concept-frequency.ts +169 -0
- package/src/memory/migrations/239-trace-events-created-at-index.ts +18 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/pkb/pkb-search.test.ts +6 -0
- package/src/memory/qdrant-client.ts +0 -13
- package/src/memory/rerank-local.ts +374 -0
- package/src/memory/search/semantic.ts +6 -67
- package/src/memory/trace-event-store.ts +1 -17
- package/src/memory/v2/__tests__/activation.test.ts +311 -250
- package/src/memory/v2/__tests__/consolidation-job.test.ts +40 -8
- package/src/memory/v2/__tests__/injection.test.ts +157 -167
- package/src/memory/v2/__tests__/prompts-consolidation.test.ts +61 -2
- package/src/memory/v2/__tests__/qdrant.test.ts +16 -0
- package/src/memory/v2/__tests__/reranker.test.ts +338 -0
- package/src/memory/v2/__tests__/sim.test.ts +5 -199
- package/src/memory/v2/__tests__/skill-store.test.ts +71 -65
- package/src/memory/v2/__tests__/static-context.test.ts +76 -1
- package/src/memory/v2/activation.ts +149 -156
- package/src/memory/v2/consolidation-job.ts +62 -12
- package/src/memory/v2/injection.ts +47 -60
- package/src/memory/v2/prompts/consolidation.ts +36 -1
- package/src/memory/v2/qdrant.ts +99 -0
- package/src/memory/v2/reranker.ts +177 -0
- package/src/memory/v2/sim.ts +10 -84
- package/src/memory/v2/skill-content.ts +4 -3
- package/src/memory/v2/skill-store.ts +82 -59
- package/src/memory/v2/static-context.ts +22 -0
- package/src/memory/v2/types.ts +10 -10
- package/src/notifications/copy-composer.ts +13 -0
- package/src/notifications/signal.ts +4 -0
- package/src/oauth/AGENTS.md +3 -1
- package/src/oauth/__tests__/oauth-connect-state.test.ts +137 -0
- package/src/oauth/connect-orchestrator.ts +2 -0
- package/src/oauth/connection-resolver.test.ts +66 -1
- package/src/oauth/connection-resolver.ts +55 -1
- package/src/oauth/oauth-connect-state.ts +77 -0
- package/src/oauth/seed-providers.ts +58 -1
- package/src/plugins/defaults/injectors.ts +35 -2
- package/src/plugins/defaults/memory-retrieval.ts +5 -6
- package/src/plugins/types.ts +7 -0
- package/src/proactive-artifact/aux-message-injector.ts +74 -0
- package/src/proactive-artifact/decision.test.ts +226 -0
- package/src/proactive-artifact/decision.ts +165 -0
- package/src/proactive-artifact/index.ts +7 -0
- package/src/proactive-artifact/job.test.ts +867 -0
- package/src/proactive-artifact/job.ts +352 -0
- package/src/proactive-artifact/message-copy.ts +41 -0
- package/src/proactive-artifact/trigger-state.test.ts +277 -0
- package/src/proactive-artifact/trigger-state.ts +119 -0
- package/src/prompts/normalize-onboarding.ts +80 -0
- package/src/prompts/persona-resolver.ts +101 -9
- package/src/prompts/system-prompt.ts +21 -7
- package/src/prompts/templates/BOOTSTRAP.md +13 -5
- package/src/providers/__tests__/retry-callsite.test.ts +222 -1
- package/src/providers/model-intents.ts +7 -0
- package/src/providers/openrouter/client.ts +8 -0
- package/src/providers/retry.ts +50 -0
- package/src/providers/types.ts +1 -0
- package/src/runtime/__tests__/agent-wake.test.ts +456 -3
- package/src/runtime/agent-wake.ts +238 -100
- package/src/runtime/assistant-event-hub.ts +36 -6
- package/src/runtime/assistant-event.ts +0 -1
- package/src/runtime/auth/__tests__/route-policy.test.ts +64 -0
- package/src/runtime/auth/route-policy.ts +14 -1
- package/src/runtime/auth/same-actor.ts +216 -0
- package/src/runtime/channel-retry-sweep.ts +65 -1
- package/src/runtime/guardian-reply-router.ts +10 -0
- package/src/runtime/local-actor-identity.ts +52 -11
- package/src/runtime/pending-interactions.ts +8 -0
- package/src/runtime/routes/__tests__/client-routes.test.ts +155 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +0 -5
- package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
- package/src/runtime/routes/client-routes.ts +20 -2
- package/src/runtime/routes/contact-routes.ts +0 -25
- package/src/runtime/routes/conversation-routes.ts +35 -26
- package/src/runtime/routes/debug-bash-routes.ts +163 -0
- package/src/runtime/routes/disk-pressure-routes.ts +121 -0
- package/src/runtime/routes/document-pdf-renderer.ts +6 -2
- package/src/runtime/routes/documents-routes.ts +2 -75
- package/src/runtime/routes/events-routes.ts +41 -9
- package/src/runtime/routes/host-bash-routes.ts +23 -3
- package/src/runtime/routes/host-cu-routes.ts +33 -6
- package/src/runtime/routes/host-file-routes.ts +32 -6
- package/src/runtime/routes/host-transfer-routes.ts +79 -16
- package/src/runtime/routes/identity-routes.ts +7 -138
- package/src/runtime/routes/inbound-message-handler.ts +77 -12
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -0
- package/src/runtime/routes/index.ts +6 -0
- package/src/runtime/routes/memory-item-routes.test.ts +41 -15
- package/src/runtime/routes/memory-v2-routes.ts +33 -0
- package/src/runtime/routes/oauth-connect-routes.ts +153 -0
- package/src/runtime/verification-outbound-actions.ts +4 -4
- package/src/schedule/run-script.ts +37 -5
- package/src/schedule/scheduler.ts +20 -1
- package/src/security/encrypted-store.ts +2 -0
- package/src/security/secure-keys.ts +55 -0
- package/src/skills/remote-skill-policy.ts +4 -10
- package/src/subagent/index.ts +1 -7
- package/src/subagent/manager.ts +1 -15
- package/src/tasks/task-runner.ts +0 -1
- package/src/tasks/task-store.ts +0 -3
- package/src/tools/background-tool-registry.ts +17 -3
- package/src/tools/host-filesystem/edit.test.ts +151 -0
- package/src/tools/host-filesystem/edit.ts +43 -1
- package/src/tools/host-filesystem/read.test.ts +129 -0
- package/src/tools/host-filesystem/read.ts +43 -1
- package/src/tools/host-filesystem/transfer.test.ts +127 -2
- package/src/tools/host-filesystem/transfer.ts +56 -11
- package/src/tools/host-filesystem/write.test.ts +134 -0
- package/src/tools/host-filesystem/write.ts +43 -1
- package/src/tools/host-terminal/host-shell.ts +13 -6
- package/src/tools/mcp/mcp-tool-factory.ts +2 -1
- package/src/tools/memory/register.test.ts +12 -9
- package/src/tools/memory/register.ts +1 -2
- package/src/tools/provider-tool-name.ts +28 -0
- package/src/tools/registry.ts +30 -9
- package/src/tools/terminal/shell.ts +9 -1
- package/src/tools/tool-approval-handler.ts +31 -6
- package/src/tools/types.ts +24 -2
- package/src/tts/provider-catalog.ts +3 -5
- package/src/util/disk-usage.ts +138 -0
- package/src/util/platform.ts +21 -11
- package/src/util/process-liveness.ts +26 -0
- package/src/workspace/heartbeat-service.ts +19 -0
- package/src/workspace/migrations/065-bump-stale-heartbeat-interval.ts +60 -0
- package/src/workspace/migrations/066-seed-heartbeat-callsite-cost-default.ts +146 -0
- package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +72 -0
- package/src/workspace/migrations/068-release-notes-local-timezone.ts +65 -0
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +0 -167
- package/src/memory/v2/__tests__/skill-qdrant.test.ts +0 -657
- package/src/memory/v2/skill-qdrant.ts +0 -404
- package/src/signals/bash.ts +0 -198
|
@@ -9,8 +9,20 @@ import { afterEach, describe, expect, mock, test } from "bun:test";
|
|
|
9
9
|
|
|
10
10
|
const sentMessages: unknown[] = [];
|
|
11
11
|
let mockHasClient = true; // Default to true for CU unified flow tests
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
// Default principal id used for both ctx.trustContext and clients in the
|
|
13
|
+
// existing single-user tests. Tests that exercise cross-user behaviour
|
|
14
|
+
// override this on individual clients and on the SurfaceConversationContext.
|
|
15
|
+
const DEFAULT_PRINCIPAL = "user-1";
|
|
16
|
+
let mockCuClients: Array<{
|
|
17
|
+
clientId: string;
|
|
18
|
+
capabilities: string[];
|
|
19
|
+
actorPrincipalId?: string;
|
|
20
|
+
}> = [
|
|
21
|
+
{
|
|
22
|
+
clientId: "mock-client-1",
|
|
23
|
+
capabilities: ["host_cu"],
|
|
24
|
+
actorPrincipalId: DEFAULT_PRINCIPAL,
|
|
25
|
+
},
|
|
14
26
|
];
|
|
15
27
|
|
|
16
28
|
mock.module("../runtime/assistant-event-hub.js", () => ({
|
|
@@ -22,6 +34,8 @@ mock.module("../runtime/assistant-event-hub.js", () => ({
|
|
|
22
34
|
cap === "host_cu" ? mockCuClients : [],
|
|
23
35
|
getClientById: (id: string) =>
|
|
24
36
|
mockCuClients.find((c) => c.clientId === id) ?? null,
|
|
37
|
+
getActorPrincipalIdForClient: (id: string) =>
|
|
38
|
+
mockCuClients.find((c) => c.clientId === id)?.actorPrincipalId,
|
|
25
39
|
},
|
|
26
40
|
}));
|
|
27
41
|
|
|
@@ -38,12 +52,25 @@ type SurfaceConversationContext =
|
|
|
38
52
|
/**
|
|
39
53
|
* Build a minimal SurfaceConversationContext with optional hostCuProxy.
|
|
40
54
|
* Only the fields required by the CU routing path are populated.
|
|
55
|
+
*
|
|
56
|
+
* `trustContext` defaults to a guardian context owned by `DEFAULT_PRINCIPAL`.
|
|
57
|
+
* Pass `null` to omit the field entirely (used to verify same-user
|
|
58
|
+
* enforcement when the conversation has no source actor principal).
|
|
41
59
|
*/
|
|
42
60
|
function buildMockContext(
|
|
43
61
|
hostCuProxy?: InstanceType<typeof HostCuProxy>,
|
|
62
|
+
trustGuardianPrincipalId: string | null = DEFAULT_PRINCIPAL,
|
|
44
63
|
): SurfaceConversationContext {
|
|
45
64
|
return {
|
|
46
65
|
conversationId: "test-session",
|
|
66
|
+
trustContext:
|
|
67
|
+
trustGuardianPrincipalId != null
|
|
68
|
+
? {
|
|
69
|
+
sourceChannel: "vellum",
|
|
70
|
+
trustClass: "guardian",
|
|
71
|
+
guardianPrincipalId: trustGuardianPrincipalId,
|
|
72
|
+
}
|
|
73
|
+
: undefined,
|
|
47
74
|
traceEmitter: { emit: () => {} },
|
|
48
75
|
sendToClient: () => {},
|
|
49
76
|
pendingSurfaceActions: new Map(),
|
|
@@ -72,7 +99,13 @@ describe("surfaceProxyResolver — CU tool routing", () => {
|
|
|
72
99
|
function setupProxy(maxSteps?: number): SurfaceConversationContext {
|
|
73
100
|
sentMessages.length = 0;
|
|
74
101
|
mockHasClient = true;
|
|
75
|
-
mockCuClients = [
|
|
102
|
+
mockCuClients = [
|
|
103
|
+
{
|
|
104
|
+
clientId: "mock-client-1",
|
|
105
|
+
capabilities: ["host_cu"],
|
|
106
|
+
actorPrincipalId: DEFAULT_PRINCIPAL,
|
|
107
|
+
},
|
|
108
|
+
];
|
|
76
109
|
proxy = new HostCuProxy(maxSteps);
|
|
77
110
|
return buildMockContext(proxy);
|
|
78
111
|
}
|
|
@@ -375,11 +408,19 @@ describe("surfaceProxyResolver — CU tool routing", () => {
|
|
|
375
408
|
// -------------------------------------------------------------------------
|
|
376
409
|
|
|
377
410
|
describe("multi-client ambiguity guard", () => {
|
|
378
|
-
test("returns error when multiple CU clients connected and no target_client_id given", async () => {
|
|
411
|
+
test("returns error when multiple same-user CU clients connected and no target_client_id given", async () => {
|
|
379
412
|
const ctx = setupProxy();
|
|
380
413
|
mockCuClients = [
|
|
381
|
-
{
|
|
382
|
-
|
|
414
|
+
{
|
|
415
|
+
clientId: "client-a",
|
|
416
|
+
capabilities: ["host_cu"],
|
|
417
|
+
actorPrincipalId: DEFAULT_PRINCIPAL,
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
clientId: "client-b",
|
|
421
|
+
capabilities: ["host_cu"],
|
|
422
|
+
actorPrincipalId: DEFAULT_PRINCIPAL,
|
|
423
|
+
},
|
|
383
424
|
];
|
|
384
425
|
|
|
385
426
|
const result = await surfaceProxyResolver(ctx, "computer_use_click", {
|
|
@@ -397,8 +438,16 @@ describe("surfaceProxyResolver — CU tool routing", () => {
|
|
|
397
438
|
test("proceeds when multiple clients connected and target_client_id is given", async () => {
|
|
398
439
|
const ctx = setupProxy();
|
|
399
440
|
mockCuClients = [
|
|
400
|
-
{
|
|
401
|
-
|
|
441
|
+
{
|
|
442
|
+
clientId: "client-a",
|
|
443
|
+
capabilities: ["host_cu"],
|
|
444
|
+
actorPrincipalId: DEFAULT_PRINCIPAL,
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
clientId: "client-b",
|
|
448
|
+
capabilities: ["host_cu"],
|
|
449
|
+
actorPrincipalId: DEFAULT_PRINCIPAL,
|
|
450
|
+
},
|
|
402
451
|
];
|
|
403
452
|
|
|
404
453
|
const resultPromise = surfaceProxyResolver(ctx, "computer_use_click", {
|
|
@@ -508,8 +557,18 @@ describe("surfaceProxyResolver — CU tool routing", () => {
|
|
|
508
557
|
test("dispatches and records action when targetClientId is valid", async () => {
|
|
509
558
|
const ctx = setupProxy();
|
|
510
559
|
mockCuClients = [
|
|
511
|
-
{
|
|
512
|
-
|
|
560
|
+
{
|
|
561
|
+
clientId: "cu-client",
|
|
562
|
+
capabilities: ["host_cu"],
|
|
563
|
+
actorPrincipalId: DEFAULT_PRINCIPAL,
|
|
564
|
+
},
|
|
565
|
+
// Second client present to ensure target_client_id resolves
|
|
566
|
+
// unambiguously and would otherwise trip the ambiguity guard.
|
|
567
|
+
{
|
|
568
|
+
clientId: "client-b",
|
|
569
|
+
capabilities: ["host_cu"],
|
|
570
|
+
actorPrincipalId: DEFAULT_PRINCIPAL,
|
|
571
|
+
},
|
|
513
572
|
];
|
|
514
573
|
|
|
515
574
|
const resultPromise = surfaceProxyResolver(ctx, "computer_use_click", {
|
|
@@ -532,6 +591,113 @@ describe("surfaceProxyResolver — CU tool routing", () => {
|
|
|
532
591
|
});
|
|
533
592
|
});
|
|
534
593
|
|
|
594
|
+
// -------------------------------------------------------------------------
|
|
595
|
+
// Same-user enforcement (dispatch layer)
|
|
596
|
+
//
|
|
597
|
+
// The proxy enforces this internally as well — these tests verify the
|
|
598
|
+
// dispatch performs the same-user rejection before the proxy is invoked
|
|
599
|
+
// (so no step is burned and no action history mutated), and uses the
|
|
600
|
+
// canonical rejection message.
|
|
601
|
+
// -------------------------------------------------------------------------
|
|
602
|
+
|
|
603
|
+
describe("same-user enforcement", () => {
|
|
604
|
+
test("rejects targeted CU dispatch from a different actor principal", async () => {
|
|
605
|
+
sentMessages.length = 0;
|
|
606
|
+
mockHasClient = true;
|
|
607
|
+
mockCuClients = [
|
|
608
|
+
{
|
|
609
|
+
clientId: "cu-client",
|
|
610
|
+
capabilities: ["host_cu"],
|
|
611
|
+
actorPrincipalId: "user-other",
|
|
612
|
+
},
|
|
613
|
+
];
|
|
614
|
+
proxy = new HostCuProxy();
|
|
615
|
+
const ctx = buildMockContext(proxy, DEFAULT_PRINCIPAL);
|
|
616
|
+
|
|
617
|
+
const result = await surfaceProxyResolver(ctx, "computer_use_click", {
|
|
618
|
+
element_id: 1,
|
|
619
|
+
reasoning: "click",
|
|
620
|
+
target_client_id: "cu-client",
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
expect(result.isError).toBe(true);
|
|
624
|
+
expect(result.content).toContain(
|
|
625
|
+
"Submitting actor does not match the target client's actor",
|
|
626
|
+
);
|
|
627
|
+
// No state mutation, no dispatch.
|
|
628
|
+
expect(proxy.stepCount).toBe(0);
|
|
629
|
+
expect(proxy.actionHistory).toHaveLength(0);
|
|
630
|
+
expect(sentMessages).toHaveLength(0);
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
test("rejects when the conversation has no source actor principal", async () => {
|
|
634
|
+
sentMessages.length = 0;
|
|
635
|
+
mockHasClient = true;
|
|
636
|
+
mockCuClients = [
|
|
637
|
+
{
|
|
638
|
+
clientId: "cu-client",
|
|
639
|
+
capabilities: ["host_cu"],
|
|
640
|
+
actorPrincipalId: DEFAULT_PRINCIPAL,
|
|
641
|
+
},
|
|
642
|
+
];
|
|
643
|
+
proxy = new HostCuProxy();
|
|
644
|
+
const ctx = buildMockContext(proxy, null);
|
|
645
|
+
|
|
646
|
+
const result = await surfaceProxyResolver(ctx, "computer_use_click", {
|
|
647
|
+
element_id: 1,
|
|
648
|
+
reasoning: "click",
|
|
649
|
+
target_client_id: "cu-client",
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
expect(result.isError).toBe(true);
|
|
653
|
+
expect(result.content).toContain(
|
|
654
|
+
"Submitting actor does not match the target client's actor",
|
|
655
|
+
);
|
|
656
|
+
expect(sentMessages).toHaveLength(0);
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
test("auto-resolves to the unique same-user CU client when cross-user clients are present", async () => {
|
|
660
|
+
// Regression: previously the dispatch counted only same-user clients
|
|
661
|
+
// for the multi-client guard, so 1 same-user + 1 cross-user passed the
|
|
662
|
+
// guard with no targetClientId — and the proxy then broadcast to ALL
|
|
663
|
+
// host_cu subscribers, including the cross-user one.
|
|
664
|
+
sentMessages.length = 0;
|
|
665
|
+
mockHasClient = true;
|
|
666
|
+
mockCuClients = [
|
|
667
|
+
{
|
|
668
|
+
clientId: "cu-mine",
|
|
669
|
+
capabilities: ["host_cu"],
|
|
670
|
+
actorPrincipalId: DEFAULT_PRINCIPAL,
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
clientId: "cu-other",
|
|
674
|
+
capabilities: ["host_cu"],
|
|
675
|
+
actorPrincipalId: "user-other",
|
|
676
|
+
},
|
|
677
|
+
];
|
|
678
|
+
proxy = new HostCuProxy();
|
|
679
|
+
const ctx = buildMockContext(proxy, DEFAULT_PRINCIPAL);
|
|
680
|
+
|
|
681
|
+
const resultPromise = surfaceProxyResolver(ctx, "computer_use_click", {
|
|
682
|
+
element_id: 1,
|
|
683
|
+
reasoning: "click",
|
|
684
|
+
// Intentionally no target_client_id — exercises auto-resolve.
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
// Broadcast happens, but with the same-user clientId set so only
|
|
688
|
+
// that client receives it.
|
|
689
|
+
expect(sentMessages).toHaveLength(1);
|
|
690
|
+
const sent = sentMessages[0] as Record<string, unknown>;
|
|
691
|
+
expect(sent.targetClientId).toBe("cu-mine");
|
|
692
|
+
|
|
693
|
+
// Manually resolve to clean up the pending promise.
|
|
694
|
+
proxy.processObservation(sent.requestId as string, {
|
|
695
|
+
executionResult: "ok",
|
|
696
|
+
});
|
|
697
|
+
await resultPromise;
|
|
698
|
+
});
|
|
699
|
+
});
|
|
700
|
+
|
|
535
701
|
describe("step limit enforcement through resolver", () => {
|
|
536
702
|
test("rejects action tools when step limit exceeded", async () => {
|
|
537
703
|
const ctx = setupProxy(2); // maxSteps = 2
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
|
+
import { UiConfigSchema } from "../config/schemas/platform.js";
|
|
3
4
|
import {
|
|
5
|
+
canonicalizeTimeZone,
|
|
4
6
|
extractUserTimeZoneFromRecall,
|
|
5
7
|
formatTurnTimestamp,
|
|
8
|
+
resolveTurnTimezoneContext,
|
|
6
9
|
} from "../daemon/date-context.js";
|
|
7
10
|
|
|
8
11
|
// ---------------------------------------------------------------------------
|
|
@@ -85,6 +88,156 @@ describe("extractUserTimeZoneFromRecall", () => {
|
|
|
85
88
|
});
|
|
86
89
|
});
|
|
87
90
|
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// UiConfigSchema timezone fields
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
describe("UiConfigSchema timezone fields", () => {
|
|
96
|
+
test("accepts canonicalizable IANA timezone identifiers", () => {
|
|
97
|
+
const result = UiConfigSchema.parse({
|
|
98
|
+
userTimezone: "america/new_york",
|
|
99
|
+
detectedTimezone: "america/los_angeles",
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(result.userTimezone).toBe("America/New_York");
|
|
103
|
+
expect(result.detectedTimezone).toBe("America/Los_Angeles");
|
|
104
|
+
expect(UiConfigSchema.parse({ userTimezone: "UTC" }).userTimezone).toBe(
|
|
105
|
+
"UTC",
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("accepts empty-string clearing sentinels", () => {
|
|
110
|
+
const result = UiConfigSchema.parse({
|
|
111
|
+
userTimezone: "",
|
|
112
|
+
detectedTimezone: "",
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
expect(result.userTimezone).toBe("");
|
|
116
|
+
expect(result.detectedTimezone).toBe("");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("rejects invalid non-empty userTimezone and detectedTimezone values", () => {
|
|
120
|
+
expect(() =>
|
|
121
|
+
UiConfigSchema.parse({ userTimezone: "not-a-timezone" }),
|
|
122
|
+
).toThrow("ui.userTimezone must be a valid IANA timezone identifier");
|
|
123
|
+
expect(() =>
|
|
124
|
+
UiConfigSchema.parse({ detectedTimezone: "Mars/Olympus_Mons" }),
|
|
125
|
+
).toThrow("ui.detectedTimezone must be a valid IANA timezone identifier");
|
|
126
|
+
expect(() => UiConfigSchema.parse({ userTimezone: "+05:30" })).toThrow(
|
|
127
|
+
"ui.userTimezone must be a valid IANA timezone identifier",
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("rejects ambiguous abbreviations and offset strings", () => {
|
|
132
|
+
for (const value of ["EST", "PST", "UTC+5:30", "GMT-0800", "+05:30"]) {
|
|
133
|
+
expect(() => UiConfigSchema.parse({ userTimezone: value })).toThrow(
|
|
134
|
+
"ui.userTimezone must be a valid IANA timezone identifier",
|
|
135
|
+
);
|
|
136
|
+
expect(() => UiConfigSchema.parse({ detectedTimezone: value })).toThrow(
|
|
137
|
+
"ui.detectedTimezone must be a valid IANA timezone identifier",
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// canonicalizeTimeZone
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
describe("canonicalizeTimeZone", () => {
|
|
148
|
+
test("returns canonical timezone identifiers and ignores empty values", () => {
|
|
149
|
+
expect(canonicalizeTimeZone("america/new_york")).toBe("America/New_York");
|
|
150
|
+
expect(canonicalizeTimeZone("")).toBeNull();
|
|
151
|
+
expect(canonicalizeTimeZone(null)).toBeNull();
|
|
152
|
+
expect(canonicalizeTimeZone("not-a-timezone")).toBeNull();
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// resolveTurnTimezoneContext
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
describe("resolveTurnTimezoneContext", () => {
|
|
161
|
+
test("prefers configured user timezone over automatic sources", () => {
|
|
162
|
+
const context = resolveTurnTimezoneContext({
|
|
163
|
+
configuredUserTimeZone: "America/New_York",
|
|
164
|
+
clientTimezone: "America/Chicago",
|
|
165
|
+
detectedTimezone: "Asia/Tokyo",
|
|
166
|
+
hostTimeZone: "Europe/London",
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
expect(context.effectiveTimezone).toBe("America/New_York");
|
|
170
|
+
expect(context.source).toBe("configuredUserTimezone");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("prefers client timezone over detected and host timezones", () => {
|
|
174
|
+
const context = resolveTurnTimezoneContext({
|
|
175
|
+
clientTimezone: "America/Chicago",
|
|
176
|
+
detectedTimezone: "Asia/Tokyo",
|
|
177
|
+
hostTimeZone: "Europe/London",
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
expect(context.effectiveTimezone).toBe("America/Chicago");
|
|
181
|
+
expect(context.source).toBe("clientTimezone");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("prefers detected timezone over host timezone", () => {
|
|
185
|
+
const context = resolveTurnTimezoneContext({
|
|
186
|
+
detectedTimezone: "Asia/Tokyo",
|
|
187
|
+
hostTimeZone: "Europe/London",
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
expect(context.effectiveTimezone).toBe("Asia/Tokyo");
|
|
191
|
+
expect(context.source).toBe("detectedTimezone");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("uses host timezone before UTC fallback", () => {
|
|
195
|
+
const context = resolveTurnTimezoneContext({
|
|
196
|
+
hostTimeZone: "Europe/London",
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
expect(context.effectiveTimezone).toBe("Europe/London");
|
|
200
|
+
expect(context.source).toBe("hostTimezone");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("falls back to UTC when no timezone resolves", () => {
|
|
204
|
+
const context = resolveTurnTimezoneContext({
|
|
205
|
+
configuredUserTimeZone: "not-a-timezone",
|
|
206
|
+
clientTimezone: "also-invalid",
|
|
207
|
+
detectedTimezone: "",
|
|
208
|
+
hostTimeZone: "still-invalid",
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
expect(context.effectiveTimezone).toBe("UTC");
|
|
212
|
+
expect(context.source).toBe("utcFallback");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("ignores empty strings during runtime resolution", () => {
|
|
216
|
+
const context = resolveTurnTimezoneContext({
|
|
217
|
+
configuredUserTimeZone: "",
|
|
218
|
+
clientTimezone: "",
|
|
219
|
+
detectedTimezone: "",
|
|
220
|
+
hostTimeZone: "UTC",
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
expect(context.configuredUserTimezone).toBeNull();
|
|
224
|
+
expect(context.clientTimezone).toBeNull();
|
|
225
|
+
expect(context.detectedTimezone).toBeNull();
|
|
226
|
+
expect(context.effectiveTimezone).toBe("UTC");
|
|
227
|
+
expect(context.source).toBe("hostTimezone");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("does not use recalled profile timezone in normal turn precedence", () => {
|
|
231
|
+
const context = resolveTurnTimezoneContext({
|
|
232
|
+
userTimeZone: "Asia/Tokyo",
|
|
233
|
+
hostTimeZone: "UTC",
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
expect(context.effectiveTimezone).toBe("UTC");
|
|
237
|
+
expect(context.source).toBe("hostTimezone");
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
88
241
|
// ---------------------------------------------------------------------------
|
|
89
242
|
// formatTurnTimestamp
|
|
90
243
|
// ---------------------------------------------------------------------------
|
|
@@ -127,11 +280,20 @@ describe("formatTurnTimestamp", () => {
|
|
|
127
280
|
expect(result).toBe("2026-04-02 (Thursday) 06:52:33 +00:00 (UTC)");
|
|
128
281
|
});
|
|
129
282
|
|
|
130
|
-
test("handles user timezone override", () => {
|
|
283
|
+
test("handles configured user timezone override", () => {
|
|
131
284
|
const result = formatTurnTimestamp({
|
|
132
285
|
nowMs: THU_APR_02_0652,
|
|
133
286
|
hostTimeZone: "UTC",
|
|
134
|
-
|
|
287
|
+
configuredUserTimeZone: "Asia/Tokyo",
|
|
288
|
+
});
|
|
289
|
+
expect(result).toBe("2026-04-02 (Thursday) 15:52:33 +09:00 (Asia/Tokyo)");
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("uses client timezone when no configured override exists", () => {
|
|
293
|
+
const result = formatTurnTimestamp({
|
|
294
|
+
nowMs: THU_APR_02_0652,
|
|
295
|
+
hostTimeZone: "UTC",
|
|
296
|
+
clientTimezone: "Asia/Tokyo",
|
|
135
297
|
});
|
|
136
298
|
expect(result).toBe("2026-04-02 (Thursday) 15:52:33 +09:00 (Asia/Tokyo)");
|
|
137
299
|
});
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import type { DiskPressureTransitionResult } from "../daemon/disk-pressure-guard.js";
|
|
4
|
+
import type { DiskUsageInfo } from "../util/disk-usage.js";
|
|
5
|
+
|
|
6
|
+
let diskSample: DiskUsageInfo | null = null;
|
|
7
|
+
let diskSampleError: unknown = null;
|
|
8
|
+
let diskSampleCalls = 0;
|
|
9
|
+
|
|
10
|
+
mock.module("../config/loader.js", () => ({
|
|
11
|
+
getConfig: () => ({}),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
mock.module("../util/disk-usage.js", () => ({
|
|
15
|
+
getDiskUsageInfo: () => {
|
|
16
|
+
diskSampleCalls += 1;
|
|
17
|
+
if (diskSampleError) throw diskSampleError;
|
|
18
|
+
return diskSample;
|
|
19
|
+
},
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
mock.module("../runtime/assistant-event.js", () => ({
|
|
23
|
+
buildAssistantEvent: (message: unknown, conversationId?: string) => ({
|
|
24
|
+
id: "event-test",
|
|
25
|
+
type: "message",
|
|
26
|
+
timestamp: new Date().toISOString(),
|
|
27
|
+
conversationId,
|
|
28
|
+
message,
|
|
29
|
+
}),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
mock.module("../runtime/assistant-event-hub.js", () => ({
|
|
33
|
+
AssistantEventHub: class {},
|
|
34
|
+
broadcastMessage: () => {},
|
|
35
|
+
capabilityForMessageType: () => undefined,
|
|
36
|
+
assistantEventHub: {
|
|
37
|
+
publish: async () => {},
|
|
38
|
+
},
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
const { _setOverridesForTesting } =
|
|
42
|
+
await import("../config/assistant-feature-flags.js");
|
|
43
|
+
const {
|
|
44
|
+
DISK_PRESSURE_OVERRIDE_CONFIRMATION,
|
|
45
|
+
DISK_PRESSURE_THRESHOLD_PERCENT,
|
|
46
|
+
__getDiskPressureGuardTimerForTests,
|
|
47
|
+
__resetDiskPressureGuardForTests,
|
|
48
|
+
acknowledgeDiskPressureLock,
|
|
49
|
+
evaluateDiskPressureNow,
|
|
50
|
+
getDiskPressureStatus,
|
|
51
|
+
overrideDiskPressureLock,
|
|
52
|
+
startDiskPressureGuard,
|
|
53
|
+
stopDiskPressureGuard,
|
|
54
|
+
} = await import("../daemon/disk-pressure-guard.js");
|
|
55
|
+
|
|
56
|
+
function setFeatureFlag(enabled: boolean): void {
|
|
57
|
+
_setOverridesForTesting({ "safe-storage-limits": enabled });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function setDiskUsage(usedMb: number, totalMb = 100): void {
|
|
61
|
+
diskSample = {
|
|
62
|
+
path: "/workspace",
|
|
63
|
+
totalMb,
|
|
64
|
+
usedMb,
|
|
65
|
+
freeMb: Math.max(0, totalMb - usedMb),
|
|
66
|
+
};
|
|
67
|
+
diskSampleError = null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function expectRejected(
|
|
71
|
+
result: DiskPressureTransitionResult,
|
|
72
|
+
reason: Exclude<DiskPressureTransitionResult, { ok: true }>["reason"],
|
|
73
|
+
): asserts result is Exclude<DiskPressureTransitionResult, { ok: true }> {
|
|
74
|
+
expect(result.ok).toBe(false);
|
|
75
|
+
if (result.ok) {
|
|
76
|
+
throw new Error("Expected disk pressure transition to be rejected");
|
|
77
|
+
}
|
|
78
|
+
expect(result.reason).toBe(reason);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
beforeEach(() => {
|
|
82
|
+
__resetDiskPressureGuardForTests();
|
|
83
|
+
setFeatureFlag(true);
|
|
84
|
+
setDiskUsage(10);
|
|
85
|
+
diskSampleCalls = 0;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
afterEach(() => {
|
|
89
|
+
__resetDiskPressureGuardForTests();
|
|
90
|
+
_setOverridesForTesting({});
|
|
91
|
+
diskSample = null;
|
|
92
|
+
diskSampleError = null;
|
|
93
|
+
diskSampleCalls = 0;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("disk pressure guard", () => {
|
|
97
|
+
test("returns a stable disabled status without sampling when the flag is disabled", () => {
|
|
98
|
+
setDiskUsage(99);
|
|
99
|
+
setFeatureFlag(false);
|
|
100
|
+
|
|
101
|
+
const status = evaluateDiskPressureNow();
|
|
102
|
+
|
|
103
|
+
expect(status.enabled).toBe(false);
|
|
104
|
+
expect(status.state).toBe("disabled");
|
|
105
|
+
expect(status.locked).toBe(false);
|
|
106
|
+
expect(status.effectivelyLocked).toBe(false);
|
|
107
|
+
expect(status.usagePercent).toBeNull();
|
|
108
|
+
expect(diskSampleCalls).toBe(0);
|
|
109
|
+
expect(getDiskPressureStatus()).toEqual(status);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("locks when sampled usage reaches the threshold", () => {
|
|
113
|
+
setDiskUsage(DISK_PRESSURE_THRESHOLD_PERCENT);
|
|
114
|
+
|
|
115
|
+
const status = evaluateDiskPressureNow();
|
|
116
|
+
|
|
117
|
+
expect(status.enabled).toBe(true);
|
|
118
|
+
expect(status.state).toBe("critical");
|
|
119
|
+
expect(status.locked).toBe(true);
|
|
120
|
+
expect(status.acknowledged).toBe(false);
|
|
121
|
+
expect(status.overrideActive).toBe(false);
|
|
122
|
+
expect(status.effectivelyLocked).toBe(true);
|
|
123
|
+
expect(status.lockId).toBeTruthy();
|
|
124
|
+
expect(status.usagePercent).toBe(DISK_PRESSURE_THRESHOLD_PERCENT);
|
|
125
|
+
expect(status.thresholdPercent).toBe(DISK_PRESSURE_THRESHOLD_PERCENT);
|
|
126
|
+
expect(status.path).toBe("/workspace");
|
|
127
|
+
expect(status.lastCheckedAt).toBeTruthy();
|
|
128
|
+
expect(status.blockedCapabilities.length).toBeGreaterThan(0);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("acknowledges an active lock without overriding it", () => {
|
|
132
|
+
setDiskUsage(99);
|
|
133
|
+
evaluateDiskPressureNow();
|
|
134
|
+
|
|
135
|
+
const result = acknowledgeDiskPressureLock();
|
|
136
|
+
|
|
137
|
+
expect(result.ok).toBe(true);
|
|
138
|
+
expect(result.status.acknowledged).toBe(true);
|
|
139
|
+
expect(result.status.overrideActive).toBe(false);
|
|
140
|
+
expect(result.status.effectivelyLocked).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("unlocks and clears acknowledgement and override when usage falls below threshold", () => {
|
|
144
|
+
setDiskUsage(99);
|
|
145
|
+
evaluateDiskPressureNow();
|
|
146
|
+
acknowledgeDiskPressureLock();
|
|
147
|
+
overrideDiskPressureLock(DISK_PRESSURE_OVERRIDE_CONFIRMATION);
|
|
148
|
+
|
|
149
|
+
setDiskUsage(20);
|
|
150
|
+
const status = evaluateDiskPressureNow();
|
|
151
|
+
|
|
152
|
+
expect(status.state).toBe("ok");
|
|
153
|
+
expect(status.locked).toBe(false);
|
|
154
|
+
expect(status.acknowledged).toBe(false);
|
|
155
|
+
expect(status.overrideActive).toBe(false);
|
|
156
|
+
expect(status.effectivelyLocked).toBe(false);
|
|
157
|
+
expect(status.lockId).toBeNull();
|
|
158
|
+
expect(status.blockedCapabilities).toEqual([]);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("overrides an active lock only with the exact confirmation after trimming whitespace", () => {
|
|
162
|
+
setDiskUsage(99);
|
|
163
|
+
evaluateDiskPressureNow();
|
|
164
|
+
|
|
165
|
+
const invalid = overrideDiskPressureLock("I accept the risks");
|
|
166
|
+
expectRejected(invalid, "invalid_confirmation");
|
|
167
|
+
expect(invalid.status.effectivelyLocked).toBe(true);
|
|
168
|
+
|
|
169
|
+
const valid = overrideDiskPressureLock(
|
|
170
|
+
` ${DISK_PRESSURE_OVERRIDE_CONFIRMATION} `,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
expect(valid.ok).toBe(true);
|
|
174
|
+
expect(valid.status.locked).toBe(true);
|
|
175
|
+
expect(valid.status.overrideActive).toBe(true);
|
|
176
|
+
expect(valid.status.effectivelyLocked).toBe(false);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("rejects acknowledgement when no lock is active", () => {
|
|
180
|
+
setDiskUsage(10);
|
|
181
|
+
evaluateDiskPressureNow();
|
|
182
|
+
|
|
183
|
+
const result = acknowledgeDiskPressureLock();
|
|
184
|
+
|
|
185
|
+
expectRejected(result, "not_locked");
|
|
186
|
+
expect(result.status.locked).toBe(false);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("rejects override when no lock is active", () => {
|
|
190
|
+
setDiskUsage(10);
|
|
191
|
+
evaluateDiskPressureNow();
|
|
192
|
+
|
|
193
|
+
const result = overrideDiskPressureLock(
|
|
194
|
+
DISK_PRESSURE_OVERRIDE_CONFIRMATION,
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
expectRejected(result, "not_locked");
|
|
198
|
+
expect(result.status.locked).toBe(false);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("rejects repeated override while preserving the existing override", () => {
|
|
202
|
+
setDiskUsage(99);
|
|
203
|
+
evaluateDiskPressureNow();
|
|
204
|
+
const first = overrideDiskPressureLock(DISK_PRESSURE_OVERRIDE_CONFIRMATION);
|
|
205
|
+
expect(first.ok).toBe(true);
|
|
206
|
+
|
|
207
|
+
const second = overrideDiskPressureLock(
|
|
208
|
+
DISK_PRESSURE_OVERRIDE_CONFIRMATION,
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
expectRejected(second, "already_overridden");
|
|
212
|
+
expect(second.status.overrideActive).toBe(true);
|
|
213
|
+
expect(second.status.effectivelyLocked).toBe(false);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("sample failures degrade open and do not preserve a prior lock", () => {
|
|
217
|
+
setDiskUsage(99);
|
|
218
|
+
evaluateDiskPressureNow();
|
|
219
|
+
expect(getDiskPressureStatus().locked).toBe(true);
|
|
220
|
+
|
|
221
|
+
diskSampleError = new Error("sample failed");
|
|
222
|
+
const status = evaluateDiskPressureNow();
|
|
223
|
+
|
|
224
|
+
expect(status.enabled).toBe(true);
|
|
225
|
+
expect(status.state).toBe("unknown");
|
|
226
|
+
expect(status.locked).toBe(false);
|
|
227
|
+
expect(status.effectivelyLocked).toBe(false);
|
|
228
|
+
expect(status.error).toBe("sample failed");
|
|
229
|
+
expect(status.lastCheckedAt).toBeTruthy();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("timer start and stop are idempotent", () => {
|
|
233
|
+
expect(__getDiskPressureGuardTimerForTests()).toBeNull();
|
|
234
|
+
|
|
235
|
+
startDiskPressureGuard();
|
|
236
|
+
const firstTimer = __getDiskPressureGuardTimerForTests();
|
|
237
|
+
expect(firstTimer).toBeTruthy();
|
|
238
|
+
|
|
239
|
+
startDiskPressureGuard();
|
|
240
|
+
expect(__getDiskPressureGuardTimerForTests()).toBe(firstTimer);
|
|
241
|
+
|
|
242
|
+
stopDiskPressureGuard();
|
|
243
|
+
expect(__getDiskPressureGuardTimerForTests()).toBeNull();
|
|
244
|
+
|
|
245
|
+
stopDiskPressureGuard();
|
|
246
|
+
expect(__getDiskPressureGuardTimerForTests()).toBeNull();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("disabling the flag clears an active timer and lock", () => {
|
|
250
|
+
setDiskUsage(99);
|
|
251
|
+
evaluateDiskPressureNow();
|
|
252
|
+
startDiskPressureGuard();
|
|
253
|
+
expect(__getDiskPressureGuardTimerForTests()).toBeTruthy();
|
|
254
|
+
|
|
255
|
+
setFeatureFlag(false);
|
|
256
|
+
const status = evaluateDiskPressureNow();
|
|
257
|
+
|
|
258
|
+
expect(status.enabled).toBe(false);
|
|
259
|
+
expect(status.locked).toBe(false);
|
|
260
|
+
expect(__getDiskPressureGuardTimerForTests()).toBeNull();
|
|
261
|
+
});
|
|
262
|
+
});
|