@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,6 +1,6 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, mock,test } from
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
2
|
|
|
3
|
-
import type { Message, ToolDefinition } from
|
|
3
|
+
import type { Message, ToolDefinition } from "../providers/types.js";
|
|
4
4
|
|
|
5
5
|
// ---------------------------------------------------------------------------
|
|
6
6
|
// Mock Anthropic SDK — must be before importing the provider
|
|
@@ -10,15 +10,15 @@ let lastStreamParams: Record<string, unknown> | null = null;
|
|
|
10
10
|
let _lastStreamOptions: Record<string, unknown> | null = null;
|
|
11
11
|
|
|
12
12
|
const fakeResponse = {
|
|
13
|
-
content: [{ type:
|
|
14
|
-
model:
|
|
13
|
+
content: [{ type: "text", text: "Hello" }],
|
|
14
|
+
model: "claude-sonnet-4-6",
|
|
15
15
|
usage: {
|
|
16
16
|
input_tokens: 100,
|
|
17
17
|
output_tokens: 20,
|
|
18
18
|
cache_creation_input_tokens: 50,
|
|
19
19
|
cache_read_input_tokens: 30,
|
|
20
20
|
},
|
|
21
|
-
stop_reason:
|
|
21
|
+
stop_reason: "end_turn",
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
class FakeAPIError extends Error {
|
|
@@ -26,16 +26,19 @@ class FakeAPIError extends Error {
|
|
|
26
26
|
constructor(status: number, message: string) {
|
|
27
27
|
super(message);
|
|
28
28
|
this.status = status;
|
|
29
|
-
this.name =
|
|
29
|
+
this.name = "APIError";
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
mock.module(
|
|
33
|
+
mock.module("@anthropic-ai/sdk", () => ({
|
|
34
34
|
default: class MockAnthropic {
|
|
35
35
|
static APIError = FakeAPIError;
|
|
36
36
|
constructor() {}
|
|
37
37
|
messages = {
|
|
38
|
-
stream: (
|
|
38
|
+
stream: (
|
|
39
|
+
params: Record<string, unknown>,
|
|
40
|
+
options?: Record<string, unknown>,
|
|
41
|
+
) => {
|
|
39
42
|
lastStreamParams = JSON.parse(JSON.stringify(params));
|
|
40
43
|
_lastStreamOptions = options ?? null;
|
|
41
44
|
const handlers: Record<string, ((...args: unknown[]) => void)[]> = {};
|
|
@@ -46,7 +49,7 @@ mock.module('@anthropic-ai/sdk', () => ({
|
|
|
46
49
|
},
|
|
47
50
|
async finalMessage() {
|
|
48
51
|
// Fire text events
|
|
49
|
-
for (const cb of handlers[
|
|
52
|
+
for (const cb of handlers["text"] ?? []) cb("Hello");
|
|
50
53
|
return fakeResponse;
|
|
51
54
|
},
|
|
52
55
|
};
|
|
@@ -56,66 +59,94 @@ mock.module('@anthropic-ai/sdk', () => ({
|
|
|
56
59
|
}));
|
|
57
60
|
|
|
58
61
|
// Import after mocking
|
|
59
|
-
import {
|
|
62
|
+
import {
|
|
63
|
+
AnthropicProvider,
|
|
64
|
+
PLACEHOLDER_BLOCKS_OMITTED,
|
|
65
|
+
PLACEHOLDER_EMPTY_TURN,
|
|
66
|
+
} from "../providers/anthropic/client.js";
|
|
60
67
|
|
|
61
68
|
// ---------------------------------------------------------------------------
|
|
62
69
|
// Helpers
|
|
63
70
|
// ---------------------------------------------------------------------------
|
|
64
71
|
|
|
65
72
|
function userMsg(text: string): Message {
|
|
66
|
-
return { role:
|
|
73
|
+
return { role: "user", content: [{ type: "text", text }] };
|
|
67
74
|
}
|
|
68
75
|
|
|
69
76
|
function assistantMsg(text: string): Message {
|
|
70
|
-
return { role:
|
|
77
|
+
return { role: "assistant", content: [{ type: "text", text }] };
|
|
71
78
|
}
|
|
72
79
|
|
|
73
80
|
function toolUseMsg(id: string, name: string): Message {
|
|
74
81
|
return {
|
|
75
|
-
role:
|
|
76
|
-
content: [{ type:
|
|
82
|
+
role: "assistant",
|
|
83
|
+
content: [{ type: "tool_use", id, name, input: {} }],
|
|
77
84
|
};
|
|
78
85
|
}
|
|
79
86
|
|
|
80
87
|
function toolResultMsg(toolUseId: string, content: string): Message {
|
|
81
88
|
return {
|
|
82
|
-
role:
|
|
83
|
-
content: [
|
|
89
|
+
role: "user",
|
|
90
|
+
content: [
|
|
91
|
+
{ type: "tool_result", tool_use_id: toolUseId, content, is_error: false },
|
|
92
|
+
],
|
|
84
93
|
};
|
|
85
94
|
}
|
|
86
95
|
|
|
87
96
|
const sampleTools: ToolDefinition[] = [
|
|
88
|
-
{
|
|
89
|
-
|
|
90
|
-
|
|
97
|
+
{
|
|
98
|
+
name: "file_read",
|
|
99
|
+
description: "Read a file",
|
|
100
|
+
input_schema: { type: "object", properties: { path: { type: "string" } } },
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "file_write",
|
|
104
|
+
description: "Write a file",
|
|
105
|
+
input_schema: {
|
|
106
|
+
type: "object",
|
|
107
|
+
properties: { path: { type: "string" }, content: { type: "string" } },
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: "bash",
|
|
112
|
+
description: "Run shell commands",
|
|
113
|
+
input_schema: {
|
|
114
|
+
type: "object",
|
|
115
|
+
properties: { command: { type: "string" } },
|
|
116
|
+
},
|
|
117
|
+
},
|
|
91
118
|
];
|
|
92
119
|
|
|
93
120
|
// ---------------------------------------------------------------------------
|
|
94
121
|
// Tests — Cache-Control Characterization
|
|
95
122
|
// ---------------------------------------------------------------------------
|
|
96
123
|
|
|
97
|
-
describe(
|
|
124
|
+
describe("AnthropicProvider — Cache-Control Characterization", () => {
|
|
98
125
|
let provider: AnthropicProvider;
|
|
99
126
|
|
|
100
127
|
beforeEach(() => {
|
|
101
128
|
lastStreamParams = null;
|
|
102
129
|
_lastStreamOptions = null;
|
|
103
|
-
provider = new AnthropicProvider(
|
|
130
|
+
provider = new AnthropicProvider("sk-ant-test", "claude-sonnet-4-6");
|
|
104
131
|
});
|
|
105
132
|
|
|
106
133
|
// -----------------------------------------------------------------------
|
|
107
134
|
// System prompt cache control
|
|
108
135
|
// -----------------------------------------------------------------------
|
|
109
|
-
test(
|
|
110
|
-
await provider.sendMessage([userMsg(
|
|
136
|
+
test("system prompt has cache_control ephemeral", async () => {
|
|
137
|
+
await provider.sendMessage([userMsg("Hi")], undefined, "You are helpful.");
|
|
111
138
|
|
|
112
|
-
const system = lastStreamParams!.system as Array<{
|
|
139
|
+
const system = lastStreamParams!.system as Array<{
|
|
140
|
+
type: string;
|
|
141
|
+
text: string;
|
|
142
|
+
cache_control?: { type: string };
|
|
143
|
+
}>;
|
|
113
144
|
expect(system).toHaveLength(1);
|
|
114
|
-
expect(system[0].cache_control).toEqual({ type:
|
|
145
|
+
expect(system[0].cache_control).toEqual({ type: "ephemeral" });
|
|
115
146
|
});
|
|
116
147
|
|
|
117
|
-
test(
|
|
118
|
-
await provider.sendMessage([userMsg(
|
|
148
|
+
test("no system param when system prompt is omitted", async () => {
|
|
149
|
+
await provider.sendMessage([userMsg("Hi")]);
|
|
119
150
|
|
|
120
151
|
expect(lastStreamParams!.system).toBeUndefined();
|
|
121
152
|
});
|
|
@@ -123,10 +154,13 @@ describe('AnthropicProvider — Cache-Control Characterization', () => {
|
|
|
123
154
|
// -----------------------------------------------------------------------
|
|
124
155
|
// Tool cache control
|
|
125
156
|
// -----------------------------------------------------------------------
|
|
126
|
-
test(
|
|
127
|
-
await provider.sendMessage([userMsg(
|
|
157
|
+
test("only last tool definition includes cache_control", async () => {
|
|
158
|
+
await provider.sendMessage([userMsg("Hi")], sampleTools);
|
|
128
159
|
|
|
129
|
-
const tools = lastStreamParams!.tools as Array<{
|
|
160
|
+
const tools = lastStreamParams!.tools as Array<{
|
|
161
|
+
name: string;
|
|
162
|
+
cache_control?: { type: string };
|
|
163
|
+
}>;
|
|
130
164
|
expect(tools).toHaveLength(3);
|
|
131
165
|
|
|
132
166
|
// First two tools: no cache_control
|
|
@@ -134,19 +168,22 @@ describe('AnthropicProvider — Cache-Control Characterization', () => {
|
|
|
134
168
|
expect(tools[1].cache_control).toBeUndefined();
|
|
135
169
|
|
|
136
170
|
// Last tool: cache_control ephemeral
|
|
137
|
-
expect(tools[2].cache_control).toEqual({ type:
|
|
171
|
+
expect(tools[2].cache_control).toEqual({ type: "ephemeral" });
|
|
138
172
|
});
|
|
139
173
|
|
|
140
|
-
test(
|
|
141
|
-
await provider.sendMessage([userMsg(
|
|
174
|
+
test("single tool gets cache_control", async () => {
|
|
175
|
+
await provider.sendMessage([userMsg("Hi")], [sampleTools[0]]);
|
|
142
176
|
|
|
143
|
-
const tools = lastStreamParams!.tools as Array<{
|
|
177
|
+
const tools = lastStreamParams!.tools as Array<{
|
|
178
|
+
name: string;
|
|
179
|
+
cache_control?: { type: string };
|
|
180
|
+
}>;
|
|
144
181
|
expect(tools).toHaveLength(1);
|
|
145
|
-
expect(tools[0].cache_control).toEqual({ type:
|
|
182
|
+
expect(tools[0].cache_control).toEqual({ type: "ephemeral" });
|
|
146
183
|
});
|
|
147
184
|
|
|
148
|
-
test(
|
|
149
|
-
await provider.sendMessage([userMsg(
|
|
185
|
+
test("no tools param when tools are omitted", async () => {
|
|
186
|
+
await provider.sendMessage([userMsg("Hi")]);
|
|
150
187
|
|
|
151
188
|
expect(lastStreamParams!.tools).toBeUndefined();
|
|
152
189
|
});
|
|
@@ -154,71 +191,89 @@ describe('AnthropicProvider — Cache-Control Characterization', () => {
|
|
|
154
191
|
// -----------------------------------------------------------------------
|
|
155
192
|
// User turn cache breakpoints — last two user turns
|
|
156
193
|
// -----------------------------------------------------------------------
|
|
157
|
-
test(
|
|
158
|
-
await provider.sendMessage([userMsg(
|
|
194
|
+
test("last user turn gets cache_control on trailing content block", async () => {
|
|
195
|
+
await provider.sendMessage([userMsg("Hello")]);
|
|
159
196
|
|
|
160
197
|
const messages = lastStreamParams!.messages as Array<{
|
|
161
198
|
role: string;
|
|
162
|
-
content: Array<{
|
|
199
|
+
content: Array<{
|
|
200
|
+
type: string;
|
|
201
|
+
text: string;
|
|
202
|
+
cache_control?: { type: string };
|
|
203
|
+
}>;
|
|
163
204
|
}>;
|
|
164
205
|
const lastUser = messages[messages.length - 1];
|
|
165
|
-
expect(lastUser.role).toBe(
|
|
166
|
-
expect(lastUser.content[lastUser.content.length - 1].cache_control).toEqual(
|
|
206
|
+
expect(lastUser.role).toBe("user");
|
|
207
|
+
expect(lastUser.content[lastUser.content.length - 1].cache_control).toEqual(
|
|
208
|
+
{ type: "ephemeral" },
|
|
209
|
+
);
|
|
167
210
|
});
|
|
168
211
|
|
|
169
|
-
test(
|
|
212
|
+
test("last two user turns get cache_control, earlier turns do not", async () => {
|
|
170
213
|
const messages: Message[] = [
|
|
171
|
-
userMsg(
|
|
172
|
-
assistantMsg(
|
|
173
|
-
userMsg(
|
|
174
|
-
assistantMsg(
|
|
175
|
-
userMsg(
|
|
214
|
+
userMsg("Turn 1"), // user turn 0 — no cache
|
|
215
|
+
assistantMsg("Response 1"),
|
|
216
|
+
userMsg("Turn 2"), // user turn 1 — cache (second-to-last)
|
|
217
|
+
assistantMsg("Response 2"),
|
|
218
|
+
userMsg("Turn 3"), // user turn 2 — cache (last)
|
|
176
219
|
];
|
|
177
220
|
await provider.sendMessage(messages);
|
|
178
221
|
|
|
179
222
|
const sent = lastStreamParams!.messages as Array<{
|
|
180
223
|
role: string;
|
|
181
|
-
content: Array<{
|
|
224
|
+
content: Array<{
|
|
225
|
+
type: string;
|
|
226
|
+
text: string;
|
|
227
|
+
cache_control?: { type: string };
|
|
228
|
+
}>;
|
|
182
229
|
}>;
|
|
183
230
|
|
|
184
231
|
// Find user messages in order
|
|
185
|
-
const userMessages = sent.filter(m => m.role ===
|
|
232
|
+
const userMessages = sent.filter((m) => m.role === "user");
|
|
186
233
|
expect(userMessages).toHaveLength(3);
|
|
187
234
|
|
|
188
235
|
// First user turn: no cache_control
|
|
189
|
-
const firstUserLastBlock =
|
|
236
|
+
const firstUserLastBlock =
|
|
237
|
+
userMessages[0].content[userMessages[0].content.length - 1];
|
|
190
238
|
expect(firstUserLastBlock.cache_control).toBeUndefined();
|
|
191
239
|
|
|
192
240
|
// Second user turn: cache_control ephemeral
|
|
193
|
-
const secondUserLastBlock =
|
|
194
|
-
|
|
241
|
+
const secondUserLastBlock =
|
|
242
|
+
userMessages[1].content[userMessages[1].content.length - 1];
|
|
243
|
+
expect(secondUserLastBlock.cache_control).toEqual({ type: "ephemeral" });
|
|
195
244
|
|
|
196
245
|
// Third user turn: cache_control ephemeral
|
|
197
|
-
const thirdUserLastBlock =
|
|
198
|
-
|
|
246
|
+
const thirdUserLastBlock =
|
|
247
|
+
userMessages[2].content[userMessages[2].content.length - 1];
|
|
248
|
+
expect(thirdUserLastBlock.cache_control).toEqual({ type: "ephemeral" });
|
|
199
249
|
});
|
|
200
250
|
|
|
201
|
-
test(
|
|
202
|
-
await provider.sendMessage([userMsg(
|
|
251
|
+
test("single user turn gets cache_control (only one user = last one)", async () => {
|
|
252
|
+
await provider.sendMessage([userMsg("Only turn")]);
|
|
203
253
|
|
|
204
254
|
const sent = lastStreamParams!.messages as Array<{
|
|
205
255
|
role: string;
|
|
206
|
-
content: Array<{
|
|
256
|
+
content: Array<{
|
|
257
|
+
type: string;
|
|
258
|
+
text: string;
|
|
259
|
+
cache_control?: { type: string };
|
|
260
|
+
}>;
|
|
207
261
|
}>;
|
|
208
|
-
const userMessages = sent.filter(m => m.role ===
|
|
262
|
+
const userMessages = sent.filter((m) => m.role === "user");
|
|
209
263
|
expect(userMessages).toHaveLength(1);
|
|
210
|
-
expect(
|
|
211
|
-
.
|
|
264
|
+
expect(
|
|
265
|
+
userMessages[0].content[userMessages[0].content.length - 1].cache_control,
|
|
266
|
+
).toEqual({ type: "ephemeral" });
|
|
212
267
|
});
|
|
213
268
|
|
|
214
269
|
// -----------------------------------------------------------------------
|
|
215
270
|
// User turn with tool_result — cache breakpoint on trailing block
|
|
216
271
|
// -----------------------------------------------------------------------
|
|
217
|
-
test(
|
|
272
|
+
test("user turn containing tool_result gets cache_control on last block", async () => {
|
|
218
273
|
const messages: Message[] = [
|
|
219
|
-
userMsg(
|
|
220
|
-
toolUseMsg(
|
|
221
|
-
toolResultMsg(
|
|
274
|
+
userMsg("Read file"),
|
|
275
|
+
toolUseMsg("tu_1", "file_read"),
|
|
276
|
+
toolResultMsg("tu_1", "file contents here"),
|
|
222
277
|
];
|
|
223
278
|
await provider.sendMessage(messages);
|
|
224
279
|
|
|
@@ -226,30 +281,34 @@ describe('AnthropicProvider — Cache-Control Characterization', () => {
|
|
|
226
281
|
role: string;
|
|
227
282
|
content: Array<{ type: string; cache_control?: { type: string } }>;
|
|
228
283
|
}>;
|
|
229
|
-
const userMsgs = sent.filter(m => m.role ===
|
|
284
|
+
const userMsgs = sent.filter((m) => m.role === "user");
|
|
230
285
|
// Both user turns (first user msg + tool_result msg) should get cache
|
|
231
286
|
for (const u of userMsgs) {
|
|
232
287
|
const last = u.content[u.content.length - 1];
|
|
233
|
-
expect(last.cache_control).toEqual({ type:
|
|
288
|
+
expect(last.cache_control).toEqual({ type: "ephemeral" });
|
|
234
289
|
}
|
|
235
290
|
});
|
|
236
291
|
|
|
237
292
|
// -----------------------------------------------------------------------
|
|
238
293
|
// Negative: assistant messages never get cache_control
|
|
239
294
|
// -----------------------------------------------------------------------
|
|
240
|
-
test(
|
|
295
|
+
test("assistant messages do not get cache_control", async () => {
|
|
241
296
|
const messages: Message[] = [
|
|
242
|
-
userMsg(
|
|
243
|
-
assistantMsg(
|
|
244
|
-
userMsg(
|
|
297
|
+
userMsg("Hi"),
|
|
298
|
+
assistantMsg("Hello!"),
|
|
299
|
+
userMsg("How are you?"),
|
|
245
300
|
];
|
|
246
301
|
await provider.sendMessage(messages);
|
|
247
302
|
|
|
248
303
|
const sent = lastStreamParams!.messages as Array<{
|
|
249
304
|
role: string;
|
|
250
|
-
content: Array<{
|
|
305
|
+
content: Array<{
|
|
306
|
+
type: string;
|
|
307
|
+
text: string;
|
|
308
|
+
cache_control?: { type: string };
|
|
309
|
+
}>;
|
|
251
310
|
}>;
|
|
252
|
-
const assistantMsgs = sent.filter(m => m.role ===
|
|
311
|
+
const assistantMsgs = sent.filter((m) => m.role === "assistant");
|
|
253
312
|
for (const a of assistantMsgs) {
|
|
254
313
|
if (Array.isArray(a.content)) {
|
|
255
314
|
for (const block of a.content) {
|
|
@@ -262,30 +321,34 @@ describe('AnthropicProvider — Cache-Control Characterization', () => {
|
|
|
262
321
|
// -----------------------------------------------------------------------
|
|
263
322
|
// Multi-block user message: cache lands on LAST block
|
|
264
323
|
// -----------------------------------------------------------------------
|
|
265
|
-
test(
|
|
324
|
+
test("multi-block user message caches only the last block", async () => {
|
|
266
325
|
const multiBlockUser: Message = {
|
|
267
|
-
role:
|
|
326
|
+
role: "user",
|
|
268
327
|
content: [
|
|
269
|
-
{ type:
|
|
270
|
-
{ type:
|
|
328
|
+
{ type: "text", text: "First block" },
|
|
329
|
+
{ type: "text", text: "Second block" },
|
|
271
330
|
],
|
|
272
331
|
};
|
|
273
332
|
await provider.sendMessage([multiBlockUser]);
|
|
274
333
|
|
|
275
334
|
const sent = lastStreamParams!.messages as Array<{
|
|
276
335
|
role: string;
|
|
277
|
-
content: Array<{
|
|
336
|
+
content: Array<{
|
|
337
|
+
type: string;
|
|
338
|
+
text: string;
|
|
339
|
+
cache_control?: { type: string };
|
|
340
|
+
}>;
|
|
278
341
|
}>;
|
|
279
342
|
const user = sent[0];
|
|
280
343
|
expect(user.content[0].cache_control).toBeUndefined();
|
|
281
|
-
expect(user.content[1].cache_control).toEqual({ type:
|
|
344
|
+
expect(user.content[1].cache_control).toEqual({ type: "ephemeral" });
|
|
282
345
|
});
|
|
283
346
|
|
|
284
347
|
// -----------------------------------------------------------------------
|
|
285
348
|
// Usage: cache tokens are aggregated into inputTokens
|
|
286
349
|
// -----------------------------------------------------------------------
|
|
287
|
-
test(
|
|
288
|
-
const result = await provider.sendMessage([userMsg(
|
|
350
|
+
test("usage aggregates cache tokens into inputTokens", async () => {
|
|
351
|
+
const result = await provider.sendMessage([userMsg("Hi")]);
|
|
289
352
|
|
|
290
353
|
expect(result.usage.inputTokens).toBe(100 + 50 + 30); // input + creation + read
|
|
291
354
|
expect(result.usage.cacheCreationInputTokens).toBe(50);
|
|
@@ -295,44 +358,61 @@ describe('AnthropicProvider — Cache-Control Characterization', () => {
|
|
|
295
358
|
// -----------------------------------------------------------------------
|
|
296
359
|
// Cache compatibility with workspace context injection
|
|
297
360
|
// -----------------------------------------------------------------------
|
|
298
|
-
test(
|
|
361
|
+
test("workspace-prepended user message caches trailing block, not workspace block", async () => {
|
|
299
362
|
// Simulates what applyRuntimeInjections does: prepend workspace block, keep user text as trailing
|
|
300
363
|
const workspaceInjectedUser: Message = {
|
|
301
|
-
role:
|
|
364
|
+
role: "user",
|
|
302
365
|
content: [
|
|
303
|
-
{
|
|
304
|
-
|
|
366
|
+
{
|
|
367
|
+
type: "text",
|
|
368
|
+
text: "<workspace_top_level>\nRoot: /sandbox\nDirectories: src, tests\n</workspace_top_level>",
|
|
369
|
+
},
|
|
370
|
+
{ type: "text", text: "What files are in src?" },
|
|
305
371
|
],
|
|
306
372
|
};
|
|
307
373
|
await provider.sendMessage([workspaceInjectedUser]);
|
|
308
374
|
|
|
309
375
|
const sent = lastStreamParams!.messages as Array<{
|
|
310
376
|
role: string;
|
|
311
|
-
content: Array<{
|
|
377
|
+
content: Array<{
|
|
378
|
+
type: string;
|
|
379
|
+
text: string;
|
|
380
|
+
cache_control?: { type: string };
|
|
381
|
+
}>;
|
|
312
382
|
}>;
|
|
313
383
|
const user = sent[0];
|
|
314
384
|
expect(user.content).toHaveLength(2);
|
|
315
385
|
// Workspace block (first): no cache_control
|
|
316
386
|
expect(user.content[0].cache_control).toBeUndefined();
|
|
317
387
|
// User text (last): gets cache_control
|
|
318
|
-
expect(user.content[1].cache_control).toEqual({ type:
|
|
388
|
+
expect(user.content[1].cache_control).toEqual({ type: "ephemeral" });
|
|
319
389
|
});
|
|
320
390
|
|
|
321
|
-
test(
|
|
391
|
+
test("workspace + dynamic profile: cache still lands on trailing block", async () => {
|
|
322
392
|
// Simulates workspace prepended + dynamic profile appended
|
|
323
393
|
const injectedUser: Message = {
|
|
324
|
-
role:
|
|
394
|
+
role: "user",
|
|
325
395
|
content: [
|
|
326
|
-
{
|
|
327
|
-
|
|
328
|
-
|
|
396
|
+
{
|
|
397
|
+
type: "text",
|
|
398
|
+
text: "<workspace_top_level>\nRoot: /sandbox\nDirectories: src, tests\n</workspace_top_level>",
|
|
399
|
+
},
|
|
400
|
+
{ type: "text", text: "Help me debug this" },
|
|
401
|
+
{
|
|
402
|
+
type: "text",
|
|
403
|
+
text: "<dynamic_profile>\nUser prefers TypeScript.\n</dynamic_profile>",
|
|
404
|
+
},
|
|
329
405
|
],
|
|
330
406
|
};
|
|
331
407
|
await provider.sendMessage([injectedUser]);
|
|
332
408
|
|
|
333
409
|
const sent = lastStreamParams!.messages as Array<{
|
|
334
410
|
role: string;
|
|
335
|
-
content: Array<{
|
|
411
|
+
content: Array<{
|
|
412
|
+
type: string;
|
|
413
|
+
text: string;
|
|
414
|
+
cache_control?: { type: string };
|
|
415
|
+
}>;
|
|
336
416
|
}>;
|
|
337
417
|
const user = sent[0];
|
|
338
418
|
expect(user.content).toHaveLength(3);
|
|
@@ -341,41 +421,47 @@ describe('AnthropicProvider — Cache-Control Characterization', () => {
|
|
|
341
421
|
// User text (middle): no cache
|
|
342
422
|
expect(user.content[1].cache_control).toBeUndefined();
|
|
343
423
|
// Dynamic profile (last): gets cache_control
|
|
344
|
-
expect(user.content[2].cache_control).toEqual({ type:
|
|
424
|
+
expect(user.content[2].cache_control).toEqual({ type: "ephemeral" });
|
|
345
425
|
});
|
|
346
426
|
|
|
347
427
|
// -----------------------------------------------------------------------
|
|
348
428
|
// ensureToolPairing — tool_use / tool_result pairing repair
|
|
349
429
|
// -----------------------------------------------------------------------
|
|
350
430
|
|
|
351
|
-
test(
|
|
431
|
+
test("tool_use with missing tool_result gets synthetic result injected", async () => {
|
|
352
432
|
const messages: Message[] = [
|
|
353
|
-
userMsg(
|
|
354
|
-
toolUseMsg(
|
|
355
|
-
userMsg(
|
|
433
|
+
userMsg("Do something"),
|
|
434
|
+
toolUseMsg("tu_1", "file_read"),
|
|
435
|
+
userMsg("Thanks"), // user text but no tool_result for tu_1
|
|
356
436
|
];
|
|
357
437
|
await provider.sendMessage(messages);
|
|
358
438
|
|
|
359
439
|
const sent = lastStreamParams!.messages as Array<{
|
|
360
440
|
role: string;
|
|
361
|
-
content: Array<{
|
|
441
|
+
content: Array<{
|
|
442
|
+
type: string;
|
|
443
|
+
tool_use_id?: string;
|
|
444
|
+
is_error?: boolean;
|
|
445
|
+
}>;
|
|
362
446
|
}>;
|
|
363
447
|
|
|
364
448
|
// The second user message (after assistant) should now contain a synthetic tool_result
|
|
365
449
|
const userAfterAssistant = sent[2];
|
|
366
|
-
expect(userAfterAssistant.role).toBe(
|
|
450
|
+
expect(userAfterAssistant.role).toBe("user");
|
|
367
451
|
// Anthropic expects tool_result blocks to start the immediate next user message.
|
|
368
|
-
expect(userAfterAssistant.content[0].type).toBe(
|
|
369
|
-
const toolResults = userAfterAssistant.content.filter(
|
|
452
|
+
expect(userAfterAssistant.content[0].type).toBe("tool_result");
|
|
453
|
+
const toolResults = userAfterAssistant.content.filter(
|
|
454
|
+
(b) => b.type === "tool_result",
|
|
455
|
+
);
|
|
370
456
|
expect(toolResults).toHaveLength(1);
|
|
371
|
-
expect(toolResults[0].tool_use_id).toBe(
|
|
457
|
+
expect(toolResults[0].tool_use_id).toBe("tu_1");
|
|
372
458
|
expect(toolResults[0].is_error).toBe(true);
|
|
373
459
|
});
|
|
374
460
|
|
|
375
|
-
test(
|
|
461
|
+
test("tool_use at end of messages gets synthetic user message appended", async () => {
|
|
376
462
|
const messages: Message[] = [
|
|
377
|
-
userMsg(
|
|
378
|
-
toolUseMsg(
|
|
463
|
+
userMsg("Read file"),
|
|
464
|
+
toolUseMsg("tu_end", "file_read"),
|
|
379
465
|
];
|
|
380
466
|
await provider.sendMessage(messages);
|
|
381
467
|
|
|
@@ -386,17 +472,17 @@ describe('AnthropicProvider — Cache-Control Characterization', () => {
|
|
|
386
472
|
|
|
387
473
|
// A synthetic user message should have been appended
|
|
388
474
|
expect(sent).toHaveLength(3);
|
|
389
|
-
expect(sent[2].role).toBe(
|
|
390
|
-
const toolResults = sent[2].content.filter((b) => b.type ===
|
|
475
|
+
expect(sent[2].role).toBe("user");
|
|
476
|
+
const toolResults = sent[2].content.filter((b) => b.type === "tool_result");
|
|
391
477
|
expect(toolResults).toHaveLength(1);
|
|
392
|
-
expect(toolResults[0].tool_use_id).toBe(
|
|
478
|
+
expect(toolResults[0].tool_use_id).toBe("tu_end");
|
|
393
479
|
});
|
|
394
480
|
|
|
395
|
-
test(
|
|
481
|
+
test("tool_use with matching tool_result passes through unchanged", async () => {
|
|
396
482
|
const messages: Message[] = [
|
|
397
|
-
userMsg(
|
|
398
|
-
toolUseMsg(
|
|
399
|
-
toolResultMsg(
|
|
483
|
+
userMsg("Read file"),
|
|
484
|
+
toolUseMsg("tu_ok", "file_read"),
|
|
485
|
+
toolResultMsg("tu_ok", "file contents"),
|
|
400
486
|
];
|
|
401
487
|
await provider.sendMessage(messages);
|
|
402
488
|
|
|
@@ -407,30 +493,43 @@ describe('AnthropicProvider — Cache-Control Characterization', () => {
|
|
|
407
493
|
|
|
408
494
|
// No synthetic messages or blocks added
|
|
409
495
|
expect(sent).toHaveLength(3);
|
|
410
|
-
const toolResults = sent[2].content.filter((b) => b.type ===
|
|
496
|
+
const toolResults = sent[2].content.filter((b) => b.type === "tool_result");
|
|
411
497
|
expect(toolResults).toHaveLength(1);
|
|
412
|
-
expect(toolResults[0].tool_use_id).toBe(
|
|
498
|
+
expect(toolResults[0].tool_use_id).toBe("tu_ok");
|
|
413
499
|
});
|
|
414
500
|
|
|
415
|
-
test(
|
|
501
|
+
test("reconstructs collapsed assistant/tool_result/user timeline before sending", async () => {
|
|
416
502
|
const messages: Message[] = [
|
|
417
|
-
userMsg(
|
|
503
|
+
userMsg("Read files"),
|
|
418
504
|
{
|
|
419
|
-
role:
|
|
505
|
+
role: "assistant",
|
|
420
506
|
content: [
|
|
421
|
-
{ type:
|
|
422
|
-
{ type:
|
|
423
|
-
{ type:
|
|
424
|
-
{ type:
|
|
507
|
+
{ type: "text", text: "Working on it." },
|
|
508
|
+
{ type: "tool_use", id: "tu_a", name: "file_read", input: {} },
|
|
509
|
+
{ type: "tool_use", id: "tu_b", name: "bash", input: {} },
|
|
510
|
+
{ type: "text", text: "One moment." },
|
|
425
511
|
],
|
|
426
512
|
},
|
|
427
513
|
{
|
|
428
|
-
role:
|
|
514
|
+
role: "user",
|
|
429
515
|
content: [
|
|
430
|
-
{
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
516
|
+
{
|
|
517
|
+
type: "text",
|
|
518
|
+
text: "<workspace_top_level>\nRoot: /sandbox\n</workspace_top_level>",
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
type: "tool_result",
|
|
522
|
+
tool_use_id: "tu_b",
|
|
523
|
+
content: "result B",
|
|
524
|
+
is_error: false,
|
|
525
|
+
},
|
|
526
|
+
{ type: "text", text: "continue please" },
|
|
527
|
+
{
|
|
528
|
+
type: "tool_result",
|
|
529
|
+
tool_use_id: "tu_a",
|
|
530
|
+
content: "result A",
|
|
531
|
+
is_error: false,
|
|
532
|
+
},
|
|
434
533
|
],
|
|
435
534
|
},
|
|
436
535
|
];
|
|
@@ -445,66 +544,97 @@ describe('AnthropicProvider — Cache-Control Characterization', () => {
|
|
|
445
544
|
// user, assistant(tool_use...), user(tool_results), assistant(carryover text), user(remaining text)
|
|
446
545
|
expect(sent).toHaveLength(5);
|
|
447
546
|
|
|
448
|
-
expect(sent[1].role).toBe(
|
|
449
|
-
expect(sent[1].content.map((b) => b.type)).toEqual([
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
547
|
+
expect(sent[1].role).toBe("assistant");
|
|
548
|
+
expect(sent[1].content.map((b) => b.type)).toEqual([
|
|
549
|
+
"text",
|
|
550
|
+
"tool_use",
|
|
551
|
+
"tool_use",
|
|
552
|
+
]);
|
|
553
|
+
|
|
554
|
+
expect(sent[2].role).toBe("user");
|
|
555
|
+
expect(sent[2].content[0]).toMatchObject({
|
|
556
|
+
type: "tool_result",
|
|
557
|
+
tool_use_id: "tu_a",
|
|
558
|
+
});
|
|
559
|
+
expect(sent[2].content[1]).toMatchObject({
|
|
560
|
+
type: "tool_result",
|
|
561
|
+
tool_use_id: "tu_b",
|
|
562
|
+
});
|
|
454
563
|
expect(sent[2].content).toHaveLength(2);
|
|
455
564
|
|
|
456
|
-
expect(sent[3].role).toBe(
|
|
457
|
-
expect(sent[3].content.map((b) => b.type)).toEqual([
|
|
565
|
+
expect(sent[3].role).toBe("assistant");
|
|
566
|
+
expect(sent[3].content.map((b) => b.type)).toEqual(["text"]);
|
|
458
567
|
|
|
459
|
-
expect(sent[4].role).toBe(
|
|
460
|
-
expect(sent[4].content.map((b) => b.type)).toEqual([
|
|
568
|
+
expect(sent[4].role).toBe("user");
|
|
569
|
+
expect(sent[4].content.map((b) => b.type)).toEqual(["text", "text"]);
|
|
461
570
|
});
|
|
462
571
|
|
|
463
|
-
test(
|
|
572
|
+
test("multiple tool_use with partial results gets missing ones filled", async () => {
|
|
464
573
|
const messages: Message[] = [
|
|
465
|
-
userMsg(
|
|
574
|
+
userMsg("Do things"),
|
|
466
575
|
{
|
|
467
|
-
role:
|
|
576
|
+
role: "assistant",
|
|
468
577
|
content: [
|
|
469
|
-
{ type:
|
|
470
|
-
{ type:
|
|
471
|
-
{ type:
|
|
578
|
+
{ type: "tool_use", id: "tu_a", name: "file_read", input: {} },
|
|
579
|
+
{ type: "tool_use", id: "tu_b", name: "file_write", input: {} },
|
|
580
|
+
{ type: "tool_use", id: "tu_c", name: "bash", input: {} },
|
|
472
581
|
],
|
|
473
582
|
},
|
|
474
583
|
// Only tu_a has a result
|
|
475
|
-
toolResultMsg(
|
|
584
|
+
toolResultMsg("tu_a", "result A"),
|
|
476
585
|
];
|
|
477
586
|
await provider.sendMessage(messages);
|
|
478
587
|
|
|
479
588
|
const sent = lastStreamParams!.messages as Array<{
|
|
480
589
|
role: string;
|
|
481
|
-
content: Array<{
|
|
590
|
+
content: Array<{
|
|
591
|
+
type: string;
|
|
592
|
+
tool_use_id?: string;
|
|
593
|
+
is_error?: boolean;
|
|
594
|
+
}>;
|
|
482
595
|
}>;
|
|
483
596
|
|
|
484
597
|
const userAfterAssistant = sent[2];
|
|
485
|
-
const toolResults = userAfterAssistant.content.filter(
|
|
598
|
+
const toolResults = userAfterAssistant.content.filter(
|
|
599
|
+
(b) => b.type === "tool_result",
|
|
600
|
+
);
|
|
486
601
|
expect(toolResults).toHaveLength(3);
|
|
487
|
-
expect(userAfterAssistant.content[0]).toMatchObject({
|
|
488
|
-
|
|
489
|
-
|
|
602
|
+
expect(userAfterAssistant.content[0]).toMatchObject({
|
|
603
|
+
type: "tool_result",
|
|
604
|
+
tool_use_id: "tu_a",
|
|
605
|
+
});
|
|
606
|
+
expect(userAfterAssistant.content[1]).toMatchObject({
|
|
607
|
+
type: "tool_result",
|
|
608
|
+
tool_use_id: "tu_b",
|
|
609
|
+
});
|
|
610
|
+
expect(userAfterAssistant.content[2]).toMatchObject({
|
|
611
|
+
type: "tool_result",
|
|
612
|
+
tool_use_id: "tu_c",
|
|
613
|
+
});
|
|
490
614
|
|
|
491
615
|
// tu_a: original result
|
|
492
|
-
expect(
|
|
616
|
+
expect(
|
|
617
|
+
toolResults.find((r) => r.tool_use_id === "tu_a")!.is_error,
|
|
618
|
+
).toBeFalsy();
|
|
493
619
|
// tu_b and tu_c: synthetic
|
|
494
|
-
expect(toolResults.find((r) => r.tool_use_id ===
|
|
495
|
-
|
|
620
|
+
expect(toolResults.find((r) => r.tool_use_id === "tu_b")!.is_error).toBe(
|
|
621
|
+
true,
|
|
622
|
+
);
|
|
623
|
+
expect(toolResults.find((r) => r.tool_use_id === "tu_c")!.is_error).toBe(
|
|
624
|
+
true,
|
|
625
|
+
);
|
|
496
626
|
});
|
|
497
627
|
|
|
498
|
-
test(
|
|
628
|
+
test("consecutive assistant messages with tool_use each get synthetic results", async () => {
|
|
499
629
|
const messages: Message[] = [
|
|
500
|
-
userMsg(
|
|
501
|
-
toolUseMsg(
|
|
630
|
+
userMsg("Start"),
|
|
631
|
+
toolUseMsg("tu_1", "file_read"),
|
|
502
632
|
// missing tool_result for tu_1, then another assistant
|
|
503
633
|
{
|
|
504
|
-
role:
|
|
505
|
-
content: [{ type:
|
|
634
|
+
role: "assistant",
|
|
635
|
+
content: [{ type: "tool_use", id: "tu_2", name: "bash", input: {} }],
|
|
506
636
|
},
|
|
507
|
-
userMsg(
|
|
637
|
+
userMsg("Done"),
|
|
508
638
|
];
|
|
509
639
|
await provider.sendMessage(messages);
|
|
510
640
|
|
|
@@ -515,24 +645,34 @@ describe('AnthropicProvider — Cache-Control Characterization', () => {
|
|
|
515
645
|
|
|
516
646
|
// Should be: user, assistant(tu_1), synthetic_user(tu_1), assistant(tu_2), user_with_synthetic(tu_2)
|
|
517
647
|
expect(sent).toHaveLength(5);
|
|
518
|
-
expect(sent[0].role).toBe(
|
|
519
|
-
expect(sent[1].role).toBe(
|
|
520
|
-
expect(sent[2].role).toBe(
|
|
521
|
-
expect(
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
648
|
+
expect(sent[0].role).toBe("user");
|
|
649
|
+
expect(sent[1].role).toBe("assistant");
|
|
650
|
+
expect(sent[2].role).toBe("user");
|
|
651
|
+
expect(
|
|
652
|
+
sent[2].content.some(
|
|
653
|
+
(b) => b.type === "tool_result" && b.tool_use_id === "tu_1",
|
|
654
|
+
),
|
|
655
|
+
).toBe(true);
|
|
656
|
+
expect(sent[3].role).toBe("assistant");
|
|
657
|
+
expect(sent[4].role).toBe("user");
|
|
658
|
+
expect(
|
|
659
|
+
sent[4].content.some(
|
|
660
|
+
(b) => b.type === "tool_result" && b.tool_use_id === "tu_2",
|
|
661
|
+
),
|
|
662
|
+
).toBe(true);
|
|
525
663
|
});
|
|
526
664
|
|
|
527
|
-
test(
|
|
665
|
+
test("assistant message with only unknown blocks gets placeholder text", async () => {
|
|
528
666
|
const messages: Message[] = [
|
|
529
|
-
userMsg(
|
|
667
|
+
userMsg("Start"),
|
|
530
668
|
// Assistant message with only ui_surface (unknown type) — will be filtered
|
|
531
669
|
{
|
|
532
|
-
role:
|
|
533
|
-
content: [
|
|
670
|
+
role: "assistant",
|
|
671
|
+
content: [
|
|
672
|
+
{ type: "ui_surface" as "text", text: "this will be filtered" },
|
|
673
|
+
],
|
|
534
674
|
},
|
|
535
|
-
userMsg(
|
|
675
|
+
userMsg("Continue"),
|
|
536
676
|
];
|
|
537
677
|
await provider.sendMessage(messages);
|
|
538
678
|
|
|
@@ -543,26 +683,26 @@ describe('AnthropicProvider — Cache-Control Characterization', () => {
|
|
|
543
683
|
|
|
544
684
|
// Should preserve alternation: user, assistant (with placeholder), user
|
|
545
685
|
expect(sent).toHaveLength(3);
|
|
546
|
-
expect(sent[0].role).toBe(
|
|
547
|
-
expect(sent[1].role).toBe(
|
|
686
|
+
expect(sent[0].role).toBe("user");
|
|
687
|
+
expect(sent[1].role).toBe("assistant");
|
|
548
688
|
expect(sent[1].content).toHaveLength(1);
|
|
549
|
-
expect(sent[1].content[0].type).toBe(
|
|
689
|
+
expect(sent[1].content[0].type).toBe("text");
|
|
550
690
|
expect(sent[1].content[0].text).toBe(PLACEHOLDER_BLOCKS_OMITTED);
|
|
551
|
-
expect(sent[2].role).toBe(
|
|
691
|
+
expect(sent[2].role).toBe("user");
|
|
552
692
|
});
|
|
553
693
|
|
|
554
|
-
test(
|
|
694
|
+
test("assistant message with mix of known and unknown blocks keeps known blocks", async () => {
|
|
555
695
|
const messages: Message[] = [
|
|
556
|
-
userMsg(
|
|
696
|
+
userMsg("Start"),
|
|
557
697
|
{
|
|
558
|
-
role:
|
|
698
|
+
role: "assistant",
|
|
559
699
|
content: [
|
|
560
|
-
{ type:
|
|
561
|
-
{ type:
|
|
562
|
-
{ type:
|
|
700
|
+
{ type: "text", text: "Valid text" },
|
|
701
|
+
{ type: "ui_surface" as "text", text: "this will be filtered" },
|
|
702
|
+
{ type: "text", text: "More valid text" },
|
|
563
703
|
],
|
|
564
704
|
},
|
|
565
|
-
userMsg(
|
|
705
|
+
userMsg("Continue"),
|
|
566
706
|
];
|
|
567
707
|
await provider.sendMessage(messages);
|
|
568
708
|
|
|
@@ -572,23 +712,23 @@ describe('AnthropicProvider — Cache-Control Characterization', () => {
|
|
|
572
712
|
}>;
|
|
573
713
|
|
|
574
714
|
expect(sent).toHaveLength(3);
|
|
575
|
-
expect(sent[1].role).toBe(
|
|
715
|
+
expect(sent[1].role).toBe("assistant");
|
|
576
716
|
expect(sent[1].content).toHaveLength(2);
|
|
577
|
-
expect(sent[1].content[0].text).toBe(
|
|
578
|
-
expect(sent[1].content[1].text).toBe(
|
|
717
|
+
expect(sent[1].content[0].text).toBe("Valid text");
|
|
718
|
+
expect(sent[1].content[1].text).toBe("More valid text");
|
|
579
719
|
});
|
|
580
720
|
|
|
581
|
-
test(
|
|
721
|
+
test("assistant message with only whitespace text gets placeholder to preserve alternation", async () => {
|
|
582
722
|
const messages: Message[] = [
|
|
583
|
-
userMsg(
|
|
723
|
+
userMsg("Start"),
|
|
584
724
|
{
|
|
585
|
-
role:
|
|
725
|
+
role: "assistant",
|
|
586
726
|
content: [
|
|
587
|
-
{ type:
|
|
588
|
-
{ type:
|
|
727
|
+
{ type: "text", text: " " },
|
|
728
|
+
{ type: "text", text: "\n\t" },
|
|
589
729
|
],
|
|
590
730
|
},
|
|
591
|
-
userMsg(
|
|
731
|
+
userMsg("Continue"),
|
|
592
732
|
];
|
|
593
733
|
await provider.sendMessage(messages);
|
|
594
734
|
|
|
@@ -600,32 +740,32 @@ describe('AnthropicProvider — Cache-Control Characterization', () => {
|
|
|
600
740
|
// Whitespace-only assistant messages between user messages must be preserved
|
|
601
741
|
// with a placeholder to maintain Anthropic's strict role alternation
|
|
602
742
|
expect(sent).toHaveLength(3);
|
|
603
|
-
expect(sent[0].role).toBe(
|
|
604
|
-
expect(sent[0].content[0].text).toBe(
|
|
605
|
-
expect(sent[1].role).toBe(
|
|
743
|
+
expect(sent[0].role).toBe("user");
|
|
744
|
+
expect(sent[0].content[0].text).toBe("Start");
|
|
745
|
+
expect(sent[1].role).toBe("assistant");
|
|
606
746
|
expect(sent[1].content).toHaveLength(1);
|
|
607
|
-
expect(sent[1].content[0].type).toBe(
|
|
747
|
+
expect(sent[1].content[0].type).toBe("text");
|
|
608
748
|
expect(sent[1].content[0].text).toBe(PLACEHOLDER_EMPTY_TURN);
|
|
609
|
-
expect(sent[2].role).toBe(
|
|
610
|
-
expect(sent[2].content[0].text).toBe(
|
|
749
|
+
expect(sent[2].role).toBe("user");
|
|
750
|
+
expect(sent[2].content[0].text).toBe("Continue");
|
|
611
751
|
});
|
|
612
752
|
|
|
613
|
-
test(
|
|
753
|
+
test("unknown-blocks-only assistant followed by empty user does not produce consecutive same-role messages", async () => {
|
|
614
754
|
// Same edge case as the empty-assistant test below, but triggered by an
|
|
615
755
|
// assistant turn whose blocks are all unknown (e.g. ui_surface). The turn
|
|
616
756
|
// becomes a [internal blocks omitted] placeholder which must also be
|
|
617
757
|
// removed when adjacent to a real assistant message.
|
|
618
758
|
const messages: Message[] = [
|
|
619
|
-
userMsg(
|
|
759
|
+
userMsg("Start"),
|
|
620
760
|
{
|
|
621
|
-
role:
|
|
622
|
-
content: [{ type:
|
|
761
|
+
role: "assistant",
|
|
762
|
+
content: [{ type: "ui_surface" as "text", text: "invisible" }], // unknown → placeholder
|
|
623
763
|
},
|
|
624
764
|
{
|
|
625
|
-
role:
|
|
626
|
-
content: [{ type:
|
|
765
|
+
role: "user",
|
|
766
|
+
content: [{ type: "text", text: " \n " }], // whitespace-only → empty after filtering
|
|
627
767
|
},
|
|
628
|
-
assistantMsg(
|
|
768
|
+
assistantMsg("Real response"),
|
|
629
769
|
];
|
|
630
770
|
await provider.sendMessage(messages);
|
|
631
771
|
|
|
@@ -640,22 +780,22 @@ describe('AnthropicProvider — Cache-Control Characterization', () => {
|
|
|
640
780
|
}
|
|
641
781
|
});
|
|
642
782
|
|
|
643
|
-
test(
|
|
783
|
+
test("empty assistant followed by empty user does not produce consecutive same-role messages", async () => {
|
|
644
784
|
// Edge case: an empty assistant turn gets a placeholder injected, but if
|
|
645
785
|
// the following user turn also filters to empty (e.g. whitespace-only),
|
|
646
786
|
// the user turn is dropped and the placeholder ends up adjacent to the
|
|
647
787
|
// next real assistant turn — producing consecutive assistant roles.
|
|
648
788
|
const messages: Message[] = [
|
|
649
|
-
userMsg(
|
|
789
|
+
userMsg("Start"),
|
|
650
790
|
{
|
|
651
|
-
role:
|
|
652
|
-
content: [{ type:
|
|
791
|
+
role: "assistant",
|
|
792
|
+
content: [{ type: "text", text: " " }], // whitespace-only → empty after filtering
|
|
653
793
|
},
|
|
654
794
|
{
|
|
655
|
-
role:
|
|
656
|
-
content: [{ type:
|
|
795
|
+
role: "user",
|
|
796
|
+
content: [{ type: "text", text: " \n " }], // whitespace-only → empty after filtering
|
|
657
797
|
},
|
|
658
|
-
assistantMsg(
|
|
798
|
+
assistantMsg("Real response"),
|
|
659
799
|
];
|
|
660
800
|
await provider.sendMessage(messages);
|
|
661
801
|
|
|
@@ -674,27 +814,32 @@ describe('AnthropicProvider — Cache-Control Characterization', () => {
|
|
|
674
814
|
// Workspace context injection + cache control
|
|
675
815
|
// -----------------------------------------------------------------------
|
|
676
816
|
|
|
677
|
-
test(
|
|
817
|
+
test("carryover with tool_result-only user turn emits synthetic user message", async () => {
|
|
678
818
|
// This tests the fix for consecutive assistant messages when:
|
|
679
819
|
// - assistant has both tool_use blocks and trailing non-tool blocks (carryover)
|
|
680
820
|
// - following user message contains ONLY tool_result blocks (no other content)
|
|
681
821
|
const messages: Message[] = [
|
|
682
|
-
userMsg(
|
|
822
|
+
userMsg("Read file"),
|
|
683
823
|
{
|
|
684
|
-
role:
|
|
824
|
+
role: "assistant",
|
|
685
825
|
content: [
|
|
686
|
-
{ type:
|
|
687
|
-
{ type:
|
|
826
|
+
{ type: "tool_use", id: "tu_1", name: "file_read", input: {} },
|
|
827
|
+
{ type: "text", text: "Checking the file now." }, // carryover content
|
|
688
828
|
],
|
|
689
829
|
},
|
|
690
830
|
{
|
|
691
|
-
role:
|
|
831
|
+
role: "user",
|
|
692
832
|
content: [
|
|
693
833
|
// ONLY tool_result, no other content
|
|
694
|
-
{
|
|
834
|
+
{
|
|
835
|
+
type: "tool_result",
|
|
836
|
+
tool_use_id: "tu_1",
|
|
837
|
+
content: "file contents",
|
|
838
|
+
is_error: false,
|
|
839
|
+
},
|
|
695
840
|
],
|
|
696
841
|
},
|
|
697
|
-
assistantMsg(
|
|
842
|
+
assistantMsg("Next response"),
|
|
698
843
|
];
|
|
699
844
|
await provider.sendMessage(messages);
|
|
700
845
|
|
|
@@ -711,47 +856,56 @@ describe('AnthropicProvider — Cache-Control Characterization', () => {
|
|
|
711
856
|
// 5. user((continue)) <-- synthetic user message to maintain alternation
|
|
712
857
|
// 6. assistant(Next response)
|
|
713
858
|
expect(sent).toHaveLength(6);
|
|
714
|
-
expect(sent[0].role).toBe(
|
|
715
|
-
expect(sent[1].role).toBe(
|
|
716
|
-
expect(sent[1].content[0].type).toBe(
|
|
717
|
-
expect(sent[2].role).toBe(
|
|
718
|
-
expect(sent[2].content[0].type).toBe(
|
|
719
|
-
expect(sent[3].role).toBe(
|
|
720
|
-
expect(sent[3].content[0].type).toBe(
|
|
721
|
-
expect(sent[3].content[0].text).toBe(
|
|
722
|
-
expect(sent[4].role).toBe(
|
|
723
|
-
expect(sent[4].content[0].type).toBe(
|
|
724
|
-
expect(sent[4].content[0].text).toBe(
|
|
725
|
-
expect(sent[5].role).toBe(
|
|
726
|
-
expect(sent[5].content[0].text).toBe(
|
|
859
|
+
expect(sent[0].role).toBe("user");
|
|
860
|
+
expect(sent[1].role).toBe("assistant");
|
|
861
|
+
expect(sent[1].content[0].type).toBe("tool_use");
|
|
862
|
+
expect(sent[2].role).toBe("user");
|
|
863
|
+
expect(sent[2].content[0].type).toBe("tool_result");
|
|
864
|
+
expect(sent[3].role).toBe("assistant");
|
|
865
|
+
expect(sent[3].content[0].type).toBe("text");
|
|
866
|
+
expect(sent[3].content[0].text).toBe("Checking the file now.");
|
|
867
|
+
expect(sent[4].role).toBe("user");
|
|
868
|
+
expect(sent[4].content[0].type).toBe("text");
|
|
869
|
+
expect(sent[4].content[0].text).toBe("(continue)");
|
|
870
|
+
expect(sent[5].role).toBe("assistant");
|
|
871
|
+
expect(sent[5].content[0].text).toBe("Next response");
|
|
727
872
|
});
|
|
728
873
|
|
|
729
|
-
test(
|
|
874
|
+
test("multi-turn with workspace injection: cache on last two user turns only", async () => {
|
|
730
875
|
const messages: Message[] = [
|
|
731
876
|
// Turn 1: workspace + user text (should NOT get cache - it's the 3rd-to-last user turn)
|
|
732
877
|
{
|
|
733
|
-
role:
|
|
878
|
+
role: "user",
|
|
734
879
|
content: [
|
|
735
|
-
{
|
|
736
|
-
|
|
880
|
+
{
|
|
881
|
+
type: "text",
|
|
882
|
+
text: "<workspace_top_level>\nRoot: /sandbox\nDirectories: src\n</workspace_top_level>",
|
|
883
|
+
},
|
|
884
|
+
{ type: "text", text: "Turn 1" },
|
|
737
885
|
],
|
|
738
886
|
},
|
|
739
|
-
assistantMsg(
|
|
887
|
+
assistantMsg("Response 1"),
|
|
740
888
|
// Turn 2: workspace + user text (should get cache - second-to-last)
|
|
741
889
|
{
|
|
742
|
-
role:
|
|
890
|
+
role: "user",
|
|
743
891
|
content: [
|
|
744
|
-
{
|
|
745
|
-
|
|
892
|
+
{
|
|
893
|
+
type: "text",
|
|
894
|
+
text: "<workspace_top_level>\nRoot: /sandbox\nDirectories: src, lib\n</workspace_top_level>",
|
|
895
|
+
},
|
|
896
|
+
{ type: "text", text: "Turn 2" },
|
|
746
897
|
],
|
|
747
898
|
},
|
|
748
|
-
assistantMsg(
|
|
899
|
+
assistantMsg("Response 2"),
|
|
749
900
|
// Turn 3: workspace + user text (should get cache - last)
|
|
750
901
|
{
|
|
751
|
-
role:
|
|
902
|
+
role: "user",
|
|
752
903
|
content: [
|
|
753
|
-
{
|
|
754
|
-
|
|
904
|
+
{
|
|
905
|
+
type: "text",
|
|
906
|
+
text: "<workspace_top_level>\nRoot: /sandbox\nDirectories: src, lib, docs\n</workspace_top_level>",
|
|
907
|
+
},
|
|
908
|
+
{ type: "text", text: "Turn 3" },
|
|
755
909
|
],
|
|
756
910
|
},
|
|
757
911
|
];
|
|
@@ -759,9 +913,13 @@ describe('AnthropicProvider — Cache-Control Characterization', () => {
|
|
|
759
913
|
|
|
760
914
|
const sent = lastStreamParams!.messages as Array<{
|
|
761
915
|
role: string;
|
|
762
|
-
content: Array<{
|
|
916
|
+
content: Array<{
|
|
917
|
+
type: string;
|
|
918
|
+
text: string;
|
|
919
|
+
cache_control?: { type: string };
|
|
920
|
+
}>;
|
|
763
921
|
}>;
|
|
764
|
-
const userMsgs = sent.filter(m => m.role ===
|
|
922
|
+
const userMsgs = sent.filter((m) => m.role === "user");
|
|
765
923
|
expect(userMsgs).toHaveLength(3);
|
|
766
924
|
|
|
767
925
|
// Turn 1: no cache on any block
|
|
@@ -770,10 +928,10 @@ describe('AnthropicProvider — Cache-Control Characterization', () => {
|
|
|
770
928
|
|
|
771
929
|
// Turn 2: cache on last block only
|
|
772
930
|
expect(userMsgs[1].content[0].cache_control).toBeUndefined();
|
|
773
|
-
expect(userMsgs[1].content[1].cache_control).toEqual({ type:
|
|
931
|
+
expect(userMsgs[1].content[1].cache_control).toEqual({ type: "ephemeral" });
|
|
774
932
|
|
|
775
933
|
// Turn 3: cache on last block only
|
|
776
934
|
expect(userMsgs[2].content[0].cache_control).toBeUndefined();
|
|
777
|
-
expect(userMsgs[2].content[1].cache_control).toEqual({ type:
|
|
935
|
+
expect(userMsgs[2].content[1].cache_control).toEqual({ type: "ephemeral" });
|
|
778
936
|
});
|
|
779
937
|
});
|