@vellumai/assistant 0.5.0 → 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__/assistant-feature-flags-integration.test.ts +7 -9
- 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-execution-feature-gates.test.ts +3 -3
- 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__/filesystem-tools.test.ts +4 -2
- 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 +103 -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__/skill-feature-flags-integration.test.ts +18 -17
- package/src/__tests__/skill-feature-flags.test.ts +13 -13
- package/src/__tests__/skill-load-feature-flag.test.ts +4 -4
- 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__/system-prompt.test.ts +8 -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 +16 -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 +46 -42
- 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/history-repair.ts +28 -8
- 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 +62 -19
- package/src/prompts/system-prompt.ts +2 -0
- 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 +4 -21
- 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
|
@@ -491,12 +491,17 @@ describe("repairHistory", () => {
|
|
|
491
491
|
expect(assistantMsg.content[1]).toMatchObject({
|
|
492
492
|
type: "web_search_tool_result",
|
|
493
493
|
tool_use_id: "stu_1",
|
|
494
|
-
content: {
|
|
494
|
+
content: {
|
|
495
|
+
type: "web_search_tool_result_error",
|
|
496
|
+
error_code: "unavailable",
|
|
497
|
+
},
|
|
495
498
|
});
|
|
496
499
|
|
|
497
500
|
// User message has no web_search_tool_result
|
|
498
501
|
const userMsg = repaired[2];
|
|
499
|
-
expect(
|
|
502
|
+
expect(
|
|
503
|
+
userMsg.content.every((b) => b.type !== "web_search_tool_result"),
|
|
504
|
+
).toBe(true);
|
|
500
505
|
});
|
|
501
506
|
|
|
502
507
|
test("migrates legacy web_search_tool_result from user message to assistant message", () => {
|
|
@@ -527,7 +532,9 @@ describe("repairHistory", () => {
|
|
|
527
532
|
{
|
|
528
533
|
type: "web_search_tool_result",
|
|
529
534
|
tool_use_id: "srvtoolu_abc",
|
|
530
|
-
content: [
|
|
535
|
+
content: [
|
|
536
|
+
{ type: "web_search_result", url: "https://example.com" },
|
|
537
|
+
],
|
|
531
538
|
},
|
|
532
539
|
{ type: "tool_result", tool_use_id: "tu_1", content: "files" },
|
|
533
540
|
],
|
|
@@ -545,8 +552,12 @@ describe("repairHistory", () => {
|
|
|
545
552
|
|
|
546
553
|
// The assistant message now has the server pair + client tool_use
|
|
547
554
|
const assistantMsg = repaired[1];
|
|
548
|
-
const serverToolUse = assistantMsg.content.find(
|
|
549
|
-
|
|
555
|
+
const serverToolUse = assistantMsg.content.find(
|
|
556
|
+
(b) => b.type === "server_tool_use",
|
|
557
|
+
);
|
|
558
|
+
const webSearchResult = assistantMsg.content.find(
|
|
559
|
+
(b) => b.type === "web_search_tool_result",
|
|
560
|
+
);
|
|
550
561
|
expect(serverToolUse).toBeDefined();
|
|
551
562
|
expect(webSearchResult).toBeDefined();
|
|
552
563
|
|
|
@@ -554,7 +565,9 @@ describe("repairHistory", () => {
|
|
|
554
565
|
const userMsg = repaired[2];
|
|
555
566
|
expect(stats.orphanToolResultsDowngraded).toBe(1);
|
|
556
567
|
expect(userMsg.content.some((b) => b.type === "tool_result")).toBe(true);
|
|
557
|
-
expect(
|
|
568
|
+
expect(
|
|
569
|
+
userMsg.content.every((b) => b.type !== "web_search_tool_result"),
|
|
570
|
+
).toBe(true);
|
|
558
571
|
});
|
|
559
572
|
|
|
560
573
|
test("trailing server_tool_use gets synthetic result in same assistant message", () => {
|
|
@@ -584,10 +597,84 @@ describe("repairHistory", () => {
|
|
|
584
597
|
expect(repaired[1].content[1]).toMatchObject({
|
|
585
598
|
type: "web_search_tool_result",
|
|
586
599
|
tool_use_id: "stu_1",
|
|
587
|
-
content: {
|
|
600
|
+
content: {
|
|
601
|
+
type: "web_search_tool_result_error",
|
|
602
|
+
error_code: "unavailable",
|
|
603
|
+
},
|
|
588
604
|
});
|
|
589
605
|
});
|
|
590
606
|
|
|
607
|
+
test("synthetic web_search_tool_result is placed immediately after its server_tool_use, not at end", () => {
|
|
608
|
+
// Regression: synthetic results appended to the end of the content array
|
|
609
|
+
// get separated from their server_tool_use by ensureToolPairing's split
|
|
610
|
+
// at tool_use boundaries, causing the API to reject with "web_search
|
|
611
|
+
// tool use without a corresponding web_search_tool_result block".
|
|
612
|
+
const messages: Message[] = [
|
|
613
|
+
{ role: "user", content: [{ type: "text", text: "Search and act" }] },
|
|
614
|
+
{
|
|
615
|
+
role: "assistant",
|
|
616
|
+
content: [
|
|
617
|
+
{ type: "text", text: "Let me search" },
|
|
618
|
+
{
|
|
619
|
+
type: "server_tool_use",
|
|
620
|
+
id: "stu_1",
|
|
621
|
+
name: "web_search",
|
|
622
|
+
input: { query: "openai" },
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
type: "server_tool_use",
|
|
626
|
+
id: "stu_2",
|
|
627
|
+
name: "web_search",
|
|
628
|
+
input: { query: "anthropic" },
|
|
629
|
+
},
|
|
630
|
+
{ type: "text", text: "Based on my research" },
|
|
631
|
+
{
|
|
632
|
+
type: "tool_use",
|
|
633
|
+
id: "tu_1",
|
|
634
|
+
name: "skill_load",
|
|
635
|
+
input: { skill: "app-builder" },
|
|
636
|
+
},
|
|
637
|
+
],
|
|
638
|
+
},
|
|
639
|
+
{
|
|
640
|
+
role: "user",
|
|
641
|
+
content: [
|
|
642
|
+
{
|
|
643
|
+
type: "tool_result",
|
|
644
|
+
tool_use_id: "tu_1",
|
|
645
|
+
content: "Skill loaded",
|
|
646
|
+
},
|
|
647
|
+
],
|
|
648
|
+
},
|
|
649
|
+
];
|
|
650
|
+
|
|
651
|
+
const { messages: repaired, stats } = repairHistory(messages);
|
|
652
|
+
|
|
653
|
+
expect(stats.missingToolResultsInserted).toBe(2);
|
|
654
|
+
|
|
655
|
+
const assistantMsg = repaired[1];
|
|
656
|
+
// Synthetic results must appear immediately after their server_tool_use,
|
|
657
|
+
// NOT after the tool_use block at the end
|
|
658
|
+
const blockTypes = assistantMsg.content.map((b) => b.type);
|
|
659
|
+
expect(blockTypes).toEqual([
|
|
660
|
+
"text",
|
|
661
|
+
"server_tool_use",
|
|
662
|
+
"web_search_tool_result", // right after stu_1
|
|
663
|
+
"server_tool_use",
|
|
664
|
+
"web_search_tool_result", // right after stu_2
|
|
665
|
+
"text",
|
|
666
|
+
"tool_use",
|
|
667
|
+
]);
|
|
668
|
+
|
|
669
|
+
// Verify the pairings are correct
|
|
670
|
+
expect(
|
|
671
|
+
(assistantMsg.content[2] as { tool_use_id: string }).tool_use_id,
|
|
672
|
+
).toBe("stu_1");
|
|
673
|
+
expect(
|
|
674
|
+
(assistantMsg.content[4] as { tool_use_id: string }).tool_use_id,
|
|
675
|
+
).toBe("stu_2");
|
|
676
|
+
});
|
|
677
|
+
|
|
591
678
|
test("downgrades type-mismatched tool_result for server_tool_use", () => {
|
|
592
679
|
// A tool_result in the user message for a server_tool_use ID is orphaned —
|
|
593
680
|
// server-side results belong in the assistant message
|
|
@@ -625,11 +712,15 @@ describe("repairHistory", () => {
|
|
|
625
712
|
|
|
626
713
|
// Assistant message has the server pair
|
|
627
714
|
const assistantMsg = repaired[1];
|
|
628
|
-
expect(
|
|
715
|
+
expect(
|
|
716
|
+
assistantMsg.content.some((b) => b.type === "web_search_tool_result"),
|
|
717
|
+
).toBe(true);
|
|
629
718
|
|
|
630
719
|
// User message has no web_search_tool_result — the tool_result was downgraded to text
|
|
631
720
|
const userMsg = repaired[2];
|
|
632
|
-
expect(
|
|
721
|
+
expect(
|
|
722
|
+
userMsg.content.every((b) => b.type !== "web_search_tool_result"),
|
|
723
|
+
).toBe(true);
|
|
633
724
|
expect(userMsg.content.every((b) => b.type !== "tool_result")).toBe(true);
|
|
634
725
|
});
|
|
635
726
|
|
|
@@ -649,7 +740,9 @@ describe("repairHistory", () => {
|
|
|
649
740
|
{
|
|
650
741
|
type: "web_search_tool_result",
|
|
651
742
|
tool_use_id: "tu_1",
|
|
652
|
-
content: [
|
|
743
|
+
content: [
|
|
744
|
+
{ type: "web_search_result", url: "https://example.com" },
|
|
745
|
+
],
|
|
653
746
|
},
|
|
654
747
|
],
|
|
655
748
|
},
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { mkdtempSync, realpathSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
5
|
+
|
|
6
|
+
const testDir = realpathSync(
|
|
7
|
+
mkdtempSync(join(tmpdir(), "http-conversation-lineage-test-")),
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
mock.module("../util/platform.js", () => ({
|
|
11
|
+
getRootDir: () => join(testDir, ".vellum"),
|
|
12
|
+
getDataDir: () => join(testDir, ".vellum", "workspace", "data"),
|
|
13
|
+
getWorkspaceDir: () => join(testDir, ".vellum", "workspace"),
|
|
14
|
+
getConversationsDir: () =>
|
|
15
|
+
join(testDir, ".vellum", "workspace", "conversations"),
|
|
16
|
+
isMacOS: () => process.platform === "darwin",
|
|
17
|
+
isLinux: () => process.platform === "linux",
|
|
18
|
+
isWindows: () => process.platform === "win32",
|
|
19
|
+
getPidPath: () => join(testDir, "test.pid"),
|
|
20
|
+
getDbPath: () => join(testDir, "test.db"),
|
|
21
|
+
getLogPath: () => join(testDir, "test.log"),
|
|
22
|
+
ensureDataDir: () => {},
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
mock.module("../util/logger.js", () => ({
|
|
26
|
+
getLogger: () =>
|
|
27
|
+
new Proxy({} as Record<string, unknown>, {
|
|
28
|
+
get: () => () => {},
|
|
29
|
+
}),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
mock.module("../config/env.js", () => ({
|
|
33
|
+
isHttpAuthDisabled: () => true,
|
|
34
|
+
hasUngatedHttpAuthDisabled: () => false,
|
|
35
|
+
getGatewayInternalBaseUrl: () => "http://127.0.0.1:7830",
|
|
36
|
+
getGatewayPort: () => 7830,
|
|
37
|
+
getRuntimeHttpPort: () => 7821,
|
|
38
|
+
getRuntimeHttpHost: () => "127.0.0.1",
|
|
39
|
+
getRuntimeGatewayOriginSecret: () => undefined,
|
|
40
|
+
getIngressPublicBaseUrl: () => undefined,
|
|
41
|
+
setIngressPublicBaseUrl: () => {},
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
mock.module("../config/loader.js", () => ({
|
|
45
|
+
getConfig: () => ({
|
|
46
|
+
ui: {},
|
|
47
|
+
model: "test",
|
|
48
|
+
provider: "test",
|
|
49
|
+
memory: { enabled: false },
|
|
50
|
+
rateLimit: { maxRequestsPerMinute: 0 },
|
|
51
|
+
secretDetection: { enabled: false },
|
|
52
|
+
}),
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
import {
|
|
56
|
+
batchSetDisplayOrders,
|
|
57
|
+
createConversation,
|
|
58
|
+
updateConversationTitle,
|
|
59
|
+
} from "../memory/conversation-crud.js";
|
|
60
|
+
import { getDb, initializeDb, rawRun, resetDb } from "../memory/db.js";
|
|
61
|
+
import { RuntimeHttpServer } from "../runtime/http-server.js";
|
|
62
|
+
|
|
63
|
+
initializeDb();
|
|
64
|
+
|
|
65
|
+
type ConversationSummary = {
|
|
66
|
+
id: string;
|
|
67
|
+
title: string;
|
|
68
|
+
displayOrder?: number | null;
|
|
69
|
+
isPinned?: boolean;
|
|
70
|
+
forkParent?: {
|
|
71
|
+
conversationId: string;
|
|
72
|
+
messageId: string;
|
|
73
|
+
title: string;
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
describe("conversation lineage in HTTP reads", () => {
|
|
78
|
+
let server: RuntimeHttpServer | null = null;
|
|
79
|
+
|
|
80
|
+
beforeEach(async () => {
|
|
81
|
+
await server?.stop();
|
|
82
|
+
server = null;
|
|
83
|
+
clearTables();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
afterAll(async () => {
|
|
87
|
+
await server?.stop();
|
|
88
|
+
resetDb();
|
|
89
|
+
try {
|
|
90
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
91
|
+
} catch {
|
|
92
|
+
/* best effort */
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("GET /v1/conversations returns forkParent for surviving parents", async () => {
|
|
97
|
+
const { child, parent } = seedForkedConversation();
|
|
98
|
+
await startServer();
|
|
99
|
+
|
|
100
|
+
const response = await fetch(url("/conversations"));
|
|
101
|
+
expect(response.status).toBe(200);
|
|
102
|
+
|
|
103
|
+
const body = (await response.json()) as {
|
|
104
|
+
conversations: ConversationSummary[];
|
|
105
|
+
hasMore: boolean;
|
|
106
|
+
};
|
|
107
|
+
const listedChild = body.conversations.find((item) => item.id === child.id);
|
|
108
|
+
|
|
109
|
+
expect(listedChild).toMatchObject({
|
|
110
|
+
id: child.id,
|
|
111
|
+
title: child.title ?? "Untitled",
|
|
112
|
+
forkParent: {
|
|
113
|
+
conversationId: parent.id,
|
|
114
|
+
messageId: "parent-msg-1",
|
|
115
|
+
title: parent.title ?? "Untitled",
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
expect(body.hasMore).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("GET /v1/conversations/:id returns forkParent for surviving parents", async () => {
|
|
122
|
+
const { child, parent } = seedForkedConversation();
|
|
123
|
+
await startServer();
|
|
124
|
+
|
|
125
|
+
const response = await fetch(url(`/conversations/${child.id}`));
|
|
126
|
+
expect(response.status).toBe(200);
|
|
127
|
+
|
|
128
|
+
const body = (await response.json()) as {
|
|
129
|
+
conversation: ConversationSummary;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
expect(body.conversation).toMatchObject({
|
|
133
|
+
id: child.id,
|
|
134
|
+
title: child.title ?? "Untitled",
|
|
135
|
+
forkParent: {
|
|
136
|
+
conversationId: parent.id,
|
|
137
|
+
messageId: "parent-msg-1",
|
|
138
|
+
title: parent.title ?? "Untitled",
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("GET /v1/conversations/:id includes pin metadata when present", async () => {
|
|
144
|
+
const conversation = createConversation("Pinned conversation");
|
|
145
|
+
batchSetDisplayOrders([
|
|
146
|
+
{ id: conversation.id, displayOrder: 7, isPinned: true },
|
|
147
|
+
]);
|
|
148
|
+
await startServer();
|
|
149
|
+
|
|
150
|
+
const response = await fetch(url(`/conversations/${conversation.id}`));
|
|
151
|
+
expect(response.status).toBe(200);
|
|
152
|
+
|
|
153
|
+
const body = (await response.json()) as {
|
|
154
|
+
conversation: ConversationSummary;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
expect(body.conversation).toMatchObject({
|
|
158
|
+
id: conversation.id,
|
|
159
|
+
title: conversation.title ?? "Untitled",
|
|
160
|
+
displayOrder: 7,
|
|
161
|
+
isPinned: true,
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("GET /v1/conversations/:id resolves the parent's current title at read time", async () => {
|
|
166
|
+
const { child, parent } = seedForkedConversation({
|
|
167
|
+
parentTitle: "Original parent title",
|
|
168
|
+
});
|
|
169
|
+
updateConversationTitle(parent.id, "Renamed parent title", 0);
|
|
170
|
+
await startServer();
|
|
171
|
+
|
|
172
|
+
const response = await fetch(url(`/conversations/${child.id}`));
|
|
173
|
+
expect(response.status).toBe(200);
|
|
174
|
+
|
|
175
|
+
const body = (await response.json()) as {
|
|
176
|
+
conversation: ConversationSummary;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
expect(body.conversation.forkParent).toEqual({
|
|
180
|
+
conversationId: parent.id,
|
|
181
|
+
messageId: "parent-msg-1",
|
|
182
|
+
title: "Renamed parent title",
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("deleted parents are omitted from list and detail responses", async () => {
|
|
187
|
+
const { child, parent } = seedForkedConversation();
|
|
188
|
+
rawRun("DELETE FROM conversations WHERE id = ?", parent.id);
|
|
189
|
+
await startServer();
|
|
190
|
+
|
|
191
|
+
const listResponse = await fetch(url("/conversations"));
|
|
192
|
+
expect(listResponse.status).toBe(200);
|
|
193
|
+
const listBody = (await listResponse.json()) as {
|
|
194
|
+
conversations: ConversationSummary[];
|
|
195
|
+
};
|
|
196
|
+
const listedChild = listBody.conversations.find(
|
|
197
|
+
(item) => item.id === child.id,
|
|
198
|
+
);
|
|
199
|
+
expect(listedChild).toBeDefined();
|
|
200
|
+
expect(listedChild?.forkParent).toBeUndefined();
|
|
201
|
+
|
|
202
|
+
const detailResponse = await fetch(url(`/conversations/${child.id}`));
|
|
203
|
+
expect(detailResponse.status).toBe(200);
|
|
204
|
+
const detailBody = (await detailResponse.json()) as {
|
|
205
|
+
conversation: ConversationSummary;
|
|
206
|
+
};
|
|
207
|
+
expect(detailBody.conversation.forkParent).toBeUndefined();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
function clearTables(): void {
|
|
211
|
+
const db = getDb();
|
|
212
|
+
db.run("DELETE FROM conversation_assistant_attention_state");
|
|
213
|
+
db.run("DELETE FROM external_conversation_bindings");
|
|
214
|
+
db.run("DELETE FROM conversation_keys");
|
|
215
|
+
db.run("DELETE FROM messages");
|
|
216
|
+
db.run("DELETE FROM conversations");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function seedForkedConversation(opts?: { parentTitle?: string }) {
|
|
220
|
+
const parent = createConversation(
|
|
221
|
+
opts?.parentTitle ?? "Parent conversation",
|
|
222
|
+
);
|
|
223
|
+
const child = createConversation("Forked conversation");
|
|
224
|
+
|
|
225
|
+
rawRun(
|
|
226
|
+
`
|
|
227
|
+
UPDATE conversations
|
|
228
|
+
SET fork_parent_conversation_id = ?, fork_parent_message_id = ?
|
|
229
|
+
WHERE id = ?
|
|
230
|
+
`,
|
|
231
|
+
parent.id,
|
|
232
|
+
"parent-msg-1",
|
|
233
|
+
child.id,
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
return { parent, child };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function startServer(): Promise<void> {
|
|
240
|
+
server = new RuntimeHttpServer({
|
|
241
|
+
port: 0,
|
|
242
|
+
bearerToken: "test-bearer-token",
|
|
243
|
+
});
|
|
244
|
+
await server.start();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function url(pathname: string): string {
|
|
248
|
+
if (!server) throw new Error("server not started");
|
|
249
|
+
return `http://127.0.0.1:${server.actualPort}/v1${pathname}`;
|
|
250
|
+
}
|
|
251
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { reinjectImageSourcePaths } from "../daemon/conversation-lifecycle.js";
|
|
4
|
+
import type { ContentBlock } from "../providers/types.js";
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// reinjectImageSourcePaths — re-inject [Attached image source: /path]
|
|
8
|
+
// annotations when loading conversation history from DB
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
describe("reinjectImageSourcePaths", () => {
|
|
12
|
+
const baseContent: ContentBlock[] = [
|
|
13
|
+
{ type: "text", text: "what is this?" },
|
|
14
|
+
{
|
|
15
|
+
type: "image",
|
|
16
|
+
source: { type: "base64", media_type: "image/jpeg", data: "base64img" },
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
test("adds annotation when user message has imageSourcePaths in metadata", () => {
|
|
21
|
+
const metadata = JSON.stringify({
|
|
22
|
+
imageSourcePaths: { "photo.jpg": "/Users/me/Desktop/photo.jpg" },
|
|
23
|
+
});
|
|
24
|
+
const result = reinjectImageSourcePaths(baseContent, "user", metadata);
|
|
25
|
+
|
|
26
|
+
expect(result).toHaveLength(3);
|
|
27
|
+
const annotation = result[2] as { type: "text"; text: string };
|
|
28
|
+
expect(annotation.type).toBe("text");
|
|
29
|
+
expect(annotation.text).toBe(
|
|
30
|
+
"[Attached image source: /Users/me/Desktop/photo.jpg]",
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("does NOT annotate assistant messages even if metadata has imageSourcePaths", () => {
|
|
35
|
+
const metadata = JSON.stringify({
|
|
36
|
+
imageSourcePaths: { "photo.jpg": "/Users/me/Desktop/photo.jpg" },
|
|
37
|
+
});
|
|
38
|
+
const result = reinjectImageSourcePaths(baseContent, "assistant", metadata);
|
|
39
|
+
|
|
40
|
+
// Should return the original content unchanged
|
|
41
|
+
expect(result).toBe(baseContent);
|
|
42
|
+
expect(result).toHaveLength(2);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("returns content unchanged when metadata is null", () => {
|
|
46
|
+
const result = reinjectImageSourcePaths(baseContent, "user", null);
|
|
47
|
+
expect(result).toBe(baseContent);
|
|
48
|
+
expect(result).toHaveLength(2);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("returns content unchanged when metadata has no imageSourcePaths", () => {
|
|
52
|
+
const metadata = JSON.stringify({
|
|
53
|
+
userMessageChannel: "desktop",
|
|
54
|
+
});
|
|
55
|
+
const result = reinjectImageSourcePaths(baseContent, "user", metadata);
|
|
56
|
+
expect(result).toBe(baseContent);
|
|
57
|
+
expect(result).toHaveLength(2);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("returns content unchanged when imageSourcePaths is empty object", () => {
|
|
61
|
+
const metadata = JSON.stringify({
|
|
62
|
+
imageSourcePaths: {},
|
|
63
|
+
});
|
|
64
|
+
const result = reinjectImageSourcePaths(baseContent, "user", metadata);
|
|
65
|
+
expect(result).toBe(baseContent);
|
|
66
|
+
expect(result).toHaveLength(2);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("handles multiple image source paths", () => {
|
|
70
|
+
const metadata = JSON.stringify({
|
|
71
|
+
imageSourcePaths: {
|
|
72
|
+
"a.jpg": "/path/to/a.jpg",
|
|
73
|
+
"b.png": "/path/to/b.png",
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
const result = reinjectImageSourcePaths(baseContent, "user", metadata);
|
|
77
|
+
|
|
78
|
+
expect(result).toHaveLength(3);
|
|
79
|
+
const annotation = result[2] as { type: "text"; text: string };
|
|
80
|
+
expect(annotation.type).toBe("text");
|
|
81
|
+
expect(annotation.text).toBe(
|
|
82
|
+
"[Attached image source: /path/to/a.jpg]\n[Attached image source: /path/to/b.png]",
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("gracefully handles malformed metadata JSON", () => {
|
|
87
|
+
const result = reinjectImageSourcePaths(
|
|
88
|
+
baseContent,
|
|
89
|
+
"user",
|
|
90
|
+
"not-valid-json{{{",
|
|
91
|
+
);
|
|
92
|
+
// Should return original content, not throw
|
|
93
|
+
expect(result).toBe(baseContent);
|
|
94
|
+
expect(result).toHaveLength(2);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("filters out non-string values in imageSourcePaths", () => {
|
|
98
|
+
const metadata = JSON.stringify({
|
|
99
|
+
imageSourcePaths: {
|
|
100
|
+
"photo.jpg": "/Users/me/Desktop/photo.jpg",
|
|
101
|
+
"bad.jpg": 42,
|
|
102
|
+
"also_bad.jpg": null,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
const result = reinjectImageSourcePaths(baseContent, "user", metadata);
|
|
106
|
+
|
|
107
|
+
expect(result).toHaveLength(3);
|
|
108
|
+
const annotation = result[2] as { type: "text"; text: string };
|
|
109
|
+
expect(annotation.text).toBe(
|
|
110
|
+
"[Attached image source: /Users/me/Desktop/photo.jpg]",
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("returns content unchanged when imageSourcePaths has only non-string values", () => {
|
|
115
|
+
const metadata = JSON.stringify({
|
|
116
|
+
imageSourcePaths: {
|
|
117
|
+
"bad.jpg": 42,
|
|
118
|
+
"also_bad.jpg": null,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
const result = reinjectImageSourcePaths(baseContent, "user", metadata);
|
|
122
|
+
expect(result).toBe(baseContent);
|
|
123
|
+
expect(result).toHaveLength(2);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("preserves original content blocks in returned array", () => {
|
|
127
|
+
const metadata = JSON.stringify({
|
|
128
|
+
imageSourcePaths: { "photo.jpg": "/path/photo.jpg" },
|
|
129
|
+
});
|
|
130
|
+
const result = reinjectImageSourcePaths(baseContent, "user", metadata);
|
|
131
|
+
|
|
132
|
+
// First two blocks should be identical to the originals
|
|
133
|
+
expect(result[0]).toEqual(baseContent[0]);
|
|
134
|
+
expect(result[1]).toEqual(baseContent[1]);
|
|
135
|
+
});
|
|
136
|
+
});
|