@vellumai/assistant 0.4.43 → 0.4.45
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 +4 -0
- package/ARCHITECTURE.md +46 -44
- package/README.md +15 -16
- package/bun.lock +10 -35
- package/docs/architecture/integrations.md +102 -215
- package/docs/architecture/keychain-broker.md +1 -1
- package/docs/architecture/memory.md +2 -2
- package/docs/architecture/scheduling.md +1 -1
- package/docs/architecture/security.md +11 -11
- package/docs/error-handling.md +1 -1
- package/docs/trusted-contact-access.md +3 -3
- package/drizzle/meta/0000_snapshot.json +34 -100
- package/drizzle/meta/_journal.json +1 -1
- package/drizzle.config.ts +4 -4
- package/package.json +3 -2
- package/scripts/capture-x-graphql.ts +237 -141
- package/scripts/generate-bundled-tool-registry.ts +223 -0
- package/src/__tests__/access-request-decision.test.ts +0 -1
- package/src/__tests__/actor-token-service.test.ts +23 -24
- package/src/__tests__/agent-loop.test.ts +0 -131
- package/src/__tests__/always-loaded-tools-guard.test.ts +71 -0
- package/src/__tests__/amazon-cdp-integration.test.ts +11 -9
- package/src/__tests__/approval-primitive.test.ts +0 -1
- package/src/__tests__/approval-routes-http.test.ts +11 -3
- package/src/__tests__/asset-materialize-tool.test.ts +0 -1
- package/src/__tests__/asset-search-tool.test.ts +0 -1
- package/src/__tests__/assistant-attachment-directive.test.ts +1 -1
- package/src/__tests__/assistant-events-sse-hardening.test.ts +0 -1
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +0 -2
- package/src/__tests__/assistant-feature-flags-integration.test.ts +70 -18
- package/src/__tests__/assistant-id-boundary-guard.test.ts +6 -6
- package/src/__tests__/attachments-store.test.ts +0 -1
- package/src/__tests__/avatar-e2e.test.ts +74 -115
- package/src/__tests__/avatar-router.test.ts +25 -62
- package/src/__tests__/browser-manager.test.ts +24 -0
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +4 -3
- package/src/__tests__/browser-skill-endstate.test.ts +8 -11
- package/src/__tests__/btw-routes.test.ts +326 -0
- package/src/__tests__/bundled-asset.test.ts +1 -1
- package/src/__tests__/bundled-skill-retrieval-guard.test.ts +23 -9
- package/src/__tests__/call-controller.test.ts +0 -1
- package/src/__tests__/call-conversation-messages.test.ts +0 -1
- package/src/__tests__/call-domain.test.ts +0 -1
- package/src/__tests__/call-pointer-messages.test.ts +0 -1
- package/src/__tests__/call-recovery.test.ts +0 -1
- package/src/__tests__/call-routes-http.test.ts +0 -1
- package/src/__tests__/call-store.test.ts +0 -1
- package/src/__tests__/canonical-guardian-store.test.ts +0 -1
- package/src/__tests__/channel-approval-routes.test.ts +1 -1
- package/src/__tests__/channel-approvals.test.ts +1 -1
- package/src/__tests__/channel-delivery-store.test.ts +0 -1
- package/src/__tests__/channel-guardian.test.ts +5 -7
- package/src/__tests__/channel-retry-sweep.test.ts +0 -1
- package/src/__tests__/checker.test.ts +32 -36
- package/src/__tests__/compaction.benchmark.test.ts +16 -14
- package/src/__tests__/computer-use-session-lifecycle.test.ts +10 -11
- package/src/__tests__/computer-use-session-working-dir.test.ts +2 -6
- package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +2 -5
- package/src/__tests__/computer-use-tools.test.ts +35 -31
- package/src/__tests__/config-schema.test.ts +11 -15
- package/src/__tests__/config-watcher.test.ts +0 -1
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -1
- package/src/__tests__/conflict-store.test.ts +0 -1
- package/src/__tests__/connection-policy.test.ts +4 -7
- package/src/__tests__/contacts-tools.test.ts +0 -1
- package/src/__tests__/context-memory-e2e.test.ts +2 -4
- package/src/__tests__/context-overflow-reducer.test.ts +2 -4
- package/src/__tests__/context-window-manager.test.ts +147 -60
- package/src/__tests__/contradiction-checker.test.ts +0 -1
- package/src/__tests__/conversation-attention-store.test.ts +0 -1
- package/src/__tests__/conversation-attention-telegram.test.ts +1 -1
- package/src/__tests__/conversation-pairing.test.ts +2 -2
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +31 -7
- package/src/__tests__/conversation-routes-slash-commands.test.ts +381 -0
- package/src/__tests__/conversation-store.test.ts +0 -1
- package/src/__tests__/conversation-unread-route.test.ts +1 -2
- package/src/__tests__/credential-security-invariants.test.ts +8 -8
- package/src/__tests__/cross-provider-web-search.test.ts +353 -0
- package/src/__tests__/daemon-assistant-events.test.ts +6 -7
- package/src/__tests__/db-schedule-syntax-migration.test.ts +15 -3
- package/src/__tests__/delete-managed-skill-tool.test.ts +5 -9
- package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -1
- package/src/__tests__/diagnostics-export.test.ts +189 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
- package/src/__tests__/emit-signal-routing-intent.test.ts +3 -3
- package/src/__tests__/entity-extractor.test.ts +0 -1
- package/src/__tests__/entity-search.test.ts +0 -1
- package/src/__tests__/ephemeral-permissions.test.ts +2 -4
- package/src/__tests__/error-handler-friendly-messages.test.ts +46 -0
- package/src/__tests__/file-read-tool.test.ts +86 -0
- package/src/__tests__/followup-tools.test.ts +0 -1
- package/src/__tests__/frontmatter.test.ts +77 -34
- package/src/__tests__/gateway-only-enforcement.test.ts +0 -1
- package/src/__tests__/gateway-only-guard.test.ts +1 -1
- package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -1
- package/src/__tests__/guardian-action-followup-executor.test.ts +0 -1
- package/src/__tests__/guardian-action-followup-store.test.ts +0 -1
- package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -1
- package/src/__tests__/guardian-action-late-reply.test.ts +0 -1
- package/src/__tests__/guardian-action-store.test.ts +0 -1
- package/src/__tests__/guardian-action-sweep.test.ts +0 -1
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +0 -1
- package/src/__tests__/guardian-dispatch.test.ts +1 -2
- package/src/__tests__/guardian-grant-minting.test.ts +1 -1
- package/src/__tests__/guardian-outbound-http.test.ts +0 -1
- package/src/__tests__/guardian-principal-id-roundtrip.test.ts +0 -1
- package/src/__tests__/guardian-routing-invariants.test.ts +1 -1
- package/src/__tests__/guardian-routing-state.test.ts +0 -1
- package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -1
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +3 -5
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +28 -426
- package/src/__tests__/host-bash-proxy.test.ts +335 -0
- package/src/__tests__/host-file-proxy.test.ts +374 -0
- package/src/__tests__/host-shell-tool.test.ts +147 -1
- package/src/__tests__/http-user-message-parity.test.ts +361 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +0 -1
- package/src/__tests__/integration-status.test.ts +3 -8
- package/src/__tests__/intent-routing.test.ts +7 -46
- package/src/__tests__/invite-redemption-service.test.ts +0 -1
- package/src/__tests__/invite-routes-http.test.ts +0 -1
- package/src/__tests__/llm-usage-store.test.ts +0 -1
- package/src/__tests__/managed-avatar-client.test.ts +101 -55
- package/src/__tests__/managed-skill-lifecycle.test.ts +9 -18
- package/src/__tests__/managed-store.test.ts +94 -21
- package/src/__tests__/media-reuse-story.e2e.test.ts +0 -1
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +2 -4
- package/src/__tests__/memory-lifecycle-e2e.test.ts +0 -1
- package/src/__tests__/memory-recall-quality.test.ts +0 -1
- package/src/__tests__/memory-regressions.experimental.test.ts +0 -1
- package/src/__tests__/memory-regressions.test.ts +0 -1
- package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -1
- package/src/__tests__/memory-upsert-concurrency.test.ts +0 -1
- package/src/__tests__/messaging-send-tool.test.ts +35 -0
- package/src/__tests__/messaging-skill-split.test.ts +138 -0
- package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1
- package/src/__tests__/migration-export-http.test.ts +2 -3
- package/src/__tests__/migration-import-commit-http.test.ts +1 -2
- package/src/__tests__/migration-import-preflight-http.test.ts +1 -2
- package/src/__tests__/migration-validate-http.test.ts +1 -2
- package/src/__tests__/native-web-search.test.ts +475 -0
- package/src/__tests__/navigate-settings-tab.test.ts +84 -0
- package/src/__tests__/non-member-access-request.test.ts +0 -1
- package/src/__tests__/notification-broadcaster.test.ts +15 -15
- package/src/__tests__/notification-decision-strategy.test.ts +6 -6
- package/src/__tests__/notification-deep-link.test.ts +7 -7
- package/src/__tests__/notification-guardian-path.test.ts +2 -3
- package/src/__tests__/notification-telegram-adapter.test.ts +1 -1
- package/src/__tests__/notification-thread-candidates.test.ts +4 -4
- package/src/__tests__/onboarding-starter-tasks.test.ts +0 -1
- package/src/__tests__/onboarding-template-contract.test.ts +0 -10
- package/src/__tests__/playbook-execution.test.ts +0 -1
- package/src/__tests__/playbook-tools.test.ts +0 -1
- package/src/__tests__/profile-compiler.test.ts +0 -1
- package/src/__tests__/provider-fail-open-selection.test.ts +12 -2
- package/src/__tests__/provider-managed-proxy-integration.test.ts +25 -0
- package/src/__tests__/qdrant-collection-migration.test.ts +223 -0
- package/src/__tests__/recording-handler.test.ts +30 -94
- package/src/__tests__/registry.test.ts +28 -35
- package/src/__tests__/relay-server.test.ts +0 -1
- package/src/__tests__/ride-shotgun-handler.test.ts +4 -20
- package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
- package/src/__tests__/runtime-events-sse-parity.test.ts +3 -4
- package/src/__tests__/runtime-events-sse.test.ts +0 -1
- package/src/__tests__/sandbox-diagnostics.test.ts +0 -1
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +30 -28
- package/src/__tests__/schedule-store.test.ts +441 -1
- package/src/__tests__/schedule-tools.test.ts +468 -7
- package/src/__tests__/scheduler-recurrence.test.ts +196 -23
- package/src/__tests__/scoped-approval-grants.test.ts +0 -1
- package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -1
- package/src/__tests__/secret-prompt-log-hygiene.test.ts +6 -3
- package/src/__tests__/secret-response-routing.test.ts +4 -1
- package/src/__tests__/send-endpoint-busy.test.ts +14 -5
- package/src/__tests__/send-notification-tool.test.ts +0 -7
- package/src/__tests__/sequence-store.test.ts +0 -1
- package/src/__tests__/server-history-render.test.ts +1 -2
- package/src/__tests__/session-abort-tool-results.test.ts +0 -1
- package/src/__tests__/session-agent-loop.test.ts +46 -6
- package/src/__tests__/session-confirmation-signals.test.ts +7 -46
- package/src/__tests__/session-conflict-gate.test.ts +2 -6
- package/src/__tests__/session-error.test.ts +5 -14
- package/src/__tests__/session-init.benchmark.test.ts +3 -5
- package/src/__tests__/session-load-history-repair.test.ts +0 -1
- package/src/__tests__/session-media-retry.test.ts +12 -74
- package/src/__tests__/session-pre-run-repair.test.ts +0 -1
- package/src/__tests__/session-profile-injection.test.ts +2 -6
- package/src/__tests__/session-provider-retry-repair.test.ts +2 -6
- package/src/__tests__/session-queue.test.ts +94 -139
- package/src/__tests__/session-skill-tools.test.ts +115 -115
- package/src/__tests__/session-slash-known.test.ts +0 -1
- package/src/__tests__/session-slash-queue.test.ts +0 -1
- package/src/__tests__/session-slash-unknown.test.ts +0 -1
- package/src/__tests__/session-surfaces-task-progress.test.ts +34 -0
- package/src/__tests__/session-usage.test.ts +0 -1
- package/src/__tests__/session-workspace-cache-state.test.ts +2 -6
- package/src/__tests__/session-workspace-injection.test.ts +2 -6
- package/src/__tests__/session-workspace-tool-tracking.test.ts +2 -6
- package/src/__tests__/skill-feature-flags-integration.test.ts +180 -184
- package/src/__tests__/skill-feature-flags.test.ts +125 -18
- package/src/__tests__/skill-load-feature-flag.test.ts +1 -2
- package/src/__tests__/skill-load-tool.test.ts +194 -2
- package/src/__tests__/skill-projection-feature-flag.test.ts +27 -16
- package/src/__tests__/skill-projection.benchmark.test.ts +15 -14
- package/src/__tests__/skills.test.ts +14 -53
- package/src/__tests__/slack-channel-config.test.ts +0 -1
- package/src/__tests__/slack-inbound-verification.test.ts +0 -1
- package/src/__tests__/slack-skill.test.ts +1 -1
- package/src/__tests__/starter-task-flow.test.ts +9 -19
- package/src/__tests__/subagent-tools.test.ts +2 -2
- package/src/__tests__/system-prompt.test.ts +7 -7
- package/src/__tests__/task-compiler.test.ts +0 -1
- package/src/__tests__/task-management-tools.test.ts +0 -1
- package/src/__tests__/task-memory-cleanup.test.ts +0 -1
- package/src/__tests__/task-runner.test.ts +0 -1
- package/src/__tests__/task-scheduler.test.ts +0 -1
- package/src/__tests__/terminal-tools.test.ts +0 -1
- package/src/__tests__/test-support/computer-use-skill-harness.ts +2 -4
- package/src/__tests__/thread-seed-composer.test.ts +5 -5
- package/src/__tests__/tool-approval-handler.test.ts +0 -1
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +0 -1
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -1
- package/src/__tests__/tool-executor.test.ts +8 -86
- package/src/__tests__/tool-grant-request-escalation.test.ts +0 -1
- package/src/__tests__/tool-notification-listener.test.ts +1 -1
- package/src/__tests__/tool-preview-lifecycle.test.ts +416 -0
- package/src/__tests__/trust-store.test.ts +84 -8
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +0 -1
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -1
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
- package/src/__tests__/trusted-contact-verification.test.ts +0 -1
- package/src/__tests__/twilio-provider.test.ts +0 -1
- package/src/__tests__/twilio-routes.test.ts +0 -1
- package/src/__tests__/{request-file-tool.test.ts → ui-file-upload-surface.test.ts} +11 -72
- package/src/__tests__/update-bulletin.test.ts +0 -1
- package/src/__tests__/usage-cache-backfill-migration.test.ts +0 -1
- package/src/__tests__/usage-routes.test.ts +0 -1
- package/src/__tests__/verification-control-plane-policy.test.ts +4 -4
- package/src/__tests__/voice-invite-redemption.test.ts +0 -1
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
- package/src/__tests__/voice-session-bridge.test.ts +9 -1
- package/src/__tests__/web-fetch.test.ts +57 -0
- package/src/__tests__/workspace-git-service.test.ts +5 -14
- package/src/__tests__/workspace-policy.test.ts +0 -1
- package/src/agent/loop.ts +22 -34
- package/src/bundler/bundle-signer.ts +4 -4
- package/src/calls/call-controller.ts +1 -1
- package/src/calls/relay-server.ts +1 -1
- package/src/calls/twilio-rest.ts +1 -1
- package/src/calls/voice-session-bridge.ts +3 -1
- package/src/cli/__tests__/notifications.test.ts +3 -4
- package/src/cli/commands/map.ts +2 -6
- package/src/cli/commands/mcp.ts +73 -15
- package/src/cli/commands/notifications.ts +4 -4
- package/src/cli/commands/sessions.ts +9 -1
- package/src/cli/commands/skills.ts +6 -10
- package/src/cli/http-client.ts +2 -3
- package/src/cli/main-screen.tsx +10 -10
- package/src/cli/program.ts +0 -4
- package/src/cli/reference.ts +0 -2
- package/src/cli.ts +15 -9
- package/src/config/__tests__/bundled-tool-registry-guard.test.ts +120 -0
- package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +11 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +6 -7
- package/src/config/bundled-skills/app-builder/TOOLS.json +0 -4
- package/src/config/bundled-skills/browser/SKILL.md +6 -1
- package/src/config/bundled-skills/chatgpt-import/SKILL.md +5 -1
- package/src/config/bundled-skills/claude-code/SKILL.md +5 -1
- package/src/config/bundled-skills/computer-use/SKILL.md +6 -1
- package/src/config/bundled-skills/computer-use/TOOLS.json +6 -69
- package/src/config/bundled-skills/computer-use/tools/computer-use-click.ts +10 -1
- package/src/config/bundled-skills/contacts/SKILL.md +10 -1
- package/src/config/bundled-skills/contacts/TOOLS.json +35 -0
- package/src/config/bundled-skills/{messaging → contacts}/tools/google-contacts.ts +9 -2
- package/src/config/bundled-skills/document/SKILL.md +4 -1
- package/src/config/bundled-skills/doordash/SKILL.md +8 -2
- package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +1 -82
- package/src/config/bundled-skills/doordash/doordash-cli.ts +17 -28
- package/src/config/bundled-skills/doordash/lib/session.ts +21 -17
- package/src/config/bundled-skills/doordash/lib/shared/platform.ts +4 -1
- package/src/config/bundled-skills/followups/SKILL.md +4 -1
- package/src/config/bundled-skills/gmail/SKILL.md +180 -0
- package/src/config/bundled-skills/gmail/TOOLS.json +506 -0
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +149 -0
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +110 -0
- package/src/config/bundled-skills/{messaging → gmail}/tools/gmail-draft.ts +1 -1
- package/src/config/bundled-skills/{messaging → gmail}/tools/gmail-filters.ts +1 -1
- package/src/config/bundled-skills/{messaging → gmail}/tools/gmail-follow-up.ts +1 -1
- package/src/config/bundled-skills/{messaging → gmail}/tools/gmail-forward.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +50 -0
- package/src/config/bundled-skills/{messaging → gmail}/tools/gmail-outreach-scan.ts +8 -90
- package/src/config/bundled-skills/{messaging → gmail}/tools/gmail-send-draft.ts +1 -1
- package/src/config/bundled-skills/{messaging → gmail}/tools/gmail-sender-digest.ts +2 -2
- package/src/config/bundled-skills/{messaging → gmail}/tools/gmail-trash.ts +1 -1
- package/src/config/bundled-skills/{messaging → gmail}/tools/gmail-unsubscribe.ts +1 -1
- package/src/config/bundled-skills/{messaging → gmail}/tools/gmail-vacation.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/shared.ts +47 -0
- package/src/config/bundled-skills/google-calendar/SKILL.md +5 -1
- package/src/config/bundled-skills/image-studio/SKILL.md +5 -1
- package/src/config/bundled-skills/knowledge-graph/SKILL.md +4 -1
- package/src/config/bundled-skills/media-processing/SKILL.md +7 -13
- package/src/config/bundled-skills/media-processing/TOOLS.json +0 -22
- package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +12 -1
- package/src/config/bundled-skills/messaging/SKILL.md +23 -139
- package/src/config/bundled-skills/messaging/TOOLS.json +33 -1215
- package/src/config/bundled-skills/messaging/tools/gmail-mime-helpers.ts +42 -0
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +165 -2
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +1 -13
- package/src/config/bundled-skills/messaging/tools/shared.ts +81 -34
- package/src/config/bundled-skills/notifications/SKILL.md +5 -1
- package/src/config/bundled-skills/orchestration/SKILL.md +30 -0
- package/src/config/bundled-skills/orchestration/TOOLS.json +35 -0
- package/src/config/bundled-skills/{reminder/tools/reminder-cancel.ts → orchestration/tools/swarm-delegate.ts} +3 -3
- package/src/config/bundled-skills/phone-calls/SKILL.md +9 -1
- package/src/config/bundled-skills/playbooks/SKILL.md +4 -1
- package/src/config/bundled-skills/schedule/SKILL.md +70 -9
- package/src/config/bundled-skills/schedule/TOOLS.json +38 -6
- package/src/config/bundled-skills/screen-watch/SKILL.md +28 -0
- package/src/config/bundled-skills/screen-watch/TOOLS.json +35 -0
- package/src/config/bundled-skills/{reminder/tools/reminder-create.ts → screen-watch/tools/start-screen-watch.ts} +3 -3
- package/src/config/bundled-skills/sequences/SKILL.md +47 -0
- package/src/config/bundled-skills/sequences/TOOLS.json +340 -0
- package/src/config/bundled-skills/sequences/tools/sequence-update.ts +128 -0
- package/src/config/bundled-skills/sequences/tools/shared.ts +9 -0
- package/src/config/bundled-skills/settings/SKILL.md +12 -0
- package/src/config/bundled-skills/settings/TOOLS.json +112 -0
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +43 -0
- package/src/config/bundled-skills/settings/tools/open-system-settings.ts +52 -0
- package/src/config/bundled-skills/{computer-use/tools/computer-use-right-click.ts → settings/tools/set-avatar.ts} +2 -6
- package/src/{tools/system/voice-config.ts → config/bundled-skills/settings/tools/voice-config-update.ts} +59 -96
- package/src/config/bundled-skills/skill-management/SKILL.md +18 -0
- package/src/config/bundled-skills/skill-management/TOOLS.json +90 -0
- package/src/config/bundled-skills/{computer-use/tools/computer-use-double-click.ts → skill-management/tools/delete-managed.ts} +2 -6
- package/src/config/bundled-skills/skill-management/tools/scaffold-managed.ts +12 -0
- package/src/config/bundled-skills/slack/SKILL.md +5 -1
- package/src/config/bundled-skills/subagent/SKILL.md +4 -1
- package/src/config/bundled-skills/tasks/SKILL.md +5 -2
- package/src/config/bundled-skills/transcribe/SKILL.md +4 -1
- package/src/config/bundled-skills/watcher/SKILL.md +4 -1
- package/src/config/bundled-tool-registry.ts +118 -107
- package/src/config/env.ts +5 -2
- package/src/config/feature-flag-registry.json +33 -9
- package/src/config/loader.ts +10 -2
- package/src/config/schema.ts +19 -16
- package/src/config/schemas/inference.ts +12 -22
- package/src/config/schemas/memory-storage.ts +19 -1
- package/src/config/schemas/platform.ts +0 -16
- package/src/config/skill-state.ts +11 -8
- package/src/config/skills.ts +83 -32
- package/src/context/token-estimator.ts +11 -0
- package/src/context/window-manager.ts +180 -151
- package/src/daemon/computer-use-session.ts +11 -43
- package/src/daemon/daemon-control.ts +4 -1
- package/src/daemon/handlers/config-channels.ts +5 -9
- package/src/daemon/handlers/config-ingress.ts +0 -4
- package/src/daemon/handlers/config-model.ts +7 -13
- package/src/daemon/handlers/config-telegram.ts +4 -8
- package/src/daemon/handlers/config-voice.ts +2 -5
- package/src/daemon/handlers/dictation.ts +2 -12
- package/src/daemon/handlers/identity.ts +0 -105
- package/src/daemon/handlers/recording.ts +3 -23
- package/src/daemon/handlers/session-history.ts +42 -10
- package/src/daemon/handlers/sessions.ts +53 -72
- package/src/daemon/handlers/shared.ts +7 -28
- package/src/daemon/handlers/skills.ts +31 -27
- package/src/daemon/host-bash-proxy.ts +148 -0
- package/src/daemon/host-file-proxy.ts +135 -0
- package/src/daemon/lifecycle.ts +53 -41
- package/src/daemon/mcp-reload-service.ts +123 -0
- package/src/daemon/message-protocol.ts +6 -0
- package/src/daemon/message-types/apps.ts +0 -25
- package/src/daemon/message-types/browser.ts +1 -1
- package/src/daemon/message-types/computer-use.ts +1 -4
- package/src/daemon/message-types/guardian-actions.ts +1 -1
- package/src/daemon/message-types/host-bash.ts +18 -0
- package/src/daemon/message-types/host-file.ts +44 -0
- package/src/daemon/message-types/integrations.ts +1 -73
- package/src/daemon/message-types/messages.ts +15 -0
- package/src/daemon/message-types/schedules.ts +11 -27
- package/src/daemon/message-types/sessions.ts +8 -2
- package/src/daemon/message-types/settings.ts +1 -1
- package/src/daemon/message-types/shared.ts +1 -1
- package/src/daemon/message-types/surfaces.ts +2 -0
- package/src/daemon/ride-shotgun-handler.ts +35 -43
- package/src/daemon/seed-files.ts +3 -27
- package/src/daemon/server.ts +45 -28
- package/src/daemon/session-agent-loop-handlers.ts +72 -9
- package/src/daemon/session-agent-loop.ts +97 -66
- package/src/daemon/session-attachments.ts +1 -1
- package/src/daemon/session-error.ts +17 -16
- package/src/daemon/session-lifecycle.ts +20 -1
- package/src/daemon/session-media-retry.ts +1 -15
- package/src/daemon/session-messaging.ts +14 -6
- package/src/daemon/session-process.ts +36 -7
- package/src/daemon/session-queue-manager.ts +62 -103
- package/src/daemon/session-runtime-assembly.ts +27 -7
- package/src/daemon/session-skill-tools.ts +12 -11
- package/src/daemon/session-slash.ts +7 -0
- package/src/daemon/session-surfaces.ts +192 -118
- package/src/daemon/session-tool-setup.ts +146 -6
- package/src/daemon/session.ts +75 -37
- package/src/errors.ts +0 -2
- package/src/export/formatter.ts +6 -0
- package/src/mcp/mcp-oauth-provider.ts +1 -3
- package/src/media/avatar-router.ts +20 -28
- package/src/media/avatar-types.ts +7 -14
- package/src/media/managed-avatar-client.ts +70 -34
- package/src/memory/app-store.ts +0 -18
- package/src/memory/conversation-title-service.ts +1 -2
- package/src/memory/db-init.ts +16 -0
- package/src/memory/embedding-backend.ts +129 -27
- package/src/memory/embedding-gemini.test.ts +256 -0
- package/src/memory/embedding-gemini.ts +47 -13
- package/src/memory/embedding-local.ts +14 -2
- package/src/memory/embedding-ollama.ts +15 -2
- package/src/memory/embedding-openai.ts +15 -2
- package/src/memory/embedding-types.test.ts +116 -0
- package/src/memory/embedding-types.ts +61 -0
- package/src/memory/fingerprint.ts +1 -1
- package/src/memory/indexer.ts +25 -1
- package/src/memory/job-handlers/embedding.test.ts +258 -0
- package/src/memory/job-handlers/embedding.ts +81 -1
- package/src/memory/job-handlers/index-maintenance.ts +35 -1
- package/src/memory/job-handlers/media-processing.ts +11 -1
- package/src/memory/job-utils.ts +21 -6
- package/src/memory/jobs-store.ts +5 -1
- package/src/memory/jobs-worker.ts +8 -0
- package/src/memory/message-content.ts +66 -0
- package/src/memory/migrations/100-core-tables.ts +1 -31
- package/src/memory/migrations/104-core-indexes.ts +0 -11
- package/src/memory/migrations/145-drop-accounts-table.ts +19 -0
- package/src/memory/migrations/146-schedule-oneshot-routing.ts +94 -0
- package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +129 -0
- package/src/memory/migrations/148-drop-reminders-table.ts +18 -0
- package/src/memory/migrations/index.ts +4 -0
- package/src/memory/migrations/registry.ts +19 -0
- package/src/memory/qdrant-client.ts +158 -43
- package/src/memory/retriever.test.ts +0 -1
- package/src/memory/retriever.ts +12 -2
- package/src/memory/schema/infrastructure.ts +5 -37
- package/src/memory/search/formatting.ts +34 -9
- package/src/memory/search/semantic.ts +57 -2
- package/src/memory/search/types.ts +2 -1
- package/src/notifications/AGENTS.md +2 -2
- package/src/notifications/README.md +59 -58
- package/src/notifications/adapters/macos.ts +1 -1
- package/src/notifications/broadcaster.ts +5 -5
- package/src/notifications/copy-composer.ts +1 -1
- package/src/notifications/decision-engine.ts +2 -2
- package/src/notifications/destination-resolver.ts +2 -2
- package/src/notifications/emit-signal.ts +8 -8
- package/src/notifications/signal.ts +1 -1
- package/src/notifications/thread-seed-composer.ts +1 -1
- package/src/oauth/connect-orchestrator.ts +1 -1
- package/src/oauth/token-persistence.ts +1 -1
- package/src/permissions/checker.ts +12 -1
- package/src/permissions/defaults.ts +13 -17
- package/src/permissions/trust-store.ts +37 -0
- package/src/permissions/workspace-policy.ts +0 -1
- package/src/prompts/__tests__/build-cli-reference-section.test.ts +11 -0
- package/src/prompts/computer-use-prompt.ts +1 -1
- package/src/prompts/system-prompt.ts +33 -35
- package/src/prompts/templates/BOOTSTRAP.md +0 -3
- package/src/prompts/templates/SOUL.md +1 -2
- package/src/prompts/templates/UPDATES.md +16 -7
- package/src/providers/anthropic/client.ts +87 -33
- package/src/providers/gemini/client.ts +6 -0
- package/src/providers/managed-proxy/constants.ts +5 -0
- package/src/providers/openai/client.ts +15 -0
- package/src/providers/registry.ts +4 -6
- package/src/providers/types.ts +24 -2
- package/src/runtime/AGENTS.md +18 -0
- package/src/runtime/assistant-event-hub.ts +2 -3
- package/src/runtime/assistant-event.ts +4 -4
- package/src/runtime/auth/__tests__/context.test.ts +5 -5
- package/src/runtime/auth/__tests__/credential-service.test.ts +0 -1
- package/src/runtime/auth/__tests__/guard-tests.test.ts +3 -2
- package/src/runtime/auth/__tests__/{ipc-auth-context.test.ts → local-auth-context.test.ts} +21 -21
- package/src/runtime/auth/__tests__/route-policy.test.ts +2 -2
- package/src/runtime/auth/__tests__/scopes.test.ts +9 -8
- package/src/runtime/auth/__tests__/subject.test.ts +8 -8
- package/src/runtime/auth/__tests__/token-service.test.ts +0 -1
- package/src/runtime/auth/route-policy.ts +8 -8
- package/src/runtime/auth/scopes.ts +2 -1
- package/src/runtime/auth/subject.ts +4 -4
- package/src/runtime/auth/token-service.ts +1 -24
- package/src/runtime/auth/types.ts +3 -3
- package/src/runtime/guardian-action-followup-executor.ts +1 -1
- package/src/runtime/guardian-action-grant-minter.ts +1 -1
- package/src/runtime/guardian-action-service.ts +3 -3
- package/src/runtime/http-server.ts +15 -2
- package/src/runtime/http-types.ts +10 -0
- package/src/runtime/invite-service.ts +3 -3
- package/src/runtime/local-actor-identity.ts +17 -22
- package/src/runtime/middleware/error-handler.ts +14 -1
- package/src/runtime/pending-interactions.ts +21 -9
- package/src/runtime/routes/app-management-routes.ts +63 -67
- package/src/runtime/routes/approval-routes.ts +1 -3
- package/src/runtime/routes/brain-graph/brain-graph.html +1845 -0
- package/src/runtime/routes/brain-graph-routes.ts +4 -42
- package/src/runtime/routes/btw-routes.ts +155 -0
- package/src/runtime/routes/computer-use-routes.ts +77 -31
- package/src/runtime/routes/conversation-routes.ts +234 -47
- package/src/runtime/routes/diagnostics-routes.ts +154 -43
- package/src/runtime/routes/documents-routes.ts +2 -2
- package/src/runtime/routes/global-search-routes.ts +1 -1
- package/src/runtime/routes/host-bash-routes.ts +83 -0
- package/src/runtime/routes/host-file-routes.ts +79 -0
- package/src/runtime/routes/integrations/slack/share.ts +1 -1
- package/src/runtime/routes/log-export-routes.ts +120 -0
- package/src/runtime/routes/mcp-routes.ts +20 -0
- package/src/runtime/routes/migration-routes.ts +3 -3
- package/src/runtime/routes/pairing-routes.ts +1 -1
- package/src/runtime/routes/recording-routes.ts +6 -4
- package/src/runtime/routes/schedule-routes.ts +31 -5
- package/src/runtime/routes/session-management-routes.ts +2 -6
- package/src/runtime/routes/session-query-routes.ts +18 -15
- package/src/runtime/routes/settings-routes.ts +7 -351
- package/src/runtime/routes/skills-routes.ts +7 -6
- package/src/runtime/routes/subagents-routes.ts +4 -10
- package/src/runtime/routes/surface-action-routes.ts +3 -14
- package/src/runtime/routes/surface-content-routes.ts +22 -5
- package/src/runtime/routes/work-items-routes.ts +21 -25
- package/src/runtime/routes/workspace-routes.test.ts +3 -3
- package/src/runtime/routes/workspace-utils.ts +1 -1
- package/src/runtime/telegram-streaming-delivery.ts +3 -0
- package/src/runtime/verification-outbound-actions.ts +2 -2
- package/src/schedule/integration-status.ts +0 -6
- package/src/schedule/schedule-store.ts +234 -43
- package/src/schedule/scheduler.ts +73 -74
- package/src/security/oauth2.ts +1 -1
- package/src/sequence/store.ts +12 -2
- package/src/skills/frontmatter.ts +19 -77
- package/src/skills/managed-store.ts +11 -2
- package/src/subagent/manager.ts +5 -3
- package/src/tasks/ephemeral-permissions.ts +3 -5
- package/src/tools/AGENTS.md +37 -0
- package/src/tools/apps/executors.ts +0 -6
- package/src/tools/browser/browser-manager.ts +17 -11
- package/src/tools/browser/jit-auth.ts +4 -1
- package/src/tools/claude-code/claude-code.ts +1 -1
- package/src/tools/computer-use/definitions.ts +48 -60
- package/src/tools/document/document-tool.ts +6 -6
- package/src/tools/document/editor-template.ts +10 -8
- package/src/tools/filesystem/edit.ts +2 -1
- package/src/tools/filesystem/read.ts +20 -2
- package/src/tools/filesystem/write.ts +2 -1
- package/src/tools/host-filesystem/edit.ts +17 -1
- package/src/tools/host-filesystem/read.ts +16 -1
- package/src/tools/host-filesystem/write.ts +15 -1
- package/src/tools/host-terminal/host-shell.ts +24 -0
- package/src/tools/memory/definitions.ts +45 -81
- package/src/tools/memory/handlers.test.ts +0 -1
- package/src/tools/memory/handlers.ts +1 -1
- package/src/tools/memory/register.ts +26 -60
- package/src/tools/network/script-proxy/session-manager.ts +6 -8
- package/src/tools/network/web-fetch.ts +7 -1
- package/src/tools/network/web-search.ts +2 -1
- package/src/tools/registry.ts +23 -0
- package/src/tools/schedule/create.ts +113 -5
- package/src/tools/schedule/list.ts +57 -15
- package/src/tools/schedule/update.ts +73 -3
- package/src/tools/shared/filesystem/image-read.ts +192 -0
- package/src/tools/side-effects.ts +1 -7
- package/src/tools/skills/delete-managed.ts +27 -64
- package/src/tools/skills/execute.ts +54 -0
- package/src/tools/skills/load.ts +127 -5
- package/src/tools/skills/scaffold-managed.ts +93 -172
- package/src/tools/subagent/message.ts +0 -7
- package/src/tools/subagent/spawn.ts +1 -1
- package/src/tools/swarm/delegate.ts +0 -3
- package/src/tools/system/avatar-generator.ts +13 -19
- package/src/tools/system/request-permission.ts +2 -1
- package/src/tools/terminal/safe-env.ts +1 -0
- package/src/tools/tool-manifest.ts +41 -47
- package/src/tools/types.ts +6 -2
- package/src/tools/ui-surface/definitions.ts +0 -55
- package/src/util/errors.ts +12 -10
- package/src/workspace/git-service.ts +0 -2
- package/src/__tests__/account-registry.test.ts +0 -258
- package/src/__tests__/email-classifier.test.ts +0 -25
- package/src/__tests__/gmail-integration.test.ts +0 -97
- package/src/__tests__/handle-user-message-secret-resume.test.ts +0 -172
- package/src/__tests__/home-base-bootstrap.test.ts +0 -84
- package/src/__tests__/managed-twitter-guardrails.test.ts +0 -353
- package/src/__tests__/prebuilt-home-base-seed.test.ts +0 -79
- package/src/__tests__/recording-intent-fallback.test.ts +0 -199
- package/src/__tests__/recording-intent.test.ts +0 -985
- package/src/__tests__/recording-state-machine.test.ts +0 -1574
- package/src/__tests__/reminder-store.test.ts +0 -350
- package/src/__tests__/reminder.test.ts +0 -337
- package/src/__tests__/scan-result-store.test.ts +0 -121
- package/src/__tests__/twitter-platform-proxy-client.test.ts +0 -450
- package/src/__tests__/view-image-tool.test.ts +0 -241
- package/src/cli/commands/amazon/cart.ts +0 -513
- package/src/cli/commands/amazon/checkout.ts +0 -394
- package/src/cli/commands/amazon/client.ts +0 -513
- package/src/cli/commands/amazon/index.ts +0 -920
- package/src/cli/commands/amazon/product-details.ts +0 -145
- package/src/cli/commands/amazon/request-extractor.ts +0 -187
- package/src/cli/commands/amazon/search.ts +0 -76
- package/src/cli/commands/amazon/session.ts +0 -116
- package/src/cli/commands/twitter/__tests__/cli-error-shaping.test.ts +0 -265
- package/src/cli/commands/twitter/__tests__/cli-read-routing.test.ts +0 -483
- package/src/cli/commands/twitter/__tests__/cli-routing.test.ts +0 -412
- package/src/cli/commands/twitter/__tests__/oauth-client.test.ts +0 -197
- package/src/cli/commands/twitter/client.ts +0 -989
- package/src/cli/commands/twitter/index.ts +0 -1160
- package/src/cli/commands/twitter/oauth-client.ts +0 -94
- package/src/cli/commands/twitter/router.ts +0 -396
- package/src/cli/commands/twitter/session.ts +0 -121
- package/src/config/bundled-skills/agentmail/SKILL.md +0 -132
- package/src/config/bundled-skills/agentmail/icon.svg +0 -21
- package/src/config/bundled-skills/amazon/SKILL.md +0 -137
- package/src/config/bundled-skills/amazon/icon.svg +0 -13
- package/src/config/bundled-skills/api-mapping/SKILL.md +0 -78
- package/src/config/bundled-skills/api-mapping/icon.svg +0 -18
- package/src/config/bundled-skills/cli-discover/SKILL.md +0 -68
- package/src/config/bundled-skills/deploy-fullstack-vercel/SKILL.md +0 -179
- package/src/config/bundled-skills/document-writer/SKILL.md +0 -195
- package/src/config/bundled-skills/elevenlabs-voice/SKILL.md +0 -140
- package/src/config/bundled-skills/email-setup/SKILL.md +0 -68
- package/src/config/bundled-skills/frontend-design/SKILL.md +0 -44
- package/src/config/bundled-skills/frontend-design/icon.svg +0 -16
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +0 -452
- package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +0 -203
- package/src/config/bundled-skills/influencer/SKILL.md +0 -144
- package/src/config/bundled-skills/influencer/scripts/client.ts +0 -1269
- package/src/config/bundled-skills/influencer/scripts/influencer.ts +0 -267
- package/src/config/bundled-skills/macos-automation/SKILL.md +0 -65
- package/src/config/bundled-skills/macos-automation/icon.svg +0 -12
- package/src/config/bundled-skills/mcp-setup/SKILL.md +0 -75
- package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +0 -184
- package/src/config/bundled-skills/messaging/tools/gmail-archive-by-query.ts +0 -80
- package/src/config/bundled-skills/messaging/tools/gmail-archive.ts +0 -29
- package/src/config/bundled-skills/messaging/tools/gmail-batch-archive.ts +0 -56
- package/src/config/bundled-skills/messaging/tools/gmail-batch-label.ts +0 -34
- package/src/config/bundled-skills/messaging/tools/gmail-download-attachment.ts +0 -47
- package/src/config/bundled-skills/messaging/tools/gmail-label.ts +0 -31
- package/src/config/bundled-skills/messaging/tools/gmail-list-attachments.ts +0 -67
- package/src/config/bundled-skills/messaging/tools/gmail-send-with-attachments.ts +0 -97
- package/src/config/bundled-skills/messaging/tools/gmail-summarize-thread.ts +0 -87
- package/src/config/bundled-skills/messaging/tools/gmail-triage.ts +0 -135
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-activity.ts +0 -24
- package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +0 -201
- package/src/config/bundled-skills/messaging/tools/send-notification.ts +0 -1
- package/src/config/bundled-skills/messaging/tools/sequence-cancel.ts +0 -27
- package/src/config/bundled-skills/messaging/tools/sequence-pause.ts +0 -48
- package/src/config/bundled-skills/messaging/tools/sequence-resume.ts +0 -27
- package/src/config/bundled-skills/messaging/tools/sequence-update.ts +0 -56
- package/src/config/bundled-skills/notion/SKILL.md +0 -240
- package/src/config/bundled-skills/notion-oauth-setup/SKILL.md +0 -126
- package/src/config/bundled-skills/oauth-setup/SKILL.md +0 -143
- package/src/config/bundled-skills/public-ingress/SKILL.md +0 -258
- package/src/config/bundled-skills/reminder/SKILL.md +0 -79
- package/src/config/bundled-skills/reminder/TOOLS.json +0 -89
- package/src/config/bundled-skills/reminder/tools/reminder-list.ts +0 -12
- package/src/config/bundled-skills/restaurant-reservation/SKILL.md +0 -141
- package/src/config/bundled-skills/screen-recording/SKILL.md +0 -148
- package/src/config/bundled-skills/self-upgrade/SKILL.md +0 -69
- package/src/config/bundled-skills/skills-catalog/SKILL.md +0 -78
- package/src/config/bundled-skills/slack-app-setup/SKILL.md +0 -178
- package/src/config/bundled-skills/slack-digest-setup/SKILL.md +0 -163
- package/src/config/bundled-skills/slack-oauth-setup/SKILL.md +0 -157
- package/src/config/bundled-skills/start-the-day/SKILL.md +0 -70
- package/src/config/bundled-skills/start-the-day/icon.svg +0 -13
- package/src/config/bundled-skills/telegram-setup/SKILL.md +0 -105
- package/src/config/bundled-skills/time-based-actions/SKILL.md +0 -142
- package/src/config/bundled-skills/twilio-setup/SKILL.md +0 -232
- package/src/config/bundled-skills/twitter/SKILL.md +0 -319
- package/src/config/bundled-skills/twitter/icon.svg +0 -14
- package/src/config/bundled-skills/typescript-eval/SKILL.md +0 -60
- package/src/config/bundled-skills/vercel-token-setup/SKILL.md +0 -214
- package/src/config/bundled-skills/voice-setup/SKILL.md +0 -131
- package/src/config/bundled-skills/voice-setup/icon.svg +0 -20
- package/src/daemon/handlers/pairing.ts +0 -119
- package/src/daemon/handlers/session-user-message.ts +0 -961
- package/src/daemon/recording-executor.ts +0 -180
- package/src/daemon/recording-intent-fallback.ts +0 -162
- package/src/daemon/recording-intent.ts +0 -493
- package/src/home-base/app-link-store.ts +0 -78
- package/src/home-base/bootstrap.ts +0 -74
- package/src/home-base/prebuilt/brain-graph.html +0 -1483
- package/src/home-base/prebuilt/index.html +0 -702
- package/src/home-base/prebuilt/seed-metadata.json +0 -21
- package/src/home-base/prebuilt/seed.ts +0 -122
- package/src/home-base/prebuilt-home-base-updater.ts +0 -36
- package/src/memory/account-store.ts +0 -117
- package/src/messaging/activity-analyzer.ts +0 -76
- package/src/messaging/email-classifier.ts +0 -208
- package/src/messaging/index.ts +0 -2
- package/src/messaging/outreach-classifier.ts +0 -185
- package/src/messaging/thread-summarizer.ts +0 -346
- package/src/messaging/types.ts +0 -17
- package/src/tools/browser/x-auto-navigate.ts +0 -254
- package/src/tools/credentials/account-registry.ts +0 -144
- package/src/tools/filesystem/view-image.ts +0 -244
- package/src/tools/reminder/reminder-store.ts +0 -194
- package/src/tools/reminder/reminder.ts +0 -158
- package/src/tools/system/navigate-settings.ts +0 -74
- package/src/tools/system/open-system-settings.ts +0 -85
- package/src/tools/system/version.ts +0 -54
- package/src/twitter/platform-proxy-client.ts +0 -405
- package/src/util/cookie-session.ts +0 -98
- /package/src/config/bundled-skills/{messaging → gmail}/tools/scan-result-store.ts +0 -0
- /package/src/config/bundled-skills/{messaging → sequences}/tools/sequence-analytics.ts +0 -0
- /package/src/config/bundled-skills/{messaging → sequences}/tools/sequence-create.ts +0 -0
- /package/src/config/bundled-skills/{messaging → sequences}/tools/sequence-delete.ts +0 -0
- /package/src/config/bundled-skills/{messaging → sequences}/tools/sequence-enroll.ts +0 -0
- /package/src/config/bundled-skills/{messaging → sequences}/tools/sequence-enrollment-list.ts +0 -0
- /package/src/config/bundled-skills/{messaging → sequences}/tools/sequence-get.ts +0 -0
- /package/src/config/bundled-skills/{messaging → sequences}/tools/sequence-import.ts +0 -0
- /package/src/config/bundled-skills/{messaging → sequences}/tools/sequence-list.ts +0 -0
|
@@ -1,1574 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
-
|
|
3
|
-
// ─── Mocks (must be before any imports that depend on them) ─────────────────
|
|
4
|
-
|
|
5
|
-
const noop = () => {};
|
|
6
|
-
const noopLogger = {
|
|
7
|
-
info: noop,
|
|
8
|
-
warn: noop,
|
|
9
|
-
error: noop,
|
|
10
|
-
debug: noop,
|
|
11
|
-
trace: noop,
|
|
12
|
-
fatal: noop,
|
|
13
|
-
child: () => noopLogger,
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
mock.module("../util/logger.js", () => ({
|
|
17
|
-
getLogger: () => noopLogger,
|
|
18
|
-
}));
|
|
19
|
-
|
|
20
|
-
mock.module("../config/loader.js", () => ({
|
|
21
|
-
getConfig: () => ({
|
|
22
|
-
ui: {},
|
|
23
|
-
|
|
24
|
-
daemon: { standaloneRecording: true },
|
|
25
|
-
provider: "mock-provider",
|
|
26
|
-
permissions: { mode: "workspace" },
|
|
27
|
-
apiKeys: {},
|
|
28
|
-
sandbox: { enabled: false },
|
|
29
|
-
timeouts: { toolExecutionTimeoutSec: 30, permissionTimeoutSec: 5 },
|
|
30
|
-
skills: { load: { extraDirs: [] } },
|
|
31
|
-
secretDetection: { enabled: false, allowOneTimeSend: false },
|
|
32
|
-
contextWindow: {
|
|
33
|
-
enabled: true,
|
|
34
|
-
maxInputTokens: 180000,
|
|
35
|
-
targetInputTokens: 110000,
|
|
36
|
-
compactThreshold: 0.8,
|
|
37
|
-
preserveRecentUserTurns: 8,
|
|
38
|
-
summaryMaxTokens: 1200,
|
|
39
|
-
chunkTokens: 12000,
|
|
40
|
-
},
|
|
41
|
-
}),
|
|
42
|
-
invalidateConfigCache: noop,
|
|
43
|
-
loadConfig: noop,
|
|
44
|
-
saveConfig: noop,
|
|
45
|
-
loadRawConfig: () => ({}),
|
|
46
|
-
saveRawConfig: noop,
|
|
47
|
-
getNestedValue: () => undefined,
|
|
48
|
-
setNestedValue: noop,
|
|
49
|
-
}));
|
|
50
|
-
|
|
51
|
-
// Conversation store mock
|
|
52
|
-
const mockMessages: Array<{ id: string; role: string; content: string }> = [];
|
|
53
|
-
let mockMessageIdCounter = 0;
|
|
54
|
-
|
|
55
|
-
mock.module("../memory/conversation-crud.js", () => ({
|
|
56
|
-
getConversationThreadType: () => "default",
|
|
57
|
-
setConversationOriginChannelIfUnset: () => {},
|
|
58
|
-
updateConversationContextWindow: () => {},
|
|
59
|
-
deleteMessageById: () => {},
|
|
60
|
-
updateConversationTitle: () => {},
|
|
61
|
-
updateConversationUsage: () => {},
|
|
62
|
-
provenanceFromTrustContext: () => ({
|
|
63
|
-
source: "user",
|
|
64
|
-
trustContext: undefined,
|
|
65
|
-
}),
|
|
66
|
-
getConversationOriginInterface: () => null,
|
|
67
|
-
getConversationOriginChannel: () => null,
|
|
68
|
-
getMessages: () => mockMessages,
|
|
69
|
-
addMessage: (_convId: string, role: string, content: string) => {
|
|
70
|
-
const msg = { id: `msg-${++mockMessageIdCounter}`, role, content };
|
|
71
|
-
mockMessages.push(msg);
|
|
72
|
-
return msg;
|
|
73
|
-
},
|
|
74
|
-
createConversation: () => ({ id: "conv-mock" }),
|
|
75
|
-
getConversation: () => ({ id: "conv-mock" }),
|
|
76
|
-
}));
|
|
77
|
-
|
|
78
|
-
// Attachments store mock
|
|
79
|
-
mock.module("../memory/attachments-store.js", () => ({
|
|
80
|
-
uploadFileBackedAttachment: () => ({
|
|
81
|
-
id: "att-mock",
|
|
82
|
-
originalFilename: "test.mov",
|
|
83
|
-
mimeType: "video/quicktime",
|
|
84
|
-
sizeBytes: 1024,
|
|
85
|
-
}),
|
|
86
|
-
linkAttachmentToMessage: noop,
|
|
87
|
-
setAttachmentThumbnail: noop,
|
|
88
|
-
}));
|
|
89
|
-
|
|
90
|
-
// Capture real modules BEFORE mocking to avoid circular resolution
|
|
91
|
-
// (mock.module('node:fs') + require('fs') inside factory = deadlock)
|
|
92
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
93
|
-
const realFs = require("fs");
|
|
94
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
95
|
-
const realPath = require("path");
|
|
96
|
-
|
|
97
|
-
// Mock node:fs
|
|
98
|
-
mock.module("node:fs", () => ({
|
|
99
|
-
...realFs,
|
|
100
|
-
existsSync: (p: string) => {
|
|
101
|
-
if (p.includes("recording") || p.includes("/tmp/")) return true;
|
|
102
|
-
return realFs.existsSync(p);
|
|
103
|
-
},
|
|
104
|
-
statSync: (p: string, opts?: any) => {
|
|
105
|
-
if (p.includes("recording") || p.includes("/tmp/")) return { size: 1024 };
|
|
106
|
-
return realFs.statSync(p, opts);
|
|
107
|
-
},
|
|
108
|
-
realpathSync: (p: string) => {
|
|
109
|
-
// Use path.resolve() to canonicalize `..` segments so traversal
|
|
110
|
-
// attacks like `${ALLOWED_DIR}/../outside.mov` are normalized,
|
|
111
|
-
// preserving the same semantics as the real realpathSync without
|
|
112
|
-
// hitting the filesystem (which would throw ENOENT for test paths).
|
|
113
|
-
return realPath.resolve(p);
|
|
114
|
-
},
|
|
115
|
-
}));
|
|
116
|
-
|
|
117
|
-
// Mock video thumbnail
|
|
118
|
-
mock.module("../daemon/video-thumbnail.js", () => ({
|
|
119
|
-
generateVideoThumbnailFromPath: async () => null,
|
|
120
|
-
}));
|
|
121
|
-
|
|
122
|
-
// ─── Imports (after mocks) ──────────────────────────────────────────────────
|
|
123
|
-
|
|
124
|
-
import {
|
|
125
|
-
__injectRecordingOwner,
|
|
126
|
-
__resetRecordingState,
|
|
127
|
-
getActiveRestartToken,
|
|
128
|
-
handleRecordingPause,
|
|
129
|
-
handleRecordingRestart,
|
|
130
|
-
handleRecordingResume,
|
|
131
|
-
handleRecordingStart,
|
|
132
|
-
handleRecordingStop,
|
|
133
|
-
isRecordingIdle,
|
|
134
|
-
recordingHandlers,
|
|
135
|
-
} from "../daemon/handlers/recording.js";
|
|
136
|
-
import type { HandlerContext } from "../daemon/handlers/shared.js";
|
|
137
|
-
import type { RecordingStatus } from "../daemon/message-types/computer-use.js";
|
|
138
|
-
import { executeRecordingIntent } from "../daemon/recording-executor.js";
|
|
139
|
-
import { DebouncerMap } from "../util/debounce.js";
|
|
140
|
-
|
|
141
|
-
// The allowed recordings directory used by the recording handler
|
|
142
|
-
const ALLOWED_RECORDINGS_DIR = `${process.env.HOME}/Library/Application Support/vellum-assistant/recordings`;
|
|
143
|
-
|
|
144
|
-
// ─── Test helpers ───────────────────────────────────────────────────────────
|
|
145
|
-
|
|
146
|
-
function createCtx(): {
|
|
147
|
-
ctx: HandlerContext;
|
|
148
|
-
sent: Array<{ type: string; [k: string]: unknown }>;
|
|
149
|
-
} {
|
|
150
|
-
const sent: Array<{ type: string; [k: string]: unknown }> = [];
|
|
151
|
-
|
|
152
|
-
const ctx: HandlerContext = {
|
|
153
|
-
sessions: new Map(),
|
|
154
|
-
cuSessions: new Map(),
|
|
155
|
-
cuObservationParseSequence: new Map(),
|
|
156
|
-
sharedRequestTimestamps: [],
|
|
157
|
-
debounceTimers: new DebouncerMap({ defaultDelayMs: 200 }),
|
|
158
|
-
suppressConfigReload: false,
|
|
159
|
-
setSuppressConfigReload: noop,
|
|
160
|
-
updateConfigFingerprint: noop,
|
|
161
|
-
send: (msg) => {
|
|
162
|
-
sent.push(msg as { type: string; [k: string]: unknown });
|
|
163
|
-
},
|
|
164
|
-
broadcast: (msg) => {
|
|
165
|
-
sent.push(msg as { type: string; [k: string]: unknown });
|
|
166
|
-
},
|
|
167
|
-
clearAllSessions: () => 0,
|
|
168
|
-
getOrCreateSession: () => {
|
|
169
|
-
throw new Error("not implemented");
|
|
170
|
-
},
|
|
171
|
-
touchSession: noop,
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
return { ctx, sent };
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// ─── Restart state machine tests ────────────────────────────────────────────
|
|
178
|
-
|
|
179
|
-
describe("handleRecordingRestart", () => {
|
|
180
|
-
beforeEach(() => {
|
|
181
|
-
__resetRecordingState();
|
|
182
|
-
mockMessages.length = 0;
|
|
183
|
-
mockMessageIdCounter = 0;
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
test("sends recording_stop and defers start until stop-ack", () => {
|
|
187
|
-
const { ctx, sent } = createCtx();
|
|
188
|
-
const conversationId = "conv-restart-1";
|
|
189
|
-
|
|
190
|
-
// Start a recording first
|
|
191
|
-
const originalId = handleRecordingStart(
|
|
192
|
-
conversationId,
|
|
193
|
-
undefined,
|
|
194
|
-
ctx,
|
|
195
|
-
);
|
|
196
|
-
expect(originalId).not.toBeNull();
|
|
197
|
-
sent.length = 0;
|
|
198
|
-
|
|
199
|
-
const result = handleRecordingRestart(conversationId, ctx);
|
|
200
|
-
|
|
201
|
-
expect(result.initiated).toBe(true);
|
|
202
|
-
expect(result.operationToken).toBeTruthy();
|
|
203
|
-
expect(result.responseText).toBe("Restarting screen recording.");
|
|
204
|
-
|
|
205
|
-
// Should have sent only recording_stop (start is deferred)
|
|
206
|
-
const stopMsgs = sent.filter((m) => m.type === "recording_stop");
|
|
207
|
-
const startMsgs = sent.filter((m) => m.type === "recording_start");
|
|
208
|
-
expect(stopMsgs).toHaveLength(1);
|
|
209
|
-
expect(startMsgs).toHaveLength(0);
|
|
210
|
-
|
|
211
|
-
// Simulate the client acknowledging the stop
|
|
212
|
-
const stoppedStatus: RecordingStatus = {
|
|
213
|
-
type: "recording_status",
|
|
214
|
-
sessionId: originalId!,
|
|
215
|
-
status: "stopped",
|
|
216
|
-
attachToConversationId: conversationId,
|
|
217
|
-
};
|
|
218
|
-
recordingHandlers.recording_status(stoppedStatus, ctx);
|
|
219
|
-
|
|
220
|
-
// NOW the deferred recording_start should have been sent
|
|
221
|
-
const startMsgsAfterAck = sent.filter((m) => m.type === "recording_start");
|
|
222
|
-
expect(startMsgsAfterAck).toHaveLength(1);
|
|
223
|
-
expect(startMsgsAfterAck[0].operationToken).toBe(result.operationToken);
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
test('returns "no active recording" with reason when nothing is recording', () => {
|
|
227
|
-
const { ctx } = createCtx();
|
|
228
|
-
|
|
229
|
-
const result = handleRecordingRestart("conv-no-rec", ctx);
|
|
230
|
-
|
|
231
|
-
expect(result.initiated).toBe(false);
|
|
232
|
-
expect(result.reason).toBe("no_active_recording");
|
|
233
|
-
expect(result.responseText).toBe("No active recording to restart.");
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
test("generates unique operation token for each restart", () => {
|
|
237
|
-
const { ctx, sent } = createCtx();
|
|
238
|
-
const conversationId = "conv-restart-unique";
|
|
239
|
-
|
|
240
|
-
// First restart cycle
|
|
241
|
-
const originalId = handleRecordingStart(
|
|
242
|
-
conversationId,
|
|
243
|
-
undefined,
|
|
244
|
-
ctx,
|
|
245
|
-
);
|
|
246
|
-
const result1 = handleRecordingRestart(conversationId, ctx);
|
|
247
|
-
|
|
248
|
-
// Simulate the stop-ack to trigger the deferred start
|
|
249
|
-
const stoppedStatus1: RecordingStatus = {
|
|
250
|
-
type: "recording_status",
|
|
251
|
-
sessionId: originalId!,
|
|
252
|
-
status: "stopped",
|
|
253
|
-
attachToConversationId: conversationId,
|
|
254
|
-
};
|
|
255
|
-
recordingHandlers.recording_status(stoppedStatus1, ctx);
|
|
256
|
-
|
|
257
|
-
// Simulate the first restart completing (started status)
|
|
258
|
-
const startMsg1 = sent.filter((m) => m.type === "recording_start").pop();
|
|
259
|
-
const status1: RecordingStatus = {
|
|
260
|
-
type: "recording_status",
|
|
261
|
-
sessionId: startMsg1!.recordingId as string,
|
|
262
|
-
status: "started",
|
|
263
|
-
operationToken: result1.operationToken,
|
|
264
|
-
};
|
|
265
|
-
recordingHandlers.recording_status(status1, ctx);
|
|
266
|
-
|
|
267
|
-
// Second restart cycle
|
|
268
|
-
sent.length = 0;
|
|
269
|
-
const result2 = handleRecordingRestart(conversationId, ctx);
|
|
270
|
-
|
|
271
|
-
expect(result1.operationToken).not.toBe(result2.operationToken);
|
|
272
|
-
});
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
// ─── Restart cancel tests ───────────────────────────────────────────────────
|
|
276
|
-
|
|
277
|
-
describe("restart_cancelled status", () => {
|
|
278
|
-
beforeEach(() => {
|
|
279
|
-
__resetRecordingState();
|
|
280
|
-
mockMessages.length = 0;
|
|
281
|
-
mockMessageIdCounter = 0;
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
test('emits restart_cancelled response, never "new recording started"', () => {
|
|
285
|
-
const { ctx, sent } = createCtx();
|
|
286
|
-
const conversationId = "conv-cancel-1";
|
|
287
|
-
|
|
288
|
-
// Start -> restart
|
|
289
|
-
const originalId = handleRecordingStart(
|
|
290
|
-
conversationId,
|
|
291
|
-
undefined,
|
|
292
|
-
ctx,
|
|
293
|
-
);
|
|
294
|
-
const restartResult = handleRecordingRestart(
|
|
295
|
-
conversationId,
|
|
296
|
-
ctx,
|
|
297
|
-
);
|
|
298
|
-
expect(restartResult.initiated).toBe(true);
|
|
299
|
-
|
|
300
|
-
// Simulate the stop-ack to trigger the deferred start
|
|
301
|
-
const stoppedStatus: RecordingStatus = {
|
|
302
|
-
type: "recording_status",
|
|
303
|
-
sessionId: originalId!,
|
|
304
|
-
status: "stopped",
|
|
305
|
-
attachToConversationId: conversationId,
|
|
306
|
-
};
|
|
307
|
-
recordingHandlers.recording_status(stoppedStatus, ctx);
|
|
308
|
-
|
|
309
|
-
// Get the new recording ID from the deferred recording_start message
|
|
310
|
-
const startMsg = sent.filter((m) => m.type === "recording_start").pop();
|
|
311
|
-
sent.length = 0;
|
|
312
|
-
|
|
313
|
-
// Client sends restart_cancelled (picker was closed) with the correct operation token
|
|
314
|
-
const cancelStatus: RecordingStatus = {
|
|
315
|
-
type: "recording_status",
|
|
316
|
-
sessionId: startMsg!.recordingId as string,
|
|
317
|
-
status: "restart_cancelled",
|
|
318
|
-
attachToConversationId: conversationId,
|
|
319
|
-
operationToken: restartResult.operationToken,
|
|
320
|
-
};
|
|
321
|
-
recordingHandlers.recording_status(cancelStatus, ctx);
|
|
322
|
-
|
|
323
|
-
// Should have emitted the cancellation message
|
|
324
|
-
const textDeltas = sent.filter((m) => m.type === "assistant_text_delta");
|
|
325
|
-
expect(textDeltas).toHaveLength(1);
|
|
326
|
-
expect(textDeltas[0].text).toBe("Recording restart cancelled.");
|
|
327
|
-
|
|
328
|
-
// Should NOT have "new recording started" anywhere
|
|
329
|
-
const startedMsgs = sent.filter(
|
|
330
|
-
(m) =>
|
|
331
|
-
m.type === "assistant_text_delta" &&
|
|
332
|
-
typeof m.text === "string" &&
|
|
333
|
-
m.text.includes("new recording started"),
|
|
334
|
-
);
|
|
335
|
-
expect(startedMsgs).toHaveLength(0);
|
|
336
|
-
|
|
337
|
-
// Recording should be truly idle after cancel
|
|
338
|
-
expect(isRecordingIdle()).toBe(true);
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
test("cleans up restart state on cancel", () => {
|
|
342
|
-
const { ctx, sent } = createCtx();
|
|
343
|
-
const conversationId = "conv-cancel-cleanup";
|
|
344
|
-
|
|
345
|
-
const originalId = handleRecordingStart(
|
|
346
|
-
conversationId,
|
|
347
|
-
undefined,
|
|
348
|
-
ctx,
|
|
349
|
-
);
|
|
350
|
-
const restartResult = handleRecordingRestart(
|
|
351
|
-
conversationId,
|
|
352
|
-
ctx,
|
|
353
|
-
);
|
|
354
|
-
|
|
355
|
-
// Before stop-ack: not idle (mid-restart)
|
|
356
|
-
expect(isRecordingIdle()).toBe(false);
|
|
357
|
-
|
|
358
|
-
// Simulate the stop-ack to trigger the deferred start
|
|
359
|
-
const stoppedStatus: RecordingStatus = {
|
|
360
|
-
type: "recording_status",
|
|
361
|
-
sessionId: originalId!,
|
|
362
|
-
status: "stopped",
|
|
363
|
-
attachToConversationId: conversationId,
|
|
364
|
-
};
|
|
365
|
-
recordingHandlers.recording_status(stoppedStatus, ctx);
|
|
366
|
-
|
|
367
|
-
// Still not idle — the new recording has started
|
|
368
|
-
expect(isRecordingIdle()).toBe(false);
|
|
369
|
-
|
|
370
|
-
const startMsg = sent.filter((m) => m.type === "recording_start").pop();
|
|
371
|
-
const cancelStatus: RecordingStatus = {
|
|
372
|
-
type: "recording_status",
|
|
373
|
-
sessionId: startMsg!.recordingId as string,
|
|
374
|
-
status: "restart_cancelled",
|
|
375
|
-
attachToConversationId: conversationId,
|
|
376
|
-
operationToken: restartResult.operationToken,
|
|
377
|
-
};
|
|
378
|
-
recordingHandlers.recording_status(cancelStatus, ctx);
|
|
379
|
-
|
|
380
|
-
// After cancel: truly idle
|
|
381
|
-
expect(isRecordingIdle()).toBe(true);
|
|
382
|
-
expect(getActiveRestartToken()).toBeNull();
|
|
383
|
-
});
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
// ─── Stale completion guard tests ───────────────────────────────────────────
|
|
387
|
-
|
|
388
|
-
describe("stale completion guard (operation token)", () => {
|
|
389
|
-
beforeEach(() => {
|
|
390
|
-
__resetRecordingState();
|
|
391
|
-
mockMessages.length = 0;
|
|
392
|
-
mockMessageIdCounter = 0;
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
test("rejects recording_status with stale operation token", () => {
|
|
396
|
-
const { ctx, sent } = createCtx();
|
|
397
|
-
const conversationId = "conv-stale-1";
|
|
398
|
-
|
|
399
|
-
// Start recording -> restart (creates operation token)
|
|
400
|
-
const originalId = handleRecordingStart(
|
|
401
|
-
conversationId,
|
|
402
|
-
undefined,
|
|
403
|
-
ctx,
|
|
404
|
-
);
|
|
405
|
-
const restartResult = handleRecordingRestart(
|
|
406
|
-
conversationId,
|
|
407
|
-
ctx,
|
|
408
|
-
);
|
|
409
|
-
expect(restartResult.initiated).toBe(true);
|
|
410
|
-
|
|
411
|
-
// Simulate the stop-ack to trigger the deferred start
|
|
412
|
-
const stoppedStatus: RecordingStatus = {
|
|
413
|
-
type: "recording_status",
|
|
414
|
-
sessionId: originalId!,
|
|
415
|
-
status: "stopped",
|
|
416
|
-
attachToConversationId: conversationId,
|
|
417
|
-
};
|
|
418
|
-
recordingHandlers.recording_status(stoppedStatus, ctx);
|
|
419
|
-
|
|
420
|
-
const startMsg = sent.filter((m) => m.type === "recording_start").pop();
|
|
421
|
-
sent.length = 0;
|
|
422
|
-
|
|
423
|
-
// Simulate a stale "started" status from a PREVIOUS restart cycle
|
|
424
|
-
const staleStatus: RecordingStatus = {
|
|
425
|
-
type: "recording_status",
|
|
426
|
-
sessionId: startMsg!.recordingId as string,
|
|
427
|
-
status: "started",
|
|
428
|
-
operationToken: "old-stale-token-from-previous-cycle",
|
|
429
|
-
};
|
|
430
|
-
recordingHandlers.recording_status(staleStatus, ctx);
|
|
431
|
-
|
|
432
|
-
// Should have been rejected — no "started" confirmation messages
|
|
433
|
-
const textDeltas = sent.filter((m) => m.type === "assistant_text_delta");
|
|
434
|
-
expect(textDeltas).toHaveLength(0);
|
|
435
|
-
|
|
436
|
-
// Active restart token should still be set (not cleared by stale completion)
|
|
437
|
-
expect(getActiveRestartToken()).toBe(restartResult.operationToken!);
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
test("accepts recording_status with matching operation token", () => {
|
|
441
|
-
const { ctx, sent } = createCtx();
|
|
442
|
-
const conversationId = "conv-matching-1";
|
|
443
|
-
|
|
444
|
-
const originalId = handleRecordingStart(
|
|
445
|
-
conversationId,
|
|
446
|
-
undefined,
|
|
447
|
-
ctx,
|
|
448
|
-
);
|
|
449
|
-
const restartResult = handleRecordingRestart(
|
|
450
|
-
conversationId,
|
|
451
|
-
ctx,
|
|
452
|
-
);
|
|
453
|
-
|
|
454
|
-
// Simulate the stop-ack to trigger the deferred start
|
|
455
|
-
const stoppedStatus: RecordingStatus = {
|
|
456
|
-
type: "recording_status",
|
|
457
|
-
sessionId: originalId!,
|
|
458
|
-
status: "stopped",
|
|
459
|
-
attachToConversationId: conversationId,
|
|
460
|
-
};
|
|
461
|
-
recordingHandlers.recording_status(stoppedStatus, ctx);
|
|
462
|
-
|
|
463
|
-
const startMsg = sent.filter((m) => m.type === "recording_start").pop();
|
|
464
|
-
|
|
465
|
-
// Send status with the CORRECT token
|
|
466
|
-
const validStatus: RecordingStatus = {
|
|
467
|
-
type: "recording_status",
|
|
468
|
-
sessionId: startMsg!.recordingId as string,
|
|
469
|
-
status: "started",
|
|
470
|
-
operationToken: restartResult.operationToken,
|
|
471
|
-
};
|
|
472
|
-
recordingHandlers.recording_status(validStatus, ctx);
|
|
473
|
-
|
|
474
|
-
// Should have been accepted — restart token cleared
|
|
475
|
-
expect(getActiveRestartToken()).toBeNull();
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
test("allows tokenless recording_status during active restart (old recording ack)", async () => {
|
|
479
|
-
const { ctx, sent } = createCtx();
|
|
480
|
-
const conversationId = "conv-tokenless-1";
|
|
481
|
-
|
|
482
|
-
// Start recording -> restart (creates operation token)
|
|
483
|
-
handleRecordingStart(conversationId, undefined, ctx);
|
|
484
|
-
const restartResult = handleRecordingRestart(
|
|
485
|
-
conversationId,
|
|
486
|
-
ctx,
|
|
487
|
-
);
|
|
488
|
-
expect(restartResult.initiated).toBe(true);
|
|
489
|
-
|
|
490
|
-
const startMsgs = sent.filter((m) => m.type === "recording_start");
|
|
491
|
-
const oldStartMsg = startMsgs[0]; // first recording_start = original/old recording
|
|
492
|
-
sent.length = 0;
|
|
493
|
-
|
|
494
|
-
// Simulate a tokenless "stopped" status arriving during the restart.
|
|
495
|
-
// This represents the OLD recording's stopped ack — it was started before
|
|
496
|
-
// the restart was initiated, so it has no operationToken. This MUST be
|
|
497
|
-
// allowed through for the deferred restart pattern to work.
|
|
498
|
-
const tokenlessStatus: RecordingStatus = {
|
|
499
|
-
type: "recording_status",
|
|
500
|
-
sessionId: oldStartMsg!.recordingId as string,
|
|
501
|
-
status: "stopped",
|
|
502
|
-
attachToConversationId: conversationId,
|
|
503
|
-
// No operationToken — from old recording, should be allowed
|
|
504
|
-
};
|
|
505
|
-
await recordingHandlers.recording_status(tokenlessStatus, ctx);
|
|
506
|
-
|
|
507
|
-
// Should have triggered the deferred restart start
|
|
508
|
-
const newStartMsgs = sent.filter((m) => m.type === "recording_start");
|
|
509
|
-
expect(newStartMsgs).toHaveLength(1);
|
|
510
|
-
|
|
511
|
-
// The old recording finalization runs (no filePath → "no file was produced"
|
|
512
|
-
// text delta). This is expected after M2: the stopped handler finalizes the
|
|
513
|
-
// old recording before starting the new one.
|
|
514
|
-
const textDeltas = sent.filter((m) => m.type === "assistant_text_delta");
|
|
515
|
-
expect(textDeltas.length).toBeGreaterThanOrEqual(1);
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
test("no ghost state after restart stop/start handoff", () => {
|
|
519
|
-
const { ctx, sent } = createCtx();
|
|
520
|
-
const conversationId = "conv-ghost-1";
|
|
521
|
-
|
|
522
|
-
const originalId = handleRecordingStart(
|
|
523
|
-
conversationId,
|
|
524
|
-
undefined,
|
|
525
|
-
ctx,
|
|
526
|
-
);
|
|
527
|
-
|
|
528
|
-
// Restart sends stop and defers start until stop-ack
|
|
529
|
-
const restartResult = handleRecordingRestart(
|
|
530
|
-
conversationId,
|
|
531
|
-
ctx,
|
|
532
|
-
);
|
|
533
|
-
expect(restartResult.initiated).toBe(true);
|
|
534
|
-
|
|
535
|
-
// Simulate the stop-ack to trigger the deferred start
|
|
536
|
-
const stoppedStatus: RecordingStatus = {
|
|
537
|
-
type: "recording_status",
|
|
538
|
-
sessionId: originalId!,
|
|
539
|
-
status: "stopped",
|
|
540
|
-
attachToConversationId: conversationId,
|
|
541
|
-
};
|
|
542
|
-
recordingHandlers.recording_status(stoppedStatus, ctx);
|
|
543
|
-
|
|
544
|
-
// The new recording should be active (not the old one)
|
|
545
|
-
const startMsgs = sent.filter((m) => m.type === "recording_start");
|
|
546
|
-
expect(startMsgs.length).toBeGreaterThanOrEqual(2); // original + deferred restart
|
|
547
|
-
|
|
548
|
-
// The last recording_start should have the operation token
|
|
549
|
-
const lastStart = startMsgs[startMsgs.length - 1];
|
|
550
|
-
expect(lastStart.operationToken).toBe(restartResult.operationToken);
|
|
551
|
-
});
|
|
552
|
-
});
|
|
553
|
-
|
|
554
|
-
// ─── Pause/resume state transition tests ────────────────────────────────────
|
|
555
|
-
|
|
556
|
-
describe("handleRecordingPause", () => {
|
|
557
|
-
beforeEach(() => {
|
|
558
|
-
__resetRecordingState();
|
|
559
|
-
});
|
|
560
|
-
|
|
561
|
-
test("sends recording_pause for active recording", () => {
|
|
562
|
-
const { ctx, sent } = createCtx();
|
|
563
|
-
const conversationId = "conv-pause-1";
|
|
564
|
-
|
|
565
|
-
const recordingId = handleRecordingStart(
|
|
566
|
-
conversationId,
|
|
567
|
-
undefined,
|
|
568
|
-
ctx,
|
|
569
|
-
);
|
|
570
|
-
expect(recordingId).not.toBeNull();
|
|
571
|
-
sent.length = 0;
|
|
572
|
-
|
|
573
|
-
const result = handleRecordingPause(conversationId, ctx);
|
|
574
|
-
|
|
575
|
-
expect(result).toBe(recordingId!);
|
|
576
|
-
expect(sent).toHaveLength(1);
|
|
577
|
-
expect(sent[0].type).toBe("recording_pause");
|
|
578
|
-
expect(sent[0].recordingId).toBe(recordingId);
|
|
579
|
-
});
|
|
580
|
-
|
|
581
|
-
test("returns undefined when no active recording", () => {
|
|
582
|
-
const { ctx } = createCtx();
|
|
583
|
-
|
|
584
|
-
const result = handleRecordingPause("conv-no-rec", ctx);
|
|
585
|
-
expect(result).toBeUndefined();
|
|
586
|
-
});
|
|
587
|
-
|
|
588
|
-
test("resolves to globally active recording from different conversation", () => {
|
|
589
|
-
const { ctx, sent } = createCtx();
|
|
590
|
-
const convA = "conv-owner-pause";
|
|
591
|
-
|
|
592
|
-
const recordingId = handleRecordingStart(convA, undefined, ctx);
|
|
593
|
-
sent.length = 0;
|
|
594
|
-
|
|
595
|
-
const result = handleRecordingPause("conv-other-pause", ctx);
|
|
596
|
-
expect(result).toBe(recordingId!);
|
|
597
|
-
});
|
|
598
|
-
});
|
|
599
|
-
|
|
600
|
-
describe("handleRecordingResume", () => {
|
|
601
|
-
beforeEach(() => {
|
|
602
|
-
__resetRecordingState();
|
|
603
|
-
});
|
|
604
|
-
|
|
605
|
-
test("sends recording_resume for active recording", () => {
|
|
606
|
-
const { ctx, sent } = createCtx();
|
|
607
|
-
const conversationId = "conv-resume-1";
|
|
608
|
-
|
|
609
|
-
const recordingId = handleRecordingStart(
|
|
610
|
-
conversationId,
|
|
611
|
-
undefined,
|
|
612
|
-
ctx,
|
|
613
|
-
);
|
|
614
|
-
expect(recordingId).not.toBeNull();
|
|
615
|
-
sent.length = 0;
|
|
616
|
-
|
|
617
|
-
const result = handleRecordingResume(conversationId, ctx);
|
|
618
|
-
|
|
619
|
-
expect(result).toBe(recordingId!);
|
|
620
|
-
expect(sent).toHaveLength(1);
|
|
621
|
-
expect(sent[0].type).toBe("recording_resume");
|
|
622
|
-
expect(sent[0].recordingId).toBe(recordingId);
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
test("returns undefined when no active recording", () => {
|
|
626
|
-
const { ctx } = createCtx();
|
|
627
|
-
|
|
628
|
-
const result = handleRecordingResume("conv-no-rec", ctx);
|
|
629
|
-
expect(result).toBeUndefined();
|
|
630
|
-
});
|
|
631
|
-
});
|
|
632
|
-
|
|
633
|
-
// ─── isRecordingIdle tests ──────────────────────────────────────────────────
|
|
634
|
-
|
|
635
|
-
describe("isRecordingIdle", () => {
|
|
636
|
-
beforeEach(() => {
|
|
637
|
-
__resetRecordingState();
|
|
638
|
-
});
|
|
639
|
-
|
|
640
|
-
test("returns true when no recording and no pending restart", () => {
|
|
641
|
-
expect(isRecordingIdle()).toBe(true);
|
|
642
|
-
});
|
|
643
|
-
|
|
644
|
-
test("returns false when recording is active", () => {
|
|
645
|
-
const { ctx } = createCtx();
|
|
646
|
-
handleRecordingStart("conv-idle-1", undefined, ctx);
|
|
647
|
-
expect(isRecordingIdle()).toBe(false);
|
|
648
|
-
});
|
|
649
|
-
|
|
650
|
-
test("returns false when mid-restart (between stop-ack and start confirmation)", () => {
|
|
651
|
-
const { ctx } = createCtx();
|
|
652
|
-
const conversationId = "conv-idle-restart";
|
|
653
|
-
|
|
654
|
-
handleRecordingStart(conversationId, undefined, ctx);
|
|
655
|
-
handleRecordingRestart(conversationId, ctx);
|
|
656
|
-
|
|
657
|
-
// Mid-restart: the old recording maps are still present AND there's a
|
|
658
|
-
// pending restart, so the system is not idle
|
|
659
|
-
expect(isRecordingIdle()).toBe(false);
|
|
660
|
-
});
|
|
661
|
-
|
|
662
|
-
test("returns true after restart completes", () => {
|
|
663
|
-
const { ctx, sent } = createCtx();
|
|
664
|
-
const conversationId = "conv-idle-complete";
|
|
665
|
-
|
|
666
|
-
const originalId = handleRecordingStart(
|
|
667
|
-
conversationId,
|
|
668
|
-
undefined,
|
|
669
|
-
ctx,
|
|
670
|
-
);
|
|
671
|
-
const restartResult = handleRecordingRestart(
|
|
672
|
-
conversationId,
|
|
673
|
-
ctx,
|
|
674
|
-
);
|
|
675
|
-
|
|
676
|
-
// Simulate the stop-ack to trigger the deferred start
|
|
677
|
-
const stoppedStatus: RecordingStatus = {
|
|
678
|
-
type: "recording_status",
|
|
679
|
-
sessionId: originalId!,
|
|
680
|
-
status: "stopped",
|
|
681
|
-
attachToConversationId: conversationId,
|
|
682
|
-
};
|
|
683
|
-
recordingHandlers.recording_status(stoppedStatus, ctx);
|
|
684
|
-
|
|
685
|
-
// Simulate the new recording starting
|
|
686
|
-
const startMsg = sent.filter((m) => m.type === "recording_start").pop();
|
|
687
|
-
const startedStatus: RecordingStatus = {
|
|
688
|
-
type: "recording_status",
|
|
689
|
-
sessionId: startMsg!.recordingId as string,
|
|
690
|
-
status: "started",
|
|
691
|
-
operationToken: restartResult.operationToken,
|
|
692
|
-
};
|
|
693
|
-
recordingHandlers.recording_status(startedStatus, ctx);
|
|
694
|
-
|
|
695
|
-
// Restart is complete, but recording is still active
|
|
696
|
-
expect(getActiveRestartToken()).toBeNull();
|
|
697
|
-
// Not idle because the new recording is still running
|
|
698
|
-
expect(isRecordingIdle()).toBe(false);
|
|
699
|
-
});
|
|
700
|
-
});
|
|
701
|
-
|
|
702
|
-
// ─── Recording executor integration tests ───────────────────────────────────
|
|
703
|
-
|
|
704
|
-
describe("executeRecordingIntent — restart/pause/resume", () => {
|
|
705
|
-
beforeEach(() => {
|
|
706
|
-
__resetRecordingState();
|
|
707
|
-
});
|
|
708
|
-
|
|
709
|
-
test("restart_only executes actual restart (deferred start)", () => {
|
|
710
|
-
const { ctx, sent } = createCtx();
|
|
711
|
-
const conversationId = "conv-exec-restart";
|
|
712
|
-
|
|
713
|
-
// Start a recording first
|
|
714
|
-
const originalId = handleRecordingStart(
|
|
715
|
-
conversationId,
|
|
716
|
-
undefined,
|
|
717
|
-
ctx,
|
|
718
|
-
);
|
|
719
|
-
sent.length = 0;
|
|
720
|
-
|
|
721
|
-
const result = executeRecordingIntent(
|
|
722
|
-
{ kind: "restart_only" },
|
|
723
|
-
{ conversationId, ctx },
|
|
724
|
-
);
|
|
725
|
-
|
|
726
|
-
expect(result.handled).toBe(true);
|
|
727
|
-
expect(result.responseText).toBe("Restarting screen recording.");
|
|
728
|
-
|
|
729
|
-
// Should have sent only stop (start is deferred until stop-ack)
|
|
730
|
-
const stopMsgs = sent.filter((m) => m.type === "recording_stop");
|
|
731
|
-
const startMsgs = sent.filter((m) => m.type === "recording_start");
|
|
732
|
-
expect(stopMsgs).toHaveLength(1);
|
|
733
|
-
expect(startMsgs).toHaveLength(0);
|
|
734
|
-
|
|
735
|
-
// Simulate the stop-ack to trigger the deferred start
|
|
736
|
-
const stoppedStatus: RecordingStatus = {
|
|
737
|
-
type: "recording_status",
|
|
738
|
-
sessionId: originalId!,
|
|
739
|
-
status: "stopped",
|
|
740
|
-
attachToConversationId: conversationId,
|
|
741
|
-
};
|
|
742
|
-
recordingHandlers.recording_status(stoppedStatus, ctx);
|
|
743
|
-
|
|
744
|
-
// NOW the deferred start should have been sent
|
|
745
|
-
const startMsgsAfterAck = sent.filter((m) => m.type === "recording_start");
|
|
746
|
-
expect(startMsgsAfterAck).toHaveLength(1);
|
|
747
|
-
});
|
|
748
|
-
|
|
749
|
-
test('restart_only returns "no active recording" when idle', () => {
|
|
750
|
-
const { ctx } = createCtx();
|
|
751
|
-
|
|
752
|
-
const result = executeRecordingIntent(
|
|
753
|
-
{ kind: "restart_only" },
|
|
754
|
-
{ conversationId: "conv-no-rec", ctx },
|
|
755
|
-
);
|
|
756
|
-
|
|
757
|
-
expect(result.handled).toBe(true);
|
|
758
|
-
expect(result.responseText).toBe("No active recording to restart.");
|
|
759
|
-
});
|
|
760
|
-
|
|
761
|
-
test("restart_with_remainder returns deferred restart", () => {
|
|
762
|
-
const { ctx } = createCtx();
|
|
763
|
-
|
|
764
|
-
const result = executeRecordingIntent(
|
|
765
|
-
{ kind: "restart_with_remainder", remainder: "do something else" },
|
|
766
|
-
{ conversationId: "conv-rem", ctx },
|
|
767
|
-
);
|
|
768
|
-
|
|
769
|
-
expect(result.handled).toBe(false);
|
|
770
|
-
expect(result.pendingRestart).toBe(true);
|
|
771
|
-
expect(result.remainderText).toBe("do something else");
|
|
772
|
-
});
|
|
773
|
-
|
|
774
|
-
test("pause_only executes actual pause", () => {
|
|
775
|
-
const { ctx, sent } = createCtx();
|
|
776
|
-
const conversationId = "conv-exec-pause";
|
|
777
|
-
|
|
778
|
-
handleRecordingStart(conversationId, undefined, ctx);
|
|
779
|
-
sent.length = 0;
|
|
780
|
-
|
|
781
|
-
const result = executeRecordingIntent(
|
|
782
|
-
{ kind: "pause_only" },
|
|
783
|
-
{ conversationId, ctx },
|
|
784
|
-
);
|
|
785
|
-
|
|
786
|
-
expect(result.handled).toBe(true);
|
|
787
|
-
expect(result.responseText).toBe("Pausing the recording.");
|
|
788
|
-
|
|
789
|
-
const pauseMsgs = sent.filter((m) => m.type === "recording_pause");
|
|
790
|
-
expect(pauseMsgs).toHaveLength(1);
|
|
791
|
-
});
|
|
792
|
-
|
|
793
|
-
test('pause_only returns "no active recording" when idle', () => {
|
|
794
|
-
const { ctx } = createCtx();
|
|
795
|
-
|
|
796
|
-
const result = executeRecordingIntent(
|
|
797
|
-
{ kind: "pause_only" },
|
|
798
|
-
{ conversationId: "conv-no-rec", ctx },
|
|
799
|
-
);
|
|
800
|
-
|
|
801
|
-
expect(result.handled).toBe(true);
|
|
802
|
-
expect(result.responseText).toBe("No active recording to pause.");
|
|
803
|
-
});
|
|
804
|
-
|
|
805
|
-
test("resume_only executes actual resume", () => {
|
|
806
|
-
const { ctx, sent } = createCtx();
|
|
807
|
-
const conversationId = "conv-exec-resume";
|
|
808
|
-
|
|
809
|
-
handleRecordingStart(conversationId, undefined, ctx);
|
|
810
|
-
sent.length = 0;
|
|
811
|
-
|
|
812
|
-
const result = executeRecordingIntent(
|
|
813
|
-
{ kind: "resume_only" },
|
|
814
|
-
{ conversationId, ctx },
|
|
815
|
-
);
|
|
816
|
-
|
|
817
|
-
expect(result.handled).toBe(true);
|
|
818
|
-
expect(result.responseText).toBe("Resuming the recording.");
|
|
819
|
-
|
|
820
|
-
const resumeMsgs = sent.filter((m) => m.type === "recording_resume");
|
|
821
|
-
expect(resumeMsgs).toHaveLength(1);
|
|
822
|
-
});
|
|
823
|
-
|
|
824
|
-
test('resume_only returns "no active recording" when idle', () => {
|
|
825
|
-
const { ctx } = createCtx();
|
|
826
|
-
|
|
827
|
-
const result = executeRecordingIntent(
|
|
828
|
-
{ kind: "resume_only" },
|
|
829
|
-
{ conversationId: "conv-no-rec", ctx },
|
|
830
|
-
);
|
|
831
|
-
|
|
832
|
-
expect(result.handled).toBe(true);
|
|
833
|
-
expect(result.responseText).toBe("No active recording to resume.");
|
|
834
|
-
});
|
|
835
|
-
});
|
|
836
|
-
|
|
837
|
-
// ─── Recording status paused/resumed acknowledgement tests ──────────────────
|
|
838
|
-
|
|
839
|
-
describe("recording_status paused/resumed", () => {
|
|
840
|
-
beforeEach(() => {
|
|
841
|
-
__resetRecordingState();
|
|
842
|
-
});
|
|
843
|
-
|
|
844
|
-
test("handles paused status without error", () => {
|
|
845
|
-
const { ctx } = createCtx();
|
|
846
|
-
const conversationId = "conv-status-paused";
|
|
847
|
-
|
|
848
|
-
const recordingId = handleRecordingStart(
|
|
849
|
-
conversationId,
|
|
850
|
-
undefined,
|
|
851
|
-
ctx,
|
|
852
|
-
);
|
|
853
|
-
expect(recordingId).not.toBeNull();
|
|
854
|
-
|
|
855
|
-
const statusMsg: RecordingStatus = {
|
|
856
|
-
type: "recording_status",
|
|
857
|
-
sessionId: recordingId!,
|
|
858
|
-
status: "paused",
|
|
859
|
-
};
|
|
860
|
-
|
|
861
|
-
expect(() => {
|
|
862
|
-
recordingHandlers.recording_status(statusMsg, ctx);
|
|
863
|
-
}).not.toThrow();
|
|
864
|
-
});
|
|
865
|
-
|
|
866
|
-
test("handles resumed status without error", () => {
|
|
867
|
-
const { ctx } = createCtx();
|
|
868
|
-
const conversationId = "conv-status-resumed";
|
|
869
|
-
|
|
870
|
-
const recordingId = handleRecordingStart(
|
|
871
|
-
conversationId,
|
|
872
|
-
undefined,
|
|
873
|
-
ctx,
|
|
874
|
-
);
|
|
875
|
-
expect(recordingId).not.toBeNull();
|
|
876
|
-
|
|
877
|
-
const statusMsg: RecordingStatus = {
|
|
878
|
-
type: "recording_status",
|
|
879
|
-
sessionId: recordingId!,
|
|
880
|
-
status: "resumed",
|
|
881
|
-
};
|
|
882
|
-
|
|
883
|
-
expect(() => {
|
|
884
|
-
recordingHandlers.recording_status(statusMsg, ctx);
|
|
885
|
-
}).not.toThrow();
|
|
886
|
-
});
|
|
887
|
-
});
|
|
888
|
-
|
|
889
|
-
// ─── Failed during restart cleans up restart state ──────────────────────────
|
|
890
|
-
|
|
891
|
-
describe("failure during restart", () => {
|
|
892
|
-
beforeEach(() => {
|
|
893
|
-
__resetRecordingState();
|
|
894
|
-
mockMessages.length = 0;
|
|
895
|
-
mockMessageIdCounter = 0;
|
|
896
|
-
});
|
|
897
|
-
|
|
898
|
-
test("failed status during restart clears pending restart state (old recording fails)", () => {
|
|
899
|
-
const { ctx, sent } = createCtx();
|
|
900
|
-
const conversationId = "conv-fail-restart";
|
|
901
|
-
|
|
902
|
-
const originalId = handleRecordingStart(
|
|
903
|
-
conversationId,
|
|
904
|
-
undefined,
|
|
905
|
-
ctx,
|
|
906
|
-
);
|
|
907
|
-
handleRecordingRestart(conversationId, ctx);
|
|
908
|
-
sent.length = 0;
|
|
909
|
-
|
|
910
|
-
// Simulate the old recording failing to stop (before stop-ack)
|
|
911
|
-
const failedStatus: RecordingStatus = {
|
|
912
|
-
type: "recording_status",
|
|
913
|
-
sessionId: originalId!,
|
|
914
|
-
status: "failed",
|
|
915
|
-
error: "Permission denied",
|
|
916
|
-
attachToConversationId: conversationId,
|
|
917
|
-
};
|
|
918
|
-
recordingHandlers.recording_status(failedStatus, ctx);
|
|
919
|
-
|
|
920
|
-
// Restart state and deferred restart should be cleaned up
|
|
921
|
-
expect(getActiveRestartToken()).toBeNull();
|
|
922
|
-
expect(isRecordingIdle()).toBe(true);
|
|
923
|
-
});
|
|
924
|
-
|
|
925
|
-
test("failed status during restart clears state (new recording fails after deferred start)", () => {
|
|
926
|
-
const { ctx, sent } = createCtx();
|
|
927
|
-
const conversationId = "conv-fail-restart-new";
|
|
928
|
-
|
|
929
|
-
const originalId = handleRecordingStart(
|
|
930
|
-
conversationId,
|
|
931
|
-
undefined,
|
|
932
|
-
ctx,
|
|
933
|
-
);
|
|
934
|
-
const restartResult = handleRecordingRestart(
|
|
935
|
-
conversationId,
|
|
936
|
-
ctx,
|
|
937
|
-
);
|
|
938
|
-
|
|
939
|
-
// Simulate the stop-ack to trigger the deferred start
|
|
940
|
-
const stoppedStatus: RecordingStatus = {
|
|
941
|
-
type: "recording_status",
|
|
942
|
-
sessionId: originalId!,
|
|
943
|
-
status: "stopped",
|
|
944
|
-
attachToConversationId: conversationId,
|
|
945
|
-
};
|
|
946
|
-
recordingHandlers.recording_status(stoppedStatus, ctx);
|
|
947
|
-
|
|
948
|
-
const startMsg = sent.filter((m) => m.type === "recording_start").pop();
|
|
949
|
-
sent.length = 0;
|
|
950
|
-
|
|
951
|
-
// Simulate new recording failing (with the correct operation token)
|
|
952
|
-
const failedStatus: RecordingStatus = {
|
|
953
|
-
type: "recording_status",
|
|
954
|
-
sessionId: startMsg!.recordingId as string,
|
|
955
|
-
status: "failed",
|
|
956
|
-
error: "Permission denied",
|
|
957
|
-
attachToConversationId: conversationId,
|
|
958
|
-
operationToken: restartResult.operationToken,
|
|
959
|
-
};
|
|
960
|
-
recordingHandlers.recording_status(failedStatus, ctx);
|
|
961
|
-
|
|
962
|
-
// Restart state should be cleaned up
|
|
963
|
-
expect(getActiveRestartToken()).toBeNull();
|
|
964
|
-
expect(isRecordingIdle()).toBe(true);
|
|
965
|
-
});
|
|
966
|
-
});
|
|
967
|
-
|
|
968
|
-
// ─── start_and_stop_only from idle state ─────────────────────────────────────
|
|
969
|
-
|
|
970
|
-
describe("start_and_stop_only fallback to plain start when idle", () => {
|
|
971
|
-
beforeEach(() => {
|
|
972
|
-
__resetRecordingState();
|
|
973
|
-
});
|
|
974
|
-
|
|
975
|
-
test("falls back to handleRecordingStart when no active recording", () => {
|
|
976
|
-
const { ctx, sent } = createCtx();
|
|
977
|
-
const conversationId = "conv-stop-start-idle";
|
|
978
|
-
|
|
979
|
-
// No recording is active — start_and_stop_only should fall back to a
|
|
980
|
-
// plain start rather than returning "No active recording to restart."
|
|
981
|
-
const result = executeRecordingIntent(
|
|
982
|
-
{ kind: "start_and_stop_only" },
|
|
983
|
-
{ conversationId, ctx },
|
|
984
|
-
);
|
|
985
|
-
|
|
986
|
-
expect(result.handled).toBe(true);
|
|
987
|
-
expect(result.recordingStarted).toBe(true);
|
|
988
|
-
expect(result.responseText).toBe("Starting screen recording.");
|
|
989
|
-
|
|
990
|
-
// Should have sent only a recording_start (no stop since nothing was active)
|
|
991
|
-
const stopMsgs = sent.filter((m) => m.type === "recording_stop");
|
|
992
|
-
const startMsgs = sent.filter((m) => m.type === "recording_start");
|
|
993
|
-
expect(stopMsgs).toHaveLength(0);
|
|
994
|
-
expect(startMsgs).toHaveLength(1);
|
|
995
|
-
});
|
|
996
|
-
|
|
997
|
-
test("goes through restart when a recording is active (deferred start)", () => {
|
|
998
|
-
const { ctx, sent } = createCtx();
|
|
999
|
-
const conversationId = "conv-stop-start-active";
|
|
1000
|
-
|
|
1001
|
-
// Start a recording first
|
|
1002
|
-
const originalId = handleRecordingStart(
|
|
1003
|
-
conversationId,
|
|
1004
|
-
undefined,
|
|
1005
|
-
ctx,
|
|
1006
|
-
);
|
|
1007
|
-
expect(originalId).not.toBeNull();
|
|
1008
|
-
sent.length = 0;
|
|
1009
|
-
|
|
1010
|
-
// Now start_and_stop_only should go through handleRecordingRestart
|
|
1011
|
-
const result = executeRecordingIntent(
|
|
1012
|
-
{ kind: "start_and_stop_only" },
|
|
1013
|
-
{ conversationId, ctx },
|
|
1014
|
-
);
|
|
1015
|
-
|
|
1016
|
-
expect(result.handled).toBe(true);
|
|
1017
|
-
expect(result.recordingStarted).toBe(true);
|
|
1018
|
-
expect(result.responseText).toBe(
|
|
1019
|
-
"Stopping current recording and starting a new one.",
|
|
1020
|
-
);
|
|
1021
|
-
|
|
1022
|
-
// Should have sent only stop (start is deferred until stop-ack)
|
|
1023
|
-
const stopMsgs = sent.filter((m) => m.type === "recording_stop");
|
|
1024
|
-
const startMsgs = sent.filter((m) => m.type === "recording_start");
|
|
1025
|
-
expect(stopMsgs).toHaveLength(1);
|
|
1026
|
-
expect(startMsgs).toHaveLength(0);
|
|
1027
|
-
|
|
1028
|
-
// Simulate the stop-ack to trigger the deferred start
|
|
1029
|
-
const stoppedStatus: RecordingStatus = {
|
|
1030
|
-
type: "recording_status",
|
|
1031
|
-
sessionId: originalId!,
|
|
1032
|
-
status: "stopped",
|
|
1033
|
-
attachToConversationId: conversationId,
|
|
1034
|
-
};
|
|
1035
|
-
recordingHandlers.recording_status(stoppedStatus, ctx);
|
|
1036
|
-
|
|
1037
|
-
// NOW the deferred start should have been sent
|
|
1038
|
-
const startMsgsAfterAck = sent.filter((m) => m.type === "recording_start");
|
|
1039
|
-
expect(startMsgsAfterAck).toHaveLength(1);
|
|
1040
|
-
});
|
|
1041
|
-
});
|
|
1042
|
-
|
|
1043
|
-
// ─── start_and_stop_with_remainder from idle state ───────────────────────────
|
|
1044
|
-
|
|
1045
|
-
describe("start_and_stop_with_remainder fallback to plain start when idle", () => {
|
|
1046
|
-
beforeEach(() => {
|
|
1047
|
-
__resetRecordingState();
|
|
1048
|
-
});
|
|
1049
|
-
|
|
1050
|
-
test("sets pendingStart (not pendingRestart) when no active recording", () => {
|
|
1051
|
-
const { ctx } = createCtx();
|
|
1052
|
-
const conversationId = "conv-rem-idle";
|
|
1053
|
-
|
|
1054
|
-
const result = executeRecordingIntent(
|
|
1055
|
-
{ kind: "start_and_stop_with_remainder", remainder: "do something" },
|
|
1056
|
-
{ conversationId, ctx },
|
|
1057
|
-
);
|
|
1058
|
-
|
|
1059
|
-
expect(result.handled).toBe(false);
|
|
1060
|
-
expect(result.pendingStart).toBe(true);
|
|
1061
|
-
expect(result.pendingRestart).toBeUndefined();
|
|
1062
|
-
expect(result.remainderText).toBe("do something");
|
|
1063
|
-
});
|
|
1064
|
-
|
|
1065
|
-
test("sets pendingRestart when a recording is active", () => {
|
|
1066
|
-
const { ctx } = createCtx();
|
|
1067
|
-
const conversationId = "conv-rem-active";
|
|
1068
|
-
|
|
1069
|
-
// Start a recording first
|
|
1070
|
-
handleRecordingStart(conversationId, undefined, ctx);
|
|
1071
|
-
|
|
1072
|
-
const result = executeRecordingIntent(
|
|
1073
|
-
{ kind: "start_and_stop_with_remainder", remainder: "do something" },
|
|
1074
|
-
{ conversationId, ctx },
|
|
1075
|
-
);
|
|
1076
|
-
|
|
1077
|
-
expect(result.handled).toBe(false);
|
|
1078
|
-
expect(result.pendingRestart).toBe(true);
|
|
1079
|
-
expect(result.pendingStart).toBeUndefined();
|
|
1080
|
-
expect(result.remainderText).toBe("do something");
|
|
1081
|
-
});
|
|
1082
|
-
});
|
|
1083
|
-
|
|
1084
|
-
// ─── Deferred restart race condition tests ───────────────────────────────────
|
|
1085
|
-
|
|
1086
|
-
describe("deferred restart prevents race condition", () => {
|
|
1087
|
-
beforeEach(() => {
|
|
1088
|
-
__resetRecordingState();
|
|
1089
|
-
mockMessages.length = 0;
|
|
1090
|
-
mockMessageIdCounter = 0;
|
|
1091
|
-
});
|
|
1092
|
-
|
|
1093
|
-
test("recording_start is NOT sent until client acks the stop", () => {
|
|
1094
|
-
const { ctx, sent } = createCtx();
|
|
1095
|
-
const conversationId = "conv-deferred-race";
|
|
1096
|
-
|
|
1097
|
-
handleRecordingStart(conversationId, undefined, ctx);
|
|
1098
|
-
sent.length = 0;
|
|
1099
|
-
|
|
1100
|
-
handleRecordingRestart(conversationId, ctx);
|
|
1101
|
-
|
|
1102
|
-
// Only recording_stop should have been sent — no recording_start yet
|
|
1103
|
-
expect(sent.filter((m) => m.type === "recording_stop")).toHaveLength(1);
|
|
1104
|
-
expect(sent.filter((m) => m.type === "recording_start")).toHaveLength(0);
|
|
1105
|
-
|
|
1106
|
-
// System is mid-restart — not idle
|
|
1107
|
-
expect(isRecordingIdle()).toBe(false);
|
|
1108
|
-
});
|
|
1109
|
-
|
|
1110
|
-
test("stop-ack timeout cleans up deferred restart state", () => {
|
|
1111
|
-
// This test uses a real timer via bun's jest-compatible API
|
|
1112
|
-
const { ctx } = createCtx();
|
|
1113
|
-
const conversationId = "conv-deferred-timeout";
|
|
1114
|
-
|
|
1115
|
-
handleRecordingStart(conversationId, undefined, ctx);
|
|
1116
|
-
handleRecordingRestart(conversationId, ctx);
|
|
1117
|
-
|
|
1118
|
-
// Mid-restart: not idle
|
|
1119
|
-
expect(isRecordingIdle()).toBe(false);
|
|
1120
|
-
|
|
1121
|
-
// We cannot easily test the setTimeout firing here without mocking timers,
|
|
1122
|
-
// but we can verify the state is correctly set up for the timeout to clean up.
|
|
1123
|
-
expect(getActiveRestartToken()).not.toBeNull();
|
|
1124
|
-
});
|
|
1125
|
-
|
|
1126
|
-
test("cross-conversation restart: conversation B restarts recording owned by A", () => {
|
|
1127
|
-
const { ctx, sent } = createCtx();
|
|
1128
|
-
const convA = "conv-owner-A";
|
|
1129
|
-
const convB = "conv-requester-B";
|
|
1130
|
-
|
|
1131
|
-
// Conversation A starts a recording
|
|
1132
|
-
const originalId = handleRecordingStart(convA, undefined, ctx);
|
|
1133
|
-
expect(originalId).not.toBeNull();
|
|
1134
|
-
sent.length = 0;
|
|
1135
|
-
|
|
1136
|
-
// Conversation B requests a restart (cross-conversation via global fallback)
|
|
1137
|
-
const result = handleRecordingRestart(convB, ctx);
|
|
1138
|
-
expect(result.initiated).toBe(true);
|
|
1139
|
-
expect(result.operationToken).toBeTruthy();
|
|
1140
|
-
|
|
1141
|
-
// Should have sent recording_stop (start is deferred)
|
|
1142
|
-
expect(sent.filter((m) => m.type === "recording_stop")).toHaveLength(1);
|
|
1143
|
-
expect(sent.filter((m) => m.type === "recording_start")).toHaveLength(0);
|
|
1144
|
-
|
|
1145
|
-
// Simulate the client acknowledging the stop. The stopped status resolves
|
|
1146
|
-
// conversationId from standaloneRecordingConversationId which maps to A.
|
|
1147
|
-
const stoppedStatus: RecordingStatus = {
|
|
1148
|
-
type: "recording_status",
|
|
1149
|
-
sessionId: originalId!,
|
|
1150
|
-
status: "stopped",
|
|
1151
|
-
attachToConversationId: convA,
|
|
1152
|
-
};
|
|
1153
|
-
recordingHandlers.recording_status(stoppedStatus, ctx);
|
|
1154
|
-
|
|
1155
|
-
// The deferred recording_start MUST have been triggered even though the
|
|
1156
|
-
// stopped callback resolved to conversation A (owner), not B (requester).
|
|
1157
|
-
const startMsgs = sent.filter((m) => m.type === "recording_start");
|
|
1158
|
-
expect(startMsgs).toHaveLength(1);
|
|
1159
|
-
expect(startMsgs[0].operationToken).toBe(result.operationToken);
|
|
1160
|
-
|
|
1161
|
-
// The new recording is owned by B (the requester). Simulate the client
|
|
1162
|
-
// confirming the new recording started. The 'started' status resolves
|
|
1163
|
-
// conversationId to B, so pendingRestartByConversation must have been
|
|
1164
|
-
// migrated from A to B for the restart cycle to complete.
|
|
1165
|
-
const newRecordingId = startMsgs[0].recordingId as string;
|
|
1166
|
-
const startedStatus: RecordingStatus = {
|
|
1167
|
-
type: "recording_status",
|
|
1168
|
-
sessionId: newRecordingId,
|
|
1169
|
-
status: "started",
|
|
1170
|
-
operationToken: result.operationToken,
|
|
1171
|
-
attachToConversationId: convB,
|
|
1172
|
-
};
|
|
1173
|
-
recordingHandlers.recording_status(startedStatus, ctx);
|
|
1174
|
-
|
|
1175
|
-
// Restart cycle must be fully complete: activeRestartToken cleared
|
|
1176
|
-
expect(getActiveRestartToken()).toBeNull();
|
|
1177
|
-
|
|
1178
|
-
// Not idle yet because the new recording is still running
|
|
1179
|
-
expect(isRecordingIdle()).toBe(false);
|
|
1180
|
-
|
|
1181
|
-
// Stop the new recording and verify system returns to idle
|
|
1182
|
-
handleRecordingStop(convB, ctx);
|
|
1183
|
-
const newStoppedStatus: RecordingStatus = {
|
|
1184
|
-
type: "recording_status",
|
|
1185
|
-
sessionId: newRecordingId,
|
|
1186
|
-
status: "stopped",
|
|
1187
|
-
attachToConversationId: convB,
|
|
1188
|
-
};
|
|
1189
|
-
recordingHandlers.recording_status(newStoppedStatus, ctx);
|
|
1190
|
-
|
|
1191
|
-
expect(isRecordingIdle()).toBe(true);
|
|
1192
|
-
});
|
|
1193
|
-
|
|
1194
|
-
test("normal stop (non-restart) does not trigger deferred start", () => {
|
|
1195
|
-
const { ctx, sent } = createCtx();
|
|
1196
|
-
const conversationId = "conv-normal-stop";
|
|
1197
|
-
|
|
1198
|
-
const recordingId = handleRecordingStart(
|
|
1199
|
-
conversationId,
|
|
1200
|
-
undefined,
|
|
1201
|
-
ctx,
|
|
1202
|
-
);
|
|
1203
|
-
expect(recordingId).not.toBeNull();
|
|
1204
|
-
|
|
1205
|
-
// Manually stop (not via restart)
|
|
1206
|
-
handleRecordingStop(conversationId, ctx);
|
|
1207
|
-
sent.length = 0;
|
|
1208
|
-
|
|
1209
|
-
// Simulate stop-ack without file path (e.g. very short recording)
|
|
1210
|
-
const stoppedStatus: RecordingStatus = {
|
|
1211
|
-
type: "recording_status",
|
|
1212
|
-
sessionId: recordingId!,
|
|
1213
|
-
status: "stopped",
|
|
1214
|
-
attachToConversationId: conversationId,
|
|
1215
|
-
};
|
|
1216
|
-
recordingHandlers.recording_status(stoppedStatus, ctx);
|
|
1217
|
-
|
|
1218
|
-
// Should NOT have sent a recording_start (no deferred restart pending)
|
|
1219
|
-
const startMsgs = sent.filter((m) => m.type === "recording_start");
|
|
1220
|
-
expect(startMsgs).toHaveLength(0);
|
|
1221
|
-
expect(isRecordingIdle()).toBe(true);
|
|
1222
|
-
});
|
|
1223
|
-
});
|
|
1224
|
-
|
|
1225
|
-
// ─── Restart finalization tests ──────────────────────────────────────────────
|
|
1226
|
-
|
|
1227
|
-
describe("restart finalization", () => {
|
|
1228
|
-
beforeEach(() => {
|
|
1229
|
-
__resetRecordingState();
|
|
1230
|
-
mockMessages.length = 0;
|
|
1231
|
-
mockMessageIdCounter = 0;
|
|
1232
|
-
});
|
|
1233
|
-
|
|
1234
|
-
test("publishes previous recording attachment on restart", async () => {
|
|
1235
|
-
const { ctx, sent } = createCtx();
|
|
1236
|
-
const conversationId = "conv-fin-publish";
|
|
1237
|
-
|
|
1238
|
-
// Start a recording
|
|
1239
|
-
const originalId = handleRecordingStart(
|
|
1240
|
-
conversationId,
|
|
1241
|
-
undefined,
|
|
1242
|
-
ctx,
|
|
1243
|
-
);
|
|
1244
|
-
expect(originalId).not.toBeNull();
|
|
1245
|
-
|
|
1246
|
-
// Trigger restart
|
|
1247
|
-
const restartResult = handleRecordingRestart(
|
|
1248
|
-
conversationId,
|
|
1249
|
-
ctx,
|
|
1250
|
-
);
|
|
1251
|
-
expect(restartResult.initiated).toBe(true);
|
|
1252
|
-
sent.length = 0;
|
|
1253
|
-
|
|
1254
|
-
// Simulate stopped for old recording with filePath and durationMs
|
|
1255
|
-
const stoppedStatus: RecordingStatus = {
|
|
1256
|
-
type: "recording_status",
|
|
1257
|
-
sessionId: originalId!,
|
|
1258
|
-
status: "stopped",
|
|
1259
|
-
filePath: `${ALLOWED_RECORDINGS_DIR}/recording-old.mov`,
|
|
1260
|
-
durationMs: 5000,
|
|
1261
|
-
attachToConversationId: conversationId,
|
|
1262
|
-
};
|
|
1263
|
-
await recordingHandlers.recording_status(stoppedStatus, ctx);
|
|
1264
|
-
|
|
1265
|
-
// Verify: a new recording_start IPC was sent (deferred start triggered)
|
|
1266
|
-
const startMsgs = sent.filter((m) => m.type === "recording_start");
|
|
1267
|
-
expect(startMsgs).toHaveLength(1);
|
|
1268
|
-
expect(startMsgs[0].operationToken).toBe(restartResult.operationToken);
|
|
1269
|
-
|
|
1270
|
-
// Verify: finalizeAndPublishRecording was called — check that sent
|
|
1271
|
-
// contains messages with attachment data (assistant_text_delta + message_complete)
|
|
1272
|
-
const textDeltas = sent.filter((m) => m.type === "assistant_text_delta");
|
|
1273
|
-
expect(textDeltas.length).toBeGreaterThanOrEqual(1);
|
|
1274
|
-
const successMsg = textDeltas.find(
|
|
1275
|
-
(m) =>
|
|
1276
|
-
typeof m.text === "string" &&
|
|
1277
|
-
m.text.includes("Screen recording complete"),
|
|
1278
|
-
);
|
|
1279
|
-
expect(successMsg).toBeTruthy();
|
|
1280
|
-
|
|
1281
|
-
const completes = sent.filter((m) => m.type === "message_complete");
|
|
1282
|
-
const attachmentComplete = completes.find((m) => m.attachments != null);
|
|
1283
|
-
expect(attachmentComplete).toBeTruthy();
|
|
1284
|
-
|
|
1285
|
-
// Verify: a message was added to the conversation store
|
|
1286
|
-
const assistantMsg = mockMessages.find((m) => m.role === "assistant");
|
|
1287
|
-
expect(assistantMsg).toBeTruthy();
|
|
1288
|
-
});
|
|
1289
|
-
|
|
1290
|
-
test("restart + picker cancel preserves previous publish", async () => {
|
|
1291
|
-
const { ctx, sent } = createCtx();
|
|
1292
|
-
const conversationId = "conv-fin-cancel-preserve";
|
|
1293
|
-
|
|
1294
|
-
// Start a recording
|
|
1295
|
-
const originalId = handleRecordingStart(
|
|
1296
|
-
conversationId,
|
|
1297
|
-
undefined,
|
|
1298
|
-
ctx,
|
|
1299
|
-
);
|
|
1300
|
-
|
|
1301
|
-
// Restart
|
|
1302
|
-
const restartResult = handleRecordingRestart(
|
|
1303
|
-
conversationId,
|
|
1304
|
-
ctx,
|
|
1305
|
-
);
|
|
1306
|
-
expect(restartResult.initiated).toBe(true);
|
|
1307
|
-
|
|
1308
|
-
// Simulate stopped for old recording with filePath
|
|
1309
|
-
const stoppedStatus: RecordingStatus = {
|
|
1310
|
-
type: "recording_status",
|
|
1311
|
-
sessionId: originalId!,
|
|
1312
|
-
status: "stopped",
|
|
1313
|
-
filePath: `${ALLOWED_RECORDINGS_DIR}/recording-preserved.mov`,
|
|
1314
|
-
durationMs: 3000,
|
|
1315
|
-
attachToConversationId: conversationId,
|
|
1316
|
-
};
|
|
1317
|
-
await recordingHandlers.recording_status(stoppedStatus, ctx);
|
|
1318
|
-
|
|
1319
|
-
// Capture sent messages so far (should include old recording's attachment)
|
|
1320
|
-
const preCancelTextDeltas = sent.filter(
|
|
1321
|
-
(m) => m.type === "assistant_text_delta",
|
|
1322
|
-
);
|
|
1323
|
-
const oldAttachmentMsg = preCancelTextDeltas.find(
|
|
1324
|
-
(m) =>
|
|
1325
|
-
typeof m.text === "string" &&
|
|
1326
|
-
m.text.includes("Screen recording complete"),
|
|
1327
|
-
);
|
|
1328
|
-
expect(oldAttachmentMsg).toBeTruthy();
|
|
1329
|
-
|
|
1330
|
-
// Get the new recording ID from the deferred recording_start message
|
|
1331
|
-
const startMsg = sent.filter((m) => m.type === "recording_start").pop();
|
|
1332
|
-
expect(startMsg).toBeTruthy();
|
|
1333
|
-
|
|
1334
|
-
// Client sends restart_cancelled (picker was closed)
|
|
1335
|
-
const cancelStatus: RecordingStatus = {
|
|
1336
|
-
type: "recording_status",
|
|
1337
|
-
sessionId: startMsg!.recordingId as string,
|
|
1338
|
-
status: "restart_cancelled",
|
|
1339
|
-
attachToConversationId: conversationId,
|
|
1340
|
-
operationToken: restartResult.operationToken,
|
|
1341
|
-
};
|
|
1342
|
-
await recordingHandlers.recording_status(cancelStatus, ctx);
|
|
1343
|
-
|
|
1344
|
-
// Verify: the old recording's attachment messages are still in sent
|
|
1345
|
-
const postCancelTextDeltas = sent.filter(
|
|
1346
|
-
(m) => m.type === "assistant_text_delta",
|
|
1347
|
-
);
|
|
1348
|
-
const oldMsgStillPresent = postCancelTextDeltas.find(
|
|
1349
|
-
(m) =>
|
|
1350
|
-
typeof m.text === "string" &&
|
|
1351
|
-
m.text.includes("Screen recording complete"),
|
|
1352
|
-
);
|
|
1353
|
-
expect(oldMsgStillPresent).toBeTruthy();
|
|
1354
|
-
|
|
1355
|
-
// Verify: restart_cancelled adds "Recording restart cancelled." text
|
|
1356
|
-
const cancelMsg = postCancelTextDeltas.find(
|
|
1357
|
-
(m) =>
|
|
1358
|
-
typeof m.text === "string" && m.text === "Recording restart cancelled.",
|
|
1359
|
-
);
|
|
1360
|
-
expect(cancelMsg).toBeTruthy();
|
|
1361
|
-
|
|
1362
|
-
// Verify: the old recording's message was not removed from conversation store
|
|
1363
|
-
const assistantMsg = mockMessages.find((m) => m.role === "assistant");
|
|
1364
|
-
expect(assistantMsg).toBeTruthy();
|
|
1365
|
-
});
|
|
1366
|
-
|
|
1367
|
-
test("emits truthful failure text when previous finalize fails", async () => {
|
|
1368
|
-
const { ctx, sent } = createCtx();
|
|
1369
|
-
const conversationId = "conv-fin-fail-truth";
|
|
1370
|
-
|
|
1371
|
-
// Start a recording
|
|
1372
|
-
const originalId = handleRecordingStart(
|
|
1373
|
-
conversationId,
|
|
1374
|
-
undefined,
|
|
1375
|
-
ctx,
|
|
1376
|
-
);
|
|
1377
|
-
|
|
1378
|
-
// Restart
|
|
1379
|
-
const restartResult = handleRecordingRestart(
|
|
1380
|
-
conversationId,
|
|
1381
|
-
ctx,
|
|
1382
|
-
);
|
|
1383
|
-
expect(restartResult.initiated).toBe(true);
|
|
1384
|
-
sent.length = 0;
|
|
1385
|
-
|
|
1386
|
-
// Simulate stopped with missing filePath (no file produced)
|
|
1387
|
-
const stoppedStatus: RecordingStatus = {
|
|
1388
|
-
type: "recording_status",
|
|
1389
|
-
sessionId: originalId!,
|
|
1390
|
-
status: "stopped",
|
|
1391
|
-
// No filePath — recording stopped without producing a file
|
|
1392
|
-
attachToConversationId: conversationId,
|
|
1393
|
-
};
|
|
1394
|
-
await recordingHandlers.recording_status(stoppedStatus, ctx);
|
|
1395
|
-
|
|
1396
|
-
// Verify: error message text is sent (not "Screen recording complete")
|
|
1397
|
-
const textDeltas = sent.filter((m) => m.type === "assistant_text_delta");
|
|
1398
|
-
const hasSuccessMsg = textDeltas.some(
|
|
1399
|
-
(m) =>
|
|
1400
|
-
typeof m.text === "string" &&
|
|
1401
|
-
m.text.includes("Screen recording complete"),
|
|
1402
|
-
);
|
|
1403
|
-
expect(hasSuccessMsg).toBe(false);
|
|
1404
|
-
|
|
1405
|
-
const hasErrorMsg = textDeltas.some(
|
|
1406
|
-
(m) =>
|
|
1407
|
-
typeof m.text === "string" && m.text.includes("no file was produced"),
|
|
1408
|
-
);
|
|
1409
|
-
expect(hasErrorMsg).toBe(true);
|
|
1410
|
-
|
|
1411
|
-
// Verify: new recording_start still triggers (deferred start)
|
|
1412
|
-
const startMsgs = sent.filter((m) => m.type === "recording_start");
|
|
1413
|
-
expect(startMsgs).toHaveLength(1);
|
|
1414
|
-
expect(startMsgs[0].operationToken).toBe(restartResult.operationToken);
|
|
1415
|
-
});
|
|
1416
|
-
|
|
1417
|
-
test("preserves previous attachment when new start fails", async () => {
|
|
1418
|
-
const { ctx, sent } = createCtx();
|
|
1419
|
-
const conversationId = "conv-fin-new-fail";
|
|
1420
|
-
|
|
1421
|
-
// Start a recording
|
|
1422
|
-
const originalId = handleRecordingStart(
|
|
1423
|
-
conversationId,
|
|
1424
|
-
undefined,
|
|
1425
|
-
ctx,
|
|
1426
|
-
);
|
|
1427
|
-
|
|
1428
|
-
// Restart
|
|
1429
|
-
const restartResult = handleRecordingRestart(
|
|
1430
|
-
conversationId,
|
|
1431
|
-
ctx,
|
|
1432
|
-
);
|
|
1433
|
-
expect(restartResult.initiated).toBe(true);
|
|
1434
|
-
|
|
1435
|
-
// Inject a blocker recording on a different conversation so that
|
|
1436
|
-
// when the stopped handler calls cleanupMaps (removing conv-A's entry)
|
|
1437
|
-
// and then tries handleRecordingStart, the global single-active guard
|
|
1438
|
-
// sees the blocker entry and returns null — exercising the start-failure
|
|
1439
|
-
// code path.
|
|
1440
|
-
__injectRecordingOwner("conv-blocker", "rec-blocker");
|
|
1441
|
-
|
|
1442
|
-
sent.length = 0;
|
|
1443
|
-
|
|
1444
|
-
const stoppedStatus: RecordingStatus = {
|
|
1445
|
-
type: "recording_status",
|
|
1446
|
-
sessionId: originalId!,
|
|
1447
|
-
status: "stopped",
|
|
1448
|
-
filePath: `${ALLOWED_RECORDINGS_DIR}/recording-old-success.mov`,
|
|
1449
|
-
durationMs: 4000,
|
|
1450
|
-
attachToConversationId: conversationId,
|
|
1451
|
-
};
|
|
1452
|
-
await recordingHandlers.recording_status(stoppedStatus, ctx);
|
|
1453
|
-
|
|
1454
|
-
// Verify: old recording attachment is published (finalization succeeded)
|
|
1455
|
-
const textDeltas = sent.filter((m) => m.type === "assistant_text_delta");
|
|
1456
|
-
const successMsg = textDeltas.find(
|
|
1457
|
-
(m) =>
|
|
1458
|
-
typeof m.text === "string" &&
|
|
1459
|
-
m.text.includes("Screen recording complete"),
|
|
1460
|
-
);
|
|
1461
|
-
expect(successMsg).toBeTruthy();
|
|
1462
|
-
|
|
1463
|
-
const completes = sent.filter((m) => m.type === "message_complete");
|
|
1464
|
-
const attachmentComplete = completes.find((m) => m.attachments != null);
|
|
1465
|
-
expect(attachmentComplete).toBeTruthy();
|
|
1466
|
-
|
|
1467
|
-
// Verify: no recording_start was sent (deferred start was blocked)
|
|
1468
|
-
const startMsgs = sent.filter((m) => m.type === "recording_start");
|
|
1469
|
-
expect(startMsgs).toHaveLength(0);
|
|
1470
|
-
|
|
1471
|
-
// Verify: follow-up message about the start failure was sent
|
|
1472
|
-
const failureMsg = textDeltas.find(
|
|
1473
|
-
(m) =>
|
|
1474
|
-
typeof m.text === "string" &&
|
|
1475
|
-
m.text.includes(
|
|
1476
|
-
"Previous recording saved. New recording failed to start.",
|
|
1477
|
-
),
|
|
1478
|
-
);
|
|
1479
|
-
expect(failureMsg).toBeTruthy();
|
|
1480
|
-
|
|
1481
|
-
// Verify: old recording message exists in conversation store
|
|
1482
|
-
const assistantMsg = mockMessages.find((m) => m.role === "assistant");
|
|
1483
|
-
expect(assistantMsg).toBeTruthy();
|
|
1484
|
-
});
|
|
1485
|
-
|
|
1486
|
-
test("duplicate stopped callback does not double-attach", async () => {
|
|
1487
|
-
const { ctx, sent } = createCtx();
|
|
1488
|
-
const conversationId = "conv-fin-dup-stop";
|
|
1489
|
-
|
|
1490
|
-
// Start a recording
|
|
1491
|
-
const originalId = handleRecordingStart(
|
|
1492
|
-
conversationId,
|
|
1493
|
-
undefined,
|
|
1494
|
-
ctx,
|
|
1495
|
-
);
|
|
1496
|
-
|
|
1497
|
-
// Restart
|
|
1498
|
-
const restartResult = handleRecordingRestart(
|
|
1499
|
-
conversationId,
|
|
1500
|
-
ctx,
|
|
1501
|
-
);
|
|
1502
|
-
expect(restartResult.initiated).toBe(true);
|
|
1503
|
-
sent.length = 0;
|
|
1504
|
-
|
|
1505
|
-
// First stopped callback with filePath
|
|
1506
|
-
const stoppedStatus: RecordingStatus = {
|
|
1507
|
-
type: "recording_status",
|
|
1508
|
-
sessionId: originalId!,
|
|
1509
|
-
status: "stopped",
|
|
1510
|
-
filePath: `${ALLOWED_RECORDINGS_DIR}/recording-dup.mov`,
|
|
1511
|
-
durationMs: 2000,
|
|
1512
|
-
attachToConversationId: conversationId,
|
|
1513
|
-
};
|
|
1514
|
-
await recordingHandlers.recording_status(stoppedStatus, ctx);
|
|
1515
|
-
|
|
1516
|
-
// Count attachment-related messages after first callback
|
|
1517
|
-
const firstCallAttachmentMsgs = sent.filter(
|
|
1518
|
-
(m) =>
|
|
1519
|
-
m.type === "assistant_text_delta" &&
|
|
1520
|
-
typeof m.text === "string" &&
|
|
1521
|
-
m.text.includes("Screen recording complete"),
|
|
1522
|
-
);
|
|
1523
|
-
expect(firstCallAttachmentMsgs).toHaveLength(1);
|
|
1524
|
-
|
|
1525
|
-
const firstCallMsgCount = mockMessages.filter(
|
|
1526
|
-
(m) => m.role === "assistant",
|
|
1527
|
-
).length;
|
|
1528
|
-
expect(firstCallMsgCount).toBe(1);
|
|
1529
|
-
|
|
1530
|
-
// Send stopped again with same recordingId (duplicate)
|
|
1531
|
-
await recordingHandlers.recording_status(stoppedStatus, ctx);
|
|
1532
|
-
|
|
1533
|
-
// Verify: only one attachment message exists in sent — the duplicate was
|
|
1534
|
-
// rejected by the idempotency guard in finalizeAndPublishRecording
|
|
1535
|
-
const allAttachmentMsgs = sent.filter(
|
|
1536
|
-
(m) =>
|
|
1537
|
-
m.type === "assistant_text_delta" &&
|
|
1538
|
-
typeof m.text === "string" &&
|
|
1539
|
-
m.text.includes("Screen recording complete"),
|
|
1540
|
-
);
|
|
1541
|
-
expect(allAttachmentMsgs).toHaveLength(1);
|
|
1542
|
-
|
|
1543
|
-
// Verify: only one assistant message in conversation store
|
|
1544
|
-
const assistantMsgs = mockMessages.filter((m) => m.role === "assistant");
|
|
1545
|
-
expect(assistantMsgs).toHaveLength(1);
|
|
1546
|
-
});
|
|
1547
|
-
|
|
1548
|
-
test("existing stale-token and restart-timeout protections still pass", () => {
|
|
1549
|
-
// This test verifies that the module-level state functions are
|
|
1550
|
-
// consistent and that __resetRecordingState properly clears all state.
|
|
1551
|
-
// The actual stale-token and restart-timeout protection tests are in
|
|
1552
|
-
// the 'stale completion guard' and 'deferred restart' describe blocks
|
|
1553
|
-
// above — this test simply validates that state is clean after reset.
|
|
1554
|
-
__resetRecordingState();
|
|
1555
|
-
expect(isRecordingIdle()).toBe(true);
|
|
1556
|
-
expect(getActiveRestartToken()).toBeNull();
|
|
1557
|
-
|
|
1558
|
-
// Start a recording, verify not idle
|
|
1559
|
-
const { ctx } = createCtx();
|
|
1560
|
-
const conversationId = "conv-fin-sanity";
|
|
1561
|
-
|
|
1562
|
-
handleRecordingStart(conversationId, undefined, ctx);
|
|
1563
|
-
expect(isRecordingIdle()).toBe(false);
|
|
1564
|
-
|
|
1565
|
-
// Restart, verify token is set
|
|
1566
|
-
handleRecordingRestart(conversationId, ctx);
|
|
1567
|
-
expect(getActiveRestartToken()).not.toBeNull();
|
|
1568
|
-
|
|
1569
|
-
// Reset everything, verify clean state
|
|
1570
|
-
__resetRecordingState();
|
|
1571
|
-
expect(isRecordingIdle()).toBe(true);
|
|
1572
|
-
expect(getActiveRestartToken()).toBeNull();
|
|
1573
|
-
});
|
|
1574
|
-
});
|