@vellumai/assistant 0.6.4 → 0.6.5
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/.prettierignore +5 -0
- package/ARCHITECTURE.md +32 -36
- package/Dockerfile +12 -0
- package/README.md +3 -4
- package/bun.lock +8 -3
- package/docs/architecture/integrations.md +1 -20
- package/docs/architecture/security.md +16 -16
- package/docs/error-handling.md +111 -0
- package/docs/skills.md +10 -10
- package/docs/stt-provider-onboarding.md +2 -1
- package/knip.json +9 -2
- package/node_modules/@vellumai/ces-contracts/package.json +2 -1
- package/node_modules/@vellumai/ces-contracts/src/__tests__/trust-rules.test.ts +471 -0
- package/node_modules/@vellumai/ces-contracts/src/trust-rules.ts +398 -4
- package/node_modules/@vellumai/credential-storage/bun.lock +2 -2
- package/node_modules/@vellumai/credential-storage/package.json +2 -2
- package/node_modules/@vellumai/credential-storage/src/oauth-runtime.ts +20 -2
- package/node_modules/@vellumai/egress-proxy/bun.lock +2 -2
- package/node_modules/@vellumai/egress-proxy/package.json +2 -2
- package/openapi.yaml +123 -11
- package/package.json +6 -3
- package/scripts/generate-openapi.ts +50 -11
- package/src/__tests__/agent-loop-callsite-precedence.test.ts +318 -0
- package/src/__tests__/agent-loop-sentry-hygiene.test.ts +137 -0
- package/src/__tests__/agent-loop.test.ts +112 -1
- package/src/__tests__/anthropic-error-formatting.test.ts +98 -0
- package/src/__tests__/anthropic-provider.test.ts +171 -2
- package/src/__tests__/approval-cascade.test.ts +31 -10
- package/src/__tests__/approval-routes-http.test.ts +134 -10
- package/src/__tests__/assistant-attachments.test.ts +44 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +29 -0
- package/src/__tests__/browser-fill-credential.test.ts +1 -1
- package/src/__tests__/browser-identifier-parity-guard.test.ts +53 -0
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +23 -33
- package/src/__tests__/browser-skill-endstate.test.ts +51 -182
- package/src/__tests__/btw-routes.test.ts +47 -1
- package/src/__tests__/call-controller.test.ts +1 -2
- package/src/__tests__/call-site-routing-provider.test.ts +214 -0
- package/src/__tests__/catalog-cache.test.ts +27 -4
- package/src/__tests__/channel-approval-routes.test.ts +4 -4
- package/src/__tests__/channel-reply-delivery.test.ts +300 -2
- package/src/__tests__/checker.test.ts +428 -501
- package/src/__tests__/cli-command-risk-guard.test.ts +30 -33
- package/src/__tests__/compaction-circuit-breaker.test.ts +336 -0
- package/src/__tests__/compaction.benchmark.test.ts +1 -1
- package/src/__tests__/config-analysis.test.ts +11 -28
- package/src/__tests__/config-loader-backfill.test.ts +174 -0
- package/src/__tests__/config-loader-corrupt.test.ts +183 -0
- package/src/__tests__/config-loader-quarantine-bulletin.test.ts +202 -0
- package/src/__tests__/config-schema-cmd.test.ts +11 -5
- package/src/__tests__/config-schema.test.ts +427 -114
- package/src/__tests__/config-watcher.test.ts +2 -2
- package/src/__tests__/contact-store-user-file.test.ts +72 -73
- package/src/__tests__/contacts-write.test.ts +4 -4
- package/src/__tests__/context-token-estimator.test.ts +191 -1
- package/src/__tests__/context-window-manager.test.ts +530 -2
- package/src/__tests__/conversation-abort-tool-results.test.ts +30 -16
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +61 -17
- package/src/__tests__/conversation-agent-loop.test.ts +412 -82
- package/src/__tests__/conversation-attachments.test.ts +1 -1
- package/src/__tests__/conversation-confirmation-signals.test.ts +30 -9
- package/src/__tests__/conversation-error.test.ts +37 -6
- package/src/__tests__/conversation-history-web-search.test.ts +6 -0
- package/src/__tests__/conversation-init.benchmark.test.ts +36 -0
- package/src/__tests__/conversation-lifecycle.test.ts +336 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +27 -10
- package/src/__tests__/conversation-pre-run-repair.test.ts +30 -16
- package/src/__tests__/conversation-process-callsite.test.ts +306 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +30 -16
- package/src/__tests__/conversation-queue.test.ts +41 -26
- package/src/__tests__/conversation-routes-disk-view.test.ts +29 -1
- package/src/__tests__/conversation-routes-slash-commands.test.ts +31 -3
- package/src/__tests__/conversation-runtime-assembly.test.ts +2735 -55
- package/src/__tests__/conversation-runtime-workspace.test.ts +12 -12
- package/src/__tests__/conversation-skill-tools.test.ts +12 -146
- package/src/__tests__/conversation-slash-queue.test.ts +34 -19
- package/src/__tests__/conversation-slash-unknown.test.ts +30 -16
- package/src/__tests__/conversation-speed-override.test.ts +30 -11
- package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +1035 -0
- package/src/__tests__/conversation-surfaces-standalone.test.ts +630 -0
- package/src/__tests__/conversation-title-service.test.ts +2 -2
- package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +1 -1
- package/src/__tests__/conversation-unread-route.test.ts +2 -2
- package/src/__tests__/conversation-usage.test.ts +3 -1
- package/src/__tests__/conversation-workspace-cache-state.test.ts +31 -10
- package/src/__tests__/conversation-workspace-injection.test.ts +43 -15
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +44 -16
- package/src/__tests__/credential-broker-browser-fill.test.ts +110 -0
- package/src/__tests__/credential-security-invariants.test.ts +3 -0
- package/src/__tests__/credential-storage-oauth-compat.test.ts +18 -0
- package/src/__tests__/credential-storage-static-compat.test.ts +28 -0
- package/src/__tests__/credential-vault-unit.test.ts +135 -19
- package/src/__tests__/credentials-cli.test.ts +1 -9
- package/src/__tests__/cross-provider-web-search.test.ts +84 -0
- package/src/__tests__/daemon-server-persist-and-process-callsite.test.ts +92 -0
- package/src/__tests__/delete-propagation.test.ts +437 -0
- package/src/__tests__/dm-backfill.test.ts +417 -0
- package/src/__tests__/dm-persistence.test.ts +227 -0
- package/src/__tests__/edit-propagation.test.ts +280 -0
- package/src/__tests__/ephemeral-permissions.test.ts +93 -3
- package/src/__tests__/estimator-calibration-integration.test.ts +208 -0
- package/src/__tests__/estimator-calibration.test.ts +213 -0
- package/src/__tests__/extension-id-sync-guard.test.ts +26 -7
- package/src/__tests__/file-write-tool.test.ts +151 -1
- package/src/__tests__/filing-service.test.ts +255 -0
- package/src/__tests__/gemini-provider.test.ts +0 -3
- package/src/__tests__/guardian-grant-minting.test.ts +8 -0
- package/src/__tests__/headless-browser-interactions.test.ts +1 -1
- package/src/__tests__/heartbeat-service.test.ts +96 -15
- package/src/__tests__/host-shell-tool.test.ts +124 -18
- package/src/__tests__/http-user-message-parity.test.ts +29 -1
- package/src/__tests__/inbound-slack-persistence.test.ts +340 -0
- package/src/__tests__/intent-routing.test.ts +1 -40
- package/src/__tests__/llm-catalog-parity.test.ts +174 -0
- package/src/__tests__/llm-context-normalization.test.ts +121 -0
- package/src/__tests__/llm-resolver.test.ts +214 -0
- package/src/__tests__/llm-schema.test.ts +223 -0
- package/src/__tests__/managed-proxy-context.test.ts +6 -2
- package/src/__tests__/messaging-skill-split.test.ts +3 -34
- package/src/__tests__/migration-import-from-url.test.ts +684 -0
- package/src/__tests__/model-intents.test.ts +9 -83
- package/src/__tests__/notification-decision-fallback.test.ts +0 -10
- package/src/__tests__/notification-decision-identity.test.ts +0 -9
- package/src/__tests__/notification-decision-recipient-context.test.ts +0 -9
- package/src/__tests__/oauth-store.test.ts +10 -7
- package/src/__tests__/oauth2-gateway-transport.test.ts +8 -3
- package/src/__tests__/oauth2-refresh-retry.test.ts +279 -0
- package/src/__tests__/openai-provider.test.ts +7 -0
- package/src/__tests__/openai-responses-provider.test.ts +396 -0
- package/src/__tests__/openrouter-provider-only.test.ts +135 -0
- package/src/__tests__/outbound-slack-persistence.test.ts +293 -0
- package/src/__tests__/permission-checker-host-gate.test.ts +1 -1
- package/src/__tests__/permission-mode.test.ts +16 -0
- package/src/__tests__/permission-types.test.ts +0 -1
- package/src/__tests__/persona-resolver.test.ts +13 -13
- package/src/__tests__/pkb-autoinject.test.ts +37 -1
- package/src/__tests__/platform-bash-auto-approve.test.ts +1 -1
- package/src/__tests__/pricing.test.ts +50 -3
- package/src/__tests__/profiler-routes.test.ts +1 -1
- package/src/__tests__/provider-commit-message-generator.test.ts +14 -84
- package/src/__tests__/provider-env-vars-scope.test.ts +52 -0
- package/src/__tests__/provider-error-scenarios.test.ts +135 -6
- package/src/__tests__/provider-managed-proxy-integration.test.ts +42 -11
- package/src/__tests__/provider-registry-ollama.test.ts +1 -2
- package/src/__tests__/proxy-approval-callback.test.ts +0 -1
- package/src/__tests__/reaction-persistence.test.ts +560 -0
- package/src/__tests__/relay-server.test.ts +1 -1
- package/src/__tests__/require-fresh-approval.test.ts +1 -1
- package/src/__tests__/retry-openrouter-only-normalization.test.ts +136 -0
- package/src/__tests__/retry-thinking-tool-choice.test.ts +226 -0
- package/src/__tests__/risk-classifier-parity.test.ts +230 -0
- package/src/__tests__/sanitize-config-for-transfer.test.ts +78 -1
- package/src/__tests__/secret-ingress-http.test.ts +28 -0
- package/src/__tests__/secret-prompter-channel-fallback.test.ts +125 -0
- package/src/__tests__/secret-routes-managed-proxy.test.ts +2 -3
- package/src/__tests__/secret-scanner-executor.test.ts +1 -1
- package/src/__tests__/send-endpoint-busy.test.ts +29 -1
- package/src/__tests__/server-history-render.test.ts +31 -0
- package/src/__tests__/shell-parser-property.test.ts +13 -13
- package/src/__tests__/skill-cache-store.test.ts +182 -0
- package/src/__tests__/skills.test.ts +19 -33
- package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
- package/src/__tests__/slack-skill.test.ts +3 -8
- package/src/__tests__/starter-bundle.test.ts +35 -0
- package/src/__tests__/subagent-call-site-routing.test.ts +280 -0
- package/src/__tests__/suggestion-routes.test.ts +160 -3
- package/src/__tests__/system-prompt.test.ts +22 -35
- package/src/__tests__/task-runner.test.ts +3 -1
- package/src/__tests__/tcc-sandbox-deny.test.ts +198 -0
- package/src/__tests__/terminal-tools.test.ts +8 -0
- package/src/__tests__/test-support/browser-skill-harness.ts +2 -52
- package/src/__tests__/thread-backfill.test.ts +941 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -2
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -2
- package/src/__tests__/tool-executor.test.ts +60 -94
- package/src/__tests__/trust-store.test.ts +442 -109
- package/src/__tests__/update-bulletin-job.test.ts +389 -0
- package/src/__tests__/usage-cache-backfill-migration.test.ts +3 -1
- package/src/__tests__/verification-control-plane-policy.test.ts +1 -22
- package/src/__tests__/voice-session-bridge.test.ts +39 -0
- package/src/__tests__/volume-security-guard.test.ts +3 -2
- package/src/__tests__/web-search-history.test.ts +337 -0
- package/src/__tests__/workspace-migration-039-drop-legacy-llm-keys.test.ts +343 -0
- package/src/__tests__/workspace-migration-043-release-notes-latex-rendering.test.ts +202 -0
- package/src/__tests__/workspace-migration-045-release-notes-meet-avatar.test.ts +210 -0
- package/src/__tests__/workspace-migration-drop-user-md.test.ts +11 -11
- package/src/__tests__/workspace-migration-unify-llm-callsite-configs.test.ts +841 -0
- package/src/__tests__/workspace-policy.test.ts +1 -13
- package/src/acp/client-handler.ts +1 -2
- package/src/agent/loop.ts +209 -17
- package/src/avatar/resvg-lazy.test.ts +136 -0
- package/src/avatar/resvg-lazy.ts +82 -9
- package/src/avatar/traits-png-sync.ts +21 -1
- package/src/browser/__tests__/operations.test.ts +163 -0
- package/src/browser/identifiers.ts +51 -0
- package/src/browser/operations.ts +660 -0
- package/src/browser/types.ts +81 -0
- package/src/calls/guardian-question-copy.ts +2 -2
- package/src/calls/telephony-stt-routing.ts +1 -1
- package/src/calls/voice-session-bridge.ts +1 -0
- package/src/cli/AGENTS.md +1 -1
- package/src/cli/commands/__tests__/attachment.test.ts +438 -0
- package/src/cli/commands/__tests__/browser.test.ts +554 -0
- package/src/cli/commands/__tests__/cache.test.ts +623 -0
- package/src/cli/commands/__tests__/email-list.test.ts +6 -0
- package/src/cli/commands/__tests__/email-send.test.ts +93 -1
- package/src/cli/commands/__tests__/image-generation.test.ts +666 -0
- package/src/cli/commands/__tests__/inference-send.test.ts +451 -0
- package/src/cli/commands/__tests__/stt-transcribe.test.ts +454 -0
- package/src/cli/commands/__tests__/task.test.ts +913 -0
- package/src/cli/commands/__tests__/tts-synthesize.test.ts +594 -0
- package/src/cli/commands/__tests__/ui-confirm.test.ts +650 -0
- package/src/cli/commands/__tests__/ui.test.ts +1215 -0
- package/src/cli/commands/__tests__/watchers.test.ts +716 -0
- package/src/cli/commands/attachment.ts +182 -0
- package/src/cli/commands/browser.ts +350 -0
- package/src/cli/commands/cache.ts +341 -0
- package/src/cli/commands/completions.ts +0 -3
- package/src/cli/commands/config.ts +6 -6
- package/src/cli/commands/conversations-import.ts +347 -0
- package/src/cli/commands/conversations.ts +14 -1
- package/src/cli/commands/email.ts +234 -194
- package/src/cli/commands/image-generation.ts +300 -0
- package/src/cli/commands/inference.ts +200 -0
- package/src/cli/commands/memory.ts +127 -17
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -1
- package/src/cli/commands/platform/__tests__/connect.test.ts +0 -1
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +0 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +0 -1
- package/src/cli/commands/stt.ts +339 -0
- package/src/cli/commands/task.ts +795 -0
- package/src/cli/commands/trust.ts +50 -19
- package/src/cli/commands/tts.ts +273 -0
- package/src/cli/commands/ui.ts +670 -0
- package/src/cli/commands/watchers.ts +509 -0
- package/src/cli/lib/daemon-credential-client.ts +0 -19
- package/src/cli/program.ts +23 -4
- package/src/cli.ts +0 -37
- package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +23 -1
- package/src/config/bundled-skills/media-processing/services/reduce.ts +1 -1
- package/src/config/bundled-skills/messaging/SKILL.md +2 -2
- package/src/config/bundled-skills/messaging/TOOLS.json +4 -0
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +8 -1
- package/src/config/bundled-skills/messaging/tools/messaging-read.ts +15 -1
- package/src/config/bundled-skills/messaging/tools/messaging-search.ts +21 -1
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +11 -12
- package/src/config/bundled-skills/phone-calls/references/CONFIG.md +9 -8
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/bundled-tool-registry.ts +0 -175
- package/src/config/env.ts +7 -2
- package/src/config/feature-flag-registry.json +25 -9
- package/src/config/llm-resolver.ts +128 -0
- package/src/config/loader.ts +194 -10
- package/src/config/raw-config-utils.ts +30 -2
- package/src/config/sanitize-for-transfer.ts +35 -0
- package/src/config/schema.ts +30 -41
- package/src/config/schemas/analysis.ts +3 -22
- package/src/config/schemas/calls.ts +0 -4
- package/src/config/schemas/filing.ts +2 -7
- package/src/config/schemas/heartbeat.ts +0 -5
- package/src/config/schemas/inference.ts +3 -23
- package/src/config/schemas/llm.ts +318 -0
- package/src/config/schemas/memory-processing.ts +1 -9
- package/src/config/schemas/notifications.ts +4 -11
- package/src/config/schemas/platform.ts +3 -9
- package/src/config/schemas/security.ts +33 -0
- package/src/config/schemas/services.ts +9 -4
- package/src/config/schemas/stt.ts +1 -0
- package/src/config/schemas/tts.ts +53 -0
- package/src/config/schemas/updates.ts +1 -1
- package/src/config/schemas/workspace-git.ts +3 -40
- package/src/config/skills.ts +2 -2
- package/src/context/__tests__/compact-prompt.test.ts +45 -0
- package/src/context/__tests__/microcompact.test.ts +805 -0
- package/src/context/estimator-calibration.ts +136 -0
- package/src/context/microcompact.ts +443 -0
- package/src/context/prompts/compact.md +12 -0
- package/src/context/token-estimator.ts +61 -3
- package/src/context/window-manager.ts +229 -25
- package/src/credential-execution/approval-bridge.ts +0 -1
- package/src/credential-execution/executable-discovery.ts +19 -8
- package/src/credential-execution/process-manager.test.ts +109 -0
- package/src/credential-execution/process-manager.ts +65 -2
- package/src/daemon/approval-generators.ts +29 -4
- package/src/daemon/assistant-attachments.ts +24 -13
- package/src/daemon/classifier.ts +2 -2
- package/src/daemon/config-watcher.ts +0 -1
- package/src/daemon/context-overflow-reducer.ts +4 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +79 -12
- package/src/daemon/conversation-agent-loop.ts +462 -80
- package/src/daemon/conversation-attachments.ts +2 -6
- package/src/daemon/conversation-error.ts +36 -1
- package/src/daemon/conversation-lifecycle.ts +30 -6
- package/src/daemon/conversation-messaging.ts +73 -4
- package/src/daemon/conversation-process.ts +10 -4
- package/src/daemon/conversation-queue-manager.ts +3 -0
- package/src/daemon/conversation-runtime-assembly.ts +760 -29
- package/src/daemon/conversation-slash.ts +2 -2
- package/src/daemon/conversation-surfaces.ts +389 -1
- package/src/daemon/conversation-tool-setup.ts +10 -5
- package/src/daemon/conversation-usage.ts +1 -1
- package/src/daemon/conversation.ts +118 -30
- package/src/daemon/external-skills-bootstrap.ts +41 -0
- package/src/daemon/guardian-action-generators.ts +34 -14
- package/src/daemon/handlers/config-model.test.ts +86 -0
- package/src/daemon/handlers/config-model.ts +54 -12
- package/src/daemon/handlers/conversations.ts +9 -2
- package/src/daemon/handlers/shared.ts +39 -11
- package/src/daemon/handlers/skills.ts +2 -2
- package/src/daemon/handlers/slack-channel-oauth-install.ts +197 -0
- package/src/daemon/lifecycle.ts +76 -14
- package/src/daemon/message-types/conversations.ts +14 -0
- package/src/daemon/message-types/messages.ts +9 -1
- package/src/daemon/message-types/trust.ts +0 -2
- package/src/daemon/parse-actual-tokens-from-error.test.ts +57 -1
- package/src/daemon/parse-actual-tokens-from-error.ts +66 -0
- package/src/daemon/pkb-context-tracker.test.ts +169 -0
- package/src/daemon/pkb-context-tracker.ts +125 -0
- package/src/daemon/pkb-reminder-builder.test.ts +70 -0
- package/src/daemon/pkb-reminder-builder.ts +31 -0
- package/src/daemon/providers-setup.ts +6 -0
- package/src/daemon/server.ts +117 -9
- package/src/daemon/tool-side-effects.ts +0 -9
- package/src/daemon/watch-handler.ts +4 -4
- package/src/daemon/web-search-history.ts +126 -0
- package/src/events/domain-events.ts +0 -1
- package/src/filing/filing-service.ts +9 -10
- package/src/heartbeat/heartbeat-service.ts +76 -28
- package/src/home/__tests__/feed-scheduler.test.ts +39 -11
- package/src/home/__tests__/rollup-producer.test.ts +44 -0
- package/src/home/assistant-feed-authoring.ts +4 -0
- package/src/home/emit-feed-event.ts +4 -0
- package/src/home/feed-scheduler.ts +20 -4
- package/src/home/feed-types.ts +56 -2
- package/src/home/relationship-state-writer.ts +2 -2
- package/src/home/rollup-producer.ts +34 -5
- package/src/home/suggested-prompts.ts +101 -0
- package/src/ipc/__tests__/attachment-ipc.test.ts +213 -0
- package/src/ipc/__tests__/browser-ipc.test.ts +339 -0
- package/src/ipc/__tests__/cache-ipc.test.ts +266 -0
- package/src/ipc/__tests__/socket-path.test.ts +73 -0
- package/src/ipc/__tests__/task-ipc.test.ts +577 -0
- package/src/ipc/__tests__/ui-request-route.test.ts +495 -0
- package/src/ipc/__tests__/watcher-ipc.test.ts +295 -0
- package/src/ipc/cli-client.ts +2 -1
- package/src/ipc/cli-server.ts +26 -8
- package/src/ipc/gateway-client.ts +4 -4
- package/src/ipc/routes/attachment.ts +114 -0
- package/src/ipc/routes/browser-context.ts +61 -0
- package/src/ipc/routes/browser.ts +96 -0
- package/src/ipc/routes/cache.ts +96 -0
- package/src/ipc/routes/index.ts +17 -1
- package/src/ipc/routes/task-queue.ts +226 -0
- package/src/ipc/routes/task.ts +173 -0
- package/src/ipc/routes/ui-request.ts +50 -0
- package/src/ipc/routes/watcher.ts +203 -0
- package/src/ipc/socket-path.ts +100 -0
- package/src/memory/__tests__/conversation-analyze-job.test.ts +9 -8
- package/src/memory/__tests__/conversation-group-migration.test.ts +99 -0
- package/src/memory/admin.ts +18 -0
- package/src/memory/conversation-analyze-job.ts +14 -13
- package/src/memory/conversation-attention-store.ts +13 -6
- package/src/memory/conversation-crud.ts +103 -3
- package/src/memory/conversation-group-migration.ts +38 -6
- package/src/memory/conversation-title-service.ts +7 -4
- package/src/memory/db-init.ts +2 -0
- package/src/memory/embedding-backend.ts +1 -1
- package/src/memory/graph/compaction.ts +299 -0
- package/src/memory/graph/consolidation.ts +4 -4
- package/src/memory/graph/conversation-graph-memory.ts +89 -29
- package/src/memory/graph/extraction.test.ts +272 -2
- package/src/memory/graph/extraction.ts +173 -51
- package/src/memory/graph/graph-search.test.ts +92 -0
- package/src/memory/graph/graph-search.ts +4 -1
- package/src/memory/graph/narrative.ts +2 -2
- package/src/memory/graph/pattern-scan.ts +2 -2
- package/src/memory/graph/retriever.test.ts +459 -0
- package/src/memory/graph/retriever.ts +230 -48
- package/src/memory/graph/store.ts +41 -0
- package/src/memory/graph/tool-handlers.ts +27 -0
- package/src/memory/graph/tools.ts +6 -1
- package/src/memory/indexer.ts +5 -5
- package/src/memory/job-handlers/conversation-starters.ts +23 -20
- package/src/memory/job-handlers/summarization.ts +2 -2
- package/src/memory/job-utils.ts +7 -1
- package/src/memory/jobs/embed-pkb-file.test.ts +168 -0
- package/src/memory/jobs/embed-pkb-file.ts +54 -0
- package/src/memory/jobs-store.ts +44 -3
- package/src/memory/jobs-worker.ts +4 -0
- package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +1 -1
- package/src/memory/migrations/220-normalize-user-file-by-principal.ts +2 -2
- package/src/memory/migrations/222-strip-placeholder-sentinels-from-messages.ts +82 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/pkb/pkb-index.test.ts +368 -0
- package/src/memory/pkb/pkb-index.ts +255 -0
- package/src/memory/pkb/pkb-reconcile.test.ts +251 -0
- package/src/memory/pkb/pkb-reconcile.ts +148 -0
- package/src/memory/pkb/pkb-search.test.ts +438 -0
- package/src/memory/pkb/pkb-search.ts +137 -0
- package/src/memory/pkb/types.ts +53 -0
- package/src/memory/qdrant-client.ts +122 -1
- package/src/memory/slack-thread-store.ts +37 -0
- package/src/messaging/providers/gmail/adapter.ts +6 -16
- package/src/messaging/providers/gmail/client.ts +22 -0
- package/src/messaging/providers/gmail/types.ts +7 -0
- package/src/messaging/providers/slack/adapter.ts +14 -2
- package/src/messaging/providers/slack/backfill.test.ts +257 -0
- package/src/messaging/providers/slack/backfill.ts +101 -0
- package/src/messaging/providers/slack/message-metadata.test.ts +316 -0
- package/src/messaging/providers/slack/message-metadata.ts +123 -0
- package/src/messaging/providers/slack/render-transcript.test.ts +1373 -0
- package/src/messaging/providers/slack/render-transcript.ts +443 -0
- package/src/messaging/style-analyzer.ts +5 -2
- package/src/notifications/README.md +9 -5
- package/src/notifications/decision-engine.ts +3 -9
- package/src/notifications/preference-extractor.ts +2 -6
- package/src/oauth/oauth-store.ts +1 -0
- package/src/oauth/platform-connection.test.ts +47 -0
- package/src/oauth/platform-connection.ts +15 -5
- package/src/oauth/seed-providers.ts +4 -2
- package/src/permissions/approval-policy.test.ts +948 -0
- package/src/permissions/approval-policy.ts +257 -0
- package/src/permissions/bash-risk-classifier.test.ts +1208 -0
- package/src/permissions/bash-risk-classifier.ts +707 -0
- package/src/permissions/checker.ts +217 -708
- package/src/permissions/command-registry.test.ts +535 -0
- package/src/permissions/command-registry.ts +825 -0
- package/src/permissions/defaults.ts +26 -78
- package/src/permissions/file-risk-classifier.test.ts +535 -0
- package/src/permissions/file-risk-classifier.ts +274 -0
- package/src/permissions/risk-types.ts +205 -0
- package/src/permissions/secret-prompter.ts +53 -2
- package/src/permissions/skill-risk-classifier.test.ts +311 -0
- package/src/permissions/skill-risk-classifier.ts +214 -0
- package/src/permissions/trust-client.ts +52 -25
- package/src/permissions/trust-store-interface.ts +1 -6
- package/src/permissions/trust-store.ts +161 -62
- package/src/permissions/types.ts +23 -14
- package/src/permissions/web-risk-classifier.test.ts +170 -0
- package/src/permissions/web-risk-classifier.ts +89 -0
- package/src/permissions/workspace-policy.ts +1 -16
- package/src/platform/client.ts +19 -1
- package/src/prompts/persona-resolver.ts +3 -3
- package/src/prompts/system-prompt.ts +19 -20
- package/src/prompts/templates/SOUL.md +2 -2
- package/src/prompts/update-bulletin-job.ts +190 -0
- package/src/providers/__tests__/context-overflow-error.test.ts +328 -0
- package/src/providers/__tests__/provider-env-vars.test.ts +102 -0
- package/src/providers/__tests__/retry-callsite.test.ts +424 -0
- package/src/providers/anthropic/client.ts +183 -14
- package/src/providers/call-site-routing.ts +71 -0
- package/src/providers/gemini/client.ts +65 -2
- package/src/providers/managed-proxy/constants.ts +2 -1
- package/src/providers/model-catalog.ts +501 -33
- package/src/providers/model-intents.ts +4 -4
- package/src/providers/openai/chat-completions-provider.ts +57 -1
- package/src/providers/openai/responses-provider.ts +86 -9
- package/src/providers/openrouter/client.ts +76 -9
- package/src/providers/provider-env-vars.ts +56 -0
- package/src/providers/provider-send-message.ts +22 -5
- package/src/providers/ratelimit.ts +4 -0
- package/src/providers/registry.ts +19 -8
- package/src/providers/retry.ts +174 -39
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +55 -0
- package/src/providers/speech-to-text/google-gemini-live-stream.ts +4 -4
- package/src/providers/speech-to-text/provider-catalog.ts +17 -0
- package/src/providers/speech-to-text/resolve.ts +7 -0
- package/src/providers/speech-to-text/xai-realtime.test.ts +578 -0
- package/src/providers/speech-to-text/xai-realtime.ts +796 -0
- package/src/providers/speech-to-text/xai.test.ts +155 -0
- package/src/providers/speech-to-text/xai.ts +97 -0
- package/src/providers/types.ts +93 -3
- package/src/runtime/AGENTS.md +2 -2
- package/src/runtime/__tests__/agent-wake.test.ts +43 -2
- package/src/runtime/__tests__/interactive-ui.test.ts +673 -0
- package/src/runtime/agent-wake.ts +63 -22
- package/src/runtime/auth/route-policy.ts +4 -0
- package/src/runtime/btw-sidechain.ts +13 -3
- package/src/runtime/channel-reply-delivery.ts +106 -2
- package/src/runtime/decision-token.ts +116 -0
- package/src/runtime/gateway-client.ts +2 -2
- package/src/runtime/http-router.ts +32 -0
- package/src/runtime/http-server.ts +52 -1
- package/src/runtime/http-types.ts +23 -1
- package/src/runtime/interactive-ui.ts +362 -0
- package/src/runtime/invite-instruction-generator.ts +2 -2
- package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +176 -0
- package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +390 -0
- package/src/runtime/migrations/__tests__/vbundle-metadata-merge.test.ts +221 -0
- package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +1540 -0
- package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +453 -0
- package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +222 -0
- package/src/runtime/migrations/gcs-signed-url.ts +162 -0
- package/src/runtime/migrations/vbundle-importer.ts +154 -9
- package/src/runtime/migrations/vbundle-metadata-merge.ts +124 -0
- package/src/runtime/migrations/vbundle-streaming-importer.ts +2522 -0
- package/src/runtime/migrations/vbundle-streaming-validator.ts +244 -0
- package/src/runtime/migrations/vbundle-tar-stream.ts +217 -0
- package/src/runtime/migrations/vbundle-validator.ts +15 -6
- package/src/runtime/routes/__tests__/home-feed-routes.test.ts +111 -0
- package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +114 -75
- package/src/runtime/routes/__tests__/migration-vellum-metadata-reconcile.test.ts +246 -0
- package/src/runtime/routes/approval-prompt-ts-tracker.ts +58 -0
- package/src/runtime/routes/approval-routes.ts +12 -17
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +9 -0
- package/src/runtime/routes/avatar-routes.ts +20 -4
- package/src/runtime/routes/btw-routes.ts +1 -4
- package/src/runtime/routes/conversation-management-routes.ts +20 -2
- package/src/runtime/routes/conversation-routes.ts +133 -27
- package/src/runtime/routes/debug-routes.ts +1 -1
- package/src/runtime/routes/diagnostics-routes.ts +6 -4
- package/src/runtime/routes/events-routes.ts +16 -0
- package/src/runtime/routes/guardian-approval-interception.ts +33 -3
- package/src/runtime/routes/guardian-approval-prompt.ts +13 -3
- package/src/runtime/routes/home-feed-routes.ts +120 -2
- package/src/runtime/routes/inbound-message-handler.ts +912 -2
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +113 -2
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -3
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +129 -6
- package/src/runtime/routes/integrations/slack/channel.ts +25 -3
- package/src/runtime/routes/llm-context-normalization.ts +23 -1
- package/src/runtime/routes/migration-routes.ts +720 -124
- package/src/runtime/routes/settings-routes.ts +4 -2
- package/src/runtime/routes/trust-rules-routes.ts +30 -14
- package/src/runtime/routes/work-items-routes.test.ts +1 -1
- package/src/runtime/routes/work-items-routes.ts +3 -2
- package/src/runtime/services/__tests__/analyze-conversation.test.ts +25 -43
- package/src/runtime/services/analyze-conversation.ts +12 -16
- package/src/runtime/skill-route-registry.ts +28 -6
- package/src/schedule/scheduler.ts +8 -0
- package/src/security/__tests__/provider-key-env-fallback.test.ts +119 -0
- package/src/security/__tests__/untrusted-content.test.ts +109 -0
- package/src/security/oauth2.ts +98 -35
- package/src/security/secure-keys.ts +7 -8
- package/src/security/token-manager.ts +27 -13
- package/src/security/untrusted-content.ts +102 -0
- package/src/skills/catalog-cache.ts +26 -7
- package/src/skills/catalog-install.ts +31 -3
- package/src/skills/skill-cache-store.ts +97 -0
- package/src/stt/__tests__/daemon-batch-transcriber.test.ts +76 -0
- package/src/stt/daemon-batch-transcriber.ts +33 -0
- package/src/stt/stt-stream-session.ts +8 -1
- package/src/stt/types.ts +5 -1
- package/src/subagent/manager.ts +41 -13
- package/src/tasks/ephemeral-permissions.ts +9 -4
- package/src/telemetry/usage-telemetry-reporter.ts +27 -5
- package/src/tools/browser/__tests__/browser-status.test.ts +45 -2
- package/src/tools/browser/browser-execution.ts +65 -38
- package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +22 -0
- package/src/tools/credentials/tool-policy.ts +39 -5
- package/src/tools/credentials/vault.ts +9 -4
- package/src/tools/executor.ts +4 -0
- package/src/tools/filesystem/write.ts +52 -0
- package/src/tools/host-terminal/host-shell.ts +45 -5
- package/src/tools/memory/register.test.ts +185 -0
- package/src/tools/memory/register.ts +3 -1
- package/src/tools/network/web-fetch.ts +20 -10
- package/src/tools/network/web-search.ts +19 -4
- package/src/tools/permission-checker.ts +36 -15
- package/src/tools/policy-context.ts +25 -8
- package/src/tools/registry.ts +55 -3
- package/src/tools/side-effects.ts +0 -11
- package/src/tools/skills/execute.ts +2 -2
- package/src/tools/skills/sandbox-runner.ts +5 -2
- package/src/tools/terminal/backends/native.ts +51 -2
- package/src/tools/terminal/safe-env.ts +3 -2
- package/src/tools/terminal/shell.ts +1 -0
- package/src/tools/tool-manifest.ts +6 -21
- package/src/tools/types.ts +12 -3
- package/src/tools/verification-control-plane-policy.ts +1 -1
- package/src/tts/__tests__/provider-adapters.test.ts +240 -13
- package/src/tts/provider-catalog.ts +18 -0
- package/src/tts/providers/index.ts +2 -0
- package/src/tts/providers/xai-provider.ts +224 -0
- package/src/tts/types.ts +46 -0
- package/src/types/tar-stream.d.ts +66 -0
- package/src/util/json.ts +17 -0
- package/src/util/platform.ts +2 -2
- package/src/util/pricing.ts +15 -5
- package/src/watcher/engine.ts +1 -1
- package/src/watcher/providers/google-calendar.ts +134 -8
- package/src/watcher/providers/outlook-calendar.ts +42 -2
- package/src/workspace/git-service.ts +23 -4
- package/src/workspace/migrations/038-unify-llm-callsite-configs.ts +516 -0
- package/src/workspace/migrations/039-drop-legacy-llm-keys.ts +171 -0
- package/src/workspace/migrations/040-seed-latency-callsite-defaults.ts +154 -0
- package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +57 -0
- package/src/workspace/migrations/042-fix-backfill-google-gmail-settings-scope.ts +70 -0
- package/src/workspace/migrations/043-release-notes-latex-rendering.ts +75 -0
- package/src/workspace/migrations/044-bump-stale-provider-stream-timeout.ts +51 -0
- package/src/workspace/migrations/045-release-notes-meet-avatar.ts +130 -0
- package/src/workspace/migrations/AGENTS.md +1 -1
- package/src/workspace/migrations/registry.ts +16 -0
- package/src/workspace/provider-commit-message-generator.ts +19 -38
- package/src/__tests__/gmail-archive-fallback.test.ts +0 -193
- package/src/__tests__/gmail-archive-gate.test.ts +0 -246
- package/src/__tests__/gmail-preferences.test.ts +0 -117
- package/src/__tests__/outlook-attachments.test.ts +0 -301
- package/src/__tests__/outlook-automation-tools.test.ts +0 -425
- package/src/__tests__/outlook-categories.test.ts +0 -212
- package/src/__tests__/outlook-compose-tools.test.ts +0 -325
- package/src/__tests__/outlook-declutter-tools.test.ts +0 -585
- package/src/__tests__/outlook-follow-up.test.ts +0 -196
- package/src/__tests__/outlook-trash.test.ts +0 -77
- package/src/__tests__/outlook-unsubscribe.test.ts +0 -279
- package/src/__tests__/update-bulletin-format.test.ts +0 -181
- package/src/__tests__/update-bulletin-state.test.ts +0 -135
- package/src/__tests__/update-bulletin.test.ts +0 -478
- package/src/__tests__/update-template-contract.test.ts +0 -29
- package/src/cli/commands/doctor.ts +0 -341
- package/src/config/bundled-skills/browser/SKILL.md +0 -88
- package/src/config/bundled-skills/browser/TOOLS.json +0 -516
- package/src/config/bundled-skills/browser/tools/browser-attach.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-click.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-close.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-detach.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-extract.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-fill-credential.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-hover.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-navigate.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-press-key.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-screenshot.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-scroll.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-select-option.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-snapshot.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-status.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-type.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +0 -49
- package/src/config/bundled-skills/browser/tools/browser-wait-for.ts +0 -12
- package/src/config/bundled-skills/chatgpt-import/SKILL.md +0 -27
- package/src/config/bundled-skills/chatgpt-import/TOOLS.json +0 -27
- package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +0 -378
- package/src/config/bundled-skills/gmail/SKILL.md +0 -221
- package/src/config/bundled-skills/gmail/TOOLS.json +0 -588
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +0 -256
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +0 -112
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +0 -44
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +0 -81
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +0 -108
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +0 -146
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +0 -53
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +0 -347
- package/src/config/bundled-skills/gmail/tools/gmail-preferences-tool.ts +0 -59
- package/src/config/bundled-skills/gmail/tools/gmail-preferences.ts +0 -82
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +0 -26
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +0 -347
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +0 -29
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +0 -122
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +0 -67
- package/src/config/bundled-skills/gmail/tools/scan-result-store.ts +0 -100
- package/src/config/bundled-skills/gmail/tools/shared.ts +0 -47
- package/src/config/bundled-skills/google-calendar/SKILL.md +0 -51
- package/src/config/bundled-skills/google-calendar/TOOLS.json +0 -226
- package/src/config/bundled-skills/google-calendar/calendar-client.ts +0 -223
- package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +0 -27
- package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +0 -48
- package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +0 -19
- package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +0 -36
- package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +0 -58
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +0 -17
- package/src/config/bundled-skills/google-calendar/types.ts +0 -97
- package/src/config/bundled-skills/outlook/SKILL.md +0 -196
- package/src/config/bundled-skills/outlook/TOOLS.json +0 -530
- package/src/config/bundled-skills/outlook/tools/outlook-attachments.ts +0 -85
- package/src/config/bundled-skills/outlook/tools/outlook-categories.ts +0 -77
- package/src/config/bundled-skills/outlook/tools/outlook-draft.ts +0 -84
- package/src/config/bundled-skills/outlook/tools/outlook-follow-up.ts +0 -94
- package/src/config/bundled-skills/outlook/tools/outlook-forward.ts +0 -49
- package/src/config/bundled-skills/outlook/tools/outlook-outreach-scan.ts +0 -237
- package/src/config/bundled-skills/outlook/tools/outlook-rules.ts +0 -161
- package/src/config/bundled-skills/outlook/tools/outlook-send-draft.ts +0 -32
- package/src/config/bundled-skills/outlook/tools/outlook-sender-digest.ts +0 -272
- package/src/config/bundled-skills/outlook/tools/outlook-trash.ts +0 -29
- package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +0 -129
- package/src/config/bundled-skills/outlook/tools/outlook-vacation.ts +0 -87
- package/src/config/bundled-skills/outlook/tools/shared.ts +0 -20
- package/src/config/bundled-skills/outlook-calendar/SKILL.md +0 -51
- package/src/config/bundled-skills/outlook-calendar/TOOLS.json +0 -221
- package/src/config/bundled-skills/outlook-calendar/calendar-client.ts +0 -252
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-check-availability.ts +0 -53
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-create-event.ts +0 -74
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-get-event.ts +0 -18
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-list-events.ts +0 -46
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-rsvp.ts +0 -36
- package/src/config/bundled-skills/outlook-calendar/tools/shared.ts +0 -17
- package/src/config/bundled-skills/outlook-calendar/types.ts +0 -120
- package/src/config/bundled-skills/slack/SKILL.md +0 -108
- package/src/config/bundled-skills/tasks/SKILL.md +0 -37
- package/src/config/bundled-skills/tasks/TOOLS.json +0 -353
- package/src/config/bundled-skills/tasks/icon.svg +0 -34
- package/src/config/bundled-skills/tasks/tools/task-delete.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-list-add.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-list-show.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-list-update.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-list.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-queue-run.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-run.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-save.ts +0 -12
- package/src/config/bundled-skills/watcher/SKILL.md +0 -31
- package/src/config/bundled-skills/watcher/TOOLS.json +0 -167
- package/src/config/bundled-skills/watcher/tools/watcher-create.ts +0 -12
- package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +0 -12
- package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +0 -12
- package/src/config/bundled-skills/watcher/tools/watcher-list.ts +0 -12
- package/src/config/bundled-skills/watcher/tools/watcher-update.ts +0 -12
- package/src/prompts/templates/UPDATES.md +0 -50
- package/src/prompts/update-bulletin-format.ts +0 -85
- package/src/prompts/update-bulletin-state.ts +0 -58
- package/src/prompts/update-bulletin-template-path.ts +0 -13
- package/src/prompts/update-bulletin.ts +0 -139
- package/src/shared/provider-env-vars.ts +0 -19
- package/src/tools/watcher/create.ts +0 -86
- package/src/tools/watcher/delete.ts +0 -36
- package/src/tools/watcher/digest.ts +0 -54
- package/src/tools/watcher/list.ts +0 -83
- package/src/tools/watcher/update.ts +0 -71
|
@@ -0,0 +1,1035 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests locking the exact payload shapes emitted by the
|
|
3
|
+
* standalone surface lifecycle (showStandaloneSurface + handleSurfaceAction).
|
|
4
|
+
*
|
|
5
|
+
* These tests verify that daemon-driven standalone surfaces (not LLM tool
|
|
6
|
+
* invocations) produce wire messages matching the contracts that macOS/iOS/web
|
|
7
|
+
* clients decode. Any payload shape drift here is a client compatibility break.
|
|
8
|
+
*
|
|
9
|
+
* Surface lifecycle under test:
|
|
10
|
+
* 1. `ui_surface_show` — daemon → client (showStandaloneSurface)
|
|
11
|
+
* 2. `ui_surface_action` — client → daemon (user click)
|
|
12
|
+
* 3. `ui_surface_complete` — daemon → client (handleSurfaceAction resolving standalone)
|
|
13
|
+
*
|
|
14
|
+
* The Swift client decodes these via:
|
|
15
|
+
* - UiSurfaceShowMessage (MessageTypes.swift)
|
|
16
|
+
* - UiSurfaceCompleteMessage (MessageTypes.swift)
|
|
17
|
+
* and dispatches them in ChatActionHandler → ChatViewModel+SurfaceHandling.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { describe, expect, test } from "bun:test";
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
buildCompletionSummary,
|
|
24
|
+
handleSurfaceAction,
|
|
25
|
+
showStandaloneSurface,
|
|
26
|
+
type SurfaceConversationContext,
|
|
27
|
+
} from "../daemon/conversation-surfaces.js";
|
|
28
|
+
import type { ServerMessage } from "../daemon/message-protocol.js";
|
|
29
|
+
|
|
30
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
function createMockContext(
|
|
33
|
+
overrides?: Partial<{
|
|
34
|
+
hasNoClient: boolean;
|
|
35
|
+
supportsDynamicUi: boolean;
|
|
36
|
+
channel: string;
|
|
37
|
+
}>,
|
|
38
|
+
): SurfaceConversationContext & {
|
|
39
|
+
sentMessages: ServerMessage[];
|
|
40
|
+
enqueuedMessages: Array<{ content: string; requestId: string }>;
|
|
41
|
+
} {
|
|
42
|
+
const sentMessages: ServerMessage[] = [];
|
|
43
|
+
const enqueuedMessages: Array<{ content: string; requestId: string }> = [];
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
conversationId: "payload-test-conv",
|
|
47
|
+
assistantId: undefined,
|
|
48
|
+
trustContext: undefined,
|
|
49
|
+
channelCapabilities: overrides?.channel
|
|
50
|
+
? {
|
|
51
|
+
channel: overrides.channel,
|
|
52
|
+
supportsDynamicUi: overrides.supportsDynamicUi ?? true,
|
|
53
|
+
}
|
|
54
|
+
: undefined,
|
|
55
|
+
traceEmitter: { emit: () => {} },
|
|
56
|
+
sendToClient: (msg: ServerMessage) => sentMessages.push(msg),
|
|
57
|
+
broadcastToAllClients: (msg: ServerMessage) => sentMessages.push(msg),
|
|
58
|
+
pendingSurfaceActions: new Map(),
|
|
59
|
+
lastSurfaceAction: new Map(),
|
|
60
|
+
surfaceState: new Map(),
|
|
61
|
+
surfaceUndoStacks: new Map(),
|
|
62
|
+
accumulatedSurfaceState: new Map(),
|
|
63
|
+
surfaceActionRequestIds: new Set(),
|
|
64
|
+
pendingStandaloneSurfaces: new Map(),
|
|
65
|
+
currentTurnSurfaces: [],
|
|
66
|
+
hostCuProxy: undefined,
|
|
67
|
+
hasNoClient: overrides?.hasNoClient ?? false,
|
|
68
|
+
isProcessing: () => false,
|
|
69
|
+
enqueueMessage: (content, _attachments, _onEvent, requestId) => {
|
|
70
|
+
enqueuedMessages.push({ content, requestId });
|
|
71
|
+
return { queued: false, requestId };
|
|
72
|
+
},
|
|
73
|
+
getQueueDepth: () => 0,
|
|
74
|
+
processMessage: async () => "msg-id",
|
|
75
|
+
withSurface: async <T>(_surfaceId: string, fn: () => T | Promise<T>) =>
|
|
76
|
+
fn(),
|
|
77
|
+
sentMessages,
|
|
78
|
+
enqueuedMessages,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
type AnyRecord = Record<string, unknown>;
|
|
83
|
+
|
|
84
|
+
function findByType(
|
|
85
|
+
messages: ServerMessage[],
|
|
86
|
+
type: string,
|
|
87
|
+
): AnyRecord | undefined {
|
|
88
|
+
return messages.find(
|
|
89
|
+
(m) => (m as unknown as AnyRecord).type === type,
|
|
90
|
+
) as unknown as AnyRecord | undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function findAllByType(messages: ServerMessage[], type: string): AnyRecord[] {
|
|
94
|
+
return messages.filter(
|
|
95
|
+
(m) => (m as unknown as AnyRecord).type === type,
|
|
96
|
+
) as unknown as AnyRecord[];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── Confirmation surface payload shapes ──────────────────────────────
|
|
100
|
+
|
|
101
|
+
describe("standalone confirmation surface payload shapes", () => {
|
|
102
|
+
test("ui_surface_show payload matches UiSurfaceShowMessage contract", async () => {
|
|
103
|
+
const ctx = createMockContext();
|
|
104
|
+
|
|
105
|
+
const resultPromise = showStandaloneSurface(
|
|
106
|
+
ctx,
|
|
107
|
+
{
|
|
108
|
+
conversationId: "payload-test-conv",
|
|
109
|
+
surfaceType: "confirmation",
|
|
110
|
+
title: "Delete project?",
|
|
111
|
+
data: {
|
|
112
|
+
message: "This will permanently delete the project and all data.",
|
|
113
|
+
detail: "This action cannot be undone.",
|
|
114
|
+
confirmLabel: "Delete",
|
|
115
|
+
cancelLabel: "Keep",
|
|
116
|
+
destructive: true,
|
|
117
|
+
},
|
|
118
|
+
actions: [
|
|
119
|
+
{ id: "confirm", label: "Delete", variant: "danger" },
|
|
120
|
+
{ id: "cancel", label: "Keep", variant: "secondary" },
|
|
121
|
+
],
|
|
122
|
+
timeoutMs: 60_000,
|
|
123
|
+
},
|
|
124
|
+
"payload-surf-1",
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const showMsg = findByType(ctx.sentMessages, "ui_surface_show");
|
|
128
|
+
expect(showMsg).toBeDefined();
|
|
129
|
+
|
|
130
|
+
// ── Fields the Swift UiSurfaceShowMessage struct decodes ──
|
|
131
|
+
// These are the exact keys the client expects. Missing or renamed
|
|
132
|
+
// keys will cause a decoding failure on the client.
|
|
133
|
+
expect(showMsg!.type).toBe("ui_surface_show");
|
|
134
|
+
expect(showMsg!.conversationId).toBe("payload-test-conv");
|
|
135
|
+
expect(showMsg!.surfaceId).toBe("payload-surf-1");
|
|
136
|
+
expect(showMsg!.surfaceType).toBe("confirmation");
|
|
137
|
+
expect(showMsg!.title).toBe("Delete project?");
|
|
138
|
+
expect(showMsg!.display).toBe("inline");
|
|
139
|
+
|
|
140
|
+
// ── data field: ConfirmationSurfaceData ──
|
|
141
|
+
const data = showMsg!.data as AnyRecord;
|
|
142
|
+
expect(data.message).toBe(
|
|
143
|
+
"This will permanently delete the project and all data.",
|
|
144
|
+
);
|
|
145
|
+
expect(data.detail).toBe("This action cannot be undone.");
|
|
146
|
+
expect(data.confirmLabel).toBe("Delete");
|
|
147
|
+
expect(data.cancelLabel).toBe("Keep");
|
|
148
|
+
expect(data.destructive).toBe(true);
|
|
149
|
+
|
|
150
|
+
// ── actions array: SurfaceAction[] ──
|
|
151
|
+
const actions = showMsg!.actions as Array<AnyRecord>;
|
|
152
|
+
expect(actions).toHaveLength(2);
|
|
153
|
+
expect(actions[0].id).toBe("confirm");
|
|
154
|
+
expect(actions[0].label).toBe("Delete");
|
|
155
|
+
expect(actions[0].style).toBe("destructive"); // "danger" maps to "destructive"
|
|
156
|
+
expect(actions[1].id).toBe("cancel");
|
|
157
|
+
expect(actions[1].label).toBe("Keep");
|
|
158
|
+
expect(actions[1].style).toBe("secondary");
|
|
159
|
+
|
|
160
|
+
// Resolve to avoid dangling timer
|
|
161
|
+
await handleSurfaceAction(ctx, "payload-surf-1", "confirm");
|
|
162
|
+
await resultPromise;
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("ui_surface_complete payload on confirm matches UiSurfaceCompleteMessage contract", async () => {
|
|
166
|
+
const ctx = createMockContext();
|
|
167
|
+
|
|
168
|
+
const resultPromise = showStandaloneSurface(
|
|
169
|
+
ctx,
|
|
170
|
+
{
|
|
171
|
+
conversationId: "payload-test-conv",
|
|
172
|
+
surfaceType: "confirmation",
|
|
173
|
+
data: {
|
|
174
|
+
message: "Proceed with deployment?",
|
|
175
|
+
confirmLabel: "Deploy",
|
|
176
|
+
cancelLabel: "Abort",
|
|
177
|
+
},
|
|
178
|
+
timeoutMs: 60_000,
|
|
179
|
+
},
|
|
180
|
+
"payload-surf-2",
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
// Clear show messages
|
|
184
|
+
ctx.sentMessages.length = 0;
|
|
185
|
+
|
|
186
|
+
// Simulate user clicking confirm with submitted data
|
|
187
|
+
await handleSurfaceAction(ctx, "payload-surf-2", "confirm", {
|
|
188
|
+
environment: "production",
|
|
189
|
+
});
|
|
190
|
+
await resultPromise;
|
|
191
|
+
|
|
192
|
+
const completeMsg = findByType(ctx.sentMessages, "ui_surface_complete");
|
|
193
|
+
expect(completeMsg).toBeDefined();
|
|
194
|
+
|
|
195
|
+
// ── Fields the Swift UiSurfaceCompleteMessage struct decodes ──
|
|
196
|
+
expect(completeMsg!.type).toBe("ui_surface_complete");
|
|
197
|
+
expect(completeMsg!.conversationId).toBe("payload-test-conv");
|
|
198
|
+
expect(completeMsg!.surfaceId).toBe("payload-surf-2");
|
|
199
|
+
expect(typeof completeMsg!.summary).toBe("string");
|
|
200
|
+
expect(completeMsg!.summary).toBe('User chose: "Deploy"');
|
|
201
|
+
// submittedData should contain the action data from the user click
|
|
202
|
+
expect(completeMsg!.submittedData).toEqual({ environment: "production" });
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("ui_surface_complete payload on cancel matches UiSurfaceCompleteMessage contract", async () => {
|
|
206
|
+
const ctx = createMockContext();
|
|
207
|
+
|
|
208
|
+
const resultPromise = showStandaloneSurface(
|
|
209
|
+
ctx,
|
|
210
|
+
{
|
|
211
|
+
conversationId: "payload-test-conv",
|
|
212
|
+
surfaceType: "confirmation",
|
|
213
|
+
data: {
|
|
214
|
+
message: "Discard changes?",
|
|
215
|
+
cancelLabel: "Keep editing",
|
|
216
|
+
},
|
|
217
|
+
timeoutMs: 60_000,
|
|
218
|
+
},
|
|
219
|
+
"payload-surf-3",
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
ctx.sentMessages.length = 0;
|
|
223
|
+
|
|
224
|
+
await handleSurfaceAction(ctx, "payload-surf-3", "cancel");
|
|
225
|
+
await resultPromise;
|
|
226
|
+
|
|
227
|
+
const completeMsg = findByType(ctx.sentMessages, "ui_surface_complete");
|
|
228
|
+
expect(completeMsg).toBeDefined();
|
|
229
|
+
|
|
230
|
+
expect(completeMsg!.type).toBe("ui_surface_complete");
|
|
231
|
+
expect(completeMsg!.conversationId).toBe("payload-test-conv");
|
|
232
|
+
expect(completeMsg!.surfaceId).toBe("payload-surf-3");
|
|
233
|
+
expect(completeMsg!.summary).toBe('User chose: "Keep editing"');
|
|
234
|
+
// No submittedData on cancel without explicit data
|
|
235
|
+
expect(completeMsg!.submittedData).toBeUndefined();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("ui_surface_complete on timeout matches UiSurfaceCompleteMessage contract", async () => {
|
|
239
|
+
const ctx = createMockContext();
|
|
240
|
+
|
|
241
|
+
const resultPromise = showStandaloneSurface(
|
|
242
|
+
ctx,
|
|
243
|
+
{
|
|
244
|
+
conversationId: "payload-test-conv",
|
|
245
|
+
surfaceType: "confirmation",
|
|
246
|
+
data: { message: "Quick confirm" },
|
|
247
|
+
timeoutMs: 50,
|
|
248
|
+
},
|
|
249
|
+
"payload-surf-4",
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
await resultPromise;
|
|
253
|
+
|
|
254
|
+
const completeMsg = findByType(ctx.sentMessages, "ui_surface_complete");
|
|
255
|
+
expect(completeMsg).toBeDefined();
|
|
256
|
+
|
|
257
|
+
expect(completeMsg!.type).toBe("ui_surface_complete");
|
|
258
|
+
expect(completeMsg!.conversationId).toBe("payload-test-conv");
|
|
259
|
+
expect(completeMsg!.surfaceId).toBe("payload-surf-4");
|
|
260
|
+
expect(completeMsg!.summary).toBe("Timed out");
|
|
261
|
+
// No submittedData on timeout
|
|
262
|
+
expect(completeMsg!).not.toHaveProperty("submittedData");
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// ── Form surface payload shapes ──────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
describe("standalone form surface payload shapes", () => {
|
|
269
|
+
test("ui_surface_show payload for form matches UiSurfaceShowMessage contract", async () => {
|
|
270
|
+
const ctx = createMockContext();
|
|
271
|
+
|
|
272
|
+
const resultPromise = showStandaloneSurface(
|
|
273
|
+
ctx,
|
|
274
|
+
{
|
|
275
|
+
conversationId: "payload-test-conv",
|
|
276
|
+
surfaceType: "form",
|
|
277
|
+
title: "Configure settings",
|
|
278
|
+
data: {
|
|
279
|
+
description: "Adjust your preferences below.",
|
|
280
|
+
fields: [
|
|
281
|
+
{
|
|
282
|
+
id: "name",
|
|
283
|
+
type: "text",
|
|
284
|
+
label: "Display Name",
|
|
285
|
+
placeholder: "Enter your name",
|
|
286
|
+
required: true,
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
id: "theme",
|
|
290
|
+
type: "select",
|
|
291
|
+
label: "Theme",
|
|
292
|
+
options: [
|
|
293
|
+
{ label: "Light", value: "light" },
|
|
294
|
+
{ label: "Dark", value: "dark" },
|
|
295
|
+
],
|
|
296
|
+
defaultValue: "dark",
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
id: "notifications",
|
|
300
|
+
type: "toggle",
|
|
301
|
+
label: "Enable notifications",
|
|
302
|
+
defaultValue: true,
|
|
303
|
+
},
|
|
304
|
+
],
|
|
305
|
+
submitLabel: "Save",
|
|
306
|
+
},
|
|
307
|
+
timeoutMs: 60_000,
|
|
308
|
+
},
|
|
309
|
+
"payload-surf-form-1",
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
const showMsg = findByType(ctx.sentMessages, "ui_surface_show");
|
|
313
|
+
expect(showMsg).toBeDefined();
|
|
314
|
+
|
|
315
|
+
// ── Core wire fields ──
|
|
316
|
+
expect(showMsg!.type).toBe("ui_surface_show");
|
|
317
|
+
expect(showMsg!.conversationId).toBe("payload-test-conv");
|
|
318
|
+
expect(showMsg!.surfaceId).toBe("payload-surf-form-1");
|
|
319
|
+
expect(showMsg!.surfaceType).toBe("form");
|
|
320
|
+
expect(showMsg!.title).toBe("Configure settings");
|
|
321
|
+
expect(showMsg!.display).toBe("inline");
|
|
322
|
+
|
|
323
|
+
// ── data field: FormSurfaceData ──
|
|
324
|
+
const data = showMsg!.data as AnyRecord;
|
|
325
|
+
expect(data.description).toBe("Adjust your preferences below.");
|
|
326
|
+
expect(data.submitLabel).toBe("Save");
|
|
327
|
+
|
|
328
|
+
const fields = data.fields as Array<AnyRecord>;
|
|
329
|
+
expect(fields).toHaveLength(3);
|
|
330
|
+
|
|
331
|
+
// Text field
|
|
332
|
+
expect(fields[0].id).toBe("name");
|
|
333
|
+
expect(fields[0].type).toBe("text");
|
|
334
|
+
expect(fields[0].label).toBe("Display Name");
|
|
335
|
+
expect(fields[0].placeholder).toBe("Enter your name");
|
|
336
|
+
expect(fields[0].required).toBe(true);
|
|
337
|
+
|
|
338
|
+
// Select field
|
|
339
|
+
expect(fields[1].id).toBe("theme");
|
|
340
|
+
expect(fields[1].type).toBe("select");
|
|
341
|
+
expect(fields[1].label).toBe("Theme");
|
|
342
|
+
expect(fields[1].options).toEqual([
|
|
343
|
+
{ label: "Light", value: "light" },
|
|
344
|
+
{ label: "Dark", value: "dark" },
|
|
345
|
+
]);
|
|
346
|
+
expect(fields[1].defaultValue).toBe("dark");
|
|
347
|
+
|
|
348
|
+
// Toggle field
|
|
349
|
+
expect(fields[2].id).toBe("notifications");
|
|
350
|
+
expect(fields[2].type).toBe("toggle");
|
|
351
|
+
expect(fields[2].label).toBe("Enable notifications");
|
|
352
|
+
expect(fields[2].defaultValue).toBe(true);
|
|
353
|
+
|
|
354
|
+
// Resolve to avoid dangling timer
|
|
355
|
+
await handleSurfaceAction(ctx, "payload-surf-form-1", "submit", {
|
|
356
|
+
name: "Alice",
|
|
357
|
+
});
|
|
358
|
+
await resultPromise;
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test("ui_surface_complete payload on form submit matches UiSurfaceCompleteMessage contract", async () => {
|
|
362
|
+
const ctx = createMockContext();
|
|
363
|
+
|
|
364
|
+
const resultPromise = showStandaloneSurface(
|
|
365
|
+
ctx,
|
|
366
|
+
{
|
|
367
|
+
conversationId: "payload-test-conv",
|
|
368
|
+
surfaceType: "form",
|
|
369
|
+
title: "User info",
|
|
370
|
+
data: {
|
|
371
|
+
fields: [
|
|
372
|
+
{ id: "email", type: "text", label: "Email", required: true },
|
|
373
|
+
{ id: "age", type: "number", label: "Age" },
|
|
374
|
+
],
|
|
375
|
+
},
|
|
376
|
+
timeoutMs: 60_000,
|
|
377
|
+
},
|
|
378
|
+
"payload-surf-form-2",
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
ctx.sentMessages.length = 0;
|
|
382
|
+
|
|
383
|
+
await handleSurfaceAction(ctx, "payload-surf-form-2", "submit", {
|
|
384
|
+
email: "alice@example.com",
|
|
385
|
+
age: 30,
|
|
386
|
+
});
|
|
387
|
+
await resultPromise;
|
|
388
|
+
|
|
389
|
+
const completeMsg = findByType(ctx.sentMessages, "ui_surface_complete");
|
|
390
|
+
expect(completeMsg).toBeDefined();
|
|
391
|
+
|
|
392
|
+
expect(completeMsg!.type).toBe("ui_surface_complete");
|
|
393
|
+
expect(completeMsg!.conversationId).toBe("payload-test-conv");
|
|
394
|
+
expect(completeMsg!.surfaceId).toBe("payload-surf-form-2");
|
|
395
|
+
expect(completeMsg!.summary).toBe("Submitted");
|
|
396
|
+
expect(completeMsg!.submittedData).toEqual({
|
|
397
|
+
email: "alice@example.com",
|
|
398
|
+
age: 30,
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
test("form dismiss action resolves as cancelled with correct payload", async () => {
|
|
403
|
+
const ctx = createMockContext();
|
|
404
|
+
|
|
405
|
+
const resultPromise = showStandaloneSurface(
|
|
406
|
+
ctx,
|
|
407
|
+
{
|
|
408
|
+
conversationId: "payload-test-conv",
|
|
409
|
+
surfaceType: "form",
|
|
410
|
+
data: { fields: [{ id: "x", type: "text", label: "X" }] },
|
|
411
|
+
timeoutMs: 60_000,
|
|
412
|
+
},
|
|
413
|
+
"payload-surf-form-3",
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
ctx.sentMessages.length = 0;
|
|
417
|
+
|
|
418
|
+
await handleSurfaceAction(ctx, "payload-surf-form-3", "dismiss");
|
|
419
|
+
const result = await resultPromise;
|
|
420
|
+
|
|
421
|
+
// Standalone result
|
|
422
|
+
expect(result.status).toBe("cancelled");
|
|
423
|
+
expect(result.surfaceId).toBe("payload-surf-form-3");
|
|
424
|
+
expect(result.actionId).toBe("dismiss");
|
|
425
|
+
|
|
426
|
+
// Client-facing ui_surface_complete
|
|
427
|
+
const completeMsg = findByType(ctx.sentMessages, "ui_surface_complete");
|
|
428
|
+
expect(completeMsg).toBeDefined();
|
|
429
|
+
expect(completeMsg!.surfaceId).toBe("payload-surf-form-3");
|
|
430
|
+
expect(typeof completeMsg!.summary).toBe("string");
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
// ── Cross-surface contract invariants ────────────────────────────────
|
|
435
|
+
|
|
436
|
+
describe("standalone surface contract invariants", () => {
|
|
437
|
+
test("standalone surfaces never enqueue LLM messages", async () => {
|
|
438
|
+
const ctx = createMockContext();
|
|
439
|
+
|
|
440
|
+
// Confirmation flow
|
|
441
|
+
const p1 = showStandaloneSurface(
|
|
442
|
+
ctx,
|
|
443
|
+
{
|
|
444
|
+
conversationId: "payload-test-conv",
|
|
445
|
+
surfaceType: "confirmation",
|
|
446
|
+
data: { message: "Yes?" },
|
|
447
|
+
timeoutMs: 60_000,
|
|
448
|
+
},
|
|
449
|
+
"invariant-surf-1",
|
|
450
|
+
);
|
|
451
|
+
await handleSurfaceAction(ctx, "invariant-surf-1", "confirm");
|
|
452
|
+
await p1;
|
|
453
|
+
|
|
454
|
+
// Form flow
|
|
455
|
+
const p2 = showStandaloneSurface(
|
|
456
|
+
ctx,
|
|
457
|
+
{
|
|
458
|
+
conversationId: "payload-test-conv",
|
|
459
|
+
surfaceType: "form",
|
|
460
|
+
data: { fields: [] },
|
|
461
|
+
timeoutMs: 60_000,
|
|
462
|
+
},
|
|
463
|
+
"invariant-surf-2",
|
|
464
|
+
);
|
|
465
|
+
await handleSurfaceAction(ctx, "invariant-surf-2", "submit", {
|
|
466
|
+
val: "test",
|
|
467
|
+
});
|
|
468
|
+
await p2;
|
|
469
|
+
|
|
470
|
+
// No messages should have been enqueued to the LLM for standalone surfaces
|
|
471
|
+
expect(ctx.enqueuedMessages).toHaveLength(0);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
test("every ui_surface_show has required fields for Swift deserialization", async () => {
|
|
475
|
+
const ctx = createMockContext();
|
|
476
|
+
|
|
477
|
+
const p1 = showStandaloneSurface(
|
|
478
|
+
ctx,
|
|
479
|
+
{
|
|
480
|
+
conversationId: "payload-test-conv",
|
|
481
|
+
surfaceType: "confirmation",
|
|
482
|
+
data: { message: "A?" },
|
|
483
|
+
timeoutMs: 60_000,
|
|
484
|
+
},
|
|
485
|
+
"schema-surf-1",
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
const p2 = showStandaloneSurface(
|
|
489
|
+
ctx,
|
|
490
|
+
{
|
|
491
|
+
conversationId: "payload-test-conv",
|
|
492
|
+
surfaceType: "form",
|
|
493
|
+
data: { fields: [] },
|
|
494
|
+
timeoutMs: 60_000,
|
|
495
|
+
},
|
|
496
|
+
"schema-surf-2",
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
const showMessages = findAllByType(ctx.sentMessages, "ui_surface_show");
|
|
500
|
+
expect(showMessages).toHaveLength(2);
|
|
501
|
+
|
|
502
|
+
for (const msg of showMessages) {
|
|
503
|
+
// Required fields per UiSurfaceShowMessage(Decodable) in MessageTypes.swift:
|
|
504
|
+
// conversationId: String? — present (nullable but present)
|
|
505
|
+
// surfaceId: String — required
|
|
506
|
+
// surfaceType: String — required
|
|
507
|
+
// data: AnyCodable — required
|
|
508
|
+
expect(msg).toHaveProperty("conversationId");
|
|
509
|
+
expect(typeof msg.surfaceId).toBe("string");
|
|
510
|
+
expect(typeof msg.surfaceType).toBe("string");
|
|
511
|
+
expect(msg.data).toBeDefined();
|
|
512
|
+
expect(msg.data).not.toBeNull();
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Cleanup
|
|
516
|
+
await handleSurfaceAction(ctx, "schema-surf-1", "confirm");
|
|
517
|
+
await handleSurfaceAction(ctx, "schema-surf-2", "submit", {});
|
|
518
|
+
await p1;
|
|
519
|
+
await p2;
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
test("every ui_surface_complete has required fields for Swift deserialization", async () => {
|
|
523
|
+
const ctx = createMockContext();
|
|
524
|
+
|
|
525
|
+
const p1 = showStandaloneSurface(
|
|
526
|
+
ctx,
|
|
527
|
+
{
|
|
528
|
+
conversationId: "payload-test-conv",
|
|
529
|
+
surfaceType: "confirmation",
|
|
530
|
+
data: { message: "B?" },
|
|
531
|
+
timeoutMs: 60_000,
|
|
532
|
+
},
|
|
533
|
+
"schema-surf-3",
|
|
534
|
+
);
|
|
535
|
+
await handleSurfaceAction(ctx, "schema-surf-3", "confirm");
|
|
536
|
+
await p1;
|
|
537
|
+
|
|
538
|
+
const p2 = showStandaloneSurface(
|
|
539
|
+
ctx,
|
|
540
|
+
{
|
|
541
|
+
conversationId: "payload-test-conv",
|
|
542
|
+
surfaceType: "form",
|
|
543
|
+
data: { fields: [] },
|
|
544
|
+
timeoutMs: 60_000,
|
|
545
|
+
},
|
|
546
|
+
"schema-surf-4",
|
|
547
|
+
);
|
|
548
|
+
await handleSurfaceAction(ctx, "schema-surf-4", "submit", { k: "v" });
|
|
549
|
+
await p2;
|
|
550
|
+
|
|
551
|
+
const completeMessages = findAllByType(
|
|
552
|
+
ctx.sentMessages,
|
|
553
|
+
"ui_surface_complete",
|
|
554
|
+
);
|
|
555
|
+
expect(completeMessages.length).toBeGreaterThanOrEqual(2);
|
|
556
|
+
|
|
557
|
+
for (const msg of completeMessages) {
|
|
558
|
+
// Required fields per UiSurfaceCompleteMessage(Decodable) in MessageTypes.swift:
|
|
559
|
+
// conversationId: String? — present (nullable but present)
|
|
560
|
+
// surfaceId: String — required
|
|
561
|
+
// summary: String — required
|
|
562
|
+
// submittedData: [String: AnyCodable]? — optional
|
|
563
|
+
expect(msg).toHaveProperty("conversationId");
|
|
564
|
+
expect(typeof msg.surfaceId).toBe("string");
|
|
565
|
+
expect(typeof msg.summary).toBe("string");
|
|
566
|
+
expect(msg.summary).not.toBe("");
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
test("standalone surface cleanup leaves no stale state", async () => {
|
|
571
|
+
const ctx = createMockContext();
|
|
572
|
+
|
|
573
|
+
const resultPromise = showStandaloneSurface(
|
|
574
|
+
ctx,
|
|
575
|
+
{
|
|
576
|
+
conversationId: "payload-test-conv",
|
|
577
|
+
surfaceType: "confirmation",
|
|
578
|
+
data: { message: "Clean?" },
|
|
579
|
+
timeoutMs: 60_000,
|
|
580
|
+
},
|
|
581
|
+
"cleanup-surf-1",
|
|
582
|
+
);
|
|
583
|
+
|
|
584
|
+
// Verify state exists before action
|
|
585
|
+
expect(ctx.pendingStandaloneSurfaces!.has("cleanup-surf-1")).toBe(true);
|
|
586
|
+
expect(ctx.surfaceState.has("cleanup-surf-1")).toBe(true);
|
|
587
|
+
|
|
588
|
+
await handleSurfaceAction(ctx, "cleanup-surf-1", "confirm");
|
|
589
|
+
await resultPromise;
|
|
590
|
+
|
|
591
|
+
// After resolution, all related state maps should be clean
|
|
592
|
+
expect(ctx.pendingStandaloneSurfaces!.has("cleanup-surf-1")).toBe(false);
|
|
593
|
+
expect(ctx.surfaceState.has("cleanup-surf-1")).toBe(false);
|
|
594
|
+
expect(ctx.pendingSurfaceActions.has("cleanup-surf-1")).toBe(false);
|
|
595
|
+
expect(ctx.lastSurfaceAction.has("cleanup-surf-1")).toBe(false);
|
|
596
|
+
expect(ctx.accumulatedSurfaceState.has("cleanup-surf-1")).toBe(false);
|
|
597
|
+
expect(ctx.surfaceUndoStacks.has("cleanup-surf-1")).toBe(false);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
test("action variant mapping: danger → destructive, unset → secondary", async () => {
|
|
601
|
+
const ctx = createMockContext();
|
|
602
|
+
|
|
603
|
+
const resultPromise = showStandaloneSurface(
|
|
604
|
+
ctx,
|
|
605
|
+
{
|
|
606
|
+
conversationId: "payload-test-conv",
|
|
607
|
+
surfaceType: "confirmation",
|
|
608
|
+
data: { message: "Variants?" },
|
|
609
|
+
actions: [
|
|
610
|
+
{ id: "a", label: "Primary", variant: "primary" },
|
|
611
|
+
{ id: "b", label: "Danger", variant: "danger" },
|
|
612
|
+
{ id: "c", label: "Secondary", variant: "secondary" },
|
|
613
|
+
{ id: "d", label: "Default" }, // no variant
|
|
614
|
+
],
|
|
615
|
+
timeoutMs: 60_000,
|
|
616
|
+
},
|
|
617
|
+
"variant-surf-1",
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
const showMsg = findByType(ctx.sentMessages, "ui_surface_show");
|
|
621
|
+
const actions = showMsg!.actions as Array<AnyRecord>;
|
|
622
|
+
|
|
623
|
+
// Verify the mapping matches what Swift SurfaceActionButton expects:
|
|
624
|
+
// "primary" → "primary"
|
|
625
|
+
// "danger" → "destructive"
|
|
626
|
+
// "secondary" → "secondary"
|
|
627
|
+
// undefined → "secondary" (default)
|
|
628
|
+
expect(actions[0].style).toBe("primary");
|
|
629
|
+
expect(actions[1].style).toBe("destructive");
|
|
630
|
+
expect(actions[2].style).toBe("secondary");
|
|
631
|
+
expect(actions[3].style).toBe("secondary");
|
|
632
|
+
|
|
633
|
+
await handleSurfaceAction(ctx, "variant-surf-1", "a");
|
|
634
|
+
await resultPromise;
|
|
635
|
+
});
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
// ── Completion summary consistency ───────────────────────────────────
|
|
639
|
+
|
|
640
|
+
describe("buildCompletionSummary for standalone surfaces", () => {
|
|
641
|
+
test("confirmation confirm with custom label", () => {
|
|
642
|
+
expect(
|
|
643
|
+
buildCompletionSummary(
|
|
644
|
+
"confirmation",
|
|
645
|
+
"confirm",
|
|
646
|
+
{},
|
|
647
|
+
{ confirmLabel: "Yes, proceed" },
|
|
648
|
+
),
|
|
649
|
+
).toBe('User chose: "Yes, proceed"');
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
test("confirmation confirm without custom label", () => {
|
|
653
|
+
expect(buildCompletionSummary("confirmation", "confirm", {}, {})).toBe(
|
|
654
|
+
"Confirmed",
|
|
655
|
+
);
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
test("confirmation cancel with custom label", () => {
|
|
659
|
+
expect(
|
|
660
|
+
buildCompletionSummary(
|
|
661
|
+
"confirmation",
|
|
662
|
+
"cancel",
|
|
663
|
+
{},
|
|
664
|
+
{ cancelLabel: "Never mind" },
|
|
665
|
+
),
|
|
666
|
+
).toBe('User chose: "Never mind"');
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
test("confirmation cancel without custom label", () => {
|
|
670
|
+
expect(buildCompletionSummary("confirmation", "cancel", {}, {})).toBe(
|
|
671
|
+
"Cancelled",
|
|
672
|
+
);
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
test("form submit", () => {
|
|
676
|
+
expect(buildCompletionSummary("form", "submit", { k: "v" })).toBe(
|
|
677
|
+
"Submitted",
|
|
678
|
+
);
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
test("confirmation deny with custom cancelLabel uses the label", () => {
|
|
682
|
+
expect(
|
|
683
|
+
buildCompletionSummary(
|
|
684
|
+
"confirmation",
|
|
685
|
+
"deny",
|
|
686
|
+
{},
|
|
687
|
+
{ cancelLabel: "Keep" },
|
|
688
|
+
),
|
|
689
|
+
).toBe('User chose: "Keep"');
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
test("confirmation deny without custom label returns Denied", () => {
|
|
693
|
+
expect(buildCompletionSummary("confirmation", "deny", {}, {})).toBe(
|
|
694
|
+
"Denied",
|
|
695
|
+
);
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
test("unknown action ID is passed through", () => {
|
|
699
|
+
expect(buildCompletionSummary("confirmation", "reject", {}, {})).toBe(
|
|
700
|
+
"User selected: reject",
|
|
701
|
+
);
|
|
702
|
+
});
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
// ── Multi-page form payload shapes ───────────────────────────────────
|
|
706
|
+
|
|
707
|
+
describe("standalone multi-page form payload shapes", () => {
|
|
708
|
+
test("multi-page form preserves pages and pageLabels in emitted payload", async () => {
|
|
709
|
+
const ctx = createMockContext();
|
|
710
|
+
|
|
711
|
+
const resultPromise = showStandaloneSurface(
|
|
712
|
+
ctx,
|
|
713
|
+
{
|
|
714
|
+
conversationId: "payload-test-conv",
|
|
715
|
+
surfaceType: "form",
|
|
716
|
+
title: "Setup Wizard",
|
|
717
|
+
data: {
|
|
718
|
+
description: "Complete the setup steps.",
|
|
719
|
+
fields: [],
|
|
720
|
+
pages: [
|
|
721
|
+
{
|
|
722
|
+
id: "page-1",
|
|
723
|
+
title: "Personal Info",
|
|
724
|
+
description: "Enter your personal details.",
|
|
725
|
+
fields: [
|
|
726
|
+
{
|
|
727
|
+
id: "name",
|
|
728
|
+
type: "text",
|
|
729
|
+
label: "Full Name",
|
|
730
|
+
required: true,
|
|
731
|
+
},
|
|
732
|
+
{ id: "email", type: "text", label: "Email", required: true },
|
|
733
|
+
],
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
id: "page-2",
|
|
737
|
+
title: "Preferences",
|
|
738
|
+
fields: [
|
|
739
|
+
{
|
|
740
|
+
id: "theme",
|
|
741
|
+
type: "select",
|
|
742
|
+
label: "Theme",
|
|
743
|
+
options: [
|
|
744
|
+
{ label: "Light", value: "light" },
|
|
745
|
+
{ label: "Dark", value: "dark" },
|
|
746
|
+
],
|
|
747
|
+
},
|
|
748
|
+
{
|
|
749
|
+
id: "notifications",
|
|
750
|
+
type: "toggle",
|
|
751
|
+
label: "Enable notifications",
|
|
752
|
+
},
|
|
753
|
+
],
|
|
754
|
+
},
|
|
755
|
+
],
|
|
756
|
+
pageLabels: {
|
|
757
|
+
next: "Continue",
|
|
758
|
+
back: "Go Back",
|
|
759
|
+
submit: "Finish Setup",
|
|
760
|
+
},
|
|
761
|
+
submitLabel: "Finish Setup",
|
|
762
|
+
},
|
|
763
|
+
timeoutMs: 60_000,
|
|
764
|
+
},
|
|
765
|
+
"payload-surf-multipage-1",
|
|
766
|
+
);
|
|
767
|
+
|
|
768
|
+
const showMsg = findByType(ctx.sentMessages, "ui_surface_show");
|
|
769
|
+
expect(showMsg).toBeDefined();
|
|
770
|
+
|
|
771
|
+
// Core wire fields
|
|
772
|
+
expect(showMsg!.surfaceType).toBe("form");
|
|
773
|
+
expect(showMsg!.title).toBe("Setup Wizard");
|
|
774
|
+
|
|
775
|
+
// data field: FormSurfaceData with pages
|
|
776
|
+
const data = showMsg!.data as AnyRecord;
|
|
777
|
+
expect(data.description).toBe("Complete the setup steps.");
|
|
778
|
+
expect(data.submitLabel).toBe("Finish Setup");
|
|
779
|
+
|
|
780
|
+
// pages should be preserved exactly
|
|
781
|
+
const pages = data.pages as Array<AnyRecord>;
|
|
782
|
+
expect(pages).toBeDefined();
|
|
783
|
+
expect(pages).toHaveLength(2);
|
|
784
|
+
expect(pages[0].id).toBe("page-1");
|
|
785
|
+
expect(pages[0].title).toBe("Personal Info");
|
|
786
|
+
expect(pages[0].description).toBe("Enter your personal details.");
|
|
787
|
+
expect(pages[0].fields as Array<AnyRecord>).toHaveLength(2);
|
|
788
|
+
expect(pages[1].id).toBe("page-2");
|
|
789
|
+
expect(pages[1].title).toBe("Preferences");
|
|
790
|
+
expect(pages[1].fields as Array<AnyRecord>).toHaveLength(2);
|
|
791
|
+
|
|
792
|
+
// pageLabels should be preserved exactly
|
|
793
|
+
const pageLabels = data.pageLabels as AnyRecord;
|
|
794
|
+
expect(pageLabels).toBeDefined();
|
|
795
|
+
expect(pageLabels.next).toBe("Continue");
|
|
796
|
+
expect(pageLabels.back).toBe("Go Back");
|
|
797
|
+
expect(pageLabels.submit).toBe("Finish Setup");
|
|
798
|
+
|
|
799
|
+
// fields should still be a valid array (defensive normalization)
|
|
800
|
+
expect(Array.isArray(data.fields)).toBe(true);
|
|
801
|
+
|
|
802
|
+
// Resolve to avoid dangling timer
|
|
803
|
+
await handleSurfaceAction(ctx, "payload-surf-multipage-1", "submit", {
|
|
804
|
+
name: "Alice",
|
|
805
|
+
email: "alice@example.com",
|
|
806
|
+
theme: "dark",
|
|
807
|
+
notifications: true,
|
|
808
|
+
});
|
|
809
|
+
await resultPromise;
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
test("pages-only form (no top-level fields) normalizes fields to empty array", async () => {
|
|
813
|
+
const ctx = createMockContext();
|
|
814
|
+
|
|
815
|
+
const resultPromise = showStandaloneSurface(
|
|
816
|
+
ctx,
|
|
817
|
+
{
|
|
818
|
+
conversationId: "payload-test-conv",
|
|
819
|
+
surfaceType: "form",
|
|
820
|
+
title: "Wizard",
|
|
821
|
+
data: {
|
|
822
|
+
pages: [
|
|
823
|
+
{
|
|
824
|
+
id: "p1",
|
|
825
|
+
title: "Step 1",
|
|
826
|
+
fields: [{ id: "x", type: "text", label: "X" }],
|
|
827
|
+
},
|
|
828
|
+
],
|
|
829
|
+
pageLabels: { next: "Next", submit: "Done" },
|
|
830
|
+
},
|
|
831
|
+
timeoutMs: 60_000,
|
|
832
|
+
},
|
|
833
|
+
"payload-surf-pages-only",
|
|
834
|
+
);
|
|
835
|
+
|
|
836
|
+
const showMsg = findByType(ctx.sentMessages, "ui_surface_show");
|
|
837
|
+
const data = showMsg!.data as AnyRecord;
|
|
838
|
+
|
|
839
|
+
// pages should be preserved
|
|
840
|
+
expect(data.pages).toBeDefined();
|
|
841
|
+
expect(data.pages as Array<AnyRecord>).toHaveLength(1);
|
|
842
|
+
|
|
843
|
+
// pageLabels should be preserved
|
|
844
|
+
expect(data.pageLabels).toEqual({ next: "Next", submit: "Done" });
|
|
845
|
+
|
|
846
|
+
// fields should default to empty array (defensive normalization)
|
|
847
|
+
expect(data.fields).toEqual([]);
|
|
848
|
+
|
|
849
|
+
// Resolve to avoid dangling timer
|
|
850
|
+
await handleSurfaceAction(ctx, "payload-surf-pages-only", "submit", {
|
|
851
|
+
x: "val",
|
|
852
|
+
});
|
|
853
|
+
await resultPromise;
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
test("multi-page form submit resolves correctly end-to-end", async () => {
|
|
857
|
+
const ctx = createMockContext();
|
|
858
|
+
|
|
859
|
+
const resultPromise = showStandaloneSurface(
|
|
860
|
+
ctx,
|
|
861
|
+
{
|
|
862
|
+
conversationId: "payload-test-conv",
|
|
863
|
+
surfaceType: "form",
|
|
864
|
+
title: "Multi-Step",
|
|
865
|
+
data: {
|
|
866
|
+
pages: [
|
|
867
|
+
{
|
|
868
|
+
id: "p1",
|
|
869
|
+
title: "Step 1",
|
|
870
|
+
fields: [{ id: "a", type: "text", label: "A" }],
|
|
871
|
+
},
|
|
872
|
+
{
|
|
873
|
+
id: "p2",
|
|
874
|
+
title: "Step 2",
|
|
875
|
+
fields: [{ id: "b", type: "number", label: "B" }],
|
|
876
|
+
},
|
|
877
|
+
],
|
|
878
|
+
pageLabels: { next: "Next", back: "Previous", submit: "Complete" },
|
|
879
|
+
},
|
|
880
|
+
timeoutMs: 60_000,
|
|
881
|
+
},
|
|
882
|
+
"payload-surf-multipage-submit",
|
|
883
|
+
);
|
|
884
|
+
|
|
885
|
+
ctx.sentMessages.length = 0;
|
|
886
|
+
|
|
887
|
+
await handleSurfaceAction(ctx, "payload-surf-multipage-submit", "submit", {
|
|
888
|
+
a: "hello",
|
|
889
|
+
b: 42,
|
|
890
|
+
});
|
|
891
|
+
const result = await resultPromise;
|
|
892
|
+
|
|
893
|
+
// Result should be submitted with the form data
|
|
894
|
+
expect(result.status).toBe("submitted");
|
|
895
|
+
expect(result.submittedData).toEqual({ a: "hello", b: 42 });
|
|
896
|
+
|
|
897
|
+
// ui_surface_complete should have been emitted
|
|
898
|
+
const completeMsg = findByType(ctx.sentMessages, "ui_surface_complete");
|
|
899
|
+
expect(completeMsg).toBeDefined();
|
|
900
|
+
expect(completeMsg!.summary).toBe("Submitted");
|
|
901
|
+
expect(completeMsg!.submittedData).toEqual({ a: "hello", b: 42 });
|
|
902
|
+
});
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
// ── Forward-compatible additive keys (regression) ────────────────────
|
|
906
|
+
|
|
907
|
+
describe("standalone form forward-compatible payload preservation", () => {
|
|
908
|
+
test("additive keys not in FormSurfaceData are preserved through the pipeline", async () => {
|
|
909
|
+
// Regression test: the form surface pipeline must preserve all keys from
|
|
910
|
+
// the input data — including ones not declared in FormSurfaceData (e.g.
|
|
911
|
+
// keys added in newer protocol versions) — so that forward-compatible
|
|
912
|
+
// clients can consume them.
|
|
913
|
+
const ctx = createMockContext();
|
|
914
|
+
|
|
915
|
+
const resultPromise = showStandaloneSurface(
|
|
916
|
+
ctx,
|
|
917
|
+
{
|
|
918
|
+
conversationId: "payload-test-conv",
|
|
919
|
+
surfaceType: "form",
|
|
920
|
+
title: "Future Form",
|
|
921
|
+
data: {
|
|
922
|
+
description: "A form with future keys.",
|
|
923
|
+
fields: [{ id: "f1", type: "text", label: "Field 1" }],
|
|
924
|
+
submitLabel: "Go",
|
|
925
|
+
// Hypothetical future keys that the protocol may add
|
|
926
|
+
futureStringField: "hello",
|
|
927
|
+
futureNumberField: 99,
|
|
928
|
+
futureBooleanField: true,
|
|
929
|
+
futureObjectField: { nested: "value", count: 3 },
|
|
930
|
+
futureArrayField: ["a", "b", "c"],
|
|
931
|
+
},
|
|
932
|
+
timeoutMs: 60_000,
|
|
933
|
+
},
|
|
934
|
+
"payload-surf-forward-compat",
|
|
935
|
+
);
|
|
936
|
+
|
|
937
|
+
const showMsg = findByType(ctx.sentMessages, "ui_surface_show");
|
|
938
|
+
const data = showMsg!.data as AnyRecord;
|
|
939
|
+
|
|
940
|
+
// Known FormSurfaceData fields should be present
|
|
941
|
+
expect(data.description).toBe("A form with future keys.");
|
|
942
|
+
expect(data.submitLabel).toBe("Go");
|
|
943
|
+
expect(data.fields as Array<AnyRecord>).toHaveLength(1);
|
|
944
|
+
|
|
945
|
+
// Future additive keys must NOT be dropped
|
|
946
|
+
expect(data.futureStringField).toBe("hello");
|
|
947
|
+
expect(data.futureNumberField).toBe(99);
|
|
948
|
+
expect(data.futureBooleanField).toBe(true);
|
|
949
|
+
expect(data.futureObjectField).toEqual({ nested: "value", count: 3 });
|
|
950
|
+
expect(data.futureArrayField).toEqual(["a", "b", "c"]);
|
|
951
|
+
|
|
952
|
+
// Resolve to avoid dangling timer
|
|
953
|
+
await handleSurfaceAction(ctx, "payload-surf-forward-compat", "submit", {});
|
|
954
|
+
await resultPromise;
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
test("existing single-page form behavior is unchanged", async () => {
|
|
958
|
+
// Ensure the refactored code does not regress the basic single-page
|
|
959
|
+
// form path that existed before the pages/pageLabels fix.
|
|
960
|
+
const ctx = createMockContext();
|
|
961
|
+
|
|
962
|
+
const resultPromise = showStandaloneSurface(
|
|
963
|
+
ctx,
|
|
964
|
+
{
|
|
965
|
+
conversationId: "payload-test-conv",
|
|
966
|
+
surfaceType: "form",
|
|
967
|
+
title: "Simple Form",
|
|
968
|
+
data: {
|
|
969
|
+
description: "A basic form.",
|
|
970
|
+
fields: [
|
|
971
|
+
{ id: "name", type: "text", label: "Name", required: true },
|
|
972
|
+
{ id: "age", type: "number", label: "Age" },
|
|
973
|
+
],
|
|
974
|
+
submitLabel: "Submit",
|
|
975
|
+
},
|
|
976
|
+
timeoutMs: 60_000,
|
|
977
|
+
},
|
|
978
|
+
"payload-surf-simple-form",
|
|
979
|
+
);
|
|
980
|
+
|
|
981
|
+
const showMsg = findByType(ctx.sentMessages, "ui_surface_show");
|
|
982
|
+
const data = showMsg!.data as AnyRecord;
|
|
983
|
+
|
|
984
|
+
expect(data.description).toBe("A basic form.");
|
|
985
|
+
expect(data.submitLabel).toBe("Submit");
|
|
986
|
+
expect(data.fields as Array<AnyRecord>).toHaveLength(2);
|
|
987
|
+
expect((data.fields as Array<AnyRecord>)[0].id).toBe("name");
|
|
988
|
+
expect((data.fields as Array<AnyRecord>)[0].required).toBe(true);
|
|
989
|
+
expect((data.fields as Array<AnyRecord>)[1].id).toBe("age");
|
|
990
|
+
|
|
991
|
+
// pages/pageLabels should not be present for single-page forms
|
|
992
|
+
expect(data.pages).toBeUndefined();
|
|
993
|
+
expect(data.pageLabels).toBeUndefined();
|
|
994
|
+
|
|
995
|
+
// Resolve to avoid dangling timer
|
|
996
|
+
await handleSurfaceAction(ctx, "payload-surf-simple-form", "submit", {
|
|
997
|
+
name: "Bob",
|
|
998
|
+
age: 25,
|
|
999
|
+
});
|
|
1000
|
+
await resultPromise;
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
test("form with neither fields nor pages normalizes fields to empty array", async () => {
|
|
1004
|
+
// Defensive normalization: an empty/missing form payload should still
|
|
1005
|
+
// produce a valid FormSurfaceData with an empty fields array rather
|
|
1006
|
+
// than undefined or a missing key.
|
|
1007
|
+
const ctx = createMockContext();
|
|
1008
|
+
|
|
1009
|
+
const resultPromise = showStandaloneSurface(
|
|
1010
|
+
ctx,
|
|
1011
|
+
{
|
|
1012
|
+
conversationId: "payload-test-conv",
|
|
1013
|
+
surfaceType: "form",
|
|
1014
|
+
title: "Empty Form",
|
|
1015
|
+
data: {
|
|
1016
|
+
description: "No fields at all.",
|
|
1017
|
+
},
|
|
1018
|
+
timeoutMs: 60_000,
|
|
1019
|
+
},
|
|
1020
|
+
"payload-surf-empty-form",
|
|
1021
|
+
);
|
|
1022
|
+
|
|
1023
|
+
const showMsg = findByType(ctx.sentMessages, "ui_surface_show");
|
|
1024
|
+
const data = showMsg!.data as AnyRecord;
|
|
1025
|
+
|
|
1026
|
+
expect(data.description).toBe("No fields at all.");
|
|
1027
|
+
// fields must always be a valid array — never undefined
|
|
1028
|
+
expect(Array.isArray(data.fields)).toBe(true);
|
|
1029
|
+
expect(data.fields).toEqual([]);
|
|
1030
|
+
|
|
1031
|
+
// Resolve to avoid dangling timer
|
|
1032
|
+
await handleSurfaceAction(ctx, "payload-surf-empty-form", "submit", {});
|
|
1033
|
+
await resultPromise;
|
|
1034
|
+
});
|
|
1035
|
+
});
|