@vellumai/assistant 0.4.16 → 0.4.18
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/Dockerfile +6 -6
- package/README.md +1 -2
- package/eslint.config.mjs +2 -2
- package/package.json +1 -1
- package/src/__tests__/access-request-decision.test.ts +128 -120
- package/src/__tests__/account-registry.test.ts +121 -110
- package/src/__tests__/active-skill-tools.test.ts +200 -172
- package/src/__tests__/actor-token-service.test.ts +341 -274
- package/src/__tests__/agent-loop-thinking.test.ts +28 -19
- package/src/__tests__/agent-loop.test.ts +798 -378
- package/src/__tests__/anthropic-provider.test.ts +405 -247
- package/src/__tests__/app-builder-tool-scripts.test.ts +97 -97
- package/src/__tests__/app-bundler.test.ts +112 -79
- package/src/__tests__/app-executors.test.ts +205 -178
- package/src/__tests__/app-git-history.test.ts +90 -73
- package/src/__tests__/app-git-service.test.ts +67 -53
- package/src/__tests__/app-open-proxy.test.ts +29 -25
- package/src/__tests__/approval-conversation-turn.test.ts +100 -81
- package/src/__tests__/approval-hardcoded-copy-guard.test.ts +45 -17
- package/src/__tests__/approval-message-composer.test.ts +119 -119
- package/src/__tests__/approval-primitive.test.ts +264 -233
- package/src/__tests__/approval-routes-http.test.ts +4 -3
- package/src/__tests__/asset-materialize-tool.test.ts +250 -178
- package/src/__tests__/asset-search-tool.test.ts +251 -191
- package/src/__tests__/assistant-attachment-directive.test.ts +187 -142
- package/src/__tests__/assistant-attachments.test.ts +254 -186
- package/src/__tests__/assistant-event-hub.test.ts +105 -63
- package/src/__tests__/assistant-event.test.ts +66 -58
- package/src/__tests__/assistant-events-sse-hardening.test.ts +113 -73
- package/src/__tests__/assistant-feature-flag-guard.test.ts +78 -52
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +48 -45
- package/src/__tests__/assistant-feature-flags-integration.test.ts +118 -77
- package/src/__tests__/assistant-id-boundary-guard.test.ts +158 -104
- package/src/__tests__/attachments-store.test.ts +240 -183
- package/src/__tests__/attachments.test.ts +70 -62
- package/src/__tests__/audit-log-rotation.test.ts +50 -35
- package/src/__tests__/browser-fill-credential.test.ts +169 -101
- package/src/__tests__/browser-manager.test.ts +97 -75
- package/src/__tests__/browser-runtime-check.test.ts +16 -15
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +12 -10
- package/src/__tests__/browser-skill-endstate.test.ts +97 -72
- package/src/__tests__/bundle-scanner.test.ts +47 -22
- package/src/__tests__/bundled-asset.test.ts +74 -47
- package/src/__tests__/call-constants.test.ts +19 -19
- package/src/__tests__/call-controller.test.ts +1073 -751
- package/src/__tests__/call-conversation-messages.test.ts +90 -65
- package/src/__tests__/call-domain.test.ts +149 -121
- package/src/__tests__/call-pointer-message-composer.test.ts +113 -83
- package/src/__tests__/call-pointer-messages.test.ts +213 -154
- package/src/__tests__/call-pointer-no-hardcoded-copy.guard.test.ts +9 -10
- package/src/__tests__/call-recovery.test.ts +232 -212
- package/src/__tests__/call-routes-http.test.ts +328 -279
- package/src/__tests__/call-start-guardian-guard.test.ts +32 -30
- package/src/__tests__/call-state-machine.test.ts +62 -51
- package/src/__tests__/call-state.test.ts +89 -75
- package/src/__tests__/call-store.test.ts +387 -316
- package/src/__tests__/callback-handoff-copy.test.ts +84 -82
- package/src/__tests__/canonical-guardian-store.test.ts +331 -280
- package/src/__tests__/channel-approval-routes.test.ts +1643 -1126
- package/src/__tests__/channel-approval.test.ts +139 -137
- package/src/__tests__/channel-approvals.test.ts +226 -182
- package/src/__tests__/channel-delivery-store.test.ts +232 -194
- package/src/__tests__/channel-guardian.test.ts +6 -3
- package/src/__tests__/channel-invite-transport.test.ts +107 -92
- package/src/__tests__/channel-policy.test.ts +42 -38
- package/src/__tests__/channel-readiness-service.test.ts +119 -102
- package/src/__tests__/channel-reply-delivery.test.ts +147 -118
- package/src/__tests__/channel-retry-sweep.test.ts +153 -110
- package/src/__tests__/checker.test.ts +3309 -1850
- package/src/__tests__/clarification-resolver.test.ts +91 -79
- package/src/__tests__/classifier.test.ts +64 -54
- package/src/__tests__/claude-code-skill-regression.test.ts +42 -37
- package/src/__tests__/claude-code-tool-profiles.test.ts +31 -29
- package/src/__tests__/clawhub.test.ts +92 -82
- package/src/__tests__/cli.test.ts +30 -30
- package/src/__tests__/clipboard.test.ts +53 -46
- package/src/__tests__/commit-guarantee.test.ts +59 -52
- package/src/__tests__/commit-message-enrichment-service.test.ts +203 -75
- package/src/__tests__/compaction.benchmark.test.ts +33 -31
- package/src/__tests__/computer-use-session-compaction.test.ts +60 -50
- package/src/__tests__/computer-use-session-lifecycle.test.ts +145 -117
- package/src/__tests__/computer-use-session-working-dir.test.ts +62 -48
- package/src/__tests__/computer-use-skill-baseline.test.ts +22 -19
- package/src/__tests__/computer-use-skill-endstate.test.ts +45 -31
- package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +121 -88
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +65 -42
- package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +33 -18
- package/src/__tests__/computer-use-tools.test.ts +121 -98
- package/src/__tests__/config-schema.test.ts +443 -347
- package/src/__tests__/config-watcher.test.ts +96 -81
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +148 -133
- package/src/__tests__/conflict-intent-tokenization.test.ts +96 -78
- package/src/__tests__/conflict-policy.test.ts +151 -80
- package/src/__tests__/conflict-store.test.ts +203 -157
- package/src/__tests__/connection-policy.test.ts +89 -59
- package/src/__tests__/contacts-tools.test.ts +247 -178
- package/src/__tests__/context-memory-e2e.test.ts +306 -214
- package/src/__tests__/context-token-estimator.test.ts +114 -74
- package/src/__tests__/context-window-manager.test.ts +269 -167
- package/src/__tests__/contradiction-checker.test.ts +161 -135
- package/src/__tests__/conversation-attention-store.test.ts +350 -290
- package/src/__tests__/conversation-attention-telegram.test.ts +156 -114
- package/src/__tests__/conversation-pairing.test.ts +220 -113
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +164 -104
- package/src/__tests__/conversation-routes.test.ts +71 -41
- package/src/__tests__/conversation-store.test.ts +390 -235
- package/src/__tests__/credential-broker-browser-fill.test.ts +325 -250
- package/src/__tests__/credential-broker-server-use.test.ts +283 -243
- package/src/__tests__/credential-broker.test.ts +128 -74
- package/src/__tests__/credential-host-pattern-match.test.ts +64 -44
- package/src/__tests__/credential-metadata-store.test.ts +360 -311
- package/src/__tests__/credential-policy-validate.test.ts +81 -65
- package/src/__tests__/credential-resolve.test.ts +212 -145
- package/src/__tests__/credential-security-e2e.test.ts +144 -103
- package/src/__tests__/credential-security-invariants.test.ts +253 -208
- package/src/__tests__/credential-selection.test.ts +254 -146
- package/src/__tests__/credential-vault-unit.test.ts +531 -341
- package/src/__tests__/credential-vault.test.ts +761 -484
- package/src/__tests__/daemon-assistant-events.test.ts +91 -66
- package/src/__tests__/daemon-lifecycle.test.ts +258 -190
- package/src/__tests__/daemon-server-session-init.test.ts +257 -191
- package/src/__tests__/date-context.test.ts +314 -249
- package/src/__tests__/db-migration-rollback.test.ts +259 -130
- package/src/__tests__/db-schedule-syntax-migration.test.ts +78 -41
- package/src/__tests__/delete-managed-skill-tool.test.ts +77 -53
- package/src/__tests__/deterministic-verification-control-plane.test.ts +183 -135
- package/src/__tests__/dictation-mode-detection.test.ts +77 -55
- package/src/__tests__/dictation-profile-store.test.ts +70 -56
- package/src/__tests__/dictation-text-processing.test.ts +53 -35
- package/src/__tests__/diff.test.ts +102 -98
- package/src/__tests__/domain-normalize.test.ts +54 -54
- package/src/__tests__/domain-policy.test.ts +71 -55
- package/src/__tests__/dynamic-page-surface.test.ts +31 -33
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +69 -69
- package/src/__tests__/edit-engine.test.ts +56 -56
- package/src/__tests__/elevenlabs-client.test.ts +117 -91
- package/src/__tests__/elevenlabs-config.test.ts +32 -31
- package/src/__tests__/email-classifier.test.ts +15 -12
- package/src/__tests__/email-cli.test.ts +121 -108
- package/src/__tests__/emit-signal-routing-intent.test.ts +76 -69
- package/src/__tests__/encrypted-store.test.ts +180 -154
- package/src/__tests__/entity-extractor.test.ts +108 -87
- package/src/__tests__/entity-search.test.ts +664 -258
- package/src/__tests__/ephemeral-permissions.test.ts +224 -188
- package/src/__tests__/event-bus.test.ts +81 -77
- package/src/__tests__/extract-email.test.ts +51 -0
- package/src/__tests__/file-edit-tool.test.ts +62 -44
- package/src/__tests__/file-ops-service.test.ts +131 -114
- package/src/__tests__/file-read-tool.test.ts +48 -31
- package/src/__tests__/file-write-tool.test.ts +43 -37
- package/src/__tests__/filesystem-tools.test.ts +238 -209
- package/src/__tests__/followup-tools.test.ts +237 -162
- package/src/__tests__/forbidden-legacy-symbols.test.ts +19 -20
- package/src/__tests__/frontmatter.test.ts +96 -81
- package/src/__tests__/fuzzy-match-property.test.ts +75 -81
- package/src/__tests__/fuzzy-match.test.ts +71 -65
- package/src/__tests__/gateway-client-managed-outbound.test.ts +76 -57
- package/src/__tests__/gateway-only-enforcement.test.ts +467 -369
- package/src/__tests__/gateway-only-guard.test.ts +54 -56
- package/src/__tests__/gemini-image-service.test.ts +113 -100
- package/src/__tests__/gemini-provider.test.ts +297 -220
- package/src/__tests__/get-weather.test.ts +188 -114
- package/src/__tests__/gmail-integration.test.ts +47 -46
- package/src/__tests__/guardian-action-conversation-turn.test.ts +226 -171
- package/src/__tests__/guardian-action-copy-generator.test.ts +111 -93
- package/src/__tests__/guardian-action-followup-executor.test.ts +215 -151
- package/src/__tests__/guardian-action-followup-store.test.ts +199 -167
- package/src/__tests__/guardian-action-grant-mint-consume.test.ts +297 -250
- package/src/__tests__/guardian-action-late-reply.test.ts +462 -316
- package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +23 -18
- package/src/__tests__/guardian-action-store.test.ts +158 -109
- package/src/__tests__/guardian-action-sweep.test.ts +114 -100
- package/src/__tests__/guardian-actions-endpoint.test.ts +440 -256
- package/src/__tests__/guardian-control-plane-policy.test.ts +497 -331
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +217 -215
- package/src/__tests__/guardian-dispatch.test.ts +316 -256
- package/src/__tests__/guardian-grant-minting.test.ts +247 -178
- package/src/__tests__/guardian-outbound-http.test.ts +337 -209
- package/src/__tests__/guardian-principal-id-roundtrip.test.ts +99 -96
- package/src/__tests__/guardian-question-copy.test.ts +17 -17
- package/src/__tests__/guardian-question-mode.test.ts +134 -100
- package/src/__tests__/guardian-routing-invariants.test.ts +679 -613
- package/src/__tests__/guardian-routing-state.test.ts +256 -209
- package/src/__tests__/guardian-verification-intent-routing.test.ts +94 -88
- package/src/__tests__/guardian-verification-voice-binding.test.ts +47 -41
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +0 -1
- package/src/__tests__/handle-user-message-secret-resume.test.ts +43 -21
- package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +92 -76
- package/src/__tests__/handlers-cu-observation-blob.test.ts +103 -70
- package/src/__tests__/handlers-ipc-blob-probe.test.ts +77 -51
- package/src/__tests__/handlers-slack-config.test.ts +63 -54
- package/src/__tests__/handlers-task-submit-slash.test.ts +18 -18
- package/src/__tests__/handlers-telegram-config.test.ts +662 -329
- package/src/__tests__/handlers-twitter-config.test.ts +525 -298
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +270 -195
- package/src/__tests__/headless-browser-interactions.test.ts +444 -280
- package/src/__tests__/headless-browser-navigate.test.ts +116 -79
- package/src/__tests__/headless-browser-read-tools.test.ts +123 -86
- package/src/__tests__/headless-browser-snapshot.test.ts +71 -56
- package/src/__tests__/heartbeat-service.test.ts +76 -58
- package/src/__tests__/history-repair-observability.test.ts +14 -14
- package/src/__tests__/history-repair.test.ts +171 -167
- package/src/__tests__/home-base-bootstrap.test.ts +30 -27
- package/src/__tests__/hooks-blocking.test.ts +86 -37
- package/src/__tests__/hooks-cli.test.ts +104 -68
- package/src/__tests__/hooks-config.test.ts +81 -43
- package/src/__tests__/hooks-discovery.test.ts +106 -96
- package/src/__tests__/hooks-integration.test.ts +78 -72
- package/src/__tests__/hooks-manager.test.ts +99 -61
- package/src/__tests__/hooks-runner.test.ts +94 -71
- package/src/__tests__/hooks-settings.test.ts +69 -64
- package/src/__tests__/hooks-templates.test.ts +85 -54
- package/src/__tests__/hooks-ts-runner.test.ts +82 -45
- package/src/__tests__/hooks-watch.test.ts +32 -22
- package/src/__tests__/host-file-edit-tool.test.ts +190 -148
- package/src/__tests__/host-file-read-tool.test.ts +86 -63
- package/src/__tests__/host-file-write-tool.test.ts +98 -64
- package/src/__tests__/host-shell-tool.test.ts +342 -233
- package/src/__tests__/inbound-invite-redemption.test.ts +194 -152
- package/src/__tests__/ingress-member-store.test.ts +163 -159
- package/src/__tests__/ingress-reconcile.test.ts +183 -142
- package/src/__tests__/ingress-routes-http.test.ts +441 -356
- package/src/__tests__/ingress-url-consistency.test.ts +125 -64
- package/src/__tests__/integration-status.test.ts +93 -73
- package/src/__tests__/intent-routing.test.ts +148 -118
- package/src/__tests__/invite-redemption-service.test.ts +163 -121
- package/src/__tests__/ipc-blob-store.test.ts +104 -91
- package/src/__tests__/ipc-contract-inventory.test.ts +27 -15
- package/src/__tests__/ipc-contract.test.ts +24 -23
- package/src/__tests__/ipc-protocol.test.ts +52 -46
- package/src/__tests__/ipc-roundtrip.benchmark.test.ts +61 -50
- package/src/__tests__/ipc-snapshot.test.ts +1135 -1056
- package/src/__tests__/ipc-validate.test.ts +240 -179
- package/src/__tests__/key-migration.test.ts +123 -90
- package/src/__tests__/keychain.test.ts +150 -123
- package/src/__tests__/lifecycle-docs-guard.test.ts +65 -64
- package/src/__tests__/llm-usage-store.test.ts +112 -87
- package/src/__tests__/managed-skill-lifecycle.test.ts +147 -108
- package/src/__tests__/managed-store.test.ts +411 -360
- package/src/__tests__/mcp-cli.test.ts +189 -123
- package/src/__tests__/mcp-health-check.test.ts +26 -21
- package/src/__tests__/media-generate-image.test.ts +122 -99
- package/src/__tests__/media-reuse-story.e2e.test.ts +282 -214
- package/src/__tests__/media-visibility-policy.test.ts +86 -38
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +146 -100
- package/src/__tests__/memory-lifecycle-e2e.test.ts +385 -297
- package/src/__tests__/memory-query-builder.test.ts +32 -33
- package/src/__tests__/memory-recall-quality.test.ts +761 -407
- package/src/__tests__/memory-regressions.experimental.test.ts +443 -380
- package/src/__tests__/memory-regressions.test.ts +3725 -2642
- package/src/__tests__/memory-retrieval-budget.test.ts +7 -8
- package/src/__tests__/memory-retrieval.benchmark.test.ts +144 -109
- package/src/__tests__/memory-upsert-concurrency.test.ts +292 -201
- package/src/__tests__/messaging-send-tool.test.ts +36 -29
- package/src/__tests__/migration-cli-flows.test.ts +69 -53
- package/src/__tests__/migration-ordering.test.ts +103 -86
- package/src/__tests__/mime-builder.test.ts +55 -32
- package/src/__tests__/mock-signup-server.test.ts +384 -246
- package/src/__tests__/model-intents.test.ts +61 -37
- package/src/__tests__/no-direct-anthropic-sdk-imports.test.ts +9 -12
- package/src/__tests__/no-is-trusted-guard.test.ts +24 -21
- package/src/__tests__/non-member-access-request.test.ts +294 -249
- package/src/__tests__/notification-broadcaster.test.ts +99 -81
- package/src/__tests__/notification-decision-fallback.test.ts +223 -178
- package/src/__tests__/notification-decision-strategy.test.ts +375 -337
- package/src/__tests__/notification-deep-link.test.ts +67 -61
- package/src/__tests__/notification-guardian-path.test.ts +248 -206
- package/src/__tests__/notification-routing-intent.test.ts +166 -93
- package/src/__tests__/notification-telegram-adapter.test.ts +60 -46
- package/src/__tests__/notification-thread-candidate-validation.test.ts +78 -75
- package/src/__tests__/notification-thread-candidates.test.ts +64 -61
- package/src/__tests__/oauth-callback-registry.test.ts +40 -30
- package/src/__tests__/oauth-connect-handler.test.ts +109 -89
- package/src/__tests__/oauth-scope-policy.test.ts +63 -55
- package/src/__tests__/oauth2-gateway-transport.test.ts +252 -174
- package/src/__tests__/onboarding-starter-tasks.test.ts +93 -89
- package/src/__tests__/onboarding-template-contract.test.ts +93 -94
- package/src/__tests__/openai-provider.test.ts +366 -274
- package/src/__tests__/pairing-concurrent.test.ts +18 -12
- package/src/__tests__/pairing-routes.test.ts +45 -41
- package/src/__tests__/parallel-tool.benchmark.test.ts +108 -58
- package/src/__tests__/parser.test.ts +316 -226
- package/src/__tests__/path-classifier.test.ts +24 -25
- package/src/__tests__/path-policy.test.ts +187 -147
- package/src/__tests__/phone.test.ts +36 -36
- package/src/__tests__/platform-move-helper.test.ts +48 -40
- package/src/__tests__/platform-socket-path.test.ts +23 -24
- package/src/__tests__/platform-workspace-migration.test.ts +464 -414
- package/src/__tests__/platform.test.ts +61 -53
- package/src/__tests__/playbook-execution.test.ts +397 -265
- package/src/__tests__/playbook-tools.test.ts +267 -196
- package/src/__tests__/prebuilt-home-base-seed.test.ts +30 -27
- package/src/__tests__/pricing.test.ts +316 -136
- package/src/__tests__/profile-compiler.test.ts +206 -188
- package/src/__tests__/provider-commit-message-generator.test.ts +114 -106
- package/src/__tests__/provider-error-scenarios.test.ts +212 -158
- package/src/__tests__/provider-fail-open-selection.test.ts +51 -44
- package/src/__tests__/provider-registry-ollama.test.ts +13 -9
- package/src/__tests__/provider-streaming.benchmark.test.ts +232 -183
- package/src/__tests__/proxy-approval-callback.test.ts +180 -119
- package/src/__tests__/public-ingress-urls.test.ts +112 -94
- package/src/__tests__/qdrant-manager.test.ts +147 -98
- package/src/__tests__/ratelimit.test.ts +152 -82
- package/src/__tests__/recording-handler.test.ts +273 -151
- package/src/__tests__/recording-intent-fallback.test.ts +94 -75
- package/src/__tests__/recording-intent-handler.test.ts +422 -292
- package/src/__tests__/recording-intent.test.ts +578 -379
- package/src/__tests__/recording-state-machine.test.ts +530 -316
- package/src/__tests__/recurrence-engine-rruleset.test.ts +150 -92
- package/src/__tests__/recurrence-engine.test.ts +81 -41
- package/src/__tests__/recurrence-types.test.ts +63 -44
- package/src/__tests__/relay-server.test.ts +2131 -1602
- package/src/__tests__/reminder-store.test.ts +158 -80
- package/src/__tests__/reminder.test.ts +113 -109
- package/src/__tests__/remote-skill-policy.test.ts +96 -72
- package/src/__tests__/request-file-tool.test.ts +74 -67
- package/src/__tests__/response-tier.test.ts +131 -74
- package/src/__tests__/runtime-attachment-metadata.test.ts +107 -70
- package/src/__tests__/runtime-events-sse-parity.test.ts +167 -145
- package/src/__tests__/runtime-events-sse.test.ts +67 -51
- package/src/__tests__/sandbox-diagnostics.test.ts +66 -56
- package/src/__tests__/sandbox-host-parity.test.ts +377 -301
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +213 -161
- package/src/__tests__/schedule-store.test.ts +268 -205
- package/src/__tests__/schedule-tools.test.ts +702 -524
- package/src/__tests__/scheduler-recurrence.test.ts +240 -130
- package/src/__tests__/scoped-approval-grants.test.ts +258 -168
- package/src/__tests__/scoped-grant-security-matrix.test.ts +160 -146
- package/src/__tests__/script-proxy-certs.test.ts +38 -35
- package/src/__tests__/script-proxy-connect-tunnel.test.ts +71 -46
- package/src/__tests__/script-proxy-decision-trace.test.ts +161 -84
- package/src/__tests__/script-proxy-http-forwarder.test.ts +146 -129
- package/src/__tests__/script-proxy-injection-runtime.test.ts +139 -113
- package/src/__tests__/script-proxy-mitm-handler.test.ts +226 -142
- package/src/__tests__/script-proxy-policy-runtime.test.ts +126 -86
- package/src/__tests__/script-proxy-policy.test.ts +308 -153
- package/src/__tests__/script-proxy-rewrite-specificity.test.ts +74 -62
- package/src/__tests__/script-proxy-router.test.ts +111 -77
- package/src/__tests__/script-proxy-session-manager.test.ts +156 -113
- package/src/__tests__/script-proxy-session-runtime.test.ts +28 -24
- package/src/__tests__/secret-allowlist.test.ts +105 -90
- package/src/__tests__/secret-ingress-handler.test.ts +41 -30
- package/src/__tests__/secret-onetime-send.test.ts +67 -50
- package/src/__tests__/secret-prompt-log-hygiene.test.ts +35 -31
- package/src/__tests__/secret-response-routing.test.ts +50 -41
- package/src/__tests__/secret-scanner-executor.test.ts +152 -111
- package/src/__tests__/secret-scanner.test.ts +495 -413
- package/src/__tests__/secure-keys.test.ts +132 -121
- package/src/__tests__/send-endpoint-busy.test.ts +313 -232
- package/src/__tests__/send-notification-tool.test.ts +43 -42
- package/src/__tests__/sensitive-output-placeholders.test.ts +72 -64
- package/src/__tests__/sequence-store.test.ts +335 -167
- package/src/__tests__/server-history-render.test.ts +341 -202
- package/src/__tests__/session-abort-tool-results.test.ts +133 -70
- package/src/__tests__/session-approval-overrides.test.ts +93 -91
- package/src/__tests__/session-confirmation-signals.test.ts +252 -160
- package/src/__tests__/session-conflict-gate.test.ts +775 -585
- package/src/__tests__/session-error.test.ts +222 -191
- package/src/__tests__/session-evictor.test.ts +79 -62
- package/src/__tests__/session-init.benchmark.test.ts +170 -108
- package/src/__tests__/session-load-history-repair.test.ts +273 -139
- package/src/__tests__/session-messaging-secret-redirect.test.ts +130 -90
- package/src/__tests__/session-pre-run-repair.test.ts +106 -59
- package/src/__tests__/session-profile-injection.test.ts +198 -130
- package/src/__tests__/session-provider-retry-repair.test.ts +223 -141
- package/src/__tests__/session-queue.test.ts +624 -321
- package/src/__tests__/session-runtime-assembly.test.ts +425 -329
- package/src/__tests__/session-runtime-workspace.test.ts +69 -61
- package/src/__tests__/session-skill-tools.test.ts +973 -678
- package/src/__tests__/session-slash-known.test.ts +185 -133
- package/src/__tests__/session-slash-queue.test.ts +147 -81
- package/src/__tests__/session-slash-unknown.test.ts +135 -90
- package/src/__tests__/session-surfaces-task-progress.test.ts +122 -87
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +338 -177
- package/src/__tests__/session-tool-setup-memory-scope.test.ts +63 -40
- package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +60 -37
- package/src/__tests__/session-tool-setup-tools-disabled.test.ts +28 -26
- package/src/__tests__/session-undo.test.ts +43 -30
- package/src/__tests__/session-workspace-cache-state.test.ts +108 -67
- package/src/__tests__/session-workspace-injection.test.ts +245 -117
- package/src/__tests__/session-workspace-tool-tracking.test.ts +260 -93
- package/src/__tests__/shared-filesystem-errors.test.ts +47 -47
- package/src/__tests__/shell-credential-ref.test.ts +126 -90
- package/src/__tests__/shell-identity.test.ts +134 -111
- package/src/__tests__/shell-parser-fuzz.test.ts +263 -179
- package/src/__tests__/shell-parser-property.test.ts +435 -288
- package/src/__tests__/shell-tool-proxy-mode.test.ts +142 -70
- package/src/__tests__/size-guard.test.ts +42 -44
- package/src/__tests__/skill-feature-flags-integration.test.ts +79 -52
- package/src/__tests__/skill-feature-flags.test.ts +75 -47
- package/src/__tests__/skill-include-graph.test.ts +143 -148
- package/src/__tests__/skill-load-feature-flag.test.ts +94 -59
- package/src/__tests__/skill-load-tool.test.ts +371 -199
- package/src/__tests__/skill-projection-feature-flag.test.ts +131 -88
- package/src/__tests__/skill-projection.benchmark.test.ts +93 -65
- package/src/__tests__/skill-script-runner-host.test.ts +460 -250
- package/src/__tests__/skill-script-runner-sandbox.test.ts +168 -108
- package/src/__tests__/skill-script-runner.test.ts +115 -74
- package/src/__tests__/skill-tool-factory.test.ts +140 -96
- package/src/__tests__/skill-tool-manifest.test.ts +306 -210
- package/src/__tests__/skill-version-hash.test.ts +70 -56
- package/src/__tests__/skills.test.ts +0 -1
- package/src/__tests__/slack-channel-config.test.ts +127 -84
- package/src/__tests__/slack-skill.test.ts +60 -47
- package/src/__tests__/slash-commands-catalog.test.ts +37 -31
- package/src/__tests__/slash-commands-parser.test.ts +71 -64
- package/src/__tests__/slash-commands-resolver.test.ts +143 -107
- package/src/__tests__/slash-commands-rewrite.test.ts +22 -22
- package/src/__tests__/sms-messaging-provider.test.ts +74 -47
- package/src/__tests__/speaker-identification.test.ts +28 -25
- package/src/__tests__/starter-bundle.test.ts +27 -23
- package/src/__tests__/starter-task-flow.test.ts +67 -52
- package/src/__tests__/subagent-manager-notify.test.ts +154 -108
- package/src/__tests__/subagent-tools.test.ts +311 -270
- package/src/__tests__/subagent-types.test.ts +40 -40
- package/src/__tests__/surface-mutex-cleanup.test.ts +42 -30
- package/src/__tests__/swarm-dag-pathological.test.ts +122 -111
- package/src/__tests__/swarm-orchestrator.test.ts +135 -101
- package/src/__tests__/swarm-plan-validator.test.ts +125 -73
- package/src/__tests__/swarm-recursion.test.ts +58 -46
- package/src/__tests__/swarm-router-planner.test.ts +99 -74
- package/src/__tests__/swarm-session-integration.test.ts +148 -91
- package/src/__tests__/swarm-tool.test.ts +65 -45
- package/src/__tests__/swarm-worker-backend.test.ts +59 -45
- package/src/__tests__/swarm-worker-runner.test.ts +133 -118
- package/src/__tests__/system-prompt.test.ts +290 -256
- package/src/__tests__/task-compiler.test.ts +176 -120
- package/src/__tests__/task-management-tools.test.ts +561 -456
- package/src/__tests__/task-memory-cleanup.test.ts +627 -362
- package/src/__tests__/task-runner.test.ts +117 -94
- package/src/__tests__/task-scheduler.test.ts +113 -84
- package/src/__tests__/task-tools.test.ts +349 -264
- package/src/__tests__/terminal-sandbox.test.ts +138 -108
- package/src/__tests__/terminal-tools.test.ts +350 -305
- package/src/__tests__/thread-seed-composer.test.ts +307 -180
- package/src/__tests__/tool-approval-handler.test.ts +238 -137
- package/src/__tests__/tool-audit-listener.test.ts +69 -69
- package/src/__tests__/tool-domain-event-publisher.test.ts +142 -132
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +153 -146
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +136 -105
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +355 -239
- package/src/__tests__/tool-executor-redaction.test.ts +112 -109
- package/src/__tests__/tool-executor-shell-integration.test.ts +130 -79
- package/src/__tests__/tool-executor.test.ts +1274 -674
- package/src/__tests__/tool-grant-request-escalation.test.ts +401 -283
- package/src/__tests__/tool-metrics-listener.test.ts +97 -85
- package/src/__tests__/tool-notification-listener.test.ts +42 -25
- package/src/__tests__/tool-permission-simulate-handler.test.ts +137 -113
- package/src/__tests__/tool-policy.test.ts +44 -25
- package/src/__tests__/tool-profiling-listener.test.ts +99 -93
- package/src/__tests__/tool-result-truncation.test.ts +5 -4
- package/src/__tests__/tool-trace-listener.test.ts +131 -111
- package/src/__tests__/top-level-renderer.test.ts +62 -58
- package/src/__tests__/top-level-scanner.test.ts +68 -64
- package/src/__tests__/trace-emitter.test.ts +56 -56
- package/src/__tests__/trust-context-guards.test.ts +65 -65
- package/src/__tests__/trust-store.test.ts +1239 -806
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +339 -275
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +484 -373
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +264 -241
- package/src/__tests__/trusted-contact-multichannel.test.ts +182 -142
- package/src/__tests__/trusted-contact-verification.test.ts +251 -231
- package/src/__tests__/turn-commit.test.ts +259 -200
- package/src/__tests__/twilio-config.test.ts +49 -41
- package/src/__tests__/twilio-provider.test.ts +140 -126
- package/src/__tests__/twilio-rest.test.ts +22 -18
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +188 -162
- package/src/__tests__/twilio-routes-twiml.test.ts +55 -55
- package/src/__tests__/twilio-routes.test.ts +389 -281
- package/src/__tests__/twitter-auth-handler.test.ts +184 -139
- package/src/__tests__/twitter-cli-error-shaping.test.ts +88 -73
- package/src/__tests__/twitter-cli-routing.test.ts +146 -99
- package/src/__tests__/twitter-oauth-client.test.ts +82 -65
- package/src/__tests__/update-bulletin-format.test.ts +69 -66
- package/src/__tests__/update-bulletin-state.test.ts +66 -60
- package/src/__tests__/update-bulletin.test.ts +150 -114
- package/src/__tests__/update-template-contract.test.ts +15 -10
- package/src/__tests__/url-safety.test.ts +288 -265
- package/src/__tests__/user-reference.test.ts +32 -32
- package/src/__tests__/view-image-tool.test.ts +118 -96
- package/src/__tests__/voice-invite-redemption.test.ts +111 -106
- package/src/__tests__/voice-quality.test.ts +117 -102
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +204 -146
- package/src/__tests__/voice-session-bridge.test.ts +351 -216
- package/src/__tests__/weather-skill-regression.test.ts +170 -120
- package/src/__tests__/web-fetch.test.ts +664 -526
- package/src/__tests__/web-search.test.ts +379 -213
- package/src/__tests__/work-item-output.test.ts +90 -53
- package/src/__tests__/workspace-git-service.test.ts +437 -356
- package/src/__tests__/workspace-heartbeat-service.test.ts +125 -91
- package/src/__tests__/workspace-lifecycle.test.ts +98 -64
- package/src/__tests__/workspace-policy.test.ts +139 -71
- package/src/commands/__tests__/cc-command-registry.test.ts +142 -134
- package/src/config/__tests__/feature-flag-registry-guard.test.ts +48 -39
- package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +44 -4
- package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +0 -1
- package/src/config/bundled-skills/messaging/SKILL.md +9 -7
- package/src/config/bundled-skills/messaging/tools/gmail-outreach-scan.ts +15 -5
- package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +16 -5
- package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +11 -7
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +34 -32
- package/src/config/bundled-tool-registry.ts +2 -0
- package/src/config/env.ts +38 -29
- package/src/daemon/handlers/skills.ts +18 -10
- package/src/daemon/ipc-contract/messages.ts +1 -0
- package/src/daemon/ipc-contract/surfaces.ts +7 -1
- package/src/daemon/session-agent-loop-handlers.ts +5 -0
- package/src/daemon/session-agent-loop.ts +1 -1
- package/src/daemon/session-process.ts +1 -1
- package/src/daemon/session-surfaces.ts +42 -2
- package/src/memory/db-connection.ts +16 -10
- package/src/messaging/providers/gmail/adapter.ts +10 -3
- package/src/messaging/providers/gmail/client.ts +280 -72
- package/src/runtime/auth/__tests__/context.test.ts +75 -65
- package/src/runtime/auth/__tests__/credential-service.test.ts +137 -114
- package/src/runtime/auth/__tests__/guard-tests.test.ts +84 -90
- package/src/runtime/auth/__tests__/ipc-auth-context.test.ts +40 -40
- package/src/runtime/auth/__tests__/middleware.test.ts +80 -74
- package/src/runtime/auth/__tests__/policy.test.ts +9 -9
- package/src/runtime/auth/__tests__/route-policy.test.ts +76 -65
- package/src/runtime/auth/__tests__/scopes.test.ts +68 -60
- package/src/runtime/auth/__tests__/subject.test.ts +54 -54
- package/src/runtime/auth/__tests__/token-service.test.ts +115 -108
- package/src/runtime/auth/scopes.ts +3 -0
- package/src/runtime/auth/token-service.ts +78 -48
- package/src/runtime/auth/types.ts +2 -1
- package/src/runtime/http-server.ts +2 -1
- package/src/security/secure-keys.ts +103 -53
- package/src/sequence/reply-matcher.ts +10 -6
- package/src/skills/frontmatter.ts +9 -6
- package/src/tools/browser/__tests__/auth-cache.test.ts +69 -63
- package/src/tools/browser/__tests__/auth-detector.test.ts +218 -157
- package/src/tools/browser/__tests__/jit-auth.test.ts +83 -99
- package/src/tools/ui-surface/definitions.ts +2 -1
- package/src/util/platform.ts +0 -12
- package/docs/architecture/http-token-refresh.md +0 -274
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
import { mkdtempSync, rmSync } from
|
|
2
|
-
import { tmpdir } from
|
|
3
|
-
import { join } from
|
|
1
|
+
import { mkdtempSync, 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";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
const testDir = mkdtempSync(join(tmpdir(), "conv-store-test-"));
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
mock.module('../util/platform.js', () => ({
|
|
8
|
+
mock.module("../util/platform.js", () => ({
|
|
10
9
|
getDataDir: () => testDir,
|
|
11
|
-
isMacOS: () => process.platform ===
|
|
12
|
-
isLinux: () => process.platform ===
|
|
13
|
-
isWindows: () => process.platform ===
|
|
14
|
-
getSocketPath: () => join(testDir,
|
|
15
|
-
getPidPath: () => join(testDir,
|
|
16
|
-
getDbPath: () => join(testDir,
|
|
17
|
-
getLogPath: () => join(testDir,
|
|
10
|
+
isMacOS: () => process.platform === "darwin",
|
|
11
|
+
isLinux: () => process.platform === "linux",
|
|
12
|
+
isWindows: () => process.platform === "win32",
|
|
13
|
+
getSocketPath: () => join(testDir, "test.sock"),
|
|
14
|
+
getPidPath: () => join(testDir, "test.pid"),
|
|
15
|
+
getDbPath: () => join(testDir, "test.db"),
|
|
16
|
+
getLogPath: () => join(testDir, "test.log"),
|
|
18
17
|
ensureDataDir: () => {},
|
|
19
18
|
}));
|
|
20
19
|
|
|
21
|
-
mock.module(
|
|
22
|
-
getLogger: () =>
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
mock.module("../util/logger.js", () => ({
|
|
21
|
+
getLogger: () =>
|
|
22
|
+
new Proxy({} as Record<string, unknown>, {
|
|
23
|
+
get: () => () => {},
|
|
24
|
+
}),
|
|
25
25
|
}));
|
|
26
26
|
|
|
27
27
|
import {
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
getAttachmentsForMessage,
|
|
30
30
|
linkAttachmentToMessage,
|
|
31
31
|
uploadAttachment,
|
|
32
|
-
} from
|
|
32
|
+
} from "../memory/attachments-store.js";
|
|
33
33
|
import {
|
|
34
34
|
addMessage,
|
|
35
35
|
clearAll,
|
|
@@ -40,18 +40,22 @@ import {
|
|
|
40
40
|
getConversationThreadType,
|
|
41
41
|
getMessages,
|
|
42
42
|
isLastUserMessageToolResult,
|
|
43
|
-
} from
|
|
44
|
-
import { getDb, initializeDb, resetDb } from
|
|
43
|
+
} from "../memory/conversation-store.js";
|
|
44
|
+
import { getDb, initializeDb, resetDb } from "../memory/db.js";
|
|
45
45
|
|
|
46
46
|
// Initialize db once before all tests
|
|
47
47
|
initializeDb();
|
|
48
48
|
|
|
49
49
|
afterAll(() => {
|
|
50
50
|
resetDb();
|
|
51
|
-
try {
|
|
51
|
+
try {
|
|
52
|
+
rmSync(testDir, { recursive: true });
|
|
53
|
+
} catch {
|
|
54
|
+
/* best effort */
|
|
55
|
+
}
|
|
52
56
|
});
|
|
53
57
|
|
|
54
|
-
describe(
|
|
58
|
+
describe("deleteLastExchange", () => {
|
|
55
59
|
beforeEach(() => {
|
|
56
60
|
// Reset database between tests by dropping and recreating tables
|
|
57
61
|
const db = getDb();
|
|
@@ -59,38 +63,38 @@ describe('deleteLastExchange', () => {
|
|
|
59
63
|
db.run(`DELETE FROM conversations`);
|
|
60
64
|
});
|
|
61
65
|
|
|
62
|
-
test(
|
|
63
|
-
const conv = createConversation(
|
|
64
|
-
addMessage(conv.id,
|
|
65
|
-
addMessage(conv.id,
|
|
66
|
-
addMessage(conv.id,
|
|
67
|
-
addMessage(conv.id,
|
|
66
|
+
test("deletes last user message and subsequent assistant messages", () => {
|
|
67
|
+
const conv = createConversation("test");
|
|
68
|
+
addMessage(conv.id, "user", "first question");
|
|
69
|
+
addMessage(conv.id, "assistant", "first answer");
|
|
70
|
+
addMessage(conv.id, "user", "second question");
|
|
71
|
+
addMessage(conv.id, "assistant", "second answer");
|
|
68
72
|
|
|
69
73
|
const deleted = deleteLastExchange(conv.id);
|
|
70
74
|
expect(deleted).toBe(2);
|
|
71
75
|
|
|
72
76
|
const remaining = getMessages(conv.id);
|
|
73
77
|
expect(remaining).toHaveLength(2);
|
|
74
|
-
expect(remaining[0].content).toBe(
|
|
75
|
-
expect(remaining[1].content).toBe(
|
|
78
|
+
expect(remaining[0].content).toBe("first question");
|
|
79
|
+
expect(remaining[1].content).toBe("first answer");
|
|
76
80
|
});
|
|
77
81
|
|
|
78
|
-
test(
|
|
79
|
-
const conv = createConversation(
|
|
80
|
-
addMessage(conv.id,
|
|
82
|
+
test("returns 0 when no user messages exist", () => {
|
|
83
|
+
const conv = createConversation("test");
|
|
84
|
+
addMessage(conv.id, "assistant", "hello");
|
|
81
85
|
|
|
82
86
|
const deleted = deleteLastExchange(conv.id);
|
|
83
87
|
expect(deleted).toBe(0);
|
|
84
88
|
});
|
|
85
89
|
|
|
86
|
-
test(
|
|
87
|
-
const conv = createConversation(
|
|
90
|
+
test("returns 0 for empty conversation", () => {
|
|
91
|
+
const conv = createConversation("test");
|
|
88
92
|
const deleted = deleteLastExchange(conv.id);
|
|
89
93
|
expect(deleted).toBe(0);
|
|
90
94
|
});
|
|
91
95
|
|
|
92
|
-
test(
|
|
93
|
-
const conv = createConversation(
|
|
96
|
+
test("uses rowid ordering so same-timestamp messages are handled correctly", () => {
|
|
97
|
+
const conv = createConversation("test");
|
|
94
98
|
const db = getDb();
|
|
95
99
|
const now = Date.now();
|
|
96
100
|
|
|
@@ -116,64 +120,101 @@ describe('deleteLastExchange', () => {
|
|
|
116
120
|
|
|
117
121
|
const remaining = getMessages(conv.id);
|
|
118
122
|
expect(remaining).toHaveLength(2);
|
|
119
|
-
expect(remaining[0].content).toBe(
|
|
120
|
-
expect(remaining[1].content).toBe(
|
|
123
|
+
expect(remaining[0].content).toBe("first");
|
|
124
|
+
expect(remaining[1].content).toBe("reply1");
|
|
121
125
|
});
|
|
122
126
|
});
|
|
123
127
|
|
|
124
|
-
describe(
|
|
128
|
+
describe("isLastUserMessageToolResult", () => {
|
|
125
129
|
beforeEach(() => {
|
|
126
130
|
const db = getDb();
|
|
127
131
|
db.run(`DELETE FROM messages`);
|
|
128
132
|
db.run(`DELETE FROM conversations`);
|
|
129
133
|
});
|
|
130
134
|
|
|
131
|
-
test(
|
|
132
|
-
const conv = createConversation(
|
|
133
|
-
addMessage(conv.id,
|
|
134
|
-
addMessage(
|
|
135
|
-
|
|
135
|
+
test("returns true when last user message is tool_result only", () => {
|
|
136
|
+
const conv = createConversation("test");
|
|
137
|
+
addMessage(conv.id, "user", "hello");
|
|
138
|
+
addMessage(
|
|
139
|
+
conv.id,
|
|
140
|
+
"assistant",
|
|
141
|
+
JSON.stringify([
|
|
142
|
+
{ type: "tool_use", id: "tu1", name: "bash", input: {} },
|
|
143
|
+
]),
|
|
144
|
+
);
|
|
145
|
+
addMessage(
|
|
146
|
+
conv.id,
|
|
147
|
+
"user",
|
|
148
|
+
JSON.stringify([
|
|
149
|
+
{ type: "tool_result", tool_use_id: "tu1", content: "ok" },
|
|
150
|
+
]),
|
|
151
|
+
);
|
|
136
152
|
|
|
137
153
|
expect(isLastUserMessageToolResult(conv.id)).toBe(true);
|
|
138
154
|
});
|
|
139
155
|
|
|
140
|
-
test(
|
|
141
|
-
const conv = createConversation(
|
|
142
|
-
addMessage(conv.id,
|
|
156
|
+
test("returns false when last user message is plain text", () => {
|
|
157
|
+
const conv = createConversation("test");
|
|
158
|
+
addMessage(conv.id, "user", "hello");
|
|
143
159
|
|
|
144
160
|
expect(isLastUserMessageToolResult(conv.id)).toBe(false);
|
|
145
161
|
});
|
|
146
162
|
|
|
147
|
-
test(
|
|
148
|
-
const conv = createConversation(
|
|
163
|
+
test("returns false when no user messages exist", () => {
|
|
164
|
+
const conv = createConversation("test");
|
|
149
165
|
expect(isLastUserMessageToolResult(conv.id)).toBe(false);
|
|
150
166
|
});
|
|
151
167
|
|
|
152
|
-
test(
|
|
153
|
-
const conv = createConversation(
|
|
154
|
-
addMessage(
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
168
|
+
test("returns false when last user message has mixed content types", () => {
|
|
169
|
+
const conv = createConversation("test");
|
|
170
|
+
addMessage(
|
|
171
|
+
conv.id,
|
|
172
|
+
"user",
|
|
173
|
+
JSON.stringify([
|
|
174
|
+
{ type: "text", text: "hello" },
|
|
175
|
+
{ type: "tool_result", tool_use_id: "tu1", content: "ok" },
|
|
176
|
+
]),
|
|
177
|
+
);
|
|
158
178
|
|
|
159
179
|
expect(isLastUserMessageToolResult(conv.id)).toBe(false);
|
|
160
180
|
});
|
|
161
181
|
});
|
|
162
182
|
|
|
163
|
-
describe(
|
|
183
|
+
describe("deleteLastExchange with tool_result messages", () => {
|
|
164
184
|
beforeEach(() => {
|
|
165
185
|
const db = getDb();
|
|
166
186
|
db.run(`DELETE FROM messages`);
|
|
167
187
|
db.run(`DELETE FROM conversations`);
|
|
168
188
|
});
|
|
169
189
|
|
|
170
|
-
test(
|
|
171
|
-
const conv = createConversation(
|
|
190
|
+
test("looping deleteLastExchange cleans up tool_result user messages", () => {
|
|
191
|
+
const conv = createConversation("test");
|
|
172
192
|
// Simulate: user asks question -> assistant uses tool -> tool_result -> assistant responds
|
|
173
|
-
addMessage(conv.id,
|
|
174
|
-
addMessage(
|
|
175
|
-
|
|
176
|
-
|
|
193
|
+
addMessage(conv.id, "user", "What files are in /tmp?");
|
|
194
|
+
addMessage(
|
|
195
|
+
conv.id,
|
|
196
|
+
"assistant",
|
|
197
|
+
JSON.stringify([
|
|
198
|
+
{
|
|
199
|
+
type: "tool_use",
|
|
200
|
+
id: "tu1",
|
|
201
|
+
name: "bash",
|
|
202
|
+
input: { command: "ls /tmp" },
|
|
203
|
+
},
|
|
204
|
+
]),
|
|
205
|
+
);
|
|
206
|
+
addMessage(
|
|
207
|
+
conv.id,
|
|
208
|
+
"user",
|
|
209
|
+
JSON.stringify([
|
|
210
|
+
{ type: "tool_result", tool_use_id: "tu1", content: "file1.txt" },
|
|
211
|
+
]),
|
|
212
|
+
);
|
|
213
|
+
addMessage(
|
|
214
|
+
conv.id,
|
|
215
|
+
"assistant",
|
|
216
|
+
JSON.stringify([{ type: "text", text: "There is file1.txt in /tmp" }]),
|
|
217
|
+
);
|
|
177
218
|
|
|
178
219
|
// First deleteLastExchange removes the tool_result user msg + final assistant msg
|
|
179
220
|
let deleted = deleteLastExchange(conv.id);
|
|
@@ -195,15 +236,43 @@ describe('deleteLastExchange with tool_result messages', () => {
|
|
|
195
236
|
expect(remaining).toHaveLength(0);
|
|
196
237
|
});
|
|
197
238
|
|
|
198
|
-
test(
|
|
199
|
-
const conv = createConversation(
|
|
239
|
+
test("looping pattern handles multiple tool uses in sequence", () => {
|
|
240
|
+
const conv = createConversation("test");
|
|
200
241
|
// user -> assistant(tool_use) -> user(tool_result) -> assistant(tool_use) -> user(tool_result) -> assistant(text)
|
|
201
|
-
addMessage(conv.id,
|
|
202
|
-
addMessage(
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
242
|
+
addMessage(conv.id, "user", "Do two things");
|
|
243
|
+
addMessage(
|
|
244
|
+
conv.id,
|
|
245
|
+
"assistant",
|
|
246
|
+
JSON.stringify([
|
|
247
|
+
{ type: "tool_use", id: "tu1", name: "bash", input: {} },
|
|
248
|
+
]),
|
|
249
|
+
);
|
|
250
|
+
addMessage(
|
|
251
|
+
conv.id,
|
|
252
|
+
"user",
|
|
253
|
+
JSON.stringify([
|
|
254
|
+
{ type: "tool_result", tool_use_id: "tu1", content: "result1" },
|
|
255
|
+
]),
|
|
256
|
+
);
|
|
257
|
+
addMessage(
|
|
258
|
+
conv.id,
|
|
259
|
+
"assistant",
|
|
260
|
+
JSON.stringify([
|
|
261
|
+
{ type: "tool_use", id: "tu2", name: "bash", input: {} },
|
|
262
|
+
]),
|
|
263
|
+
);
|
|
264
|
+
addMessage(
|
|
265
|
+
conv.id,
|
|
266
|
+
"user",
|
|
267
|
+
JSON.stringify([
|
|
268
|
+
{ type: "tool_result", tool_use_id: "tu2", content: "result2" },
|
|
269
|
+
]),
|
|
270
|
+
);
|
|
271
|
+
addMessage(
|
|
272
|
+
conv.id,
|
|
273
|
+
"assistant",
|
|
274
|
+
JSON.stringify([{ type: "text", text: "Done both" }]),
|
|
275
|
+
);
|
|
207
276
|
|
|
208
277
|
// First delete: removes last tool_result user (row 5) + final assistant (row 6)
|
|
209
278
|
deleteLastExchange(conv.id);
|
|
@@ -223,21 +292,25 @@ describe('deleteLastExchange with tool_result messages', () => {
|
|
|
223
292
|
});
|
|
224
293
|
});
|
|
225
294
|
|
|
226
|
-
describe(
|
|
295
|
+
describe("attachment orphan cleanup", () => {
|
|
227
296
|
beforeEach(() => {
|
|
228
297
|
const db = getDb();
|
|
229
|
-
db.run(
|
|
230
|
-
db.run(
|
|
231
|
-
db.run(
|
|
232
|
-
db.run(
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
test(
|
|
236
|
-
const conv = createConversation(
|
|
237
|
-
await addMessage(conv.id,
|
|
238
|
-
const assistantMsg = await addMessage(
|
|
298
|
+
db.run("DELETE FROM message_attachments");
|
|
299
|
+
db.run("DELETE FROM attachments");
|
|
300
|
+
db.run("DELETE FROM messages");
|
|
301
|
+
db.run("DELETE FROM conversations");
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("deleteLastExchange cleans up orphaned attachments", async () => {
|
|
305
|
+
const conv = createConversation("test");
|
|
306
|
+
await addMessage(conv.id, "user", "hello");
|
|
307
|
+
const assistantMsg = await addMessage(
|
|
308
|
+
conv.id,
|
|
309
|
+
"assistant",
|
|
310
|
+
"Here is a file",
|
|
311
|
+
);
|
|
239
312
|
|
|
240
|
-
const stored = uploadAttachment(
|
|
313
|
+
const stored = uploadAttachment("chart.png", "image/png", "iVBOR");
|
|
241
314
|
linkAttachmentToMessage(assistantMsg.id, stored.id, 0);
|
|
242
315
|
|
|
243
316
|
// Verify attachment is linked
|
|
@@ -247,18 +320,24 @@ describe('attachment orphan cleanup', () => {
|
|
|
247
320
|
deleteLastExchange(conv.id);
|
|
248
321
|
|
|
249
322
|
// Attachment row should be gone
|
|
250
|
-
const raw = (
|
|
251
|
-
|
|
323
|
+
const raw = (
|
|
324
|
+
getDb() as unknown as {
|
|
325
|
+
$client: import("bun:sqlite").Database;
|
|
326
|
+
}
|
|
327
|
+
).$client;
|
|
328
|
+
const remaining = raw
|
|
329
|
+
.query("SELECT COUNT(*) AS c FROM attachments")
|
|
330
|
+
.get() as { c: number };
|
|
252
331
|
expect(remaining.c).toBe(0);
|
|
253
332
|
});
|
|
254
333
|
|
|
255
|
-
test(
|
|
256
|
-
const conv = createConversation(
|
|
257
|
-
const msg1 = await addMessage(conv.id,
|
|
258
|
-
await addMessage(conv.id,
|
|
259
|
-
const msg2 = await addMessage(conv.id,
|
|
334
|
+
test("deleteLastExchange preserves attachments still linked to other messages", async () => {
|
|
335
|
+
const conv = createConversation("test");
|
|
336
|
+
const msg1 = await addMessage(conv.id, "assistant", "first");
|
|
337
|
+
await addMessage(conv.id, "user", "question");
|
|
338
|
+
const msg2 = await addMessage(conv.id, "assistant", "second");
|
|
260
339
|
|
|
261
|
-
const shared = uploadAttachment(
|
|
340
|
+
const shared = uploadAttachment("shared.png", "image/png", "AAAA");
|
|
262
341
|
linkAttachmentToMessage(msg1.id, shared.id, 0);
|
|
263
342
|
linkAttachmentToMessage(msg2.id, shared.id, 0);
|
|
264
343
|
|
|
@@ -266,162 +345,186 @@ describe('attachment orphan cleanup', () => {
|
|
|
266
345
|
deleteLastExchange(conv.id);
|
|
267
346
|
|
|
268
347
|
// Attachment should survive because msg1 still links to it
|
|
269
|
-
const raw = (
|
|
270
|
-
|
|
348
|
+
const raw = (
|
|
349
|
+
getDb() as unknown as {
|
|
350
|
+
$client: import("bun:sqlite").Database;
|
|
351
|
+
}
|
|
352
|
+
).$client;
|
|
353
|
+
const remaining = raw
|
|
354
|
+
.query("SELECT COUNT(*) AS c FROM attachments")
|
|
355
|
+
.get() as { c: number };
|
|
271
356
|
expect(remaining.c).toBe(1);
|
|
272
357
|
});
|
|
273
358
|
|
|
274
|
-
test(
|
|
275
|
-
const conv = createConversation(
|
|
276
|
-
const msg = await addMessage(conv.id,
|
|
277
|
-
const stored = uploadAttachment(
|
|
359
|
+
test("clearAll removes all attachments", async () => {
|
|
360
|
+
const conv = createConversation("test");
|
|
361
|
+
const msg = await addMessage(conv.id, "assistant", "file");
|
|
362
|
+
const stored = uploadAttachment("doc.pdf", "application/pdf", "JVBER");
|
|
278
363
|
linkAttachmentToMessage(msg.id, stored.id, 0);
|
|
279
364
|
|
|
280
365
|
clearAll();
|
|
281
366
|
|
|
282
|
-
const raw = (
|
|
283
|
-
|
|
284
|
-
|
|
367
|
+
const raw = (
|
|
368
|
+
getDb() as unknown as {
|
|
369
|
+
$client: import("bun:sqlite").Database;
|
|
370
|
+
}
|
|
371
|
+
).$client;
|
|
372
|
+
const attachmentCount = raw
|
|
373
|
+
.query("SELECT COUNT(*) AS c FROM attachments")
|
|
374
|
+
.get() as { c: number };
|
|
375
|
+
const linkCount = raw
|
|
376
|
+
.query("SELECT COUNT(*) AS c FROM message_attachments")
|
|
377
|
+
.get() as { c: number };
|
|
285
378
|
expect(attachmentCount.c).toBe(0);
|
|
286
379
|
expect(linkCount.c).toBe(0);
|
|
287
380
|
});
|
|
288
381
|
|
|
289
|
-
test(
|
|
290
|
-
const conv = createConversation(
|
|
291
|
-
await addMessage(conv.id,
|
|
292
|
-
const assistantMsg = await addMessage(
|
|
382
|
+
test("deleteLastExchange does not delete unlinked user uploads", async () => {
|
|
383
|
+
const conv = createConversation("test");
|
|
384
|
+
await addMessage(conv.id, "user", "hello");
|
|
385
|
+
const assistantMsg = await addMessage(
|
|
386
|
+
conv.id,
|
|
387
|
+
"assistant",
|
|
388
|
+
"Here is a file",
|
|
389
|
+
);
|
|
293
390
|
|
|
294
391
|
// An attachment linked to the assistant message (should be cleaned up)
|
|
295
|
-
const linked = uploadAttachment(
|
|
392
|
+
const linked = uploadAttachment("chart.png", "image/png", "iVBOR");
|
|
296
393
|
linkAttachmentToMessage(assistantMsg.id, linked.id, 0);
|
|
297
394
|
|
|
298
395
|
// A freshly uploaded attachment not linked to any message (should survive)
|
|
299
|
-
uploadAttachment(
|
|
396
|
+
uploadAttachment("pending.png", "image/png", "AAAA");
|
|
300
397
|
|
|
301
398
|
deleteLastExchange(conv.id);
|
|
302
399
|
|
|
303
|
-
const raw = (
|
|
304
|
-
|
|
400
|
+
const raw = (
|
|
401
|
+
getDb() as unknown as {
|
|
402
|
+
$client: import("bun:sqlite").Database;
|
|
403
|
+
}
|
|
404
|
+
).$client;
|
|
405
|
+
const remaining = raw
|
|
406
|
+
.query("SELECT COUNT(*) AS c FROM attachments")
|
|
407
|
+
.get() as { c: number };
|
|
305
408
|
expect(remaining.c).toBe(1); // only the unlinked upload survives
|
|
306
409
|
});
|
|
307
410
|
});
|
|
308
411
|
|
|
309
|
-
describe(
|
|
412
|
+
describe("conversation thread metadata defaults", () => {
|
|
310
413
|
beforeEach(() => {
|
|
311
414
|
const db = getDb();
|
|
312
415
|
db.run(`DELETE FROM messages`);
|
|
313
416
|
db.run(`DELETE FROM conversations`);
|
|
314
417
|
});
|
|
315
418
|
|
|
316
|
-
test(
|
|
317
|
-
const conv = createConversation(
|
|
318
|
-
expect(conv.threadType).toBe(
|
|
419
|
+
test("new conversation has threadType defaulting to standard", () => {
|
|
420
|
+
const conv = createConversation("test");
|
|
421
|
+
expect(conv.threadType).toBe("standard");
|
|
319
422
|
});
|
|
320
423
|
|
|
321
|
-
test(
|
|
322
|
-
const conv = createConversation(
|
|
323
|
-
expect(conv.memoryScopeId).toBe(
|
|
424
|
+
test("new conversation has memoryScopeId defaulting to default", () => {
|
|
425
|
+
const conv = createConversation("test");
|
|
426
|
+
expect(conv.memoryScopeId).toBe("default");
|
|
324
427
|
});
|
|
325
428
|
|
|
326
|
-
test(
|
|
327
|
-
const conv = createConversation(
|
|
429
|
+
test("defaults are persisted and retrievable from DB", () => {
|
|
430
|
+
const conv = createConversation("test");
|
|
328
431
|
const loaded = getConversation(conv.id);
|
|
329
432
|
expect(loaded).not.toBeNull();
|
|
330
|
-
expect(loaded!.threadType).toBe(
|
|
331
|
-
expect(loaded!.memoryScopeId).toBe(
|
|
433
|
+
expect(loaded!.threadType).toBe("standard");
|
|
434
|
+
expect(loaded!.memoryScopeId).toBe("default");
|
|
332
435
|
});
|
|
333
436
|
|
|
334
|
-
test(
|
|
437
|
+
test("existing conversations without explicit values get defaults via migration", () => {
|
|
335
438
|
// Insert a conversation row directly without the new columns
|
|
336
439
|
// (simulates a pre-migration row — the ALTER TABLE DEFAULT handles it)
|
|
337
440
|
const db = getDb();
|
|
338
441
|
const now = Date.now();
|
|
339
|
-
const id =
|
|
442
|
+
const id = "legacy-conv-" + now;
|
|
340
443
|
db.run(
|
|
341
444
|
`INSERT INTO conversations (id, title, created_at, updated_at) VALUES ('${id}', 'legacy', ${now}, ${now})`,
|
|
342
445
|
);
|
|
343
446
|
|
|
344
447
|
const loaded = getConversation(id);
|
|
345
448
|
expect(loaded).not.toBeNull();
|
|
346
|
-
expect(loaded!.threadType).toBe(
|
|
347
|
-
expect(loaded!.memoryScopeId).toBe(
|
|
449
|
+
expect(loaded!.threadType).toBe("standard");
|
|
450
|
+
expect(loaded!.memoryScopeId).toBe("default");
|
|
348
451
|
});
|
|
349
452
|
});
|
|
350
453
|
|
|
351
|
-
describe(
|
|
454
|
+
describe("createConversation with thread type option", () => {
|
|
352
455
|
beforeEach(() => {
|
|
353
456
|
const db = getDb();
|
|
354
457
|
db.run(`DELETE FROM messages`);
|
|
355
458
|
db.run(`DELETE FROM conversations`);
|
|
356
459
|
});
|
|
357
460
|
|
|
358
|
-
test(
|
|
359
|
-
const conv = createConversation(
|
|
360
|
-
expect(conv.title).toBe(
|
|
361
|
-
expect(conv.threadType).toBe(
|
|
362
|
-
expect(conv.memoryScopeId).toBe(
|
|
461
|
+
test("standard create with string title uses defaults", () => {
|
|
462
|
+
const conv = createConversation("hello");
|
|
463
|
+
expect(conv.title).toBe("hello");
|
|
464
|
+
expect(conv.threadType).toBe("standard");
|
|
465
|
+
expect(conv.memoryScopeId).toBe("default");
|
|
363
466
|
});
|
|
364
467
|
|
|
365
|
-
test(
|
|
366
|
-
const conv = createConversation({ title:
|
|
367
|
-
expect(conv.threadType).toBe(
|
|
368
|
-
expect(conv.memoryScopeId).toBe(
|
|
468
|
+
test("standard create with options object uses defaults", () => {
|
|
469
|
+
const conv = createConversation({ title: "hello", threadType: "standard" });
|
|
470
|
+
expect(conv.threadType).toBe("standard");
|
|
471
|
+
expect(conv.memoryScopeId).toBe("default");
|
|
369
472
|
});
|
|
370
473
|
|
|
371
|
-
test(
|
|
372
|
-
const conv = createConversation({ title:
|
|
373
|
-
expect(conv.threadType).toBe(
|
|
474
|
+
test("private create sets threadType and derives memoryScopeId", () => {
|
|
475
|
+
const conv = createConversation({ title: "secret", threadType: "private" });
|
|
476
|
+
expect(conv.threadType).toBe("private");
|
|
374
477
|
expect(conv.memoryScopeId).toBe(`private:${conv.id}`);
|
|
375
478
|
});
|
|
376
479
|
|
|
377
|
-
test(
|
|
378
|
-
const conv = createConversation({ threadType:
|
|
480
|
+
test("private create memoryScopeId is persisted", () => {
|
|
481
|
+
const conv = createConversation({ threadType: "private" });
|
|
379
482
|
const loaded = getConversation(conv.id);
|
|
380
483
|
expect(loaded).not.toBeNull();
|
|
381
|
-
expect(loaded!.threadType).toBe(
|
|
484
|
+
expect(loaded!.threadType).toBe("private");
|
|
382
485
|
expect(loaded!.memoryScopeId).toBe(`private:${conv.id}`);
|
|
383
486
|
});
|
|
384
487
|
|
|
385
|
-
test(
|
|
488
|
+
test("no-arg create uses defaults", () => {
|
|
386
489
|
const conv = createConversation();
|
|
387
|
-
expect(conv.threadType).toBe(
|
|
388
|
-
expect(conv.memoryScopeId).toBe(
|
|
490
|
+
expect(conv.threadType).toBe("standard");
|
|
491
|
+
expect(conv.memoryScopeId).toBe("default");
|
|
389
492
|
});
|
|
390
493
|
});
|
|
391
494
|
|
|
392
|
-
describe(
|
|
495
|
+
describe("conversation metadata read helpers", () => {
|
|
393
496
|
beforeEach(() => {
|
|
394
497
|
const db = getDb();
|
|
395
498
|
db.run(`DELETE FROM messages`);
|
|
396
499
|
db.run(`DELETE FROM conversations`);
|
|
397
500
|
});
|
|
398
501
|
|
|
399
|
-
test(
|
|
400
|
-
const conv = createConversation(
|
|
401
|
-
expect(getConversationThreadType(conv.id)).toBe(
|
|
502
|
+
test("getConversationThreadType returns standard for standard conversation", () => {
|
|
503
|
+
const conv = createConversation("test");
|
|
504
|
+
expect(getConversationThreadType(conv.id)).toBe("standard");
|
|
402
505
|
});
|
|
403
506
|
|
|
404
|
-
test(
|
|
405
|
-
const conv = createConversation({ threadType:
|
|
406
|
-
expect(getConversationThreadType(conv.id)).toBe(
|
|
507
|
+
test("getConversationThreadType returns private for private conversation", () => {
|
|
508
|
+
const conv = createConversation({ threadType: "private" });
|
|
509
|
+
expect(getConversationThreadType(conv.id)).toBe("private");
|
|
407
510
|
});
|
|
408
511
|
|
|
409
|
-
test(
|
|
410
|
-
expect(getConversationThreadType(
|
|
512
|
+
test("getConversationThreadType returns standard for missing conversation", () => {
|
|
513
|
+
expect(getConversationThreadType("nonexistent-id")).toBe("standard");
|
|
411
514
|
});
|
|
412
515
|
|
|
413
|
-
test(
|
|
414
|
-
const conv = createConversation(
|
|
415
|
-
expect(getConversationMemoryScopeId(conv.id)).toBe(
|
|
516
|
+
test("getConversationMemoryScopeId returns default for standard conversation", () => {
|
|
517
|
+
const conv = createConversation("test");
|
|
518
|
+
expect(getConversationMemoryScopeId(conv.id)).toBe("default");
|
|
416
519
|
});
|
|
417
520
|
|
|
418
|
-
test(
|
|
419
|
-
const conv = createConversation({ threadType:
|
|
521
|
+
test("getConversationMemoryScopeId returns private scope for private conversation", () => {
|
|
522
|
+
const conv = createConversation({ threadType: "private" });
|
|
420
523
|
expect(getConversationMemoryScopeId(conv.id)).toBe(`private:${conv.id}`);
|
|
421
524
|
});
|
|
422
525
|
|
|
423
|
-
test(
|
|
424
|
-
expect(getConversationMemoryScopeId(
|
|
526
|
+
test("getConversationMemoryScopeId returns default for missing conversation", () => {
|
|
527
|
+
expect(getConversationMemoryScopeId("nonexistent-id")).toBe("default");
|
|
425
528
|
});
|
|
426
529
|
});
|
|
427
530
|
|
|
@@ -429,42 +532,42 @@ describe('conversation metadata read helpers', () => {
|
|
|
429
532
|
// Baseline: attachment reuse across threads
|
|
430
533
|
// ---------------------------------------------------------------------------
|
|
431
534
|
|
|
432
|
-
describe(
|
|
535
|
+
describe("attachment reuse across thread lifecycles", () => {
|
|
433
536
|
beforeEach(() => {
|
|
434
537
|
const db = getDb();
|
|
435
|
-
db.run(
|
|
436
|
-
db.run(
|
|
437
|
-
db.run(
|
|
438
|
-
db.run(
|
|
538
|
+
db.run("DELETE FROM message_attachments");
|
|
539
|
+
db.run("DELETE FROM attachments");
|
|
540
|
+
db.run("DELETE FROM messages");
|
|
541
|
+
db.run("DELETE FROM conversations");
|
|
439
542
|
});
|
|
440
543
|
|
|
441
|
-
test(
|
|
442
|
-
const convA = createConversation(
|
|
443
|
-
const msgA = await addMessage(convA.id,
|
|
444
|
-
const stored = uploadAttachment(
|
|
544
|
+
test("attachment uploaded in conversation A is retrievable by ID without any conversation reference", async () => {
|
|
545
|
+
const convA = createConversation("Thread A");
|
|
546
|
+
const msgA = await addMessage(convA.id, "assistant", "Here is a file");
|
|
547
|
+
const stored = uploadAttachment("report.pdf", "application/pdf", "JVBER");
|
|
445
548
|
linkAttachmentToMessage(msgA.id, stored.id, 0);
|
|
446
549
|
|
|
447
550
|
// Create a completely separate conversation
|
|
448
|
-
const convB = createConversation(
|
|
449
|
-
await addMessage(convB.id,
|
|
551
|
+
const convB = createConversation("Thread B");
|
|
552
|
+
await addMessage(convB.id, "user", "hello");
|
|
450
553
|
|
|
451
554
|
// The attachment is retrievable by ID regardless of which conversation is active.
|
|
452
555
|
const fetched = getAttachmentById(stored.id);
|
|
453
556
|
expect(fetched).not.toBeNull();
|
|
454
557
|
expect(fetched!.id).toBe(stored.id);
|
|
455
|
-
expect(fetched!.originalFilename).toBe(
|
|
456
|
-
expect(fetched!.dataBase64).toBe(
|
|
558
|
+
expect(fetched!.originalFilename).toBe("report.pdf");
|
|
559
|
+
expect(fetched!.dataBase64).toBe("JVBER");
|
|
457
560
|
});
|
|
458
561
|
|
|
459
|
-
test(
|
|
460
|
-
const convA = createConversation(
|
|
461
|
-
const convB = createConversation(
|
|
562
|
+
test("attachment can be linked to messages in different conversations", async () => {
|
|
563
|
+
const convA = createConversation("Thread A");
|
|
564
|
+
const convB = createConversation("Thread B");
|
|
462
565
|
|
|
463
|
-
const msgA = await addMessage(convA.id,
|
|
464
|
-
const msgB = await addMessage(convB.id,
|
|
566
|
+
const msgA = await addMessage(convA.id, "assistant", "Original file");
|
|
567
|
+
const msgB = await addMessage(convB.id, "assistant", "Reused file");
|
|
465
568
|
|
|
466
569
|
// Upload once, link to both conversations
|
|
467
|
-
const stored = uploadAttachment(
|
|
570
|
+
const stored = uploadAttachment("shared.png", "image/png", "iVBORw0K");
|
|
468
571
|
linkAttachmentToMessage(msgA.id, stored.id, 0);
|
|
469
572
|
linkAttachmentToMessage(msgB.id, stored.id, 0);
|
|
470
573
|
|
|
@@ -478,18 +581,18 @@ describe('attachment reuse across thread lifecycles', () => {
|
|
|
478
581
|
expect(linkedB[0].id).toBe(stored.id);
|
|
479
582
|
});
|
|
480
583
|
|
|
481
|
-
test(
|
|
482
|
-
const convA = createConversation(
|
|
483
|
-
const convB = createConversation(
|
|
584
|
+
test("deleting conversation A does not orphan attachment reused in conversation B", async () => {
|
|
585
|
+
const convA = createConversation("Thread A");
|
|
586
|
+
const convB = createConversation("Thread B");
|
|
484
587
|
|
|
485
588
|
// deleteLastExchange deletes from the last user message onward,
|
|
486
589
|
// so we need a user message before the assistant message that carries the attachment.
|
|
487
|
-
await addMessage(convA.id,
|
|
488
|
-
const msgA = await addMessage(convA.id,
|
|
489
|
-
await addMessage(convB.id,
|
|
490
|
-
const msgB = await addMessage(convB.id,
|
|
590
|
+
await addMessage(convA.id, "user", "Please generate a chart");
|
|
591
|
+
const msgA = await addMessage(convA.id, "assistant", "Original");
|
|
592
|
+
await addMessage(convB.id, "user", "Show me the chart");
|
|
593
|
+
const msgB = await addMessage(convB.id, "assistant", "Reused");
|
|
491
594
|
|
|
492
|
-
const stored = uploadAttachment(
|
|
595
|
+
const stored = uploadAttachment("chart.png", "image/png", "AAAA");
|
|
493
596
|
linkAttachmentToMessage(msgA.id, stored.id, 0);
|
|
494
597
|
linkAttachmentToMessage(msgB.id, stored.id, 0);
|
|
495
598
|
|
|
@@ -506,16 +609,16 @@ describe('attachment reuse across thread lifecycles', () => {
|
|
|
506
609
|
expect(linkedB[0].id).toBe(stored.id);
|
|
507
610
|
});
|
|
508
611
|
|
|
509
|
-
test(
|
|
510
|
-
const convA = createConversation(
|
|
511
|
-
const convB = createConversation(
|
|
612
|
+
test("content-hash dedup works across conversations", async () => {
|
|
613
|
+
const convA = createConversation("Thread A");
|
|
614
|
+
const convB = createConversation("Thread B");
|
|
512
615
|
|
|
513
|
-
await addMessage(convA.id,
|
|
514
|
-
await addMessage(convB.id,
|
|
616
|
+
await addMessage(convA.id, "user", "upload in A");
|
|
617
|
+
await addMessage(convB.id, "user", "upload in B");
|
|
515
618
|
|
|
516
619
|
// Same content uploaded in two different conversation contexts
|
|
517
|
-
const first = uploadAttachment(
|
|
518
|
-
const second = uploadAttachment(
|
|
620
|
+
const first = uploadAttachment("photo.png", "image/png", "DEDUPCROSS");
|
|
621
|
+
const second = uploadAttachment("photo.png", "image/png", "DEDUPCROSS");
|
|
519
622
|
|
|
520
623
|
// Dedup returns the same attachment row
|
|
521
624
|
expect(second.id).toBe(first.id);
|
|
@@ -526,37 +629,58 @@ describe('attachment reuse across thread lifecycles', () => {
|
|
|
526
629
|
// Baseline: no private-thread visibility boundary for attachments
|
|
527
630
|
// ---------------------------------------------------------------------------
|
|
528
631
|
|
|
529
|
-
describe(
|
|
632
|
+
describe("no private-thread attachment visibility boundary", () => {
|
|
530
633
|
beforeEach(() => {
|
|
531
634
|
const db = getDb();
|
|
532
|
-
db.run(
|
|
533
|
-
db.run(
|
|
534
|
-
db.run(
|
|
535
|
-
db.run(
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
test(
|
|
539
|
-
const privateConv = createConversation({
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
635
|
+
db.run("DELETE FROM message_attachments");
|
|
636
|
+
db.run("DELETE FROM attachments");
|
|
637
|
+
db.run("DELETE FROM messages");
|
|
638
|
+
db.run("DELETE FROM conversations");
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
test("attachment from a private thread is visible via getAttachmentById (no thread scoping)", async () => {
|
|
642
|
+
const privateConv = createConversation({
|
|
643
|
+
title: "Secret",
|
|
644
|
+
threadType: "private",
|
|
645
|
+
});
|
|
646
|
+
expect(privateConv.threadType).toBe("private");
|
|
647
|
+
|
|
648
|
+
const msg = await addMessage(
|
|
649
|
+
privateConv.id,
|
|
650
|
+
"assistant",
|
|
651
|
+
"Private content",
|
|
652
|
+
);
|
|
653
|
+
const stored = uploadAttachment("secret.pdf", "application/pdf", "JVBER");
|
|
544
654
|
linkAttachmentToMessage(msg.id, stored.id, 0);
|
|
545
655
|
|
|
546
656
|
// Attachment is globally visible by ID — no thread-type filter exists
|
|
547
657
|
const fetched = getAttachmentById(stored.id);
|
|
548
658
|
expect(fetched).not.toBeNull();
|
|
549
|
-
expect(fetched!.originalFilename).toBe(
|
|
550
|
-
});
|
|
551
|
-
|
|
552
|
-
test(
|
|
553
|
-
const privateConv = createConversation({
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
const
|
|
659
|
+
expect(fetched!.originalFilename).toBe("secret.pdf");
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
test("attachment from private thread can be linked to a standard thread message", async () => {
|
|
663
|
+
const privateConv = createConversation({
|
|
664
|
+
title: "Private",
|
|
665
|
+
threadType: "private",
|
|
666
|
+
});
|
|
667
|
+
const standardConv = createConversation({
|
|
668
|
+
title: "Standard",
|
|
669
|
+
threadType: "standard",
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
const privateMsg = await addMessage(
|
|
673
|
+
privateConv.id,
|
|
674
|
+
"assistant",
|
|
675
|
+
"Private file",
|
|
676
|
+
);
|
|
677
|
+
const standardMsg = await addMessage(
|
|
678
|
+
standardConv.id,
|
|
679
|
+
"assistant",
|
|
680
|
+
"Reusing private file",
|
|
681
|
+
);
|
|
558
682
|
|
|
559
|
-
const stored = uploadAttachment(
|
|
683
|
+
const stored = uploadAttachment("private-doc.png", "image/png", "PRIVDATA");
|
|
560
684
|
linkAttachmentToMessage(privateMsg.id, stored.id, 0);
|
|
561
685
|
linkAttachmentToMessage(standardMsg.id, stored.id, 0);
|
|
562
686
|
|
|
@@ -569,10 +693,13 @@ describe('no private-thread attachment visibility boundary', () => {
|
|
|
569
693
|
expect(linkedStandard[0].id).toBe(stored.id);
|
|
570
694
|
});
|
|
571
695
|
|
|
572
|
-
test(
|
|
573
|
-
const privateConv = createConversation({
|
|
574
|
-
|
|
575
|
-
|
|
696
|
+
test("getAttachmentsForMessage returns private thread attachments", async () => {
|
|
697
|
+
const privateConv = createConversation({
|
|
698
|
+
title: "Private",
|
|
699
|
+
threadType: "private",
|
|
700
|
+
});
|
|
701
|
+
const msg = await addMessage(privateConv.id, "assistant", "Private media");
|
|
702
|
+
const stored = uploadAttachment("photo.jpg", "image/jpeg", "AAAA");
|
|
576
703
|
linkAttachmentToMessage(msg.id, stored.id, 0);
|
|
577
704
|
|
|
578
705
|
const linked = getAttachmentsForMessage(msg.id);
|
|
@@ -580,34 +707,62 @@ describe('no private-thread attachment visibility boundary', () => {
|
|
|
580
707
|
expect(linked[0].id).toBe(stored.id);
|
|
581
708
|
});
|
|
582
709
|
|
|
583
|
-
test(
|
|
584
|
-
createConversation({ title:
|
|
585
|
-
createConversation({ title:
|
|
710
|
+
test("content-hash dedup works across private and standard threads", () => {
|
|
711
|
+
createConversation({ title: "Private", threadType: "private" });
|
|
712
|
+
createConversation({ title: "Standard", threadType: "standard" });
|
|
586
713
|
|
|
587
714
|
// Same content uploaded in private and standard contexts
|
|
588
|
-
const fromPrivate = uploadAttachment(
|
|
589
|
-
|
|
715
|
+
const fromPrivate = uploadAttachment(
|
|
716
|
+
"file.png",
|
|
717
|
+
"image/png",
|
|
718
|
+
"CROSSTHREAD",
|
|
719
|
+
);
|
|
720
|
+
const fromStandard = uploadAttachment(
|
|
721
|
+
"file.png",
|
|
722
|
+
"image/png",
|
|
723
|
+
"CROSSTHREAD",
|
|
724
|
+
);
|
|
590
725
|
|
|
591
726
|
// Dedup returns the same row — no thread-type isolation
|
|
592
727
|
expect(fromStandard.id).toBe(fromPrivate.id);
|
|
593
728
|
});
|
|
594
729
|
|
|
595
|
-
test(
|
|
596
|
-
const privateConv = createConversation({
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
const
|
|
730
|
+
test("clearAll removes attachments from both private and standard threads", async () => {
|
|
731
|
+
const privateConv = createConversation({
|
|
732
|
+
title: "Private",
|
|
733
|
+
threadType: "private",
|
|
734
|
+
});
|
|
735
|
+
const standardConv = createConversation({
|
|
736
|
+
title: "Standard",
|
|
737
|
+
threadType: "standard",
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
const privateMsg = await addMessage(
|
|
741
|
+
privateConv.id,
|
|
742
|
+
"assistant",
|
|
743
|
+
"Private file",
|
|
744
|
+
);
|
|
745
|
+
const standardMsg = await addMessage(
|
|
746
|
+
standardConv.id,
|
|
747
|
+
"assistant",
|
|
748
|
+
"Standard file",
|
|
749
|
+
);
|
|
601
750
|
|
|
602
|
-
const att1 = uploadAttachment(
|
|
603
|
-
const att2 = uploadAttachment(
|
|
751
|
+
const att1 = uploadAttachment("private.png", "image/png", "PRIV");
|
|
752
|
+
const att2 = uploadAttachment("standard.png", "image/png", "STD");
|
|
604
753
|
linkAttachmentToMessage(privateMsg.id, att1.id, 0);
|
|
605
754
|
linkAttachmentToMessage(standardMsg.id, att2.id, 0);
|
|
606
755
|
|
|
607
756
|
clearAll();
|
|
608
757
|
|
|
609
|
-
const raw = (
|
|
610
|
-
|
|
758
|
+
const raw = (
|
|
759
|
+
getDb() as unknown as {
|
|
760
|
+
$client: import("bun:sqlite").Database;
|
|
761
|
+
}
|
|
762
|
+
).$client;
|
|
763
|
+
const attachmentCount = raw
|
|
764
|
+
.query("SELECT COUNT(*) AS c FROM attachments")
|
|
765
|
+
.get() as { c: number };
|
|
611
766
|
expect(attachmentCount.c).toBe(0);
|
|
612
767
|
});
|
|
613
768
|
});
|