@vellumai/assistant 0.5.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +54 -54
- package/docs/architecture/integrations.md +62 -67
- package/docs/credential-execution-service.md +3 -3
- package/package.json +1 -1
- package/src/__tests__/agent-loop.test.ts +111 -0
- package/src/__tests__/always-loaded-tools-guard.test.ts +3 -4
- package/src/__tests__/app-builder-tool-scripts.test.ts +13 -151
- package/src/__tests__/app-dir-path-guard.test.ts +78 -0
- package/src/__tests__/app-executors.test.ts +1 -291
- package/src/__tests__/app-git-history.test.ts +4 -4
- package/src/__tests__/app-routes-csp.test.ts +1 -0
- package/src/__tests__/app-store-dir-names.test.ts +426 -0
- package/src/__tests__/attachments-store.test.ts +169 -21
- package/src/__tests__/attachments.test.ts +115 -1
- package/src/__tests__/btw-routes.test.ts +1 -0
- package/src/__tests__/canonical-guardian-store.test.ts +38 -0
- package/src/__tests__/channel-reply-delivery.test.ts +55 -0
- package/src/__tests__/checker.test.ts +54 -0
- package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
- package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
- package/src/__tests__/compaction.benchmark.test.ts +2 -1
- package/src/__tests__/config-schema-cmd.test.ts +68 -21
- package/src/__tests__/config-schema.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +149 -5
- package/src/__tests__/conversation-agent-loop.test.ts +290 -2
- package/src/__tests__/conversation-attachments.test.ts +17 -19
- package/src/__tests__/conversation-disk-view-integration.test.ts +277 -0
- package/src/__tests__/conversation-disk-view.test.ts +810 -0
- package/src/__tests__/conversation-error.test.ts +1 -1
- package/src/__tests__/conversation-fork-crud.test.ts +551 -0
- package/src/__tests__/conversation-fork-route.test.ts +386 -0
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-key-store-disk-view.test.ts +130 -0
- package/src/__tests__/conversation-media-retry.test.ts +8 -2
- package/src/__tests__/conversation-queue.test.ts +36 -1
- package/src/__tests__/conversation-routes-disk-view.test.ts +439 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +2 -2
- package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -7
- package/src/__tests__/conversation-runtime-assembly.test.ts +17 -2
- package/src/__tests__/conversation-skill-tools.test.ts +4 -9
- package/src/__tests__/conversation-slash-commands.test.ts +149 -0
- package/src/__tests__/conversation-store.test.ts +24 -21
- package/src/__tests__/conversation-surfaces-state-update.test.ts +246 -0
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -0
- package/src/__tests__/conversation-title-service.test.ts +137 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +25 -315
- package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +1 -0
- package/src/__tests__/conversation-tool-setup-side-effect-flag.test.ts +1 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +44 -2
- package/src/__tests__/conversation-workspace-injection.test.ts +11 -0
- package/src/__tests__/credential-security-invariants.test.ts +3 -0
- package/src/__tests__/credential-vault-unit.test.ts +5 -10
- package/src/__tests__/cu-unified-flow.test.ts +1 -0
- package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +241 -0
- package/src/__tests__/db-llm-request-log-provider-migration.test.ts +214 -0
- package/src/__tests__/diagnostics-export.test.ts +70 -1
- package/src/__tests__/first-greeting.test.ts +80 -0
- package/src/__tests__/gateway-only-guard.test.ts +1 -0
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +3 -7
- package/src/__tests__/history-repair.test.ts +32 -10
- package/src/__tests__/http-conversation-lineage.test.ts +251 -0
- package/src/__tests__/image-source-path-reinject.test.ts +136 -0
- package/src/__tests__/llm-context-normalization.test.ts +1116 -0
- package/src/__tests__/llm-context-route-provider.test.ts +217 -0
- package/src/__tests__/llm-request-log-turn-query.test.ts +270 -0
- package/src/__tests__/media-generate-image.test.ts +47 -94
- package/src/__tests__/memory-lifecycle-e2e.test.ts +3 -1
- package/src/__tests__/memory-recall-quality.test.ts +5 -5
- package/src/__tests__/migration-cross-version-compatibility.test.ts +4 -1
- package/src/__tests__/migration-export-http.test.ts +3 -1
- package/src/__tests__/migration-import-commit-http.test.ts +18 -4
- package/src/__tests__/migration-import-preflight-http.test.ts +1 -3
- package/src/__tests__/mime-builder.test.ts +3 -2
- package/src/__tests__/non-member-access-request.test.ts +12 -1
- package/src/__tests__/notification-decision-identity.test.ts +52 -0
- package/src/__tests__/oauth-apps-routes.test.ts +103 -0
- package/src/__tests__/oauth-store.test.ts +115 -0
- package/src/__tests__/provider-error-scenarios.test.ts +1 -3
- package/src/__tests__/provider-failover-actual-provider.test.ts +66 -0
- package/src/__tests__/recording-handler.test.ts +17 -0
- package/src/__tests__/registry.test.ts +3 -8
- package/src/__tests__/relay-server.test.ts +1 -1
- package/src/__tests__/runtime-attachment-metadata.test.ts +7 -3
- package/src/__tests__/schema-transforms.test.ts +165 -5
- package/src/__tests__/server-history-render.test.ts +2 -2
- package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
- package/src/__tests__/slack-inbound-verification.test.ts +2 -2
- package/src/__tests__/starter-task-flow.test.ts +1 -0
- package/src/__tests__/suggestion-routes.test.ts +443 -0
- package/src/__tests__/swarm-conversation-integration.test.ts +1 -0
- package/src/__tests__/swarm-recursion.test.ts +1 -0
- package/src/__tests__/swarm-tool.test.ts +1 -0
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +32 -5
- package/src/__tests__/top-level-renderer.test.ts +22 -0
- package/src/__tests__/turn-boundary-resolution.test.ts +243 -0
- package/src/__tests__/web-fetch.test.ts +6 -2
- package/src/__tests__/workspace-migration-006-services-config.test.ts +335 -0
- package/src/__tests__/workspace-migration-007-web-search-provider-rename.test.ts +312 -0
- package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +278 -0
- package/src/__tests__/workspace-migration-010-app-dir-rename.test.ts +275 -0
- package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +77 -0
- package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +401 -0
- package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +328 -0
- package/src/__tests__/workspace-migration-seed-device-id.test.ts +6 -10
- package/src/agent/attachments.ts +27 -1
- package/src/agent/loop.ts +29 -1
- package/src/avatar/traits-png-sync.ts +80 -25
- package/src/bundler/app-bundler.ts +4 -4
- package/src/calls/call-domain.ts +1 -0
- package/src/calls/voice-session-bridge.ts +1 -0
- package/src/cli/commands/auth.ts +92 -0
- package/src/cli/commands/avatar.ts +7 -6
- package/src/cli/commands/config.ts +2 -0
- package/src/cli/commands/oauth/providers.ts +29 -0
- package/src/cli/program.ts +12 -0
- package/src/cli.ts +15 -48
- package/src/config/bundled-skills/app-builder/SKILL.md +103 -28
- package/src/config/bundled-skills/app-builder/TOOLS.json +5 -199
- package/src/config/bundled-skills/app-builder/tools/{app-query.ts → app-refresh.ts} +2 -2
- package/src/config/bundled-skills/contacts/tools/google-contacts.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +6 -9
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +4 -6
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +4 -6
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +2 -3
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
- package/src/config/bundled-skills/image-studio/SKILL.md +2 -2
- package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +45 -72
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/shared.ts +1 -1
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -3
- package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
- package/src/config/bundled-skills/slack/tools/shared.ts +19 -4
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +2 -3
- package/src/config/bundled-skills/transcribe/SKILL.md +1 -1
- package/src/config/bundled-skills/transcribe/TOOLS.json +2 -6
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +19 -83
- package/src/config/bundled-tool-registry.ts +2 -14
- package/src/config/feature-flag-registry.json +8 -0
- package/src/config/loader.ts +64 -0
- package/src/config/raw-config-utils.ts +30 -0
- package/src/config/schema-utils.ts +28 -7
- package/src/config/schema.ts +8 -0
- package/src/config/schemas/elevenlabs.ts +18 -0
- package/src/config/schemas/memory-lifecycle.ts +4 -2
- package/src/config/schemas/memory-storage.ts +1 -1
- package/src/config/schemas/services.ts +8 -6
- package/src/contacts/contact-store.ts +13 -6
- package/src/contacts/contacts-write.ts +0 -1
- package/src/context/window-manager.ts +13 -2
- package/src/daemon/conversation-agent-loop-handlers.ts +48 -7
- package/src/daemon/conversation-agent-loop.ts +56 -19
- package/src/daemon/conversation-attachments.ts +18 -36
- package/src/daemon/conversation-error.ts +2 -1
- package/src/daemon/conversation-history.ts +18 -4
- package/src/daemon/conversation-lifecycle.ts +39 -15
- package/src/daemon/conversation-messaging.ts +70 -26
- package/src/daemon/conversation-process.ts +58 -34
- package/src/daemon/conversation-runtime-assembly.ts +21 -38
- package/src/daemon/conversation-slash.ts +121 -256
- package/src/daemon/conversation-surfaces.ts +143 -20
- package/src/daemon/conversation-tool-setup.ts +0 -6
- package/src/daemon/conversation-workspace.ts +21 -1
- package/src/daemon/conversation.ts +51 -29
- package/src/daemon/first-greeting.ts +35 -0
- package/src/daemon/handlers/config-embeddings.ts +148 -0
- package/src/daemon/handlers/config-model.ts +71 -26
- package/src/daemon/handlers/conversations.ts +0 -23
- package/src/daemon/handlers/recording.ts +26 -21
- package/src/daemon/host-cu-proxy.ts +2 -2
- package/src/daemon/lifecycle.ts +106 -64
- package/src/daemon/message-protocol.ts +3 -0
- package/src/daemon/message-types/conversations.ts +19 -0
- package/src/daemon/message-types/messages.ts +1 -0
- package/src/daemon/message-types/shared.ts +2 -0
- package/src/daemon/message-types/surfaces.ts +2 -0
- package/src/daemon/message-types/upgrades.ts +23 -0
- package/src/daemon/server.ts +83 -12
- package/src/daemon/shutdown-handlers.ts +8 -5
- package/src/daemon/startup-error.ts +9 -0
- package/src/daemon/tool-side-effects.ts +11 -28
- package/src/events/tool-permission-telemetry-listener.ts +1 -3
- package/src/instrument.ts +0 -4
- package/src/media/app-icon-generator.ts +2 -2
- package/src/memory/app-git-service.ts +28 -16
- package/src/memory/app-store.ts +230 -41
- package/src/memory/attachments-store.ts +558 -130
- package/src/memory/conversation-attention-store.ts +70 -0
- package/src/memory/conversation-crud.ts +442 -3
- package/src/memory/conversation-directories.ts +125 -0
- package/src/memory/conversation-disk-view.ts +390 -0
- package/src/memory/conversation-key-store.ts +17 -5
- package/src/memory/conversation-queries.ts +5 -1
- package/src/memory/conversation-title-service.ts +21 -49
- package/src/memory/db-init.ts +28 -0
- package/src/memory/embedding-backend.ts +42 -53
- package/src/memory/embedding-gemini.test.ts +4 -4
- package/src/memory/embedding-local.ts +1 -3
- package/src/memory/embedding-ollama.ts +1 -3
- package/src/memory/embedding-openai.ts +1 -3
- package/src/memory/indexer.ts +9 -7
- package/src/memory/items-extractor.ts +42 -13
- package/src/memory/job-handlers/conversation-starters.ts +6 -1
- package/src/memory/job-handlers/embedding.test.ts +1 -4
- package/src/memory/llm-request-log-store.ts +100 -1
- package/src/memory/migrations/102-alter-table-columns.ts +5 -0
- package/src/memory/migrations/146-schedule-oneshot-routing.ts +3 -3
- package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +66 -70
- package/src/memory/migrations/148-drop-reminders-table.ts +5 -9
- package/src/memory/migrations/160-drop-loopback-port-column.ts +1 -3
- package/src/memory/migrations/174-rename-thread-starters-table.ts +0 -7
- package/src/memory/migrations/178-oauth-providers-managed-service-config-key.ts +15 -0
- package/src/memory/migrations/179-llm-request-log-message-id.ts +16 -0
- package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +66 -0
- package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +46 -0
- package/src/memory/migrations/182-oauth-providers-display-metadata.ts +20 -0
- package/src/memory/migrations/183-add-conversation-fork-lineage.ts +22 -0
- package/src/memory/migrations/184-llm-request-log-provider.ts +12 -0
- package/src/memory/migrations/index.ts +7 -0
- package/src/memory/migrations/registry.ts +13 -0
- package/src/memory/retriever.test.ts +601 -2
- package/src/memory/retriever.ts +85 -9
- package/src/memory/schema/conversations.ts +6 -0
- package/src/memory/schema/infrastructure.ts +13 -7
- package/src/memory/schema/oauth.ts +6 -0
- package/src/messaging/providers/gmail/mime-builder.ts +3 -1
- package/src/notifications/copy-composer.ts +26 -0
- package/src/notifications/decision-engine.ts +14 -1
- package/src/notifications/emit-signal.ts +1 -1
- package/src/notifications/signal.ts +36 -0
- package/src/oauth/byo-connection.test.ts +1 -45
- package/src/oauth/byo-connection.ts +2 -8
- package/src/oauth/connect-orchestrator.ts +15 -11
- package/src/oauth/connection-resolver.test.ts +191 -0
- package/src/oauth/connection-resolver.ts +66 -38
- package/src/oauth/connection.ts +0 -1
- package/src/oauth/oauth-store.ts +97 -47
- package/src/oauth/platform-connection.test.ts +0 -1
- package/src/oauth/platform-connection.ts +11 -3
- package/src/oauth/seed-providers.ts +78 -3
- package/src/oauth/token-persistence.ts +16 -10
- package/src/permissions/checker.ts +71 -8
- package/src/prompts/templates/BOOTSTRAP.md +2 -0
- package/src/providers/anthropic/client.ts +8 -1
- package/src/providers/failover.ts +4 -1
- package/src/providers/gemini/client.ts +50 -0
- package/src/providers/model-catalog.ts +92 -0
- package/src/providers/model-intents.ts +29 -20
- package/src/providers/openai/client.ts +49 -0
- package/src/providers/types.ts +2 -0
- package/src/runtime/access-request-helper.ts +16 -7
- package/src/runtime/auth/credential-service.ts +3 -1
- package/src/runtime/auth/route-policy.ts +14 -1
- package/src/runtime/btw-sidechain.ts +101 -0
- package/src/runtime/channel-reply-delivery.ts +17 -1
- package/src/runtime/http-router.ts +3 -1
- package/src/runtime/http-server.ts +196 -141
- package/src/runtime/http-types.ts +1 -0
- package/src/runtime/migrations/vbundle-builder.ts +5 -1
- package/src/runtime/routes/access-request-decision.ts +41 -0
- package/src/runtime/routes/app-management-routes.ts +6 -3
- package/src/runtime/routes/app-routes.ts +7 -3
- package/src/runtime/routes/approval-routes.ts +1 -0
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +34 -2
- package/src/runtime/routes/attachment-routes.ts +45 -15
- package/src/runtime/routes/btw-routes.ts +21 -61
- package/src/runtime/routes/conversation-management-routes.ts +68 -0
- package/src/runtime/routes/conversation-query-routes.ts +180 -10
- package/src/runtime/routes/conversation-routes.ts +222 -28
- package/src/runtime/routes/conversation-starter-routes.ts +9 -11
- package/src/runtime/routes/diagnostics-routes.ts +1 -0
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -2
- package/src/runtime/routes/llm-context-normalization.ts +1199 -0
- package/src/runtime/routes/log-export-routes.ts +3 -0
- package/src/runtime/routes/memory-item-routes.test.ts +34 -0
- package/src/runtime/routes/memory-item-routes.ts +4 -0
- package/src/runtime/routes/migration-routes.ts +4 -1
- package/src/runtime/routes/oauth-apps.ts +291 -0
- package/src/runtime/routes/secret-routes.ts +28 -1
- package/src/runtime/routes/settings-routes.ts +14 -0
- package/src/runtime/routes/trace-event-routes.ts +4 -1
- package/src/schedule/schedule-store.ts +9 -21
- package/src/security/secure-keys.ts +21 -0
- package/src/signals/bash.ts +1 -1
- package/src/swarm/backend-claude-code.ts +3 -6
- package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
- package/src/telemetry/usage-telemetry-reporter.ts +3 -1
- package/src/tools/AGENTS.md +6 -10
- package/src/tools/apps/executors.ts +17 -232
- package/src/tools/claude-code/claude-code.ts +2 -3
- package/src/tools/credentials/vault.ts +7 -12
- package/src/tools/host-filesystem/read.ts +13 -10
- package/src/tools/network/__tests__/web-search.test.ts +4 -2
- package/src/tools/schedule/list.ts +2 -7
- package/src/tools/schema-transforms.ts +5 -0
- package/src/tools/shared/filesystem/format-diff.ts +2 -7
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/tool-manifest.ts +0 -6
- package/src/tools/ui-surface/definitions.ts +2 -2
- package/src/util/device-id.ts +28 -5
- package/src/util/platform.ts +6 -0
- package/src/util/pricing.ts +1 -0
- package/src/util/retry.ts +1 -3
- package/src/workspace/migrations/002-backfill-installation-id.ts +23 -12
- package/src/workspace/migrations/003-seed-device-id.ts +3 -4
- package/src/workspace/migrations/006-services-config.ts +5 -0
- package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +12 -0
- package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +10 -0
- package/src/workspace/migrations/010-app-dir-rename.ts +223 -0
- package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +64 -0
- package/src/workspace/migrations/013-repair-conversation-disk-view.ts +11 -0
- package/src/workspace/migrations/rebuild-conversation-disk-view.ts +186 -0
- package/src/workspace/migrations/registry.ts +10 -0
- package/src/workspace/top-level-renderer.ts +12 -0
- package/src/__tests__/asset-materialize-tool.test.ts +0 -523
- package/src/__tests__/asset-search-tool.test.ts +0 -536
- package/src/__tests__/fixtures/media-reuse-fixtures.ts +0 -56
- package/src/__tests__/media-reuse-story.e2e.test.ts +0 -762
- package/src/__tests__/media-visibility-policy.test.ts +0 -190
- package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +0 -14
- package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +0 -13
- package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +0 -21
- package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +0 -14
- package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -13
- package/src/config/bundled-skills/app-builder/tools/app-update.ts +0 -23
- package/src/daemon/media-visibility-policy.ts +0 -59
- package/src/tools/assets/materialize.ts +0 -248
- package/src/tools/assets/search.ts +0 -400
|
@@ -549,7 +549,11 @@ describe("attachment reuse across conversation lifecycles", () => {
|
|
|
549
549
|
test("attachment uploaded in conversation A is retrievable by ID without any conversation reference", async () => {
|
|
550
550
|
const convA = createConversation("Conversation A");
|
|
551
551
|
const msgA = await addMessage(convA.id, "assistant", "Here is a file");
|
|
552
|
-
const stored = uploadAttachment(
|
|
552
|
+
const stored = uploadAttachment(
|
|
553
|
+
"report.pdf",
|
|
554
|
+
"application/pdf",
|
|
555
|
+
"JVBERA==",
|
|
556
|
+
);
|
|
553
557
|
linkAttachmentToMessage(msgA.id, stored.id, 0);
|
|
554
558
|
|
|
555
559
|
// Create a completely separate conversation
|
|
@@ -557,14 +561,14 @@ describe("attachment reuse across conversation lifecycles", () => {
|
|
|
557
561
|
await addMessage(convB.id, "user", "hello");
|
|
558
562
|
|
|
559
563
|
// The attachment is retrievable by ID regardless of which conversation is active.
|
|
560
|
-
const fetched = getAttachmentById(stored.id);
|
|
564
|
+
const fetched = getAttachmentById(stored.id, { hydrateFileData: true });
|
|
561
565
|
expect(fetched).not.toBeNull();
|
|
562
566
|
expect(fetched!.id).toBe(stored.id);
|
|
563
567
|
expect(fetched!.originalFilename).toBe("report.pdf");
|
|
564
|
-
expect(fetched!.dataBase64).toBe("
|
|
568
|
+
expect(fetched!.dataBase64).toBe("JVBERA==");
|
|
565
569
|
});
|
|
566
570
|
|
|
567
|
-
test("
|
|
571
|
+
test("re-linking an attachment across conversations creates a conversation-local row", async () => {
|
|
568
572
|
const convA = createConversation("Conversation A");
|
|
569
573
|
const convB = createConversation("Conversation B");
|
|
570
574
|
|
|
@@ -576,17 +580,17 @@ describe("attachment reuse across conversation lifecycles", () => {
|
|
|
576
580
|
linkAttachmentToMessage(msgA.id, stored.id, 0);
|
|
577
581
|
linkAttachmentToMessage(msgB.id, stored.id, 0);
|
|
578
582
|
|
|
579
|
-
// Both messages see the attachment
|
|
583
|
+
// Both messages see the attachment, but each conversation keeps its own row.
|
|
580
584
|
const linkedA = getAttachmentsForMessage(msgA.id);
|
|
581
585
|
expect(linkedA).toHaveLength(1);
|
|
582
586
|
expect(linkedA[0].id).toBe(stored.id);
|
|
583
587
|
|
|
584
588
|
const linkedB = getAttachmentsForMessage(msgB.id);
|
|
585
589
|
expect(linkedB).toHaveLength(1);
|
|
586
|
-
expect(linkedB[0].id).toBe(stored.id);
|
|
590
|
+
expect(linkedB[0].id).not.toBe(stored.id);
|
|
587
591
|
});
|
|
588
592
|
|
|
589
|
-
test("deleting conversation A does not
|
|
593
|
+
test("deleting conversation A does not remove the copied attachment in conversation B", async () => {
|
|
590
594
|
const convA = createConversation("Conversation A");
|
|
591
595
|
const convB = createConversation("Conversation B");
|
|
592
596
|
|
|
@@ -600,33 +604,33 @@ describe("attachment reuse across conversation lifecycles", () => {
|
|
|
600
604
|
const stored = uploadAttachment("chart.png", "image/png", "AAAA");
|
|
601
605
|
linkAttachmentToMessage(msgA.id, stored.id, 0);
|
|
602
606
|
linkAttachmentToMessage(msgB.id, stored.id, 0);
|
|
607
|
+
const linkedB = getAttachmentsForMessage(msgB.id);
|
|
608
|
+
expect(linkedB).toHaveLength(1);
|
|
603
609
|
|
|
604
610
|
// Delete conversation A's exchange
|
|
605
611
|
deleteLastExchange(convA.id);
|
|
606
612
|
|
|
607
|
-
//
|
|
608
|
-
const fetched = getAttachmentById(
|
|
613
|
+
// Conversation B keeps its own attachment row and file.
|
|
614
|
+
const fetched = getAttachmentById(linkedB[0].id);
|
|
609
615
|
expect(fetched).not.toBeNull();
|
|
610
616
|
|
|
611
617
|
// convB's message still has the attachment linked
|
|
612
|
-
const
|
|
613
|
-
expect(
|
|
614
|
-
expect(
|
|
618
|
+
const linkedBAfterDelete = getAttachmentsForMessage(msgB.id);
|
|
619
|
+
expect(linkedBAfterDelete).toHaveLength(1);
|
|
620
|
+
expect(linkedBAfterDelete[0].id).toBe(linkedB[0].id);
|
|
615
621
|
});
|
|
616
622
|
|
|
617
|
-
test("
|
|
623
|
+
test("identical uploads remain distinct across conversations", async () => {
|
|
618
624
|
const convA = createConversation("Conversation A");
|
|
619
625
|
const convB = createConversation("Conversation B");
|
|
620
626
|
|
|
621
627
|
await addMessage(convA.id, "user", "upload in A");
|
|
622
628
|
await addMessage(convB.id, "user", "upload in B");
|
|
623
629
|
|
|
624
|
-
// Same content uploaded in two different conversation contexts
|
|
625
630
|
const first = uploadAttachment("photo.png", "image/png", "DEDUPCROSS");
|
|
626
631
|
const second = uploadAttachment("photo.png", "image/png", "DEDUPCROSS");
|
|
627
632
|
|
|
628
|
-
|
|
629
|
-
expect(second.id).toBe(first.id);
|
|
633
|
+
expect(second.id).not.toBe(first.id);
|
|
630
634
|
});
|
|
631
635
|
});
|
|
632
636
|
|
|
@@ -664,7 +668,7 @@ describe("no private-conversation attachment visibility boundary", () => {
|
|
|
664
668
|
expect(fetched!.originalFilename).toBe("secret.pdf");
|
|
665
669
|
});
|
|
666
670
|
|
|
667
|
-
test("attachment from private conversation
|
|
671
|
+
test("attachment from a private conversation is copied when linked into a standard conversation", async () => {
|
|
668
672
|
const privateConv = createConversation({
|
|
669
673
|
title: "Private",
|
|
670
674
|
conversationType: "private",
|
|
@@ -695,7 +699,7 @@ describe("no private-conversation attachment visibility boundary", () => {
|
|
|
695
699
|
|
|
696
700
|
const linkedStandard = getAttachmentsForMessage(standardMsg.id);
|
|
697
701
|
expect(linkedStandard).toHaveLength(1);
|
|
698
|
-
expect(linkedStandard[0].id).toBe(stored.id);
|
|
702
|
+
expect(linkedStandard[0].id).not.toBe(stored.id);
|
|
699
703
|
});
|
|
700
704
|
|
|
701
705
|
test("getAttachmentsForMessage returns private conversation attachments", async () => {
|
|
@@ -712,7 +716,7 @@ describe("no private-conversation attachment visibility boundary", () => {
|
|
|
712
716
|
expect(linked[0].id).toBe(stored.id);
|
|
713
717
|
});
|
|
714
718
|
|
|
715
|
-
test("
|
|
719
|
+
test("identical uploads remain distinct across private and standard conversations", () => {
|
|
716
720
|
createConversation({ title: "Private", conversationType: "private" });
|
|
717
721
|
createConversation({ title: "Standard", conversationType: "standard" });
|
|
718
722
|
|
|
@@ -728,8 +732,7 @@ describe("no private-conversation attachment visibility boundary", () => {
|
|
|
728
732
|
"CROSSCONVERSATION",
|
|
729
733
|
);
|
|
730
734
|
|
|
731
|
-
|
|
732
|
-
expect(fromStandard.id).toBe(fromPrivate.id);
|
|
735
|
+
expect(fromStandard.id).not.toBe(fromPrivate.id);
|
|
733
736
|
});
|
|
734
737
|
|
|
735
738
|
test("clearAll removes attachments from both private and standard conversations", async () => {
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createSurfaceMutex,
|
|
5
|
+
handleSurfaceAction,
|
|
6
|
+
type SurfaceConversationContext,
|
|
7
|
+
surfaceProxyResolver,
|
|
8
|
+
} from "../daemon/conversation-surfaces.js";
|
|
9
|
+
import type {
|
|
10
|
+
ServerMessage,
|
|
11
|
+
SurfaceData,
|
|
12
|
+
SurfaceType,
|
|
13
|
+
} from "../daemon/message-protocol.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Build a minimal SurfaceConversationContext for testing.
|
|
17
|
+
* Tracks calls to enqueueMessage and processMessage so tests can assert
|
|
18
|
+
* whether an LLM turn was triggered.
|
|
19
|
+
*/
|
|
20
|
+
function makeContext(opts?: {
|
|
21
|
+
sent?: ServerMessage[];
|
|
22
|
+
}): SurfaceConversationContext & {
|
|
23
|
+
enqueueCalls: Array<{ content: string; requestId: string }>;
|
|
24
|
+
processCalls: Array<{ content: string; requestId?: string }>;
|
|
25
|
+
} {
|
|
26
|
+
const sent = opts?.sent ?? [];
|
|
27
|
+
const enqueueCalls: Array<{ content: string; requestId: string }> = [];
|
|
28
|
+
const processCalls: Array<{ content: string; requestId?: string }> = [];
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
conversationId: "test-session",
|
|
32
|
+
traceEmitter: { emit: () => {} },
|
|
33
|
+
sendToClient: (msg) => sent.push(msg),
|
|
34
|
+
pendingSurfaceActions: new Map<string, { surfaceType: SurfaceType }>(),
|
|
35
|
+
lastSurfaceAction: new Map<
|
|
36
|
+
string,
|
|
37
|
+
{ actionId: string; data?: Record<string, unknown> }
|
|
38
|
+
>(),
|
|
39
|
+
surfaceState: new Map<
|
|
40
|
+
string,
|
|
41
|
+
{ surfaceType: SurfaceType; data: SurfaceData; title?: string }
|
|
42
|
+
>(),
|
|
43
|
+
surfaceUndoStacks: new Map<string, string[]>(),
|
|
44
|
+
accumulatedSurfaceState: new Map<string, Record<string, unknown>>(),
|
|
45
|
+
surfaceActionRequestIds: new Set<string>(),
|
|
46
|
+
currentTurnSurfaces: [],
|
|
47
|
+
isProcessing: () => false,
|
|
48
|
+
enqueueMessage: (content, _attachments, _onEvent, requestId) => {
|
|
49
|
+
enqueueCalls.push({ content, requestId });
|
|
50
|
+
return { queued: false, requestId };
|
|
51
|
+
},
|
|
52
|
+
getQueueDepth: () => 0,
|
|
53
|
+
processMessage: async (content, _attachments, _onEvent, requestId) => {
|
|
54
|
+
processCalls.push({ content, requestId });
|
|
55
|
+
return "ok";
|
|
56
|
+
},
|
|
57
|
+
withSurface: createSurfaceMutex(),
|
|
58
|
+
enqueueCalls,
|
|
59
|
+
processCalls,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Register a dynamic_page surface in the context so state_update is accepted. */
|
|
64
|
+
function registerDynamicPage(
|
|
65
|
+
ctx: SurfaceConversationContext,
|
|
66
|
+
surfaceId: string,
|
|
67
|
+
): void {
|
|
68
|
+
ctx.pendingSurfaceActions.set(surfaceId, { surfaceType: "dynamic_page" });
|
|
69
|
+
ctx.surfaceState.set(surfaceId, {
|
|
70
|
+
surfaceType: "dynamic_page",
|
|
71
|
+
data: { html: "<div>test</div>" } as SurfaceData,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe("state_update silent accumulation", () => {
|
|
76
|
+
test("accumulates state from multiple calls via shallow merge", () => {
|
|
77
|
+
const ctx = makeContext();
|
|
78
|
+
registerDynamicPage(ctx, "surface-1");
|
|
79
|
+
|
|
80
|
+
handleSurfaceAction(ctx, "surface-1", "state_update", { page: 2 });
|
|
81
|
+
handleSurfaceAction(ctx, "surface-1", "state_update", {
|
|
82
|
+
selectedTab: "overview",
|
|
83
|
+
});
|
|
84
|
+
handleSurfaceAction(ctx, "surface-1", "state_update", { page: 5 });
|
|
85
|
+
|
|
86
|
+
const accumulated = ctx.accumulatedSurfaceState.get("surface-1");
|
|
87
|
+
expect(accumulated).toEqual({ page: 5, selectedTab: "overview" });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("ignores calls with undefined data", () => {
|
|
91
|
+
const ctx = makeContext();
|
|
92
|
+
registerDynamicPage(ctx, "surface-1");
|
|
93
|
+
|
|
94
|
+
handleSurfaceAction(ctx, "surface-1", "state_update", { count: 1 });
|
|
95
|
+
handleSurfaceAction(ctx, "surface-1", "state_update", undefined);
|
|
96
|
+
|
|
97
|
+
const accumulated = ctx.accumulatedSurfaceState.get("surface-1");
|
|
98
|
+
expect(accumulated).toEqual({ count: 1 });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("does not accumulate for non-dynamic_page surfaces", () => {
|
|
102
|
+
const ctx = makeContext();
|
|
103
|
+
// Register as a table surface instead of dynamic_page
|
|
104
|
+
ctx.pendingSurfaceActions.set("surface-table", { surfaceType: "table" });
|
|
105
|
+
ctx.surfaceState.set("surface-table", {
|
|
106
|
+
surfaceType: "table",
|
|
107
|
+
data: {
|
|
108
|
+
columns: [],
|
|
109
|
+
rows: [],
|
|
110
|
+
} as unknown as SurfaceData,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
handleSurfaceAction(ctx, "surface-table", "state_update", { page: 1 });
|
|
114
|
+
|
|
115
|
+
const accumulated = ctx.accumulatedSurfaceState.get("surface-table");
|
|
116
|
+
expect(accumulated).toBeUndefined();
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("state_update does not trigger LLM", () => {
|
|
121
|
+
test("does not call enqueueMessage or processMessage", () => {
|
|
122
|
+
const ctx = makeContext();
|
|
123
|
+
registerDynamicPage(ctx, "surface-1");
|
|
124
|
+
|
|
125
|
+
handleSurfaceAction(ctx, "surface-1", "state_update", {
|
|
126
|
+
currentSlide: 3,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
expect(ctx.enqueueCalls).toHaveLength(0);
|
|
130
|
+
expect(ctx.processCalls).toHaveLength(0);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("does not add to surfaceActionRequestIds", () => {
|
|
134
|
+
const ctx = makeContext();
|
|
135
|
+
registerDynamicPage(ctx, "surface-1");
|
|
136
|
+
|
|
137
|
+
handleSurfaceAction(ctx, "surface-1", "state_update", { zoom: 1.5 });
|
|
138
|
+
|
|
139
|
+
expect(ctx.surfaceActionRequestIds.size).toBe(0);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe("accumulated state injection into reactive actions", () => {
|
|
144
|
+
test("subsequent reactive action includes accumulated state in message content", () => {
|
|
145
|
+
const ctx = makeContext();
|
|
146
|
+
registerDynamicPage(ctx, "surface-1");
|
|
147
|
+
|
|
148
|
+
// Accumulate some state
|
|
149
|
+
handleSurfaceAction(ctx, "surface-1", "state_update", { page: 3 });
|
|
150
|
+
handleSurfaceAction(ctx, "surface-1", "state_update", {
|
|
151
|
+
selectedItem: "item-42",
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Fire a reactive action (e.g. "save")
|
|
155
|
+
handleSurfaceAction(ctx, "surface-1", "save");
|
|
156
|
+
|
|
157
|
+
// The enqueueMessage call should include the accumulated state
|
|
158
|
+
expect(ctx.enqueueCalls).toHaveLength(1);
|
|
159
|
+
const content = ctx.enqueueCalls[0].content;
|
|
160
|
+
expect(content).toContain("Accumulated surface state:");
|
|
161
|
+
expect(content).toContain('"page":3');
|
|
162
|
+
expect(content).toContain('"selectedItem":"item-42"');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("empty accumulated state is not appended", () => {
|
|
166
|
+
const ctx = makeContext();
|
|
167
|
+
registerDynamicPage(ctx, "surface-1");
|
|
168
|
+
|
|
169
|
+
// Fire a reactive action without any prior state_update
|
|
170
|
+
handleSurfaceAction(ctx, "surface-1", "refresh");
|
|
171
|
+
|
|
172
|
+
expect(ctx.enqueueCalls).toHaveLength(1);
|
|
173
|
+
const content = ctx.enqueueCalls[0].content;
|
|
174
|
+
expect(content).not.toContain("Accumulated surface state:");
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe("per-surface state isolation", () => {
|
|
179
|
+
test("accumulated state from surface A does not appear in surface B reactive action", () => {
|
|
180
|
+
const ctx = makeContext();
|
|
181
|
+
registerDynamicPage(ctx, "surface-a");
|
|
182
|
+
registerDynamicPage(ctx, "surface-b");
|
|
183
|
+
|
|
184
|
+
// Accumulate state only on surface A
|
|
185
|
+
handleSurfaceAction(ctx, "surface-a", "state_update", {
|
|
186
|
+
filterA: "active",
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Fire a reactive action on surface B
|
|
190
|
+
handleSurfaceAction(ctx, "surface-b", "submit");
|
|
191
|
+
|
|
192
|
+
expect(ctx.enqueueCalls).toHaveLength(1);
|
|
193
|
+
const content = ctx.enqueueCalls[0].content;
|
|
194
|
+
expect(content).not.toContain("filterA");
|
|
195
|
+
expect(content).not.toContain("Accumulated surface state:");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("each surface maintains its own accumulated state", () => {
|
|
199
|
+
const ctx = makeContext();
|
|
200
|
+
registerDynamicPage(ctx, "surface-a");
|
|
201
|
+
registerDynamicPage(ctx, "surface-b");
|
|
202
|
+
|
|
203
|
+
handleSurfaceAction(ctx, "surface-a", "state_update", { page: 1 });
|
|
204
|
+
handleSurfaceAction(ctx, "surface-b", "state_update", { page: 99 });
|
|
205
|
+
|
|
206
|
+
expect(ctx.accumulatedSurfaceState.get("surface-a")).toEqual({ page: 1 });
|
|
207
|
+
expect(ctx.accumulatedSurfaceState.get("surface-b")).toEqual({ page: 99 });
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe("cleanup on dismiss", () => {
|
|
212
|
+
test("ui_dismiss clears accumulated state for the surface", async () => {
|
|
213
|
+
const ctx = makeContext();
|
|
214
|
+
registerDynamicPage(ctx, "surface-1");
|
|
215
|
+
|
|
216
|
+
// Accumulate state
|
|
217
|
+
handleSurfaceAction(ctx, "surface-1", "state_update", { dirty: true });
|
|
218
|
+
expect(ctx.accumulatedSurfaceState.get("surface-1")).toEqual({
|
|
219
|
+
dirty: true,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Dismiss via surfaceProxyResolver (ui_dismiss)
|
|
223
|
+
await surfaceProxyResolver(ctx, "ui_dismiss", {
|
|
224
|
+
surface_id: "surface-1",
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Accumulated state should be cleared
|
|
228
|
+
expect(ctx.accumulatedSurfaceState.has("surface-1")).toBe(false);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("ui_dismiss does not affect other surfaces accumulated state", async () => {
|
|
232
|
+
const ctx = makeContext();
|
|
233
|
+
registerDynamicPage(ctx, "surface-1");
|
|
234
|
+
registerDynamicPage(ctx, "surface-2");
|
|
235
|
+
|
|
236
|
+
handleSurfaceAction(ctx, "surface-1", "state_update", { x: 1 });
|
|
237
|
+
handleSurfaceAction(ctx, "surface-2", "state_update", { y: 2 });
|
|
238
|
+
|
|
239
|
+
await surfaceProxyResolver(ctx, "ui_dismiss", {
|
|
240
|
+
surface_id: "surface-1",
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
expect(ctx.accumulatedSurfaceState.has("surface-1")).toBe(false);
|
|
244
|
+
expect(ctx.accumulatedSurfaceState.get("surface-2")).toEqual({ y: 2 });
|
|
245
|
+
});
|
|
246
|
+
});
|
|
@@ -34,6 +34,7 @@ function makeContext(
|
|
|
34
34
|
{ surfaceType: SurfaceType; data: SurfaceData; title?: string }
|
|
35
35
|
>(),
|
|
36
36
|
surfaceUndoStacks: new Map<string, string[]>(),
|
|
37
|
+
accumulatedSurfaceState: new Map<string, Record<string, unknown>>(),
|
|
37
38
|
surfaceActionRequestIds: new Set<string>(),
|
|
38
39
|
currentTurnSurfaces: [],
|
|
39
40
|
isProcessing: () => false,
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
const mockRunBtwSidechain = mock(async () => ({
|
|
4
|
+
text: "Project kickoff",
|
|
5
|
+
hadTextDeltas: true,
|
|
6
|
+
response: {
|
|
7
|
+
content: [{ type: "text", text: "Project kickoff" }],
|
|
8
|
+
model: "test-model",
|
|
9
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
10
|
+
stopReason: "end_turn",
|
|
11
|
+
},
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
const mockGetConversation = mock(
|
|
15
|
+
(_conversationId: string) =>
|
|
16
|
+
({
|
|
17
|
+
title: "Generating title...",
|
|
18
|
+
isAutoTitle: 1,
|
|
19
|
+
}) as {
|
|
20
|
+
title: string;
|
|
21
|
+
isAutoTitle: number;
|
|
22
|
+
},
|
|
23
|
+
);
|
|
24
|
+
const mockGetMessages = mock(() => [
|
|
25
|
+
{ role: "user", content: "first message" },
|
|
26
|
+
{ role: "assistant", content: "first reply" },
|
|
27
|
+
{ role: "user", content: "follow-up" },
|
|
28
|
+
]);
|
|
29
|
+
const mockUpdateConversationTitle = mock(() => {});
|
|
30
|
+
const mockGetConfiguredProvider = mock(async () => null);
|
|
31
|
+
const mockGetConfig = mock(() => ({
|
|
32
|
+
daemon: {
|
|
33
|
+
titleGenerationMaxTokens: 37,
|
|
34
|
+
},
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
mock.module("../runtime/btw-sidechain.js", () => ({
|
|
38
|
+
runBtwSidechain: mockRunBtwSidechain,
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
mock.module("../memory/conversation-crud.js", () => ({
|
|
42
|
+
getConversation: mockGetConversation,
|
|
43
|
+
getMessages: mockGetMessages,
|
|
44
|
+
updateConversationTitle: mockUpdateConversationTitle,
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
mock.module("../providers/provider-send-message.js", () => ({
|
|
48
|
+
getConfiguredProvider: mockGetConfiguredProvider,
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
mock.module("../config/loader.js", () => ({
|
|
52
|
+
getConfig: mockGetConfig,
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
mock.module("../util/logger.js", () => ({
|
|
56
|
+
getLogger: () =>
|
|
57
|
+
new Proxy({} as Record<string, unknown>, {
|
|
58
|
+
get: () => () => {},
|
|
59
|
+
}),
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
import {
|
|
63
|
+
generateAndPersistConversationTitle,
|
|
64
|
+
regenerateConversationTitle,
|
|
65
|
+
} from "../memory/conversation-title-service.js";
|
|
66
|
+
|
|
67
|
+
describe("conversation-title-service", () => {
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
mockRunBtwSidechain.mockClear();
|
|
70
|
+
mockGetConversation.mockClear();
|
|
71
|
+
mockGetMessages.mockClear();
|
|
72
|
+
mockUpdateConversationTitle.mockClear();
|
|
73
|
+
mockGetConfiguredProvider.mockClear();
|
|
74
|
+
mockGetConfig.mockClear();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("uses the BTW side-chain helper for initial title generation", async () => {
|
|
78
|
+
const provider = {
|
|
79
|
+
name: "test-provider",
|
|
80
|
+
sendMessage: mock(async () => {
|
|
81
|
+
throw new Error("provider.sendMessage should not be called directly");
|
|
82
|
+
}),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const result = await generateAndPersistConversationTitle({
|
|
86
|
+
conversationId: "conv-1",
|
|
87
|
+
provider,
|
|
88
|
+
userMessage: "Help me plan the kickoff",
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(result).toEqual({ title: "Project kickoff", updated: true });
|
|
92
|
+
expect(mockRunBtwSidechain).toHaveBeenCalledTimes(1);
|
|
93
|
+
expect(mockRunBtwSidechain).toHaveBeenCalledWith(
|
|
94
|
+
expect.objectContaining({
|
|
95
|
+
provider,
|
|
96
|
+
maxTokens: 37,
|
|
97
|
+
modelIntent: "latency-optimized",
|
|
98
|
+
timeoutMs: 10_000,
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
expect(mockUpdateConversationTitle).toHaveBeenCalledWith(
|
|
102
|
+
"conv-1",
|
|
103
|
+
"Project kickoff",
|
|
104
|
+
1,
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("uses the BTW side-chain helper for title regeneration", async () => {
|
|
109
|
+
const provider = {
|
|
110
|
+
name: "test-provider",
|
|
111
|
+
sendMessage: mock(async () => {
|
|
112
|
+
throw new Error("provider.sendMessage should not be called directly");
|
|
113
|
+
}),
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const result = await regenerateConversationTitle({
|
|
117
|
+
conversationId: "conv-1",
|
|
118
|
+
provider,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(result).toEqual({ title: "Project kickoff", updated: true });
|
|
122
|
+
expect(mockRunBtwSidechain).toHaveBeenCalledTimes(1);
|
|
123
|
+
expect(mockRunBtwSidechain).toHaveBeenCalledWith(
|
|
124
|
+
expect.objectContaining({
|
|
125
|
+
provider,
|
|
126
|
+
maxTokens: 37,
|
|
127
|
+
modelIntent: "latency-optimized",
|
|
128
|
+
timeoutMs: 10_000,
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
expect(mockUpdateConversationTitle).toHaveBeenCalledWith(
|
|
132
|
+
"conv-1",
|
|
133
|
+
"Project kickoff",
|
|
134
|
+
1,
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
});
|