@vellumai/assistant 0.6.3 → 0.6.4
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 +273 -10
- package/Dockerfile +2 -3
- package/bun.lock +5 -13
- package/docs/backup-troubleshooting.md +52 -0
- package/docs/browser-use-architecture-phase2.md +174 -0
- package/docs/stt-provider-onboarding.md +120 -0
- package/knip.json +12 -2
- package/node_modules/@vellumai/ces-contracts/bun.lock +8 -6
- package/node_modules/@vellumai/ces-contracts/package.json +3 -3
- package/openapi.yaml +982 -72
- package/package.json +4 -6
- package/scripts/generate-openapi.ts +0 -1
- package/scripts/test.sh +73 -18
- package/src/__tests__/agent-image-optimize.test.ts +28 -0
- package/src/__tests__/agent-loop.test.ts +123 -0
- package/src/__tests__/anthropic-provider.test.ts +263 -10
- package/src/__tests__/auto-analysis-end-to-end.test.ts +550 -0
- package/src/__tests__/auto-analysis-prompt.test.ts +50 -0
- package/src/__tests__/browser-fill-credential.test.ts +11 -0
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
- package/src/__tests__/browser-skill-endstate.test.ts +31 -7
- package/src/__tests__/btw-routes.test.ts +7 -0
- package/src/__tests__/call-controller.test.ts +581 -20
- package/src/__tests__/catalog-files.test.ts +138 -0
- package/src/__tests__/channel-invite-transport.test.ts +2 -2
- package/src/__tests__/channel-readiness-routes.test.ts +16 -20
- package/src/__tests__/channel-readiness-service.test.ts +12 -7
- package/src/__tests__/checker.test.ts +157 -10
- package/src/__tests__/clawhub-files.test.ts +347 -0
- package/src/__tests__/commit-message-enrichment-service.test.ts +36 -19
- package/src/__tests__/config-analysis.test.ts +100 -0
- package/src/__tests__/config-schema.test.ts +1013 -66
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +339 -0
- package/src/__tests__/config-watcher.test.ts +43 -8
- package/src/__tests__/contact-store-user-file.test.ts +512 -0
- package/src/__tests__/contacts-write.test.ts +197 -0
- package/src/__tests__/context-window-manager.test.ts +88 -0
- package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop.test.ts +98 -2
- package/src/__tests__/conversation-confirmation-signals.test.ts +135 -0
- package/src/__tests__/conversation-error.test.ts +70 -0
- package/src/__tests__/conversation-history-web-search.test.ts +11 -4
- package/src/__tests__/conversation-init.benchmark.test.ts +6 -1
- package/src/__tests__/conversation-launcher-skill-regression.test.ts +51 -0
- package/src/__tests__/conversation-list-source.test.ts +145 -0
- package/src/__tests__/conversation-pre-run-repair.test.ts +2 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
- package/src/__tests__/conversation-queue.test.ts +901 -60
- package/src/__tests__/conversation-routes-disk-view.test.ts +270 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +55 -0
- package/src/__tests__/conversation-skill-tools.test.ts +7 -4
- package/src/__tests__/conversation-slash-commands.test.ts +33 -0
- package/src/__tests__/conversation-slash-queue.test.ts +89 -18
- package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
- package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +226 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
- package/src/__tests__/credential-health-service.test.ts +352 -0
- package/src/__tests__/credential-security-invariants.test.ts +5 -3
- package/src/__tests__/credential-vault-unit.test.ts +379 -3
- package/src/__tests__/credentials-cli.test.ts +40 -16
- package/src/__tests__/cross-provider-web-search.test.ts +146 -35
- package/src/__tests__/deterministic-verification-control-plane.test.ts +10 -1
- package/src/__tests__/device-id.test.ts +112 -0
- package/src/__tests__/docker-signing-key-bootstrap.test.ts +167 -4
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -3
- package/src/__tests__/email-html-renderer.test.ts +71 -0
- package/src/__tests__/email-invite-adapter.test.ts +36 -32
- package/src/__tests__/emit-event-signal.test.ts +71 -0
- package/src/__tests__/extension-id-sync-guard.test.ts +75 -8
- package/src/__tests__/fixtures/mock-chrome-extension.ts +11 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +206 -1
- package/src/__tests__/gateway-only-guard.test.ts +0 -1
- package/src/__tests__/gemini-provider.test.ts +64 -0
- package/src/__tests__/get-skill-detail-audit.test.ts +325 -0
- package/src/__tests__/gmail-archive-fallback.test.ts +193 -0
- package/src/__tests__/gmail-archive-gate.test.ts +246 -0
- package/src/__tests__/gmail-preferences.test.ts +117 -0
- package/src/__tests__/headless-browser-interactions.test.ts +43 -0
- package/src/__tests__/headless-browser-mode.test.ts +614 -0
- package/src/__tests__/headless-browser-navigate.test.ts +142 -5
- package/src/__tests__/headless-browser-read-tools.test.ts +11 -0
- package/src/__tests__/headless-browser-snapshot.test.ts +10 -0
- package/src/__tests__/heartbeat-service.test.ts +70 -17
- package/src/__tests__/home-state-routes.test.ts +162 -0
- package/src/__tests__/host-bash-proxy.test.ts +0 -5
- package/src/__tests__/host-browser-e2e-cloud.test.ts +138 -4
- package/src/__tests__/host-browser-e2e-self-hosted.test.ts +4 -4
- package/src/__tests__/host-browser-ws-events-e2e.test.ts +103 -0
- package/src/__tests__/host-cu-proxy.test.ts +0 -5
- package/src/__tests__/identity-intro-cache.test.ts +40 -10
- package/src/__tests__/init-feature-flag-overrides.test.ts +38 -112
- package/src/__tests__/jobs-store-upsert-debounced.test.ts +141 -0
- package/src/__tests__/llm-context-normalization.test.ts +488 -0
- package/src/__tests__/llm-context-route-provider.test.ts +86 -5
- package/src/__tests__/llm-usage-store.test.ts +363 -0
- package/src/__tests__/media-stream-output.test.ts +555 -0
- package/src/__tests__/media-stream-parser.test.ts +374 -0
- package/src/__tests__/media-stream-server-integration.test.ts +1234 -0
- package/src/__tests__/media-stream-stt-session.test.ts +588 -0
- package/src/__tests__/media-turn-detector.test.ts +440 -0
- package/src/__tests__/message-queue.test.ts +125 -0
- package/src/__tests__/migration-export-http.test.ts +6 -6
- package/src/__tests__/migration-import-commit-http.test.ts +8 -6
- package/src/__tests__/migration-import-preflight-http.test.ts +6 -5
- package/src/__tests__/migration-validate-http.test.ts +3 -3
- package/src/__tests__/mock-gateway-ipc.ts +151 -0
- package/src/__tests__/model-intents.test.ts +2 -2
- package/src/__tests__/oauth-apps-routes.test.ts +1 -0
- package/src/__tests__/oauth-cli.test.ts +2 -0
- package/src/__tests__/oauth-connect-orchestrator.test.ts +2 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +1 -0
- package/src/__tests__/oauth-providers-routes.test.ts +2 -0
- package/src/__tests__/oauth-store.test.ts +85 -0
- package/src/__tests__/oauth2-gateway-transport.test.ts +249 -6
- package/src/__tests__/onboarding-template-contract.test.ts +6 -13
- package/src/__tests__/openai-provider.test.ts +176 -0
- package/src/__tests__/openai-responses-cutover-guard.test.ts +184 -0
- package/src/__tests__/openai-responses-provider.test.ts +1105 -0
- package/src/__tests__/openrouter-token-estimation.test.ts +100 -0
- package/src/__tests__/outlook-unsubscribe.test.ts +31 -2
- package/src/__tests__/persona-resolver.test.ts +251 -0
- package/src/__tests__/platform-bash-auto-approve.test.ts +4 -0
- package/src/__tests__/platform.test.ts +92 -1
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +47 -0
- package/src/__tests__/prechat-onboarding-contract.test.ts +267 -0
- package/src/__tests__/pricing.test.ts +174 -0
- package/src/__tests__/qdrant-manager.test.ts +29 -8
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +194 -0
- package/src/__tests__/relationship-state-contract.test.ts +175 -0
- package/src/__tests__/relay-server.test.ts +423 -5
- package/src/__tests__/search-skills-unified.test.ts +118 -0
- package/src/__tests__/secret-scanner-executor.test.ts +4 -0
- package/src/__tests__/secure-keys.test.ts +107 -0
- package/src/__tests__/send-endpoint-busy.test.ts +5 -1
- package/src/__tests__/sequence-store.test.ts +1 -1
- package/src/__tests__/server-history-render.test.ts +49 -0
- package/src/__tests__/settings-routes.test.ts +201 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +1 -0
- package/src/__tests__/skills-file-content-endpoint.test.ts +276 -145
- package/src/__tests__/skills-files-catalog-fallback.test.ts +381 -93
- package/src/__tests__/skills.test.ts +5 -2
- package/src/__tests__/skillssh-files.test.ts +446 -0
- package/src/__tests__/slack-block-formatting.test.ts +110 -0
- package/src/__tests__/slack-channel-config.test.ts +564 -1
- package/src/__tests__/stt-catalog-parity.test.ts +282 -0
- package/src/__tests__/stt-stream-session.test.ts +535 -0
- package/src/__tests__/system-prompt.test.ts +112 -26
- package/src/__tests__/telephony-stt-routing.test.ts +329 -0
- package/src/__tests__/terminal-tools.test.ts +18 -7
- package/src/__tests__/test-preload.ts +18 -0
- package/src/__tests__/test-support/browser-skill-harness.ts +4 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +9 -5
- package/src/__tests__/tool-executor-shell-integration.test.ts +4 -0
- package/src/__tests__/tool-executor.test.ts +33 -24
- package/src/__tests__/tool-result-truncation.test.ts +36 -0
- package/src/__tests__/trust-store.test.ts +7 -1
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -1
- package/src/__tests__/tts-catalog-parity.test.ts +345 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +512 -114
- package/src/__tests__/twilio-routes.test.ts +376 -0
- package/src/__tests__/unicode.test.ts +293 -0
- package/src/__tests__/update-bulletin-format.test.ts +59 -0
- package/src/__tests__/update-bulletin.test.ts +206 -5
- package/src/__tests__/usage-routes.test.ts +25 -4
- package/src/__tests__/user-reference.test.ts +46 -61
- package/src/__tests__/verification-control-plane-policy.test.ts +4 -0
- package/src/__tests__/voice-config-update.test.ts +403 -0
- package/src/__tests__/voice-quality.test.ts +434 -19
- package/src/__tests__/workspace-heartbeat-service.test.ts +7 -0
- package/src/__tests__/workspace-migration-033-stt-service-explicit-config.test.ts +547 -0
- package/src/__tests__/workspace-migration-034-remove-calls-voice-transcription-provider.test.ts +596 -0
- package/src/__tests__/workspace-migration-drop-user-md.test.ts +368 -0
- package/src/__tests__/workspace-migration-meets.test.ts +244 -0
- package/src/__tests__/workspace-migration-seed-device-id.test.ts +14 -20
- package/src/__tests__/workspace-policy.test.ts +2 -0
- package/src/agent/image-optimize.ts +24 -12
- package/src/agent/loop.ts +43 -3
- package/src/backup/__tests__/backup-key.test.ts +152 -0
- package/src/backup/__tests__/backup-worker.test.ts +767 -0
- package/src/backup/__tests__/list-snapshots.test.ts +87 -0
- package/src/backup/__tests__/local-writer.test.ts +218 -0
- package/src/backup/__tests__/offsite-writer.test.ts +641 -0
- package/src/backup/__tests__/paths.test.ts +300 -0
- package/src/backup/__tests__/restore.test.ts +498 -0
- package/src/backup/__tests__/snapshot-lock.test.ts +352 -0
- package/src/backup/__tests__/stream-crypt.test.ts +228 -0
- package/src/backup/backup-key.ts +137 -0
- package/src/backup/backup-worker.ts +459 -0
- package/src/backup/list-snapshots.ts +147 -0
- package/src/backup/local-writer.ts +133 -0
- package/src/backup/offsite-writer.ts +222 -0
- package/src/backup/paths.ts +226 -0
- package/src/backup/restore.ts +322 -0
- package/src/backup/snapshot-lock.ts +431 -0
- package/src/backup/stream-crypt.ts +263 -0
- package/src/bundler/package-resolver.ts +4 -0
- package/src/calls/audio-store.ts +11 -5
- package/src/calls/call-controller.ts +226 -71
- package/src/calls/call-domain.ts +9 -0
- package/src/calls/call-speech-output.ts +190 -0
- package/src/calls/call-transport.ts +77 -0
- package/src/calls/media-stream-audio-transcode.ts +173 -0
- package/src/calls/media-stream-output.ts +660 -0
- package/src/calls/media-stream-parser.ts +300 -0
- package/src/calls/media-stream-protocol.ts +166 -0
- package/src/calls/media-stream-server.ts +592 -0
- package/src/calls/media-stream-stt-session.ts +460 -0
- package/src/calls/media-turn-detector.ts +230 -0
- package/src/calls/relay-server.ts +90 -75
- package/src/calls/resolve-call-tts-provider.ts +136 -0
- package/src/calls/telephony-stt-routing.ts +145 -0
- package/src/calls/tts-call-strategy.ts +161 -0
- package/src/calls/tts-text-sanitizer.ts +32 -16
- package/src/calls/twilio-routes.ts +281 -17
- package/src/calls/voice-quality.ts +78 -35
- package/src/calls/voice-session-bridge.ts +8 -1
- package/src/channels/types.ts +16 -0
- package/src/cli/__tests__/run-assistant-command.ts +11 -1
- package/src/cli/commands/__tests__/backup.test.ts +1165 -0
- package/src/cli/commands/__tests__/domain-register.test.ts +234 -0
- package/src/cli/commands/__tests__/domain-status.test.ts +132 -0
- package/src/cli/commands/__tests__/email-attachment.test.ts +422 -0
- package/src/cli/commands/__tests__/email-download.test.ts +16 -1
- package/src/cli/commands/__tests__/email-list.test.ts +22 -4
- package/src/cli/commands/__tests__/email-register.test.ts +4 -4
- package/src/cli/commands/__tests__/email-send.test.ts +37 -4
- package/src/cli/commands/__tests__/email-status.test.ts +5 -1
- package/src/cli/commands/__tests__/email-unregister.test.ts +34 -5
- package/src/cli/commands/backup.ts +993 -0
- package/src/cli/commands/conversations.ts +77 -0
- package/src/cli/commands/credentials.ts +0 -1
- package/src/cli/commands/domain.ts +210 -0
- package/src/cli/commands/email.ts +255 -3
- package/src/cli/commands/oauth/__tests__/connect.test.ts +12 -0
- package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +1 -0
- package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -0
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -0
- package/src/cli/commands/oauth/mode.ts +12 -3
- package/src/cli/commands/oauth/providers.ts +15 -0
- package/src/cli/commands/oauth/shared.ts +2 -1
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +4 -9
- package/src/cli/commands/platform/__tests__/connect.test.ts +6 -0
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +6 -0
- package/src/cli/program.ts +30 -4
- package/src/config/__tests__/backup-schema.test.ts +134 -0
- package/src/config/assistant-feature-flags.ts +61 -62
- package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +37 -1
- package/src/config/bundled-skills/browser/SKILL.md +30 -5
- package/src/config/bundled-skills/browser/TOOLS.json +123 -0
- package/src/config/bundled-skills/browser/tools/browser-attach.ts +12 -0
- package/src/config/bundled-skills/browser/tools/browser-detach.ts +12 -0
- package/src/config/bundled-skills/browser/tools/browser-status.ts +12 -0
- package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +17 -0
- package/src/config/bundled-skills/contacts/SKILL.md +2 -2
- package/src/config/bundled-skills/gmail/SKILL.md +53 -7
- package/src/config/bundled-skills/gmail/TOOLS.json +33 -3
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +116 -9
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +138 -11
- package/src/config/bundled-skills/gmail/tools/gmail-preferences-tool.ts +59 -0
- package/src/config/bundled-skills/gmail/tools/gmail-preferences.ts +82 -0
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +113 -17
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -2
- package/src/config/bundled-skills/media-processing/SKILL.md +3 -9
- package/src/config/bundled-skills/media-processing/TOOLS.json +1 -6
- package/src/config/bundled-skills/media-processing/__tests__/audio-transcribe.test.ts +125 -0
- package/src/config/bundled-skills/media-processing/__tests__/extract-keyframes.test.ts +181 -0
- package/src/config/bundled-skills/media-processing/__tests__/preprocess-audio.test.ts +141 -0
- package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +32 -87
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +8 -4
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +0 -10
- package/src/config/bundled-skills/messaging/SKILL.md +3 -3
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
- package/src/config/bundled-skills/outlook/SKILL.md +2 -2
- package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +2 -2
- package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
- package/src/config/bundled-skills/phone-calls/references/CONFIG.md +27 -18
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +3 -3
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +26 -22
- package/src/config/bundled-skills/slack/SKILL.md +1 -0
- package/src/config/bundled-skills/transcribe/SKILL.md +9 -14
- package/src/config/bundled-skills/transcribe/TOOLS.json +2 -7
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +256 -0
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +38 -188
- package/src/config/bundled-tool-registry.ts +8 -0
- package/src/config/env-registry.ts +24 -0
- package/src/config/env.ts +34 -10
- package/src/config/feature-flag-registry.json +46 -14
- package/src/config/loader.ts +26 -12
- package/src/config/schema.ts +35 -10
- package/src/config/schemas/__tests__/stt.test.ts +43 -0
- package/src/config/schemas/analysis.ts +51 -0
- package/src/config/schemas/backup.ts +72 -0
- package/src/config/schemas/calls.ts +1 -26
- package/src/config/schemas/elevenlabs.ts +0 -59
- package/src/config/schemas/filing.ts +47 -7
- package/src/config/schemas/heartbeat.ts +27 -5
- package/src/config/schemas/host-browser.ts +47 -1
- package/src/config/schemas/inference.ts +1 -1
- package/src/config/schemas/memory-lifecycle.ts +14 -2
- package/src/config/schemas/services.ts +44 -0
- package/src/config/schemas/stt.ts +59 -0
- package/src/config/schemas/tts.ts +230 -0
- package/src/config/schemas/updates.ts +14 -0
- package/src/config/skills.ts +4 -0
- package/src/config/types.ts +4 -0
- package/src/contacts/contact-store.ts +56 -11
- package/src/contacts/contacts-write.ts +38 -1
- package/src/context/post-turn-tool-result-truncation.ts +3 -2
- package/src/context/tool-result-truncation.ts +2 -1
- package/src/context/window-manager.ts +45 -12
- package/src/credential-execution/executable-discovery.ts +12 -2
- package/src/credential-execution/process-manager.ts +33 -2
- package/src/credential-health/credential-health-service.ts +366 -0
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +324 -0
- package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +497 -0
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +17 -8
- package/src/daemon/__tests__/lifecycle-startup-ordering.test.ts +127 -0
- package/src/daemon/config-watcher.ts +99 -5
- package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
- package/src/daemon/conversation-agent-loop.ts +101 -24
- package/src/daemon/conversation-error.ts +11 -0
- package/src/daemon/conversation-history.ts +40 -6
- package/src/daemon/conversation-launch.ts +220 -0
- package/src/daemon/conversation-lifecycle.ts +59 -9
- package/src/daemon/conversation-messaging.ts +37 -3
- package/src/daemon/conversation-notifiers.ts +5 -0
- package/src/daemon/conversation-process.ts +581 -19
- package/src/daemon/conversation-queue-manager.ts +24 -0
- package/src/daemon/conversation-runtime-assembly.ts +11 -1
- package/src/daemon/conversation-slash.ts +36 -0
- package/src/daemon/conversation-surfaces.ts +94 -4
- package/src/daemon/conversation-tool-setup.ts +25 -0
- package/src/daemon/conversation-usage.ts +7 -4
- package/src/daemon/conversation.ts +86 -28
- package/src/daemon/handlers/config-slack-channel.ts +269 -94
- package/src/daemon/handlers/conversations.ts +4 -1
- package/src/daemon/handlers/shared.ts +22 -0
- package/src/daemon/handlers/skills.ts +321 -77
- package/src/daemon/host-browser-proxy.ts +2 -1
- package/src/daemon/lifecycle.ts +122 -25
- package/src/daemon/message-protocol.ts +6 -0
- package/src/daemon/message-types/conversations.ts +34 -1
- package/src/daemon/message-types/home.ts +40 -0
- package/src/daemon/message-types/meet.ts +143 -0
- package/src/daemon/message-types/messages.ts +14 -0
- package/src/daemon/message-types/schedules.ts +34 -2
- package/src/daemon/message-types/skills.ts +16 -0
- package/src/daemon/message-types/surfaces.ts +2 -0
- package/src/daemon/server.ts +347 -2
- package/src/daemon/shutdown-handlers.ts +32 -4
- package/src/daemon/shutdown-registry.ts +40 -0
- package/src/daemon/tool-side-effects.ts +9 -0
- package/src/email/html-renderer.ts +76 -0
- package/src/heartbeat/heartbeat-service.ts +93 -7
- package/src/home/__tests__/assistant-feed-authoring.test.ts +156 -0
- package/src/home/__tests__/emit-feed-event.test.ts +169 -0
- package/src/home/__tests__/feed-scheduler.test.ts +194 -0
- package/src/home/__tests__/feed-types.test.ts +275 -0
- package/src/home/__tests__/feed-writer.test.ts +688 -0
- package/src/home/__tests__/phase5-exit-criteria.test.ts +212 -0
- package/src/home/__tests__/platform-gmail-digest.test.ts +222 -0
- package/src/home/__tests__/progress-formula.test.ts +213 -0
- package/src/home/__tests__/relationship-state-writer.test.ts +740 -0
- package/src/home/__tests__/rollup-producer.test.ts +398 -0
- package/src/home/assistant-feed-authoring.ts +124 -0
- package/src/home/emit-feed-event.ts +158 -0
- package/src/home/feed-scheduler.ts +247 -0
- package/src/home/feed-types.ts +181 -0
- package/src/home/feed-writer.ts +469 -0
- package/src/home/platform-gmail-digest.ts +163 -0
- package/src/home/progress-formula.ts +86 -0
- package/src/home/relationship-state-writer.ts +824 -0
- package/src/home/relationship-state.ts +143 -0
- package/src/home/rollup-producer.ts +384 -0
- package/src/hooks/runner.ts +7 -0
- package/src/inbound/platform-callback-registration.ts +12 -3
- package/src/inbound/public-ingress-urls.ts +12 -0
- package/src/instrument.ts +1 -1
- package/src/ipc/__tests__/cli-ipc.test.ts +200 -0
- package/src/ipc/cli-client.ts +151 -0
- package/src/ipc/cli-server.ts +234 -0
- package/src/ipc/gateway-client.ts +180 -0
- package/src/ipc/routes/index.ts +5 -0
- package/src/ipc/routes/wake-conversation.ts +19 -0
- package/src/memory/__tests__/auto-analysis-enqueue.test.ts +356 -0
- package/src/memory/__tests__/auto-analysis-guard.test.ts +57 -0
- package/src/memory/__tests__/conversation-analyze-job.test.ts +232 -0
- package/src/memory/__tests__/find-analysis-conversation.test.ts +196 -0
- package/src/memory/app-store.ts +1 -1
- package/src/memory/attachments-store.ts +70 -0
- package/src/memory/auto-analysis-enqueue.ts +127 -0
- package/src/memory/auto-analysis-guard.ts +27 -0
- package/src/memory/cleanup-schedule-state.ts +37 -0
- package/src/memory/conversation-analyze-job.ts +73 -0
- package/src/memory/conversation-crud.ts +99 -0
- package/src/memory/conversation-disk-view.ts +7 -0
- package/src/memory/conversation-group-migration.ts +34 -2
- package/src/memory/conversation-queries.ts +6 -5
- package/src/memory/db-init.ts +6 -0
- package/src/memory/db-maintenance.ts +108 -0
- package/src/memory/db.ts +1 -0
- package/src/memory/graph/conversation-graph-memory.ts +15 -0
- package/src/memory/graph/extraction.test.ts +23 -0
- package/src/memory/graph/extraction.ts +8 -0
- package/src/memory/graph/retriever.ts +27 -18
- package/src/memory/graph/scoring.test.ts +186 -0
- package/src/memory/graph/scoring.ts +31 -1
- package/src/memory/graph/tools.ts +1 -1
- package/src/memory/group-crud.ts +6 -1
- package/src/memory/indexer.ts +95 -16
- package/src/memory/job-handlers/cleanup.ts +11 -8
- package/src/memory/job-handlers/conversation-starters.ts +16 -10
- package/src/memory/jobs-store.ts +64 -4
- package/src/memory/jobs-worker.ts +22 -9
- package/src/memory/llm-usage-store.ts +92 -56
- package/src/memory/migrations/219-oauth-providers-token-exchange-body-format.ts +15 -0
- package/src/memory/migrations/220-normalize-user-file-by-principal.ts +190 -0
- package/src/memory/migrations/221-conversations-archived-at.ts +16 -0
- package/src/memory/migrations/index.ts +6 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/qdrant-manager.ts +43 -16
- package/src/memory/schema/conversations.ts +2 -0
- package/src/memory/schema/oauth.ts +3 -0
- package/src/memory/usage-buckets.ts +396 -0
- package/src/messaging/providers/gmail/client.ts +57 -6
- package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +282 -0
- package/src/messaging/providers/slack/adapter.ts +143 -38
- package/src/messaging/providers/slack/client.ts +16 -0
- package/src/messaging/providers/slack/types.ts +4 -0
- package/src/notifications/decision-engine.ts +3 -3
- package/src/notifications/signal.ts +5 -0
- package/src/oauth/__tests__/identity-verifier.test.ts +1 -0
- package/src/oauth/byo-connection.test.ts +18 -1
- package/src/oauth/byo-connection.ts +3 -1
- package/src/oauth/connect-orchestrator.ts +2 -0
- package/src/oauth/connection-resolver.ts +6 -2
- package/src/oauth/connection.ts +2 -0
- package/src/oauth/oauth-store.ts +9 -0
- package/src/oauth/platform-connection.test.ts +98 -0
- package/src/oauth/platform-connection.ts +52 -31
- package/src/oauth/seed-providers.ts +7 -0
- package/src/permissions/checker.ts +16 -6
- package/src/permissions/defaults.ts +49 -1
- package/src/permissions/trust-store.ts +3 -3
- package/src/permissions/workspace-policy.ts +3 -0
- package/src/platform/client.test.ts +10 -0
- package/src/platform/sync-identity.ts +129 -0
- package/src/prompts/persona-resolver.ts +126 -2
- package/src/prompts/system-prompt.ts +59 -18
- package/src/prompts/templates/BOOTSTRAP.md +5 -5
- package/src/prompts/templates/SOUL.md +3 -1
- package/src/prompts/templates/UPDATES.md +12 -0
- package/src/prompts/templates/channels/slack.md +20 -0
- package/src/prompts/update-bulletin-format.ts +26 -9
- package/src/prompts/update-bulletin.ts +34 -23
- package/src/prompts/user-reference.ts +20 -17
- package/src/providers/__tests__/provider-secret-catalog.test.ts +42 -0
- package/src/providers/anthropic/client.ts +157 -61
- package/src/providers/fireworks/client.ts +2 -2
- package/src/providers/gemini/client.ts +9 -1
- package/src/providers/model-catalog.ts +6 -0
- package/src/providers/model-intents.ts +4 -4
- package/src/providers/ollama/client.ts +2 -2
- package/src/providers/openai/chat-completions-provider.ts +474 -0
- package/src/providers/openai/client.ts +25 -440
- package/src/providers/openai/responses-provider.ts +502 -0
- package/src/providers/openrouter/client.ts +101 -4
- package/src/providers/provider-secret-catalog.ts +139 -0
- package/src/providers/registry.ts +2 -2
- package/src/providers/retry.ts +14 -3
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +251 -0
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +828 -0
- package/src/providers/speech-to-text/deepgram-realtime.test.ts +980 -0
- package/src/providers/speech-to-text/deepgram-realtime.ts +767 -0
- package/src/providers/speech-to-text/deepgram.test.ts +332 -0
- package/src/providers/speech-to-text/deepgram.ts +115 -0
- package/src/providers/speech-to-text/google-gemini-live-stream.test.ts +743 -0
- package/src/providers/speech-to-text/google-gemini-live-stream.ts +625 -0
- package/src/providers/speech-to-text/google-gemini.test.ts +226 -0
- package/src/providers/speech-to-text/google-gemini.ts +101 -0
- package/src/providers/speech-to-text/openai-whisper-stream.test.ts +564 -0
- package/src/providers/speech-to-text/openai-whisper-stream.ts +381 -0
- package/src/providers/speech-to-text/openai-whisper.test.ts +1 -37
- package/src/providers/speech-to-text/openai-whisper.ts +63 -33
- package/src/providers/speech-to-text/provider-catalog.ts +306 -0
- package/src/providers/speech-to-text/resolve.ts +386 -6
- package/src/providers/types.ts +9 -0
- package/src/runtime/AGENTS.md +43 -1
- package/src/runtime/__tests__/agent-wake.test.ts +831 -0
- package/src/runtime/__tests__/runtime-mode.test.ts +62 -0
- package/src/runtime/__tests__/slack-block-formatting.test.ts +481 -0
- package/src/runtime/agent-wake.ts +512 -0
- package/src/runtime/auth/__tests__/route-policy.test.ts +40 -0
- package/src/runtime/auth/route-policy.ts +30 -5
- package/src/runtime/auth/token-service.ts +56 -1
- package/src/runtime/btw-sidechain.ts +2 -0
- package/src/runtime/capability-tokens.ts +10 -10
- package/src/runtime/channel-invite-transport.ts +1 -1
- package/src/runtime/channel-invite-transports/email.ts +14 -6
- package/src/runtime/channel-readiness-service.ts +12 -22
- package/src/runtime/chrome-extension-registry.ts +38 -2
- package/src/runtime/http-server.ts +395 -10
- package/src/runtime/http-types.ts +6 -2
- package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +36 -0
- package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +360 -0
- package/src/runtime/migrations/migration-transport.ts +1 -0
- package/src/runtime/migrations/migration-wizard.ts +1 -0
- package/src/runtime/migrations/vbundle-import-analyzer.ts +77 -1
- package/src/runtime/migrations/vbundle-importer.ts +34 -0
- package/src/runtime/pending-interactions.ts +0 -11
- package/src/runtime/routes/__tests__/backup-routes.test.ts +967 -0
- package/src/runtime/routes/__tests__/home-feed-routes.test.ts +507 -0
- package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +208 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +406 -0
- package/src/runtime/routes/__tests__/tts-routes.test.ts +474 -0
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +148 -17
- package/src/runtime/routes/app-management-routes.ts +12 -18
- package/src/runtime/routes/attachment-routes.test.ts +9 -3
- package/src/runtime/routes/attachment-routes.ts +216 -17
- package/src/runtime/routes/backup-routes.ts +519 -0
- package/src/runtime/routes/browser-extension-pair-routes.ts +82 -23
- package/src/runtime/routes/btw-routes.ts +8 -6
- package/src/runtime/routes/contact-routes.test.ts +298 -0
- package/src/runtime/routes/contact-routes.ts +132 -5
- package/src/runtime/routes/conversation-analysis-routes.ts +22 -142
- package/src/runtime/routes/conversation-management-routes.ts +115 -0
- package/src/runtime/routes/conversation-routes.ts +367 -146
- package/src/runtime/routes/filing-routes.ts +93 -0
- package/src/runtime/routes/home-feed-routes.ts +334 -0
- package/src/runtime/routes/home-state-routes.ts +138 -0
- package/src/runtime/routes/host-browser-routes.ts +3 -14
- package/src/runtime/routes/identity-intro-cache.ts +7 -3
- package/src/runtime/routes/identity-routes.ts +3 -17
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +46 -39
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +15 -15
- package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +137 -0
- package/src/runtime/routes/integrations/slack/__tests__/share.test.ts +179 -0
- package/src/runtime/routes/integrations/slack/channel.ts +11 -3
- package/src/runtime/routes/integrations/slack/share.ts +45 -7
- package/src/runtime/routes/llm-context-normalization.ts +303 -0
- package/src/runtime/routes/memory-item-routes.test.ts +3 -2
- package/src/runtime/routes/migration-routes.ts +40 -5
- package/src/runtime/routes/settings-routes.ts +22 -5
- package/src/runtime/routes/skills-routes.ts +76 -7
- package/src/runtime/routes/stt-routes.ts +233 -0
- package/src/runtime/routes/surface-action-routes.ts +41 -2
- package/src/runtime/routes/tts-routes.ts +108 -24
- package/src/runtime/routes/usage-routes.ts +30 -2
- package/src/runtime/routes/user-route-dispatcher.ts +50 -5
- package/src/runtime/routes/user-routes.ts +13 -1
- package/src/runtime/routes/work-items-routes.ts +8 -1
- package/src/runtime/runtime-mode.ts +33 -0
- package/src/runtime/services/__tests__/analyze-conversation.test.ts +444 -0
- package/src/runtime/services/__tests__/analyze-deps-singleton.test.ts +67 -0
- package/src/runtime/services/__tests__/auto-analysis-prompt.test.ts +53 -0
- package/src/runtime/services/__tests__/manual-analysis-prompt.test.ts +41 -0
- package/src/runtime/services/analyze-conversation.ts +344 -0
- package/src/runtime/services/analyze-deps-singleton.ts +32 -0
- package/src/runtime/services/auto-analysis-prompt.ts +55 -0
- package/src/runtime/skill-route-registry.ts +49 -0
- package/src/runtime/slack-block-formatting.ts +437 -10
- package/src/schedule/scheduler.ts +50 -0
- package/src/security/oauth2.ts +26 -4
- package/src/security/secure-keys.ts +25 -2
- package/src/security/token-manager.ts +8 -0
- package/src/sequence/engine.ts +23 -0
- package/src/sequence/types.ts +1 -1
- package/src/skills/catalog-files.ts +64 -2
- package/src/skills/category-inference.ts +122 -0
- package/src/skills/clawhub-files.ts +213 -0
- package/src/skills/clawhub.ts +84 -23
- package/src/skills/skill-file-provider.ts +40 -0
- package/src/skills/skillssh-files.ts +395 -0
- package/src/skills/skillssh-registry.ts +4 -4
- package/src/stt/__tests__/daemon-batch-transcriber.test.ts +392 -0
- package/src/stt/__tests__/types.test.ts +89 -0
- package/src/stt/daemon-batch-transcriber.ts +195 -0
- package/src/stt/stt-stream-session.ts +499 -0
- package/src/stt/types.ts +330 -0
- package/src/stt/wav-encoder.test.ts +373 -0
- package/src/stt/wav-encoder.ts +175 -0
- package/src/subagent/manager.ts +38 -14
- package/src/tools/browser/__tests__/browser-mode.test.ts +119 -0
- package/src/tools/browser/__tests__/browser-status.test.ts +123 -0
- package/src/tools/browser/browser-execution.ts +1163 -23
- package/src/tools/browser/browser-manager.ts +45 -0
- package/src/tools/browser/browser-mode-constants.ts +12 -0
- package/src/tools/browser/browser-mode.ts +92 -0
- package/src/tools/browser/browser-status-constants.ts +33 -0
- package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +393 -0
- package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +29 -0
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +1648 -32
- package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +264 -0
- package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +183 -17
- package/src/tools/browser/cdp-client/cdp-inspect-client.ts +254 -21
- package/src/tools/browser/cdp-client/errors.ts +15 -0
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +39 -16
- package/src/tools/browser/cdp-client/factory.ts +797 -87
- package/src/tools/browser/cdp-client/index.ts +16 -2
- package/src/tools/browser/cdp-client/types.ts +68 -0
- package/src/tools/credentials/vault.ts +35 -6
- package/src/tools/network/web-fetch.ts +5 -2
- package/src/tools/network/web-search.ts +5 -2
- package/src/tools/shared/shell-output.ts +3 -1
- package/src/tools/side-effects.ts +2 -0
- package/src/tools/skills/sandbox-runner.ts +3 -2
- package/src/tools/terminal/safe-env.ts +10 -2
- package/src/tools/terminal/shell.ts +15 -4
- package/src/tools/tool-manifest.ts +21 -0
- package/src/tools/types.ts +17 -0
- package/src/tools/ui-surface/definitions.ts +6 -1
- package/src/tts/__tests__/provider-adapters.test.ts +834 -0
- package/src/tts/__tests__/provider-catalog-consistency.test.ts +196 -0
- package/src/tts/__tests__/provider-catalog.test.ts +183 -0
- package/src/tts/__tests__/provider-registry.test.ts +90 -0
- package/src/tts/provider-catalog.ts +201 -0
- package/src/tts/provider-registry.ts +73 -0
- package/src/tts/providers/deepgram-provider.ts +219 -0
- package/src/tts/providers/elevenlabs-provider.ts +211 -0
- package/src/tts/providers/fish-audio-provider.ts +183 -0
- package/src/tts/providers/index.ts +42 -0
- package/src/tts/providers/register-builtins.ts +130 -0
- package/src/tts/synthesize-text.ts +110 -0
- package/src/tts/tts-config-resolver.ts +78 -0
- package/src/tts/types.ts +153 -0
- package/src/types/onboarding-context.ts +7 -0
- package/src/util/abort-reasons.ts +58 -0
- package/src/util/device-id.ts +32 -16
- package/src/util/errors.ts +9 -1
- package/src/util/platform.ts +54 -10
- package/src/util/pricing.ts +66 -3
- package/src/util/spawn.ts +1 -1
- package/src/util/truncate.ts +4 -2
- package/src/util/unicode.ts +201 -0
- package/src/version.ts +19 -24
- package/src/watcher/engine.ts +23 -0
- package/src/watcher/watcher-store.ts +31 -0
- package/src/workspace/migrations/003-seed-device-id.ts +9 -3
- package/src/workspace/migrations/017-seed-persona-dirs.ts +68 -4
- package/src/workspace/migrations/029-seed-pkb.ts +1 -1
- package/src/workspace/migrations/031-drop-user-md.ts +317 -0
- package/src/workspace/migrations/031-llm-log-retention-zero-to-null.ts +73 -0
- package/src/workspace/migrations/032-tts-provider-unification.ts +227 -0
- package/src/workspace/migrations/033-stt-service-explicit-config.ts +122 -0
- package/src/workspace/migrations/034-remove-calls-voice-transcription-provider.ts +215 -0
- package/src/workspace/migrations/035-seed-slack-channel-persona.ts +50 -0
- package/src/workspace/migrations/036-update-pkb-index-bar.ts +37 -0
- package/src/workspace/migrations/037-create-meets-dir.ts +61 -0
- package/src/workspace/migrations/registry.ts +16 -0
- package/src/workspace/top-level-renderer.ts +13 -1
- package/src/workspace/turn-commit.ts +31 -0
- package/src/__tests__/email-cli.test.ts +0 -297
- package/src/__tests__/email-service-config-fallback.test.ts +0 -102
- package/src/cli/commands/browser-relay.ts +0 -466
- package/src/email/guardrails.ts +0 -221
- package/src/email/provider.ts +0 -117
- package/src/email/providers/agentmail.ts +0 -361
- package/src/email/providers/index.ts +0 -65
- package/src/email/service.ts +0 -384
- package/src/email/types.ts +0 -126
- package/src/prompts/templates/USER.md +0 -13
- package/src/providers/speech-to-text/types.ts +0 -17
- package/src/runtime/routes/browser-cdp-routes.ts +0 -229
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 5 exit-criteria end-to-end test.
|
|
3
|
+
*
|
|
4
|
+
* Exercises the full home-feed flow in-process against a tmpdir
|
|
5
|
+
* workspace: the platform-baseline generator writes a digest, the
|
|
6
|
+
* assistant-authoring helper writes a nudge, the HTTP route serves
|
|
7
|
+
* both with a context banner, and the patch endpoint flips an item
|
|
8
|
+
* to `seen` — all without spinning up the macOS app.
|
|
9
|
+
*
|
|
10
|
+
* Exit-criteria mapping (from the TDD checklist):
|
|
11
|
+
*
|
|
12
|
+
* [x] platform-baseline Gmail digest surfaces via the feed route
|
|
13
|
+
* → `generateGmailDigest(now)` + `GET /v1/home/feed` → 1 item
|
|
14
|
+
* [x] assistant-authored nudges coexist with platform digests
|
|
15
|
+
* → `writeAssistantFeedItem(...)` → 2 items visible
|
|
16
|
+
* [x] `minTimeAway` filtering behaves at route boundary
|
|
17
|
+
* → seed an item with `minTimeAway: 3600` and query with
|
|
18
|
+
* `timeAwaySeconds: 12 * 3600` → included
|
|
19
|
+
* [x] context banner reports `newCount` across all authors
|
|
20
|
+
* → 2 new items → banner.newCount === 2
|
|
21
|
+
* [x] `PATCH /v1/home/feed/:id` flips status and mutates banner
|
|
22
|
+
* → patchFeedItemStatus(nudgeId, "seen") → newCount drops to 1
|
|
23
|
+
*
|
|
24
|
+
* This test is a wiring-regression safety net — the per-PR unit
|
|
25
|
+
* tests in this directory (feed-writer, feed-types, authoring,
|
|
26
|
+
* gmail-digest, plus the route tests under `runtime/routes/__tests__`)
|
|
27
|
+
* still carry the per-component coverage. Keep this file fast (<5s)
|
|
28
|
+
* by doing all persistence against a single tmpdir workspace and
|
|
29
|
+
* calling route handlers directly instead of standing up the HTTP
|
|
30
|
+
* server.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
34
|
+
import { tmpdir } from "node:os";
|
|
35
|
+
import { join } from "node:path";
|
|
36
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
37
|
+
|
|
38
|
+
// ─── assistantEventHub mock ────────────────────────────────────────────
|
|
39
|
+
// The real feed writer publishes `home_feed_updated` via the in-process
|
|
40
|
+
// event hub on every write. This test doesn't assert on those events —
|
|
41
|
+
// it asserts on the feed file + HTTP-route output — so we stub the hub
|
|
42
|
+
// to an inert publisher. Must run before the first dynamic import of
|
|
43
|
+
// anything that transitively imports `feed-writer.js`.
|
|
44
|
+
const publishSpy = mock<(event: unknown) => Promise<void>>(async () => {});
|
|
45
|
+
mock.module("../../runtime/assistant-event-hub.js", () => ({
|
|
46
|
+
assistantEventHub: {
|
|
47
|
+
publish: publishSpy,
|
|
48
|
+
subscribe: () => () => {},
|
|
49
|
+
},
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
// ─── createConversation / addMessage stub ──────────────────────────────
|
|
53
|
+
// The feed action endpoint (`POST /v1/home/feed/:id/actions/:actionId`)
|
|
54
|
+
// calls into the real conversation store, which drags in a large chunk
|
|
55
|
+
// of daemon state we don't want this test to depend on. The exit-
|
|
56
|
+
// criteria flow we care about is GET + PATCH — not the action POST —
|
|
57
|
+
// so we stub the conversation module to a trivial in-memory fake and
|
|
58
|
+
// never exercise it. If a future exit-criteria item needs the action
|
|
59
|
+
// path, swap this for a real-ish fake.
|
|
60
|
+
mock.module("../../runtime/conversation-store.js", () => ({
|
|
61
|
+
createConversation: (args: { title: string; source: string }) => ({
|
|
62
|
+
id: "stub-conversation",
|
|
63
|
+
title: args.title,
|
|
64
|
+
source: args.source,
|
|
65
|
+
}),
|
|
66
|
+
addMessage: async () => {},
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
const { generateGmailDigest } = await import("../platform-gmail-digest.js");
|
|
70
|
+
const { writeAssistantFeedItem } =
|
|
71
|
+
await import("../assistant-feed-authoring.js");
|
|
72
|
+
const { patchFeedItemStatus, readHomeFeed } = await import("../feed-writer.js");
|
|
73
|
+
const { handleGetHomeFeed } =
|
|
74
|
+
await import("../../runtime/routes/home-feed-routes.js");
|
|
75
|
+
|
|
76
|
+
// ─── tmpdir workspace lifecycle ────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
let workspaceDir: string;
|
|
79
|
+
let origWorkspaceDir: string | undefined;
|
|
80
|
+
|
|
81
|
+
beforeEach(() => {
|
|
82
|
+
workspaceDir = mkdtempSync(join(tmpdir(), "vellum-phase5-e2e-"));
|
|
83
|
+
origWorkspaceDir = process.env.VELLUM_WORKSPACE_DIR;
|
|
84
|
+
process.env.VELLUM_WORKSPACE_DIR = workspaceDir;
|
|
85
|
+
publishSpy.mockClear();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
afterEach(() => {
|
|
89
|
+
if (origWorkspaceDir === undefined) {
|
|
90
|
+
delete process.env.VELLUM_WORKSPACE_DIR;
|
|
91
|
+
} else {
|
|
92
|
+
process.env.VELLUM_WORKSPACE_DIR = origWorkspaceDir;
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
rmSync(workspaceDir, { recursive: true, force: true });
|
|
96
|
+
} catch {
|
|
97
|
+
// best-effort — tests must not fail on cleanup
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ─── Helpers ───────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
/** Hit the GET handler directly with a synthetic Request object. */
|
|
104
|
+
async function getFeed(timeAwaySeconds: number): Promise<{
|
|
105
|
+
items: Array<{
|
|
106
|
+
id: string;
|
|
107
|
+
status: string;
|
|
108
|
+
type: string;
|
|
109
|
+
source?: string;
|
|
110
|
+
author: string;
|
|
111
|
+
}>;
|
|
112
|
+
updatedAt: string;
|
|
113
|
+
contextBanner: { greeting: string; timeAwayLabel: string; newCount: number };
|
|
114
|
+
}> {
|
|
115
|
+
const req = new Request(
|
|
116
|
+
`http://daemon.local/v1/home/feed?timeAwaySeconds=${timeAwaySeconds}`,
|
|
117
|
+
);
|
|
118
|
+
const res = await handleGetHomeFeed(req);
|
|
119
|
+
expect(res.status).toBe(200);
|
|
120
|
+
return (await res.json()) as Awaited<ReturnType<typeof getFeed>>;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ─── Tests ─────────────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
describe("Phase 5 exit criteria — end-to-end", () => {
|
|
126
|
+
test("platform digest + assistant nudge surface via the feed route, and patch flips newCount", async () => {
|
|
127
|
+
const now = new Date("2026-04-14T12:00:00.000Z");
|
|
128
|
+
|
|
129
|
+
// 1. Platform baseline: Gmail digest — mechanical "3 new emails"
|
|
130
|
+
const digest = await generateGmailDigest(now, async () => 3);
|
|
131
|
+
expect(digest).not.toBeNull();
|
|
132
|
+
expect(digest!.source).toBe("gmail");
|
|
133
|
+
expect(digest!.author).toBe("platform");
|
|
134
|
+
expect(digest!.type).toBe("digest");
|
|
135
|
+
|
|
136
|
+
// 2. Assistant-authored nudge — the hybrid-authoring surface
|
|
137
|
+
const nudge = await writeAssistantFeedItem({
|
|
138
|
+
type: "nudge",
|
|
139
|
+
source: "assistant",
|
|
140
|
+
title: "Follow up on the Figma file",
|
|
141
|
+
summary: "Alice is asking when the deck lands.",
|
|
142
|
+
actions: [
|
|
143
|
+
{
|
|
144
|
+
id: "open",
|
|
145
|
+
label: "Open conversation",
|
|
146
|
+
prompt: "Remind me about the Figma file.",
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
minTimeAway: 3600,
|
|
150
|
+
});
|
|
151
|
+
expect(nudge.author).toBe("assistant");
|
|
152
|
+
expect(nudge.type).toBe("nudge");
|
|
153
|
+
|
|
154
|
+
// 3. On-disk sanity: both items made it into home-feed.json and
|
|
155
|
+
// the digest coexists with the assistant nudge (different
|
|
156
|
+
// (type, source) so the one-per-source replacement does not
|
|
157
|
+
// collapse them into a single item).
|
|
158
|
+
const onDisk = readHomeFeed();
|
|
159
|
+
const ids = onDisk.items.map((i) => i.id).sort();
|
|
160
|
+
expect(ids.length).toBe(2);
|
|
161
|
+
expect(ids).toContain(digest!.id);
|
|
162
|
+
expect(ids).toContain(nudge.id);
|
|
163
|
+
|
|
164
|
+
// 4. Route: `GET /v1/home/feed?timeAwaySeconds=43200` — both items
|
|
165
|
+
// pass the `minTimeAway` gate (3600 <= 43200). Banner reports
|
|
166
|
+
// `newCount: 2` since both are still `.new`.
|
|
167
|
+
const initial = await getFeed(12 * 3600);
|
|
168
|
+
expect(initial.items.length).toBe(2);
|
|
169
|
+
expect(initial.contextBanner.newCount).toBe(2);
|
|
170
|
+
expect(initial.contextBanner.timeAwayLabel.length).toBeGreaterThan(0);
|
|
171
|
+
expect(initial.contextBanner.greeting.length).toBeGreaterThan(0);
|
|
172
|
+
|
|
173
|
+
// 5. Flip the nudge to `seen` via the writer (the route's PATCH
|
|
174
|
+
// handler is a thin wrapper around this — we skip the HTTP
|
|
175
|
+
// layer here so the test stays at the in-process seam).
|
|
176
|
+
const patched = await patchFeedItemStatus(nudge.id, "seen");
|
|
177
|
+
expect(patched).not.toBeNull();
|
|
178
|
+
expect(patched!.status).toBe("seen");
|
|
179
|
+
|
|
180
|
+
// 6. Re-fetch: `newCount` drops to 1 (only the digest is still
|
|
181
|
+
// `.new`). Total item count is unchanged — patching status
|
|
182
|
+
// must never remove items from the feed.
|
|
183
|
+
const afterPatch = await getFeed(12 * 3600);
|
|
184
|
+
expect(afterPatch.items.length).toBe(2);
|
|
185
|
+
expect(afterPatch.contextBanner.newCount).toBe(1);
|
|
186
|
+
const seenItem = afterPatch.items.find((i) => i.id === nudge.id);
|
|
187
|
+
expect(seenItem?.status).toBe("seen");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("minTimeAway gate excludes items when the user has not been away long enough", async () => {
|
|
191
|
+
// Assistant nudge with a 1-hour away gate
|
|
192
|
+
const nudge = await writeAssistantFeedItem({
|
|
193
|
+
type: "nudge",
|
|
194
|
+
source: "assistant",
|
|
195
|
+
title: "Come back after lunch",
|
|
196
|
+
summary: "This only shows after an hour away.",
|
|
197
|
+
minTimeAway: 3600,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// User has only been away 10 minutes → item must be filtered
|
|
201
|
+
const brief = await getFeed(600);
|
|
202
|
+
const briefIds = brief.items.map((i) => i.id);
|
|
203
|
+
expect(briefIds).not.toContain(nudge.id);
|
|
204
|
+
expect(brief.contextBanner.newCount).toBe(0);
|
|
205
|
+
|
|
206
|
+
// User has been away 2 hours → item must now appear
|
|
207
|
+
const long = await getFeed(2 * 3600);
|
|
208
|
+
const longIds = long.items.map((i) => i.id);
|
|
209
|
+
expect(longIds).toContain(nudge.id);
|
|
210
|
+
expect(long.contextBanner.newCount).toBe(1);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
5
|
+
|
|
6
|
+
// ─── assistantEventHub mock ────────────────────────────────────────────
|
|
7
|
+
// The real feed-writer publishes `home_feed_updated` via the in-process
|
|
8
|
+
// event hub on every write; tests don't care about that side effect, so
|
|
9
|
+
// we stub the hub to an inert publisher. Must be in place before the
|
|
10
|
+
// first dynamic import of feed-writer.js so the module graph picks up
|
|
11
|
+
// the mock.
|
|
12
|
+
const publishSpy = mock<(event: unknown) => Promise<void>>(async () => {});
|
|
13
|
+
|
|
14
|
+
mock.module("../../runtime/assistant-event-hub.js", () => ({
|
|
15
|
+
assistantEventHub: {
|
|
16
|
+
publish: publishSpy,
|
|
17
|
+
subscribe: () => () => {},
|
|
18
|
+
},
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
// Dynamic import so the mock above resolves before the real module is
|
|
22
|
+
// evaluated.
|
|
23
|
+
const { generateGmailDigest } = await import("../platform-gmail-digest.js");
|
|
24
|
+
const { getHomeFeedPath, readHomeFeed } = await import("../feed-writer.js");
|
|
25
|
+
|
|
26
|
+
// ─── tmpdir workspace lifecycle ────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
let workspaceDir: string;
|
|
29
|
+
let origWorkspaceDir: string | undefined;
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
workspaceDir = mkdtempSync(join(tmpdir(), "vellum-pgd-"));
|
|
33
|
+
origWorkspaceDir = process.env.VELLUM_WORKSPACE_DIR;
|
|
34
|
+
process.env.VELLUM_WORKSPACE_DIR = workspaceDir;
|
|
35
|
+
publishSpy.mockClear();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
if (origWorkspaceDir === undefined) {
|
|
40
|
+
delete process.env.VELLUM_WORKSPACE_DIR;
|
|
41
|
+
} else {
|
|
42
|
+
process.env.VELLUM_WORKSPACE_DIR = origWorkspaceDir;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
rmSync(workspaceDir, { recursive: true, force: true });
|
|
46
|
+
} catch {
|
|
47
|
+
// best-effort
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// ─── Tests ─────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
describe("generateGmailDigest", () => {
|
|
54
|
+
test("returns null and does not write when count is 0", async () => {
|
|
55
|
+
const result = await generateGmailDigest(
|
|
56
|
+
new Date("2026-04-14T12:00:00.000Z"),
|
|
57
|
+
async () => 0,
|
|
58
|
+
);
|
|
59
|
+
expect(result).toBeNull();
|
|
60
|
+
|
|
61
|
+
// No file should have been written — readHomeFeed returns the
|
|
62
|
+
// empty sentinel with the Unix-epoch updatedAt.
|
|
63
|
+
const feed = readHomeFeed();
|
|
64
|
+
expect(feed.items).toEqual([]);
|
|
65
|
+
expect(feed.updatedAt).toBe(new Date(0).toISOString());
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("returns a digest FeedItem with the expected shape when count > 0", async () => {
|
|
69
|
+
const now = new Date("2026-04-14T12:00:00.000Z");
|
|
70
|
+
const result = await generateGmailDigest(now, async () => 7);
|
|
71
|
+
|
|
72
|
+
expect(result).not.toBeNull();
|
|
73
|
+
expect(result!.type).toBe("digest");
|
|
74
|
+
expect(result!.source).toBe("gmail");
|
|
75
|
+
expect(result!.author).toBe("platform");
|
|
76
|
+
expect(result!.priority).toBe(40);
|
|
77
|
+
expect(result!.minTimeAway).toBe(3600);
|
|
78
|
+
expect(result!.status).toBe("new");
|
|
79
|
+
expect(result!.title).toContain("7");
|
|
80
|
+
expect(result!.timestamp).toBe(now.toISOString());
|
|
81
|
+
expect(result!.createdAt).toBe(now.toISOString());
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("minTimeAway is exactly 3600 seconds (1 hour)", async () => {
|
|
85
|
+
const result = await generateGmailDigest(
|
|
86
|
+
new Date("2026-04-14T12:00:00.000Z"),
|
|
87
|
+
async () => 1,
|
|
88
|
+
);
|
|
89
|
+
expect(result).not.toBeNull();
|
|
90
|
+
expect(result!.minTimeAway).toBe(3600);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("title uses singular form for count of 1, plural otherwise", async () => {
|
|
94
|
+
const now = new Date("2026-04-14T12:00:00.000Z");
|
|
95
|
+
|
|
96
|
+
const one = await generateGmailDigest(now, async () => 1);
|
|
97
|
+
expect(one!.title).toBe("1 new email");
|
|
98
|
+
|
|
99
|
+
const many = await generateGmailDigest(now, async () => 3);
|
|
100
|
+
expect(many!.title).toBe("3 new emails");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("returns null when count source throws", async () => {
|
|
104
|
+
const result = await generateGmailDigest(
|
|
105
|
+
new Date("2026-04-14T12:00:00.000Z"),
|
|
106
|
+
async () => {
|
|
107
|
+
throw new Error("boom");
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
expect(result).toBeNull();
|
|
111
|
+
|
|
112
|
+
const feed = readHomeFeed();
|
|
113
|
+
expect(feed.items).toEqual([]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("writes the item to home-feed.json via appendFeedItem", async () => {
|
|
117
|
+
const now = new Date("2026-04-14T12:00:00.000Z");
|
|
118
|
+
const result = await generateGmailDigest(now, async () => 5);
|
|
119
|
+
expect(result).not.toBeNull();
|
|
120
|
+
|
|
121
|
+
const raw = readFileSync(getHomeFeedPath(), "utf-8");
|
|
122
|
+
const file = JSON.parse(raw) as {
|
|
123
|
+
items: Array<{
|
|
124
|
+
id: string;
|
|
125
|
+
type: string;
|
|
126
|
+
source?: string;
|
|
127
|
+
author: string;
|
|
128
|
+
priority: number;
|
|
129
|
+
title: string;
|
|
130
|
+
}>;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
expect(file.items.length).toBe(1);
|
|
134
|
+
expect(file.items[0]).toMatchObject({
|
|
135
|
+
id: result!.id,
|
|
136
|
+
type: "digest",
|
|
137
|
+
source: "gmail",
|
|
138
|
+
author: "platform",
|
|
139
|
+
priority: 40,
|
|
140
|
+
title: "5 new emails",
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("summary uses generic check-in copy on first-ever digest", async () => {
|
|
145
|
+
const result = await generateGmailDigest(
|
|
146
|
+
new Date("2026-04-14T12:00:00.000Z"),
|
|
147
|
+
async () => 4,
|
|
148
|
+
);
|
|
149
|
+
expect(result).not.toBeNull();
|
|
150
|
+
expect(result!.summary).toBe("Since your last check-in");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("summary references the prior digest timestamp on subsequent call", async () => {
|
|
154
|
+
const first = await generateGmailDigest(
|
|
155
|
+
new Date("2026-04-14T14:32:00.000Z"),
|
|
156
|
+
async () => 2,
|
|
157
|
+
);
|
|
158
|
+
expect(first).not.toBeNull();
|
|
159
|
+
|
|
160
|
+
// Same-day follow-up → "Since h:mm AM/PM" pulled from the first
|
|
161
|
+
// digest's timestamp (2:32 PM UTC rendered via en-US locale).
|
|
162
|
+
const sameDay = await generateGmailDigest(
|
|
163
|
+
new Date("2026-04-14T16:00:00.000Z"),
|
|
164
|
+
async () => 5,
|
|
165
|
+
);
|
|
166
|
+
expect(sameDay).not.toBeNull();
|
|
167
|
+
expect(sameDay!.summary).toMatch(/^Since \d{1,2}:\d{2}\s?(AM|PM)$/);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("summary includes weekday when prior digest is on a different day", async () => {
|
|
171
|
+
const first = await generateGmailDigest(
|
|
172
|
+
new Date("2026-04-13T22:00:00.000Z"),
|
|
173
|
+
async () => 1,
|
|
174
|
+
);
|
|
175
|
+
expect(first).not.toBeNull();
|
|
176
|
+
|
|
177
|
+
const nextDay = await generateGmailDigest(
|
|
178
|
+
new Date("2026-04-14T14:00:00.000Z"),
|
|
179
|
+
async () => 3,
|
|
180
|
+
);
|
|
181
|
+
expect(nextDay).not.toBeNull();
|
|
182
|
+
// Cross-day → "Since <Weekday> h:mm AM/PM" — weekday prefix set.
|
|
183
|
+
expect(nextDay!.summary).toMatch(
|
|
184
|
+
/^Since (Mon|Tue|Wed|Thu|Fri|Sat|Sun) \d{1,2}:\d{2}\s?(AM|PM)$/,
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Integration: exercise the writer's one-per-source rule. Two
|
|
189
|
+
// successive generator calls should leave exactly one Gmail digest
|
|
190
|
+
// on disk — the newer call replaces the earlier one in place.
|
|
191
|
+
test("second call replaces the first via one-per-source rule", async () => {
|
|
192
|
+
const first = await generateGmailDigest(
|
|
193
|
+
new Date("2026-04-14T12:00:00.000Z"),
|
|
194
|
+
async () => 2,
|
|
195
|
+
);
|
|
196
|
+
expect(first).not.toBeNull();
|
|
197
|
+
|
|
198
|
+
const second = await generateGmailDigest(
|
|
199
|
+
new Date("2026-04-14T13:00:00.000Z"),
|
|
200
|
+
async () => 9,
|
|
201
|
+
);
|
|
202
|
+
expect(second).not.toBeNull();
|
|
203
|
+
expect(second!.id).not.toBe(first!.id);
|
|
204
|
+
|
|
205
|
+
const raw = readFileSync(getHomeFeedPath(), "utf-8");
|
|
206
|
+
const file = JSON.parse(raw) as {
|
|
207
|
+
items: Array<{
|
|
208
|
+
id: string;
|
|
209
|
+
type: string;
|
|
210
|
+
source?: string;
|
|
211
|
+
title: string;
|
|
212
|
+
}>;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const gmailDigests = file.items.filter(
|
|
216
|
+
(i) => i.type === "digest" && i.source === "gmail",
|
|
217
|
+
);
|
|
218
|
+
expect(gmailDigests.length).toBe(1);
|
|
219
|
+
expect(gmailDigests[0]!.id).toBe(second!.id);
|
|
220
|
+
expect(gmailDigests[0]!.title).toBe("9 new emails");
|
|
221
|
+
});
|
|
222
|
+
});
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
computeProgressPercent,
|
|
5
|
+
computeTier,
|
|
6
|
+
PROGRESS_TARGETS,
|
|
7
|
+
PROGRESS_WEIGHTS,
|
|
8
|
+
} from "../progress-formula.js";
|
|
9
|
+
import type { Capability, Fact } from "../relationship-state.js";
|
|
10
|
+
|
|
11
|
+
function fact(id: string): Fact {
|
|
12
|
+
return {
|
|
13
|
+
id,
|
|
14
|
+
category: "world",
|
|
15
|
+
text: "placeholder",
|
|
16
|
+
confidence: "strong",
|
|
17
|
+
source: "inferred",
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function cap(id: string, tier: Capability["tier"]): Capability {
|
|
22
|
+
return {
|
|
23
|
+
id,
|
|
24
|
+
name: id,
|
|
25
|
+
description: "",
|
|
26
|
+
tier,
|
|
27
|
+
gate: "",
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function facts(n: number): Fact[] {
|
|
32
|
+
return Array.from({ length: n }, (_, i) => fact(`f-${i}`));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function unlockedCaps(n: number): Capability[] {
|
|
36
|
+
return Array.from({ length: n }, (_, i) => cap(`c-${i}`, "unlocked"));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("progress-formula", () => {
|
|
40
|
+
describe("PROGRESS_WEIGHTS", () => {
|
|
41
|
+
test("sum to exactly 1 so a fully saturated input yields 100", () => {
|
|
42
|
+
const sum =
|
|
43
|
+
PROGRESS_WEIGHTS.facts +
|
|
44
|
+
PROGRESS_WEIGHTS.capabilities +
|
|
45
|
+
PROGRESS_WEIGHTS.conversations;
|
|
46
|
+
// Use toBeCloseTo for float safety.
|
|
47
|
+
expect(sum).toBeCloseTo(1, 10);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("computeProgressPercent", () => {
|
|
52
|
+
test("zero state -> 0", () => {
|
|
53
|
+
expect(
|
|
54
|
+
computeProgressPercent({
|
|
55
|
+
facts: [],
|
|
56
|
+
capabilities: [],
|
|
57
|
+
conversationCount: 0,
|
|
58
|
+
}),
|
|
59
|
+
).toBe(0);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("all saturated -> 100", () => {
|
|
63
|
+
expect(
|
|
64
|
+
computeProgressPercent({
|
|
65
|
+
facts: facts(PROGRESS_TARGETS.facts),
|
|
66
|
+
capabilities: unlockedCaps(PROGRESS_TARGETS.capabilities),
|
|
67
|
+
conversationCount: PROGRESS_TARGETS.conversations,
|
|
68
|
+
}),
|
|
69
|
+
).toBe(100);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("saturation clamps: inputs beyond target do not exceed 100", () => {
|
|
73
|
+
expect(
|
|
74
|
+
computeProgressPercent({
|
|
75
|
+
facts: facts(PROGRESS_TARGETS.facts * 3),
|
|
76
|
+
capabilities: unlockedCaps(PROGRESS_TARGETS.capabilities * 3),
|
|
77
|
+
conversationCount: PROGRESS_TARGETS.conversations * 3,
|
|
78
|
+
}),
|
|
79
|
+
).toBe(100);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("facts-only at target contributes exactly its weight", () => {
|
|
83
|
+
expect(
|
|
84
|
+
computeProgressPercent({
|
|
85
|
+
facts: facts(PROGRESS_TARGETS.facts),
|
|
86
|
+
capabilities: [],
|
|
87
|
+
conversationCount: 0,
|
|
88
|
+
}),
|
|
89
|
+
).toBe(Math.round(PROGRESS_WEIGHTS.facts * 100));
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("capabilities-only at target contributes exactly its weight", () => {
|
|
93
|
+
expect(
|
|
94
|
+
computeProgressPercent({
|
|
95
|
+
facts: [],
|
|
96
|
+
capabilities: unlockedCaps(PROGRESS_TARGETS.capabilities),
|
|
97
|
+
conversationCount: 0,
|
|
98
|
+
}),
|
|
99
|
+
).toBe(Math.round(PROGRESS_WEIGHTS.capabilities * 100));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("conversations-only at target contributes exactly its weight", () => {
|
|
103
|
+
expect(
|
|
104
|
+
computeProgressPercent({
|
|
105
|
+
facts: [],
|
|
106
|
+
capabilities: [],
|
|
107
|
+
conversationCount: PROGRESS_TARGETS.conversations,
|
|
108
|
+
}),
|
|
109
|
+
).toBe(Math.round(PROGRESS_WEIGHTS.conversations * 100));
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("non-unlocked capabilities do not contribute", () => {
|
|
113
|
+
const mixed: Capability[] = [
|
|
114
|
+
cap("a", "next-up"),
|
|
115
|
+
cap("b", "earned"),
|
|
116
|
+
cap("c", "unlocked"),
|
|
117
|
+
];
|
|
118
|
+
// Only 1 of PROGRESS_TARGETS.capabilities counts toward the caps weight.
|
|
119
|
+
const expected = Math.round(
|
|
120
|
+
((PROGRESS_WEIGHTS.capabilities * 1) / PROGRESS_TARGETS.capabilities) *
|
|
121
|
+
100,
|
|
122
|
+
);
|
|
123
|
+
expect(
|
|
124
|
+
computeProgressPercent({
|
|
125
|
+
facts: [],
|
|
126
|
+
capabilities: mixed,
|
|
127
|
+
conversationCount: 0,
|
|
128
|
+
}),
|
|
129
|
+
).toBe(expected);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("specific mix: half facts, half caps, half conversations -> 50", () => {
|
|
133
|
+
// Each signal at half its target contributes half its weight; total
|
|
134
|
+
// is half of the combined weight (1) * 100 = 50.
|
|
135
|
+
expect(
|
|
136
|
+
computeProgressPercent({
|
|
137
|
+
facts: facts(PROGRESS_TARGETS.facts / 2),
|
|
138
|
+
capabilities: unlockedCaps(PROGRESS_TARGETS.capabilities / 2),
|
|
139
|
+
conversationCount: PROGRESS_TARGETS.conversations / 2,
|
|
140
|
+
}),
|
|
141
|
+
).toBe(50);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("computeTier", () => {
|
|
146
|
+
test("zero state -> tier 1 (Getting to know you)", () => {
|
|
147
|
+
expect(
|
|
148
|
+
computeTier({ facts: [], capabilities: [], conversationCount: 0 }),
|
|
149
|
+
).toBe(1);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("5 conversations + 3 facts -> tier 2 (Finding my footing)", () => {
|
|
153
|
+
expect(
|
|
154
|
+
computeTier({
|
|
155
|
+
facts: facts(3),
|
|
156
|
+
capabilities: [],
|
|
157
|
+
conversationCount: 5,
|
|
158
|
+
}),
|
|
159
|
+
).toBe(2);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("just below tier 2 threshold on facts -> still tier 1", () => {
|
|
163
|
+
expect(
|
|
164
|
+
computeTier({
|
|
165
|
+
facts: facts(2),
|
|
166
|
+
capabilities: [],
|
|
167
|
+
conversationCount: 5,
|
|
168
|
+
}),
|
|
169
|
+
).toBe(1);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("just below tier 2 threshold on conversations -> still tier 1", () => {
|
|
173
|
+
expect(
|
|
174
|
+
computeTier({
|
|
175
|
+
facts: facts(3),
|
|
176
|
+
capabilities: [],
|
|
177
|
+
conversationCount: 4,
|
|
178
|
+
}),
|
|
179
|
+
).toBe(1);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("20 conversations + 3 unlocked caps -> tier 3 (Hitting our stride)", () => {
|
|
183
|
+
expect(
|
|
184
|
+
computeTier({
|
|
185
|
+
facts: facts(5),
|
|
186
|
+
capabilities: unlockedCaps(3),
|
|
187
|
+
conversationCount: 20,
|
|
188
|
+
}),
|
|
189
|
+
).toBe(3);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("20 conversations + 2 unlocked caps -> tier 2 (fallthrough)", () => {
|
|
193
|
+
expect(
|
|
194
|
+
computeTier({
|
|
195
|
+
facts: facts(3),
|
|
196
|
+
capabilities: unlockedCaps(2),
|
|
197
|
+
conversationCount: 20,
|
|
198
|
+
}),
|
|
199
|
+
).toBe(2);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("tier 4 is reserved and unreachable from the default heuristic", () => {
|
|
203
|
+
// Even a fully-saturated input should not produce tier 4 today.
|
|
204
|
+
const result = computeTier({
|
|
205
|
+
facts: facts(50),
|
|
206
|
+
capabilities: unlockedCaps(6),
|
|
207
|
+
conversationCount: 1_000,
|
|
208
|
+
});
|
|
209
|
+
expect(result).not.toBe(4);
|
|
210
|
+
expect(result).toBe(3);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
});
|