@vellumai/assistant 0.6.4 → 0.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierignore +5 -0
- package/AGENTS.md +9 -1
- package/ARCHITECTURE.md +43 -49
- package/Dockerfile +17 -3
- package/README.md +3 -4
- package/__tests__/permissions/gateway-threshold-reader.test.ts +283 -0
- package/bun.lock +8 -3
- package/docs/architecture/integrations.md +33 -59
- package/docs/architecture/memory.md +25 -30
- package/docs/architecture/security.md +19 -18
- package/docs/browser-use-architecture-phase2.md +63 -20
- package/docs/error-handling.md +111 -0
- package/docs/plugins.md +761 -0
- package/docs/skills.md +10 -10
- package/docs/stt-provider-onboarding.md +2 -1
- package/examples/plugins/echo/README.md +132 -0
- package/examples/plugins/echo/package.json +17 -0
- package/examples/plugins/echo/register.ts +187 -0
- package/knip.json +9 -2
- package/node_modules/@vellumai/ces-contracts/package.json +2 -1
- package/node_modules/@vellumai/ces-contracts/src/__tests__/trust-rules.test.ts +471 -0
- package/node_modules/@vellumai/ces-contracts/src/trust-rules.ts +398 -4
- package/node_modules/@vellumai/credential-storage/bun.lock +2 -2
- package/node_modules/@vellumai/credential-storage/package.json +2 -2
- package/node_modules/@vellumai/credential-storage/src/oauth-runtime.ts +20 -2
- package/node_modules/@vellumai/egress-proxy/bun.lock +2 -2
- package/node_modules/@vellumai/egress-proxy/package.json +2 -2
- package/node_modules/@vellumai/egress-proxy/src/types.ts +19 -0
- package/openapi.yaml +334 -78
- package/package.json +6 -3
- package/scripts/generate-openapi.ts +50 -11
- package/src/__tests__/agent-loop-callsite-precedence.test.ts +318 -0
- package/src/__tests__/agent-loop-sentry-hygiene.test.ts +137 -0
- package/src/__tests__/agent-loop.test.ts +112 -1
- package/src/__tests__/anthropic-error-formatting.test.ts +98 -0
- package/src/__tests__/anthropic-provider.test.ts +171 -2
- package/src/__tests__/app-compiler.test.ts +57 -0
- package/src/__tests__/approval-cascade.test.ts +36 -10
- package/src/__tests__/approval-routes-http.test.ts +134 -10
- package/src/__tests__/assistant-attachments.test.ts +44 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +29 -0
- package/src/__tests__/auto-analysis-end-to-end.test.ts +1 -0
- package/src/__tests__/avatar-generator.test.ts +4 -2
- package/src/__tests__/browser-fill-credential.test.ts +1 -1
- package/src/__tests__/browser-identifier-parity-guard.test.ts +53 -0
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +23 -33
- package/src/__tests__/browser-skill-endstate.test.ts +51 -182
- package/src/__tests__/btw-routes.test.ts +47 -1
- package/src/__tests__/bundled-asset.test.ts +6 -6
- package/src/__tests__/call-controller.test.ts +1 -2
- package/src/__tests__/call-site-routing-provider.test.ts +214 -0
- package/src/__tests__/catalog-cache.test.ts +96 -4
- package/src/__tests__/channel-approval-routes.test.ts +4 -4
- package/src/__tests__/channel-reply-delivery.test.ts +300 -2
- package/src/__tests__/checker.test.ts +870 -655
- package/src/__tests__/circuit-breaker-pipeline.test.ts +406 -0
- package/src/__tests__/cli-command-risk-guard.test.ts +30 -33
- package/src/__tests__/compaction-events.test.ts +501 -0
- package/src/__tests__/compaction-pipeline.test.ts +210 -0
- package/src/__tests__/compaction-strip-metadata-clear.test.ts +181 -0
- package/src/__tests__/compaction-timeout-recovery.test.ts +262 -0
- package/src/__tests__/compaction.benchmark.test.ts +1 -1
- package/src/__tests__/config-analysis.test.ts +11 -28
- package/src/__tests__/config-loader-backfill.test.ts +174 -0
- package/src/__tests__/config-loader-corrupt.test.ts +183 -0
- package/src/__tests__/config-loader-quarantine-bulletin.test.ts +202 -0
- package/src/__tests__/config-model-image-provider.test.ts +110 -0
- package/src/__tests__/config-schema-cmd.test.ts +11 -5
- package/src/__tests__/config-schema.test.ts +440 -114
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +0 -4
- package/src/__tests__/config-watcher.test.ts +2 -2
- package/src/__tests__/contact-store-user-file.test.ts +72 -73
- package/src/__tests__/contacts-tools.test.ts +26 -0
- package/src/__tests__/contacts-write.test.ts +4 -4
- package/src/__tests__/context-overflow-policy.test.ts +7 -7
- package/src/__tests__/context-token-estimator.test.ts +191 -1
- package/src/__tests__/context-window-manager.test.ts +883 -4
- package/src/__tests__/conversation-abort-tool-results.test.ts +32 -15
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +86 -46
- package/src/__tests__/conversation-agent-loop.test.ts +435 -216
- package/src/__tests__/conversation-attachments.test.ts +1 -1
- package/src/__tests__/conversation-confirmation-signals.test.ts +36 -10
- package/src/__tests__/conversation-error.test.ts +37 -6
- package/src/__tests__/conversation-history-web-search.test.ts +7 -0
- package/src/__tests__/conversation-init.benchmark.test.ts +34 -12
- package/src/__tests__/conversation-lifecycle.test.ts +336 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +27 -10
- package/src/__tests__/conversation-pairing.test.ts +174 -10
- package/src/__tests__/conversation-pre-run-repair.test.ts +32 -15
- package/src/__tests__/conversation-process-callsite.test.ts +309 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +44 -21
- package/src/__tests__/conversation-queue.test.ts +68 -38
- package/src/__tests__/conversation-routes-disk-view.test.ts +36 -7
- package/src/__tests__/conversation-routes-slash-commands.test.ts +31 -3
- package/src/__tests__/conversation-runtime-assembly.test.ts +2877 -152
- package/src/__tests__/conversation-runtime-workspace.test.ts +35 -50
- package/src/__tests__/conversation-seed-composer.test.ts +2 -2
- package/src/__tests__/conversation-skill-tools.test.ts +12 -146
- package/src/__tests__/conversation-slash-queue.test.ts +39 -19
- package/src/__tests__/conversation-slash-unknown.test.ts +53 -16
- package/src/__tests__/conversation-speed-override.test.ts +36 -12
- package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +1035 -0
- package/src/__tests__/conversation-surfaces-standalone.test.ts +630 -0
- package/src/__tests__/conversation-title-service.test.ts +118 -2
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +41 -2
- package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +1 -1
- package/src/__tests__/conversation-unread-route.test.ts +2 -2
- package/src/__tests__/conversation-usage.test.ts +4 -2
- package/src/__tests__/conversation-workspace-cache-state.test.ts +33 -9
- package/src/__tests__/conversation-workspace-injection.test.ts +46 -15
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +46 -15
- package/src/__tests__/credential-broker-browser-fill.test.ts +110 -0
- package/src/__tests__/credential-health-service.test.ts +78 -9
- package/src/__tests__/credential-security-invariants.test.ts +5 -2
- package/src/__tests__/credential-storage-oauth-compat.test.ts +18 -0
- package/src/__tests__/credential-storage-static-compat.test.ts +28 -0
- package/src/__tests__/credential-vault-unit.test.ts +135 -19
- package/src/__tests__/credentials-cli.test.ts +1 -9
- package/src/__tests__/cross-provider-web-search.test.ts +84 -0
- package/src/__tests__/daemon-server-persist-and-process-callsite.test.ts +92 -0
- package/src/__tests__/db-schedule-syntax-migration.test.ts +1 -0
- package/src/__tests__/delete-propagation.test.ts +437 -0
- package/src/__tests__/dm-backfill.test.ts +417 -0
- package/src/__tests__/dm-persistence.test.ts +227 -0
- package/src/__tests__/edit-propagation.test.ts +280 -0
- package/src/__tests__/empty-response-pipeline.test.ts +305 -0
- package/src/__tests__/ephemeral-permissions.test.ts +93 -3
- package/src/__tests__/estimator-calibration-integration.test.ts +208 -0
- package/src/__tests__/estimator-calibration.test.ts +213 -0
- package/src/__tests__/extension-id-sync-guard.test.ts +29 -10
- package/src/__tests__/file-write-tool.test.ts +151 -1
- package/src/__tests__/filing-service.test.ts +255 -0
- package/src/__tests__/first-greeting.test.ts +247 -5
- package/src/__tests__/gemini-provider.test.ts +0 -3
- package/src/__tests__/guardian-grant-minting.test.ts +8 -0
- package/src/__tests__/headless-browser-interactions.test.ts +1 -1
- package/src/__tests__/headless-browser-mode.test.ts +57 -0
- package/src/__tests__/heartbeat-service.test.ts +96 -15
- package/src/__tests__/history-repair-pipeline.test.ts +399 -0
- package/src/__tests__/host-browser-e2e-cloud.test.ts +307 -0
- package/src/__tests__/host-browser-e2e-self-hosted.test.ts +3 -3
- package/src/__tests__/host-proxy-interface.test.ts +36 -2
- package/src/__tests__/host-shell-tool.test.ts +124 -18
- package/src/__tests__/http-user-message-parity.test.ts +29 -1
- package/src/__tests__/image-credentials.test.ts +137 -0
- package/src/__tests__/image-service-dispatcher.test.ts +186 -0
- package/src/__tests__/inbound-slack-persistence.test.ts +340 -0
- package/src/__tests__/injector-chain.test.ts +526 -0
- package/src/__tests__/intent-routing.test.ts +1 -66
- package/src/__tests__/llm-call-pipeline.test.ts +285 -0
- package/src/__tests__/llm-catalog-parity.test.ts +174 -0
- package/src/__tests__/llm-context-normalization.test.ts +121 -0
- package/src/__tests__/llm-resolver.test.ts +214 -0
- package/src/__tests__/llm-schema.test.ts +223 -0
- package/src/__tests__/managed-proxy-context.test.ts +6 -2
- package/src/__tests__/media-generate-image.test.ts +119 -13
- package/src/__tests__/memory-retrieval-pipeline.test.ts +401 -0
- package/src/__tests__/memory-upsert-concurrency.test.ts +1 -0
- package/src/__tests__/messaging-skill-split.test.ts +3 -34
- package/src/__tests__/migration-import-from-url.test.ts +621 -0
- package/src/__tests__/model-intents.test.ts +11 -83
- package/src/__tests__/notification-broadcaster.test.ts +3 -3
- package/src/__tests__/notification-decision-fallback.test.ts +0 -10
- package/src/__tests__/notification-decision-identity.test.ts +0 -9
- package/src/__tests__/notification-decision-recipient-context.test.ts +0 -9
- package/src/__tests__/notification-decision-strategy.test.ts +0 -11
- package/src/__tests__/notification-schedule-notify-dedup.test.ts +108 -0
- package/src/__tests__/oauth-apps-routes.test.ts +1 -1
- package/src/__tests__/oauth-cli.test.ts +14 -12
- package/src/__tests__/oauth-connect-orchestrator.test.ts +4 -13
- package/src/__tests__/oauth-provider-serializer.test.ts +6 -4
- package/src/__tests__/oauth-provider-visibility.test.ts +3 -5
- package/src/__tests__/oauth-providers-routes.test.ts +3 -2
- package/src/__tests__/oauth-store.test.ts +46 -78
- package/src/__tests__/oauth2-gateway-transport.test.ts +8 -3
- package/src/__tests__/oauth2-refresh-retry.test.ts +279 -0
- package/src/__tests__/onboarding-template-contract.test.ts +16 -64
- package/src/__tests__/openai-image-service.test.ts +368 -0
- package/src/__tests__/openai-provider.test.ts +7 -0
- package/src/__tests__/openai-responses-provider.test.ts +396 -0
- package/src/__tests__/openrouter-provider-only.test.ts +135 -0
- package/src/__tests__/outbound-slack-persistence.test.ts +293 -0
- package/src/__tests__/overflow-reduce-pipeline.test.ts +676 -0
- package/src/__tests__/permission-checker-host-gate.test.ts +1 -25
- package/src/__tests__/permission-mode.test.ts +16 -0
- package/src/__tests__/permission-types.test.ts +0 -1
- package/src/__tests__/persist-onboarding-artifacts.test.ts +266 -0
- package/src/__tests__/persistence-pipeline.test.ts +377 -0
- package/src/__tests__/persona-resolver.test.ts +13 -13
- package/src/__tests__/pipeline-runner.test.ts +565 -0
- package/src/__tests__/pkb-autoinject.test.ts +37 -1
- package/src/__tests__/platform-bash-auto-approve.test.ts +1 -1
- package/src/__tests__/platform.test.ts +5 -2
- package/src/__tests__/plugin-bootstrap.test.ts +483 -0
- package/src/__tests__/plugin-registry.test.ts +273 -0
- package/src/__tests__/plugin-route-contribution.test.ts +288 -0
- package/src/__tests__/plugin-skill-contribution.test.ts +367 -0
- package/src/__tests__/plugin-tool-contribution.test.ts +286 -0
- package/src/__tests__/plugin-types.test.ts +320 -0
- package/src/__tests__/pricing.test.ts +93 -14
- package/src/__tests__/profiler-routes.test.ts +1 -1
- package/src/__tests__/provider-commit-message-generator.test.ts +14 -84
- package/src/__tests__/provider-env-vars-scope.test.ts +52 -0
- package/src/__tests__/provider-error-scenarios.test.ts +135 -6
- package/src/__tests__/provider-managed-proxy-integration.test.ts +42 -11
- package/src/__tests__/provider-registry-ollama.test.ts +1 -2
- package/src/__tests__/proxy-approval-callback.test.ts +69 -9
- package/src/__tests__/reaction-persistence.test.ts +561 -0
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +1 -0
- package/src/__tests__/registry.test.ts +0 -2
- package/src/__tests__/relay-server.test.ts +1 -1
- package/src/__tests__/require-fresh-approval.test.ts +1 -1
- package/src/__tests__/retry-openrouter-only-normalization.test.ts +136 -0
- package/src/__tests__/retry-thinking-tool-choice.test.ts +226 -0
- package/src/__tests__/risk-classifier-parity.test.ts +230 -0
- package/src/__tests__/sanitize-config-for-transfer.test.ts +78 -1
- package/src/__tests__/schedule-routes.test.ts +131 -1
- package/src/__tests__/scheduler-recurrence.test.ts +14 -70
- package/src/__tests__/scheduler-reuse-conversation.test.ts +10 -50
- package/src/__tests__/secret-detection-handler.test.ts +0 -10
- package/src/__tests__/secret-ingress-http.test.ts +28 -0
- package/src/__tests__/secret-prompter-channel-fallback.test.ts +125 -0
- package/src/__tests__/secret-routes-managed-proxy.test.ts +2 -3
- package/src/__tests__/secret-scanner-executor.test.ts +1 -1
- package/src/__tests__/send-endpoint-busy.test.ts +29 -1
- package/src/__tests__/server-history-render.test.ts +31 -0
- package/src/__tests__/shell-identity.test.ts +0 -134
- package/src/__tests__/shell-parser-property.test.ts +13 -13
- package/src/__tests__/skill-cache-store.test.ts +182 -0
- package/src/__tests__/skills.test.ts +19 -33
- package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
- package/src/__tests__/slack-skill.test.ts +3 -8
- package/src/__tests__/starter-bundle.test.ts +35 -0
- package/src/__tests__/subagent-call-site-routing.test.ts +280 -0
- package/src/__tests__/suggestion-routes.test.ts +259 -3
- package/src/__tests__/system-prompt.test.ts +22 -35
- package/src/__tests__/task-memory-cleanup.test.ts +1 -0
- package/src/__tests__/task-runner.test.ts +3 -1
- package/src/__tests__/task-scheduler.test.ts +3 -15
- package/src/__tests__/tcc-sandbox-deny.test.ts +198 -0
- package/src/__tests__/terminal-tools.test.ts +8 -0
- package/src/__tests__/test-preload.ts +11 -0
- package/src/__tests__/test-support/browser-skill-harness.ts +2 -52
- package/src/__tests__/thread-backfill.test.ts +941 -0
- package/src/__tests__/title-generate-pipeline.test.ts +224 -0
- package/src/__tests__/token-estimate-pipeline.test.ts +431 -0
- package/src/__tests__/tool-error-pipeline.test.ts +244 -0
- package/src/__tests__/tool-execute-pipeline.test.ts +431 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -8
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -2
- package/src/__tests__/tool-executor-shell-integration.test.ts +7 -10
- package/src/__tests__/tool-executor.test.ts +201 -94
- package/src/__tests__/tool-result-truncate-pipeline.test.ts +356 -0
- package/src/__tests__/tool-result-truncation.test.ts +0 -110
- package/src/__tests__/trust-store.test.ts +442 -109
- package/src/__tests__/update-bulletin-job.test.ts +389 -0
- package/src/__tests__/usage-cache-backfill-migration.test.ts +3 -1
- package/src/__tests__/user-plugin-loader.test.ts +191 -0
- package/src/__tests__/verification-control-plane-policy.test.ts +1 -22
- package/src/__tests__/voice-session-bridge.test.ts +39 -0
- package/src/__tests__/volume-security-guard.test.ts +3 -2
- package/src/__tests__/web-search-history.test.ts +337 -0
- package/src/__tests__/workspace-migration-039-drop-legacy-llm-keys.test.ts +343 -0
- package/src/__tests__/workspace-migration-043-release-notes-latex-rendering.test.ts +202 -0
- package/src/__tests__/workspace-migration-045-release-notes-meet-avatar.test.ts +210 -0
- package/src/__tests__/workspace-migration-046-seed-conversation-starters-callsite.test.ts +185 -0
- package/src/__tests__/workspace-migration-049-release-notes-default-sonnet.test.ts +100 -0
- package/src/__tests__/workspace-migration-050-seed-main-agent-opus-callsite.test.ts +171 -0
- package/src/__tests__/workspace-migration-051-seed-conversation-summarization-callsite.test.ts +252 -0
- package/src/__tests__/workspace-migration-drop-user-md.test.ts +11 -11
- package/src/__tests__/workspace-migration-remove-hooks.test.ts +99 -0
- package/src/__tests__/workspace-migration-unify-llm-callsite-configs.test.ts +841 -0
- package/src/__tests__/workspace-policy.test.ts +22 -16
- package/src/acp/client-handler.ts +1 -2
- package/src/agent/loop.ts +545 -115
- package/src/approvals/__tests__/guardian-feed-event.test.ts +304 -0
- package/src/approvals/guardian-request-resolvers.ts +80 -0
- package/src/avatar/resvg-lazy.test.ts +136 -0
- package/src/avatar/resvg-lazy.ts +82 -9
- package/src/avatar/traits-png-sync.ts +21 -1
- package/src/backup/__tests__/backup-worker.test.ts +2 -13
- package/src/backup/backup-worker.ts +3 -15
- package/src/browser/__tests__/operations.test.ts +163 -0
- package/src/browser/identifiers.ts +51 -0
- package/src/browser/operations.ts +660 -0
- package/src/browser/types.ts +81 -0
- package/src/bundler/app-compiler.ts +84 -1
- package/src/calls/call-state.ts +2 -2
- package/src/calls/guardian-question-copy.ts +2 -2
- package/src/calls/telephony-stt-routing.ts +1 -1
- package/src/calls/voice-session-bridge.ts +1 -0
- package/src/channels/__tests__/types.test.ts +3 -3
- package/src/channels/types.ts +6 -4
- package/src/cli/AGENTS.md +1 -1
- package/src/cli/__tests__/notifications.test.ts +87 -211
- package/src/cli/commands/__tests__/attachment.test.ts +438 -0
- package/src/cli/commands/__tests__/backup.test.ts +1 -1
- package/src/cli/commands/__tests__/browser.test.ts +554 -0
- package/src/cli/commands/__tests__/cache.test.ts +623 -0
- package/src/cli/commands/__tests__/email-list.test.ts +6 -0
- package/src/cli/commands/__tests__/email-send.test.ts +93 -1
- package/src/cli/commands/__tests__/image-generation.test.ts +886 -0
- package/src/cli/commands/__tests__/inference-send.test.ts +463 -0
- package/src/cli/commands/__tests__/stt-transcribe.test.ts +454 -0
- package/src/cli/commands/__tests__/task.test.ts +913 -0
- package/src/cli/commands/__tests__/tts-synthesize.test.ts +606 -0
- package/src/cli/commands/__tests__/ui-confirm.test.ts +650 -0
- package/src/cli/commands/__tests__/ui.test.ts +1215 -0
- package/src/cli/commands/__tests__/watchers.test.ts +716 -0
- package/src/cli/commands/attachment.ts +182 -0
- package/src/cli/commands/backup.ts +2 -2
- package/src/cli/commands/browser.ts +350 -0
- package/src/cli/commands/cache.ts +341 -0
- package/src/cli/commands/clients.ts +138 -0
- package/src/cli/commands/completions.ts +2 -12
- package/src/cli/commands/config.ts +6 -6
- package/src/cli/commands/conversations-import.ts +347 -0
- package/src/cli/commands/conversations.ts +69 -8
- package/src/cli/commands/email.ts +234 -194
- package/src/cli/commands/image-generation.ts +299 -0
- package/src/cli/commands/inference.ts +200 -0
- package/src/cli/commands/memory.ts +127 -17
- package/src/cli/commands/notifications.ts +68 -103
- package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -1
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
- package/src/cli/commands/oauth/connect.ts +2 -2
- package/src/cli/commands/oauth/providers.ts +176 -8
- package/src/cli/commands/oauth/status.ts +46 -36
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -1
- package/src/cli/commands/platform/__tests__/connect.test.ts +0 -1
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +0 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +0 -1
- package/src/cli/commands/skills.ts +3 -4
- package/src/cli/commands/stt.ts +339 -0
- package/src/cli/commands/task.ts +795 -0
- package/src/cli/commands/trust.ts +50 -19
- package/src/cli/commands/tts.ts +273 -0
- package/src/cli/commands/ui.ts +670 -0
- package/src/cli/commands/watchers.ts +509 -0
- package/src/cli/lib/daemon-credential-client.ts +0 -19
- package/src/cli/program.ts +39 -24
- package/src/cli.ts +0 -37
- package/src/config/__tests__/backup-schema.test.ts +7 -2
- package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
- package/src/config/bundled-skills/app-builder/references/WIDGETS.md +10 -10
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +66 -87
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +28 -51
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +22 -40
- package/src/config/bundled-skills/image-studio/SKILL.md +2 -1
- package/src/config/bundled-skills/image-studio/TOOLS.json +2 -1
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +23 -39
- package/src/config/bundled-skills/media-processing/services/reduce.ts +1 -1
- package/src/config/bundled-skills/messaging/SKILL.md +5 -5
- package/src/config/bundled-skills/messaging/TOOLS.json +4 -0
- package/src/config/bundled-skills/messaging/tools/__tests__/messaging-feed-events.test.ts +207 -0
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +20 -1
- package/src/config/bundled-skills/messaging/tools/messaging-read.ts +15 -1
- package/src/config/bundled-skills/messaging/tools/messaging-search.ts +21 -1
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +69 -12
- package/src/config/bundled-skills/phone-calls/references/CONFIG.md +9 -8
- package/src/config/bundled-skills/schedule/SKILL.md +8 -3
- package/src/config/bundled-skills/schedule/TOOLS.json +15 -7
- package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +59 -0
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/bundled-tool-registry.ts +0 -190
- package/src/config/env.ts +7 -2
- package/src/config/feature-flag-registry.json +42 -10
- package/src/config/llm-resolver.ts +128 -0
- package/src/config/loader.ts +194 -10
- package/src/config/raw-config-utils.ts +30 -2
- package/src/config/sanitize-for-transfer.ts +35 -0
- package/src/config/schema.ts +49 -41
- package/src/config/schemas/analysis.ts +3 -22
- package/src/config/schemas/backup.ts +1 -1
- package/src/config/schemas/calls.ts +0 -4
- package/src/config/schemas/conversations.ts +16 -0
- package/src/config/schemas/filing.ts +2 -7
- package/src/config/schemas/heartbeat.ts +0 -5
- package/src/config/schemas/inference.ts +3 -23
- package/src/config/schemas/llm.ts +317 -0
- package/src/config/schemas/memory-processing.ts +1 -9
- package/src/config/schemas/notifications.ts +4 -11
- package/src/config/schemas/platform.ts +3 -9
- package/src/config/schemas/security.ts +33 -0
- package/src/config/schemas/services.ts +9 -4
- package/src/config/schemas/stt.ts +1 -0
- package/src/config/schemas/tts.ts +64 -0
- package/src/config/schemas/updates.ts +1 -1
- package/src/config/schemas/workspace-git.ts +3 -40
- package/src/config/skill-state.ts +6 -2
- package/src/config/skills.ts +96 -7
- package/src/context/__tests__/compact-prompt.test.ts +63 -0
- package/src/context/__tests__/microcompact.test.ts +805 -0
- package/src/context/estimator-calibration.ts +136 -0
- package/src/context/microcompact.ts +443 -0
- package/src/context/prompts/compact.md +26 -0
- package/src/context/token-estimator.ts +61 -3
- package/src/context/tool-result-truncation.ts +3 -63
- package/src/context/window-manager.ts +417 -39
- package/src/credential-execution/approval-bridge.ts +0 -1
- package/src/credential-execution/executable-discovery.ts +19 -8
- package/src/credential-execution/process-manager.test.ts +109 -0
- package/src/credential-execution/process-manager.ts +65 -2
- package/src/credential-health/credential-health-service.ts +19 -6
- package/src/daemon/__tests__/conversation-feed-event.test.ts +317 -0
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +4 -12
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +14 -15
- package/src/daemon/approval-generators.ts +29 -4
- package/src/daemon/assistant-attachments.ts +24 -13
- package/src/daemon/classifier.ts +2 -2
- package/src/daemon/config-watcher.ts +0 -3
- package/src/daemon/context-overflow-policy.ts +4 -13
- package/src/daemon/context-overflow-reducer.ts +4 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +162 -34
- package/src/daemon/conversation-agent-loop.ts +1282 -599
- package/src/daemon/conversation-attachments.ts +2 -6
- package/src/daemon/conversation-error.ts +36 -1
- package/src/daemon/conversation-history.ts +10 -19
- package/src/daemon/conversation-lifecycle.ts +59 -17
- package/src/daemon/conversation-messaging.ts +73 -4
- package/src/daemon/conversation-notifiers.ts +2 -110
- package/src/daemon/conversation-process.ts +24 -11
- package/src/daemon/conversation-queue-manager.ts +3 -0
- package/src/daemon/conversation-runtime-assembly.ts +1063 -211
- package/src/daemon/conversation-slash.ts +2 -2
- package/src/daemon/conversation-surfaces.ts +389 -1
- package/src/daemon/conversation-tool-setup.ts +51 -9
- package/src/daemon/conversation-usage.ts +1 -1
- package/src/daemon/conversation.ts +197 -64
- package/src/daemon/external-plugins-bootstrap.ts +478 -0
- package/src/daemon/external-skills-bootstrap.ts +41 -0
- package/src/daemon/first-greeting.ts +191 -14
- package/src/daemon/guardian-action-generators.ts +34 -14
- package/src/daemon/handlers/config-model.test.ts +86 -0
- package/src/daemon/handlers/config-model.ts +65 -12
- package/src/daemon/handlers/conversations.ts +9 -2
- package/src/daemon/handlers/shared.ts +39 -11
- package/src/daemon/handlers/skills.ts +7 -3
- package/src/daemon/handlers/slack-channel-oauth-install.ts +197 -0
- package/src/daemon/lifecycle.ts +109 -82
- package/src/daemon/message-types/computer-use.ts +2 -34
- package/src/daemon/message-types/conversations.ts +63 -0
- package/src/daemon/message-types/messages.ts +21 -1
- package/src/daemon/message-types/trust.ts +0 -2
- package/src/daemon/parse-actual-tokens-from-error.test.ts +57 -1
- package/src/daemon/parse-actual-tokens-from-error.ts +66 -0
- package/src/daemon/pkb-context-tracker.test.ts +169 -0
- package/src/daemon/pkb-context-tracker.ts +125 -0
- package/src/daemon/pkb-reminder-builder.test.ts +70 -0
- package/src/daemon/pkb-reminder-builder.ts +31 -0
- package/src/daemon/providers-setup.ts +6 -0
- package/src/daemon/server.ts +122 -12
- package/src/daemon/shutdown-handlers.ts +2 -12
- package/src/daemon/tool-side-effects.ts +14 -65
- package/src/daemon/web-search-history.ts +126 -0
- package/src/events/domain-events.ts +0 -1
- package/src/filing/filing-service.ts +9 -10
- package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +160 -0
- package/src/heartbeat/heartbeat-service.ts +99 -28
- package/src/home/__tests__/feed-population-integration.test.ts +312 -0
- package/src/home/__tests__/feed-scheduler.test.ts +39 -11
- package/src/home/__tests__/rollup-producer.test.ts +44 -0
- package/src/home/assistant-feed-authoring.ts +4 -0
- package/src/home/emit-feed-event.ts +11 -0
- package/src/home/feed-scheduler.ts +20 -4
- package/src/home/feed-types.ts +97 -4
- package/src/home/relationship-state-writer.ts +2 -2
- package/src/home/rewrite-command-preview.ts +66 -0
- package/src/home/rollup-producer.ts +34 -5
- package/src/home/suggested-prompts.ts +101 -0
- package/src/ipc/__tests__/attachment-ipc.test.ts +213 -0
- package/src/ipc/__tests__/browser-ipc.test.ts +339 -0
- package/src/ipc/__tests__/cache-ipc.test.ts +266 -0
- package/src/ipc/__tests__/socket-path.test.ts +34 -0
- package/src/ipc/__tests__/task-ipc.test.ts +577 -0
- package/src/ipc/__tests__/ui-request-route.test.ts +495 -0
- package/src/ipc/__tests__/watcher-ipc.test.ts +295 -0
- package/src/ipc/cli-client.ts +2 -1
- package/src/ipc/cli-server.ts +26 -8
- package/src/ipc/gateway-client.ts +6 -3
- package/src/ipc/routes/attachment.ts +114 -0
- package/src/ipc/routes/browser-context.ts +63 -0
- package/src/ipc/routes/browser.ts +97 -0
- package/src/ipc/routes/cache.ts +96 -0
- package/src/ipc/routes/get-contact.ts +16 -0
- package/src/ipc/routes/index.ts +31 -1
- package/src/ipc/routes/list-clients.ts +31 -0
- package/src/ipc/routes/merge-contacts.ts +17 -0
- package/src/ipc/routes/notification.ts +133 -0
- package/src/ipc/routes/rename-conversation.ts +59 -0
- package/src/ipc/routes/search-contacts.ts +19 -0
- package/src/ipc/routes/task-queue.ts +226 -0
- package/src/ipc/routes/task.ts +173 -0
- package/src/ipc/routes/ui-request.ts +50 -0
- package/src/ipc/routes/upsert-contact.ts +25 -0
- package/src/ipc/routes/watcher.ts +203 -0
- package/src/ipc/socket-path.ts +76 -0
- package/src/media/app-icon-generator.ts +23 -46
- package/src/media/avatar-router.ts +26 -41
- package/src/media/gemini-image-service.ts +8 -41
- package/src/media/image-credentials.ts +73 -0
- package/src/media/image-service.ts +85 -0
- package/src/media/openai-image-service.ts +131 -0
- package/src/media/types.ts +46 -0
- package/src/memory/__tests__/conversation-analyze-job.test.ts +9 -8
- package/src/memory/__tests__/conversation-group-migration.test.ts +99 -0
- package/src/memory/admin.ts +18 -0
- package/src/memory/conversation-analyze-job.ts +14 -13
- package/src/memory/conversation-attention-store.ts +13 -6
- package/src/memory/conversation-crud.ts +133 -3
- package/src/memory/conversation-group-migration.ts +38 -6
- package/src/memory/conversation-queries.ts +57 -4
- package/src/memory/conversation-title-service.ts +32 -4
- package/src/memory/db-init.ts +10 -0
- package/src/memory/embedding-backend.ts +1 -1
- package/src/memory/embedding-gemini.test.ts +41 -2
- package/src/memory/embedding-gemini.ts +6 -1
- package/src/memory/graph/bootstrap.test.ts +282 -0
- package/src/memory/graph/bootstrap.ts +8 -5
- package/src/memory/graph/compaction.ts +299 -0
- package/src/memory/graph/consolidation.ts +4 -4
- package/src/memory/graph/conversation-graph-memory.ts +89 -29
- package/src/memory/graph/extraction.test.ts +272 -2
- package/src/memory/graph/extraction.ts +183 -53
- package/src/memory/graph/graph-search.test.ts +93 -0
- package/src/memory/graph/graph-search.ts +4 -1
- package/src/memory/graph/inspect.ts +2 -2
- package/src/memory/graph/narrative.ts +2 -2
- package/src/memory/graph/pattern-scan.ts +2 -2
- package/src/memory/graph/retriever.test.ts +459 -0
- package/src/memory/graph/retriever.ts +237 -48
- package/src/memory/graph/store.ts +41 -0
- package/src/memory/graph/tool-handlers.ts +27 -0
- package/src/memory/graph/tools.ts +6 -1
- package/src/memory/indexer.ts +5 -5
- package/src/memory/job-handlers/conversation-starters.ts +23 -20
- package/src/memory/job-handlers/summarization.ts +2 -2
- package/src/memory/job-utils.ts +7 -1
- package/src/memory/jobs/embed-pkb-file.test.ts +168 -0
- package/src/memory/jobs/embed-pkb-file.ts +54 -0
- package/src/memory/jobs-store.ts +44 -3
- package/src/memory/jobs-worker.ts +4 -0
- package/src/memory/migrations/041-approval-prompt-ts-tracker.ts +26 -0
- package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +1 -1
- package/src/memory/migrations/149-oauth-tables.ts +1 -0
- package/src/memory/migrations/220-normalize-user-file-by-principal.ts +2 -2
- package/src/memory/migrations/222-strip-placeholder-sentinels-from-messages.ts +82 -0
- package/src/memory/migrations/223-schedule-script-column.ts +11 -0
- package/src/memory/migrations/224-oauth-providers-managed-service-is-paid.ts +24 -0
- package/src/memory/migrations/225-oauth-providers-available-scopes.ts +13 -0
- package/src/memory/migrations/index.ts +5 -0
- package/src/memory/pkb/pkb-index.test.ts +369 -0
- package/src/memory/pkb/pkb-index.ts +255 -0
- package/src/memory/pkb/pkb-reconcile.test.ts +252 -0
- package/src/memory/pkb/pkb-reconcile.ts +148 -0
- package/src/memory/pkb/pkb-search.test.ts +499 -0
- package/src/memory/pkb/pkb-search.ts +159 -0
- package/src/memory/pkb/types.ts +53 -0
- package/src/memory/qdrant-client.test.ts +60 -0
- package/src/memory/qdrant-client.ts +147 -1
- package/src/memory/schema/infrastructure.ts +1 -0
- package/src/memory/schema/oauth.ts +4 -1
- package/src/memory/slack-thread-store.ts +37 -0
- package/src/messaging/providers/gmail/adapter.ts +6 -16
- package/src/messaging/providers/gmail/client.ts +22 -0
- package/src/messaging/providers/gmail/types.ts +7 -0
- package/src/messaging/providers/slack/adapter.ts +14 -2
- package/src/messaging/providers/slack/backfill.test.ts +257 -0
- package/src/messaging/providers/slack/backfill.ts +101 -0
- package/src/messaging/providers/slack/message-metadata.test.ts +316 -0
- package/src/messaging/providers/slack/message-metadata.ts +123 -0
- package/src/messaging/providers/slack/render-transcript.test.ts +1421 -0
- package/src/messaging/providers/slack/render-transcript.ts +501 -0
- package/src/messaging/style-analyzer.ts +5 -2
- package/src/notifications/README.md +9 -5
- package/src/notifications/conversation-pairing.ts +78 -19
- package/src/notifications/copy-composer.ts +0 -5
- package/src/notifications/decision-engine.ts +3 -9
- package/src/notifications/emit-signal.ts +1 -1
- package/src/notifications/preference-extractor.ts +2 -6
- package/src/notifications/signal.ts +1 -2
- package/src/oauth/AGENTS.md +1 -1
- package/src/oauth/__tests__/identity-verifier.test.ts +2 -1
- package/src/oauth/connect-orchestrator.ts +8 -34
- package/src/oauth/connect-types.ts +6 -10
- package/src/oauth/manual-token-connection.ts +23 -0
- package/src/oauth/oauth-store.ts +31 -14
- package/src/oauth/platform-connection.test.ts +47 -0
- package/src/oauth/platform-connection.ts +15 -5
- package/src/oauth/provider-serializer.ts +6 -1
- package/src/oauth/seed-providers.ts +56 -106
- package/src/outbound-proxy/http-forwarder.ts +9 -0
- package/src/permissions/approval-policy.test.ts +1223 -0
- package/src/permissions/approval-policy.ts +309 -0
- package/src/permissions/arg-parser.test.ts +161 -0
- package/src/permissions/arg-parser.ts +141 -0
- package/src/permissions/bash-risk-classifier.test.ts +1620 -0
- package/src/permissions/bash-risk-classifier.ts +950 -0
- package/src/permissions/checker.ts +348 -711
- package/src/permissions/command-registry.test.ts +774 -0
- package/src/permissions/command-registry.ts +1005 -0
- package/src/permissions/defaults.ts +28 -79
- package/src/permissions/file-risk-classifier.test.ts +535 -0
- package/src/permissions/file-risk-classifier.ts +274 -0
- package/src/permissions/gateway-threshold-reader.ts +196 -0
- package/src/permissions/prompter.ts +4 -0
- package/src/permissions/risk-types.ts +262 -0
- package/src/permissions/schedule-risk-classifier.test.ts +129 -0
- package/src/permissions/schedule-risk-classifier.ts +85 -0
- package/src/permissions/secret-prompter.ts +53 -2
- package/src/permissions/shell-identity.ts +2 -42
- package/src/permissions/skill-risk-classifier.test.ts +311 -0
- package/src/permissions/skill-risk-classifier.ts +214 -0
- package/src/permissions/trust-client.ts +52 -25
- package/src/permissions/trust-store-interface.ts +1 -6
- package/src/permissions/trust-store.ts +161 -62
- package/src/permissions/types.ts +25 -14
- package/src/permissions/web-risk-classifier.test.ts +170 -0
- package/src/permissions/web-risk-classifier.ts +89 -0
- package/src/permissions/workspace-policy.ts +9 -19
- package/src/platform/client.ts +19 -1
- package/src/plugins/defaults/circuit-breaker.ts +146 -0
- package/src/plugins/defaults/compaction.ts +145 -0
- package/src/plugins/defaults/empty-response.ts +126 -0
- package/src/plugins/defaults/history-repair.ts +85 -0
- package/src/plugins/defaults/index.ts +116 -0
- package/src/plugins/defaults/injectors.ts +491 -0
- package/src/plugins/defaults/llm-call.ts +82 -0
- package/src/plugins/defaults/memory-retrieval.ts +226 -0
- package/src/plugins/defaults/overflow-reduce.ts +181 -0
- package/src/plugins/defaults/persistence.ts +129 -0
- package/src/plugins/defaults/title-generate.ts +95 -0
- package/src/plugins/defaults/token-estimate.ts +104 -0
- package/src/plugins/defaults/tool-error.ts +126 -0
- package/src/plugins/defaults/tool-execute.ts +89 -0
- package/src/plugins/defaults/tool-result-truncate.ts +88 -0
- package/src/plugins/pipeline.ts +316 -0
- package/src/plugins/plugin-skill-contributions.ts +292 -0
- package/src/plugins/registry.ts +241 -0
- package/src/plugins/types.ts +1134 -0
- package/src/plugins/user-loader.ts +177 -0
- package/src/prompts/persona-resolver.ts +3 -3
- package/src/prompts/system-prompt.ts +19 -20
- package/src/prompts/templates/BOOTSTRAP.md +27 -77
- package/src/prompts/templates/SOUL.md +2 -2
- package/src/prompts/update-bulletin-job.ts +190 -0
- package/src/providers/__tests__/context-overflow-error.test.ts +328 -0
- package/src/providers/__tests__/provider-env-vars.test.ts +102 -0
- package/src/providers/__tests__/retry-callsite.test.ts +424 -0
- package/src/providers/anthropic/client.ts +183 -14
- package/src/providers/call-site-routing.ts +71 -0
- package/src/providers/gemini/client.ts +65 -2
- package/src/providers/managed-proxy/constants.ts +2 -1
- package/src/providers/model-catalog.ts +524 -33
- package/src/providers/model-intents.ts +4 -4
- package/src/providers/openai/chat-completions-provider.ts +57 -1
- package/src/providers/openai/responses-provider.ts +86 -9
- package/src/providers/openrouter/client.ts +80 -9
- package/src/providers/provider-env-vars.ts +56 -0
- package/src/providers/provider-send-message.ts +22 -5
- package/src/providers/ratelimit.ts +4 -0
- package/src/providers/registry.ts +19 -8
- package/src/providers/retry.ts +174 -39
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +55 -0
- package/src/providers/speech-to-text/deepgram-realtime.test.ts +61 -0
- package/src/providers/speech-to-text/deepgram-realtime.ts +57 -0
- package/src/providers/speech-to-text/google-gemini-live-stream.ts +4 -4
- package/src/providers/speech-to-text/provider-catalog.ts +17 -0
- package/src/providers/speech-to-text/resolve.ts +7 -0
- package/src/providers/speech-to-text/xai-realtime.test.ts +646 -0
- package/src/providers/speech-to-text/xai-realtime.ts +821 -0
- package/src/providers/speech-to-text/xai.test.ts +155 -0
- package/src/providers/speech-to-text/xai.ts +97 -0
- package/src/providers/types.ts +93 -3
- package/src/runtime/AGENTS.md +27 -18
- package/src/runtime/__tests__/agent-wake.test.ts +43 -2
- package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +3 -3
- package/src/runtime/__tests__/client-registry.test.ts +293 -0
- package/src/runtime/__tests__/interactive-ui.test.ts +673 -0
- package/src/runtime/agent-wake.ts +63 -22
- package/src/runtime/auth/route-policy.ts +4 -0
- package/src/runtime/btw-sidechain.ts +13 -3
- package/src/runtime/channel-reply-delivery.ts +106 -2
- package/src/runtime/client-registry.ts +261 -0
- package/src/runtime/decision-token.ts +116 -0
- package/src/runtime/gateway-client.ts +2 -2
- package/src/runtime/http-router.ts +32 -0
- package/src/runtime/http-server.ts +129 -9
- package/src/runtime/http-types.ts +23 -3
- package/src/runtime/interactive-ui.ts +362 -0
- package/src/runtime/invite-instruction-generator.ts +2 -2
- package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +176 -0
- package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +390 -0
- package/src/runtime/migrations/__tests__/vbundle-metadata-merge.test.ts +221 -0
- package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +1540 -0
- package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +453 -0
- package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +222 -0
- package/src/runtime/migrations/gcs-signed-url.ts +162 -0
- package/src/runtime/migrations/vbundle-builder.ts +1 -22
- package/src/runtime/migrations/vbundle-importer.ts +154 -9
- package/src/runtime/migrations/vbundle-metadata-merge.ts +124 -0
- package/src/runtime/migrations/vbundle-streaming-importer.ts +2522 -0
- package/src/runtime/migrations/vbundle-streaming-validator.ts +244 -0
- package/src/runtime/migrations/vbundle-tar-stream.ts +217 -0
- package/src/runtime/migrations/vbundle-validator.ts +15 -6
- package/src/runtime/routes/__tests__/home-feed-routes.test.ts +111 -0
- package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +114 -75
- package/src/runtime/routes/__tests__/migration-vellum-metadata-reconcile.test.ts +246 -0
- package/src/runtime/routes/approval-prompt-ts-tracker.ts +78 -0
- package/src/runtime/routes/approval-routes.ts +29 -17
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +9 -0
- package/src/runtime/routes/avatar-routes.ts +20 -4
- package/src/runtime/routes/browser-extension-pair-routes.ts +27 -8
- package/src/runtime/routes/btw-routes.ts +1 -4
- package/src/runtime/routes/conversation-management-routes.ts +20 -2
- package/src/runtime/routes/conversation-routes.ts +351 -138
- package/src/runtime/routes/debug-routes.ts +1 -1
- package/src/runtime/routes/diagnostics-routes.ts +6 -4
- package/src/runtime/routes/events-routes.ts +16 -0
- package/src/runtime/routes/guardian-approval-interception.ts +33 -3
- package/src/runtime/routes/guardian-approval-prompt.ts +13 -3
- package/src/runtime/routes/home-feed-routes.ts +120 -2
- package/src/runtime/routes/inbound-message-handler.ts +987 -2
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +113 -2
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -3
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +129 -6
- package/src/runtime/routes/integrations/slack/channel.ts +25 -3
- package/src/runtime/routes/llm-context-normalization.ts +23 -1
- package/src/runtime/routes/memory-item-routes.test.ts +1 -0
- package/src/runtime/routes/migration-routes.ts +720 -127
- package/src/runtime/routes/playground/__tests__/force-compact.test.ts +284 -0
- package/src/runtime/routes/playground/__tests__/guard.test.ts +80 -0
- package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +294 -0
- package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +271 -0
- package/src/runtime/routes/playground/__tests__/seed-conversation.test.ts +202 -0
- package/src/runtime/routes/playground/__tests__/seeded-conversations.test.ts +309 -0
- package/src/runtime/routes/playground/__tests__/state.test.ts +224 -0
- package/src/runtime/routes/playground/conversation-not-found.ts +29 -0
- package/src/runtime/routes/playground/deps.ts +56 -0
- package/src/runtime/routes/playground/force-compact.ts +73 -0
- package/src/runtime/routes/playground/guard.ts +37 -0
- package/src/runtime/routes/playground/index.ts +28 -0
- package/src/runtime/routes/playground/inject-failures.ts +159 -0
- package/src/runtime/routes/playground/reset-circuit.ts +115 -0
- package/src/runtime/routes/playground/seed-conversation.ts +139 -0
- package/src/runtime/routes/playground/seeded-conversations.ts +78 -0
- package/src/runtime/routes/playground/state.ts +78 -0
- package/src/runtime/routes/schedule-routes.ts +89 -8
- package/src/runtime/routes/settings-routes.ts +4 -2
- package/src/runtime/routes/trust-rules-routes.ts +30 -14
- package/src/runtime/routes/work-items-routes.test.ts +1 -1
- package/src/runtime/routes/work-items-routes.ts +3 -2
- package/src/runtime/services/__tests__/analyze-conversation.test.ts +25 -43
- package/src/runtime/services/analyze-conversation.ts +12 -16
- package/src/runtime/skill-route-registry.ts +97 -15
- package/src/schedule/run-script.ts +68 -0
- package/src/schedule/schedule-store.ts +7 -1
- package/src/schedule/scheduler.ts +56 -8
- package/src/security/__tests__/provider-key-env-fallback.test.ts +119 -0
- package/src/security/__tests__/untrusted-content.test.ts +109 -0
- package/src/security/oauth2.ts +98 -35
- package/src/security/secure-keys.ts +7 -8
- package/src/security/token-manager.ts +27 -13
- package/src/security/untrusted-content.ts +102 -0
- package/src/skills/catalog-cache.ts +35 -9
- package/src/skills/catalog-install.ts +31 -3
- package/src/skills/skill-cache-store.ts +97 -0
- package/src/stt/__tests__/daemon-batch-transcriber.test.ts +76 -0
- package/src/stt/daemon-batch-transcriber.ts +33 -0
- package/src/stt/stt-stream-session.ts +8 -1
- package/src/stt/types.ts +5 -1
- package/src/subagent/manager.ts +41 -13
- package/src/tasks/ephemeral-permissions.ts +9 -4
- package/src/telemetry/usage-telemetry-reporter.ts +27 -5
- package/src/tools/browser/__tests__/browser-status.test.ts +234 -2
- package/src/tools/browser/browser-execution.ts +150 -54
- package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +230 -0
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +146 -3
- package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +22 -0
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +54 -3
- package/src/tools/browser/cdp-client/factory.ts +15 -4
- package/src/tools/credentials/tool-policy.ts +39 -5
- package/src/tools/credentials/vault.ts +9 -4
- package/src/tools/executor.ts +129 -73
- package/src/tools/filesystem/write.ts +52 -0
- package/src/tools/host-terminal/host-shell.ts +45 -5
- package/src/tools/memory/register.test.ts +185 -0
- package/src/tools/memory/register.ts +3 -1
- package/src/tools/network/script-proxy/session-manager.ts +37 -1
- package/src/tools/network/web-fetch.ts +20 -10
- package/src/tools/network/web-search.ts +19 -4
- package/src/tools/permission-checker.ts +116 -46
- package/src/tools/policy-context.ts +29 -8
- package/src/tools/registry.ts +195 -6
- package/src/tools/schedule/create.ts +23 -8
- package/src/tools/schedule/update.ts +3 -1
- package/src/tools/secret-detection-handler.ts +0 -51
- package/src/tools/side-effects.ts +0 -11
- package/src/tools/skills/execute.ts +2 -2
- package/src/tools/skills/sandbox-runner.ts +5 -2
- package/src/tools/system/avatar-generator.ts +6 -2
- package/src/tools/terminal/backends/native.ts +51 -2
- package/src/tools/terminal/safe-env.ts +3 -2
- package/src/tools/terminal/shell.ts +1 -0
- package/src/tools/tool-manifest.ts +6 -21
- package/src/tools/types.ts +40 -5
- package/src/tools/verification-control-plane-policy.ts +1 -1
- package/src/tts/__tests__/provider-adapters.test.ts +240 -13
- package/src/tts/provider-catalog.ts +18 -0
- package/src/tts/providers/index.ts +2 -0
- package/src/tts/providers/xai-provider.ts +224 -0
- package/src/tts/types.ts +46 -0
- package/src/types/tar-stream.d.ts +66 -0
- package/src/util/json.ts +17 -0
- package/src/util/platform.ts +9 -4
- package/src/util/pricing.ts +41 -8
- package/src/watcher/engine.ts +1 -1
- package/src/watcher/providers/google-calendar.ts +134 -8
- package/src/watcher/providers/outlook-calendar.ts +42 -2
- package/src/workspace/git-service.ts +23 -4
- package/src/workspace/migrations/006-services-config.ts +2 -4
- package/src/workspace/migrations/022-move-hooks-to-workspace.ts +2 -3
- package/src/workspace/migrations/038-unify-llm-callsite-configs.ts +516 -0
- package/src/workspace/migrations/039-drop-legacy-llm-keys.ts +171 -0
- package/src/workspace/migrations/040-seed-latency-callsite-defaults.ts +154 -0
- package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +56 -0
- package/src/workspace/migrations/042-fix-backfill-google-gmail-settings-scope.ts +70 -0
- package/src/workspace/migrations/043-release-notes-latex-rendering.ts +75 -0
- package/src/workspace/migrations/044-bump-stale-provider-stream-timeout.ts +51 -0
- package/src/workspace/migrations/045-release-notes-meet-avatar.ts +130 -0
- package/src/workspace/migrations/046-seed-conversation-starters-callsite.ts +108 -0
- package/src/workspace/migrations/047-remove-watch-callsites.ts +54 -0
- package/src/workspace/migrations/048-remove-workspace-hooks.ts +81 -0
- package/src/workspace/migrations/049-release-notes-default-sonnet.ts +80 -0
- package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +86 -0
- package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +128 -0
- package/src/workspace/migrations/AGENTS.md +1 -1
- package/src/workspace/migrations/registry.ts +28 -0
- package/src/workspace/provider-commit-message-generator.ts +19 -38
- package/tsconfig.json +1 -1
- package/hook-templates/debug-prompt-logger/hook.json +0 -7
- package/hook-templates/debug-prompt-logger/run.sh +0 -66
- package/src/__tests__/context-overflow-approval.test.ts +0 -156
- package/src/__tests__/gmail-archive-fallback.test.ts +0 -193
- package/src/__tests__/gmail-archive-gate.test.ts +0 -246
- package/src/__tests__/gmail-preferences.test.ts +0 -117
- package/src/__tests__/hooks-blocking.test.ts +0 -178
- package/src/__tests__/hooks-cli.test.ts +0 -182
- package/src/__tests__/hooks-config.test.ts +0 -108
- package/src/__tests__/hooks-discovery.test.ts +0 -211
- package/src/__tests__/hooks-integration.test.ts +0 -196
- package/src/__tests__/hooks-manager.test.ts +0 -226
- package/src/__tests__/hooks-runner.test.ts +0 -175
- package/src/__tests__/hooks-settings.test.ts +0 -160
- package/src/__tests__/hooks-templates.test.ts +0 -169
- package/src/__tests__/hooks-ts-runner.test.ts +0 -170
- package/src/__tests__/hooks-watch.test.ts +0 -112
- package/src/__tests__/notification-schedule-dedup.test.ts +0 -213
- package/src/__tests__/oauth-scope-policy.test.ts +0 -180
- package/src/__tests__/outlook-attachments.test.ts +0 -301
- package/src/__tests__/outlook-automation-tools.test.ts +0 -425
- package/src/__tests__/outlook-categories.test.ts +0 -212
- package/src/__tests__/outlook-compose-tools.test.ts +0 -325
- package/src/__tests__/outlook-declutter-tools.test.ts +0 -585
- package/src/__tests__/outlook-follow-up.test.ts +0 -196
- package/src/__tests__/outlook-trash.test.ts +0 -77
- package/src/__tests__/outlook-unsubscribe.test.ts +0 -279
- package/src/__tests__/send-notification-tool.test.ts +0 -83
- package/src/__tests__/update-bulletin-format.test.ts +0 -181
- package/src/__tests__/update-bulletin-state.test.ts +0 -135
- package/src/__tests__/update-bulletin.test.ts +0 -478
- package/src/__tests__/update-template-contract.test.ts +0 -29
- package/src/cli/commands/doctor.ts +0 -341
- package/src/cli/commands/shotgun.ts +0 -266
- package/src/config/bundled-skills/browser/SKILL.md +0 -88
- package/src/config/bundled-skills/browser/TOOLS.json +0 -516
- package/src/config/bundled-skills/browser/tools/browser-attach.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-click.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-close.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-detach.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-extract.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-fill-credential.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-hover.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-navigate.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-press-key.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-screenshot.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-scroll.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-select-option.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-snapshot.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-status.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-type.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +0 -49
- package/src/config/bundled-skills/browser/tools/browser-wait-for.ts +0 -12
- package/src/config/bundled-skills/chatgpt-import/SKILL.md +0 -27
- package/src/config/bundled-skills/chatgpt-import/TOOLS.json +0 -27
- package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +0 -378
- package/src/config/bundled-skills/conversations/SKILL.md +0 -20
- package/src/config/bundled-skills/conversations/TOOLS.json +0 -23
- package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +0 -66
- package/src/config/bundled-skills/gmail/SKILL.md +0 -221
- package/src/config/bundled-skills/gmail/TOOLS.json +0 -588
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +0 -256
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +0 -112
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +0 -44
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +0 -81
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +0 -108
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +0 -146
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +0 -53
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +0 -347
- package/src/config/bundled-skills/gmail/tools/gmail-preferences-tool.ts +0 -59
- package/src/config/bundled-skills/gmail/tools/gmail-preferences.ts +0 -82
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +0 -26
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +0 -347
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +0 -29
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +0 -122
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +0 -67
- package/src/config/bundled-skills/gmail/tools/scan-result-store.ts +0 -100
- package/src/config/bundled-skills/gmail/tools/shared.ts +0 -47
- package/src/config/bundled-skills/google-calendar/SKILL.md +0 -51
- package/src/config/bundled-skills/google-calendar/TOOLS.json +0 -226
- package/src/config/bundled-skills/google-calendar/calendar-client.ts +0 -223
- package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +0 -27
- package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +0 -48
- package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +0 -19
- package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +0 -36
- package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +0 -58
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +0 -17
- package/src/config/bundled-skills/google-calendar/types.ts +0 -97
- package/src/config/bundled-skills/heartbeat/SKILL.md +0 -43
- package/src/config/bundled-skills/notifications/SKILL.md +0 -40
- package/src/config/bundled-skills/notifications/TOOLS.json +0 -80
- package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -152
- package/src/config/bundled-skills/notifications/tools/shared.ts +0 -13
- package/src/config/bundled-skills/outlook/SKILL.md +0 -196
- package/src/config/bundled-skills/outlook/TOOLS.json +0 -530
- package/src/config/bundled-skills/outlook/tools/outlook-attachments.ts +0 -85
- package/src/config/bundled-skills/outlook/tools/outlook-categories.ts +0 -77
- package/src/config/bundled-skills/outlook/tools/outlook-draft.ts +0 -84
- package/src/config/bundled-skills/outlook/tools/outlook-follow-up.ts +0 -94
- package/src/config/bundled-skills/outlook/tools/outlook-forward.ts +0 -49
- package/src/config/bundled-skills/outlook/tools/outlook-outreach-scan.ts +0 -237
- package/src/config/bundled-skills/outlook/tools/outlook-rules.ts +0 -161
- package/src/config/bundled-skills/outlook/tools/outlook-send-draft.ts +0 -32
- package/src/config/bundled-skills/outlook/tools/outlook-sender-digest.ts +0 -272
- package/src/config/bundled-skills/outlook/tools/outlook-trash.ts +0 -29
- package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +0 -129
- package/src/config/bundled-skills/outlook/tools/outlook-vacation.ts +0 -87
- package/src/config/bundled-skills/outlook/tools/shared.ts +0 -20
- package/src/config/bundled-skills/outlook-calendar/SKILL.md +0 -51
- package/src/config/bundled-skills/outlook-calendar/TOOLS.json +0 -221
- package/src/config/bundled-skills/outlook-calendar/calendar-client.ts +0 -252
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-check-availability.ts +0 -53
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-create-event.ts +0 -74
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-get-event.ts +0 -18
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-list-events.ts +0 -46
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-rsvp.ts +0 -36
- package/src/config/bundled-skills/outlook-calendar/tools/shared.ts +0 -17
- package/src/config/bundled-skills/outlook-calendar/types.ts +0 -120
- package/src/config/bundled-skills/screen-watch/SKILL.md +0 -27
- package/src/config/bundled-skills/screen-watch/TOOLS.json +0 -35
- package/src/config/bundled-skills/screen-watch/tools/start-screen-watch.ts +0 -12
- package/src/config/bundled-skills/skills-catalog/SKILL.md +0 -84
- package/src/config/bundled-skills/slack/SKILL.md +0 -108
- package/src/config/bundled-skills/tasks/SKILL.md +0 -37
- package/src/config/bundled-skills/tasks/TOOLS.json +0 -353
- package/src/config/bundled-skills/tasks/icon.svg +0 -34
- package/src/config/bundled-skills/tasks/tools/task-delete.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-list-add.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-list-show.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-list-update.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-list.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-queue-run.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-run.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-save.ts +0 -12
- package/src/config/bundled-skills/watcher/SKILL.md +0 -31
- package/src/config/bundled-skills/watcher/TOOLS.json +0 -167
- package/src/config/bundled-skills/watcher/tools/watcher-create.ts +0 -12
- package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +0 -12
- package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +0 -12
- package/src/config/bundled-skills/watcher/tools/watcher-list.ts +0 -12
- package/src/config/bundled-skills/watcher/tools/watcher-update.ts +0 -12
- package/src/daemon/context-overflow-approval.ts +0 -52
- package/src/daemon/watch-handler.ts +0 -399
- package/src/hooks/cli.ts +0 -253
- package/src/hooks/config.ts +0 -100
- package/src/hooks/discovery.ts +0 -135
- package/src/hooks/manager.ts +0 -179
- package/src/hooks/runner.ts +0 -117
- package/src/hooks/templates.ts +0 -77
- package/src/hooks/types.ts +0 -75
- package/src/oauth/scope-policy.ts +0 -89
- package/src/prompts/templates/UPDATES.md +0 -50
- package/src/prompts/update-bulletin-format.ts +0 -85
- package/src/prompts/update-bulletin-state.ts +0 -58
- package/src/prompts/update-bulletin-template-path.ts +0 -13
- package/src/prompts/update-bulletin.ts +0 -139
- package/src/runtime/gateway-internal-client.ts +0 -94
- package/src/runtime/routes/watch-routes.ts +0 -156
- package/src/shared/provider-env-vars.ts +0 -19
- package/src/signals/shotgun.ts +0 -203
- package/src/tools/watch/screen-watch.ts +0 -144
- package/src/tools/watch/watch-state.ts +0 -142
- package/src/tools/watcher/create.ts +0 -86
- package/src/tools/watcher/delete.ts +0 -36
- package/src/tools/watcher/digest.ts +0 -54
- package/src/tools/watcher/list.ts +0 -83
- package/src/tools/watcher/update.ts +0 -71
|
@@ -0,0 +1,1421 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the pure chronological Slack transcript renderer.
|
|
3
|
+
*
|
|
4
|
+
* Covers tag variants (top-level, reply, edit, delete, reaction add/remove),
|
|
5
|
+
* stable parent aliases, reaction cap, sort stability under identical ts,
|
|
6
|
+
* the four scenarios from the design brief, and mixed legacy/post-upgrade
|
|
7
|
+
* fixtures.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, test } from "bun:test";
|
|
11
|
+
|
|
12
|
+
import type { Message } from "../../../providers/types.js";
|
|
13
|
+
import {
|
|
14
|
+
extractTagLineTexts,
|
|
15
|
+
isReactionTagLine,
|
|
16
|
+
parentAlias,
|
|
17
|
+
type RenderableSlackMessage,
|
|
18
|
+
renderSlackTranscript,
|
|
19
|
+
} from "./render-transcript.js";
|
|
20
|
+
|
|
21
|
+
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
// Anchor times: 14:25:00 UTC on 2023-11-14 = 1699971900 (Slack ts seconds).
|
|
24
|
+
// We work entirely in UTC because the renderer formats UTC MM/DD/YY HH:MM.
|
|
25
|
+
const TS_14_24 = "1699971840.000050"; // 14:24 UTC
|
|
26
|
+
const TS_14_25 = "1699971900.000100"; // 14:25 UTC
|
|
27
|
+
const TS_14_26 = "1699971960.000200"; // 14:26 UTC
|
|
28
|
+
const TS_14_28 = "1699972080.000300"; // 14:28 UTC
|
|
29
|
+
const TS_14_30 = "1699972200.000400"; // 14:30 UTC
|
|
30
|
+
|
|
31
|
+
const MS_14_25 = 1699971900_000;
|
|
32
|
+
const MS_14_30 = 1699972200_000;
|
|
33
|
+
const MS_14_32 = 1699972320_000;
|
|
34
|
+
|
|
35
|
+
const CHANNEL = "C0001";
|
|
36
|
+
|
|
37
|
+
function userMsg(
|
|
38
|
+
ts: string,
|
|
39
|
+
sender: string | null,
|
|
40
|
+
content: string,
|
|
41
|
+
opts: {
|
|
42
|
+
threadTs?: string;
|
|
43
|
+
editedAt?: number;
|
|
44
|
+
deletedAt?: number;
|
|
45
|
+
role?: "user" | "assistant";
|
|
46
|
+
createdAt?: number;
|
|
47
|
+
} = {},
|
|
48
|
+
): RenderableSlackMessage {
|
|
49
|
+
return {
|
|
50
|
+
role: opts.role ?? "user",
|
|
51
|
+
content,
|
|
52
|
+
senderLabel: sender,
|
|
53
|
+
createdAt: opts.createdAt ?? Number.parseFloat(ts) * 1000,
|
|
54
|
+
metadata: {
|
|
55
|
+
source: "slack",
|
|
56
|
+
channelId: CHANNEL,
|
|
57
|
+
channelTs: ts,
|
|
58
|
+
threadTs: opts.threadTs,
|
|
59
|
+
eventKind: "message",
|
|
60
|
+
editedAt: opts.editedAt,
|
|
61
|
+
deletedAt: opts.deletedAt,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function reactionMsg(
|
|
67
|
+
ts: string,
|
|
68
|
+
actor: string | null,
|
|
69
|
+
emoji: string,
|
|
70
|
+
targetTs: string,
|
|
71
|
+
op: "added" | "removed" = "added",
|
|
72
|
+
role: "user" | "assistant" = "user",
|
|
73
|
+
): RenderableSlackMessage {
|
|
74
|
+
return {
|
|
75
|
+
role,
|
|
76
|
+
content: "",
|
|
77
|
+
senderLabel: actor,
|
|
78
|
+
createdAt: Number.parseFloat(ts) * 1000,
|
|
79
|
+
metadata: {
|
|
80
|
+
source: "slack",
|
|
81
|
+
channelId: CHANNEL,
|
|
82
|
+
channelTs: ts,
|
|
83
|
+
eventKind: "reaction",
|
|
84
|
+
reaction: {
|
|
85
|
+
emoji,
|
|
86
|
+
targetChannelTs: targetTs,
|
|
87
|
+
op,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function legacyMsg(
|
|
94
|
+
createdAt: number,
|
|
95
|
+
sender: string | null,
|
|
96
|
+
content: string,
|
|
97
|
+
role: "user" | "assistant" = "user",
|
|
98
|
+
): RenderableSlackMessage {
|
|
99
|
+
return { role, content, senderLabel: sender, createdAt, metadata: null };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Build an expected `Message` fixture with a single text content block. */
|
|
103
|
+
function textMsg(role: "user" | "assistant", text: string): Message {
|
|
104
|
+
return { role, content: [{ type: "text", text }] };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── basics ───────────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
describe("renderSlackTranscript — basics", () => {
|
|
110
|
+
test("empty array yields empty array", () => {
|
|
111
|
+
expect(renderSlackTranscript([])).toEqual([]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("renders top-level message with MM/DD/YY HH:MM tag", () => {
|
|
115
|
+
const out = renderSlackTranscript([userMsg(TS_14_25, "@alice", "hi")]);
|
|
116
|
+
expect(out).toEqual([textMsg("user", "[11/14/23 14:25 @alice]: hi")]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("renders thread reply with parent alias arrow", () => {
|
|
120
|
+
const out = renderSlackTranscript([
|
|
121
|
+
userMsg(TS_14_28, "@bob", "got it", { threadTs: TS_14_25 }),
|
|
122
|
+
]);
|
|
123
|
+
const alias = parentAlias(TS_14_25);
|
|
124
|
+
expect(out).toEqual([
|
|
125
|
+
textMsg("user", `[11/14/23 14:28 @bob → ${alias}]: got it`),
|
|
126
|
+
]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("renders edited message with editedAt suffix", () => {
|
|
130
|
+
const out = renderSlackTranscript([
|
|
131
|
+
userMsg(TS_14_25, "@alice", "hi (revised)", { editedAt: MS_14_30 }),
|
|
132
|
+
]);
|
|
133
|
+
expect(out).toEqual([
|
|
134
|
+
textMsg(
|
|
135
|
+
"user",
|
|
136
|
+
"[11/14/23 14:25 @alice, edited 11/14/23 14:30]: hi (revised)",
|
|
137
|
+
),
|
|
138
|
+
]);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("edited marker uses editedAt time, not channelTs", () => {
|
|
142
|
+
// channelTs at 14:25 (original send time), edited later at 14:32.
|
|
143
|
+
// The opening time bracket must reflect 14:25 and the suffix must
|
|
144
|
+
// reflect 14:32 — derived from editedAt, not from channelTs.
|
|
145
|
+
const out = renderSlackTranscript([
|
|
146
|
+
userMsg(TS_14_25, "@alice", "v2", { editedAt: MS_14_32 }),
|
|
147
|
+
]);
|
|
148
|
+
expect(out).toEqual([
|
|
149
|
+
textMsg("user", "[11/14/23 14:25 @alice, edited 11/14/23 14:32]: v2"),
|
|
150
|
+
]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("edited message in a thread renders both arrow and edit suffix", () => {
|
|
154
|
+
const out = renderSlackTranscript([
|
|
155
|
+
userMsg(TS_14_28, "@bob", "got it (edit)", {
|
|
156
|
+
threadTs: TS_14_25,
|
|
157
|
+
editedAt: MS_14_30,
|
|
158
|
+
}),
|
|
159
|
+
]);
|
|
160
|
+
const alias = parentAlias(TS_14_25);
|
|
161
|
+
expect(out).toEqual([
|
|
162
|
+
textMsg(
|
|
163
|
+
"user",
|
|
164
|
+
`[11/14/23 14:28 @bob → ${alias}, edited 11/14/23 14:30]: got it (edit)`,
|
|
165
|
+
),
|
|
166
|
+
]);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("renders deleted message with deletedAt — content elided", () => {
|
|
170
|
+
const out = renderSlackTranscript([
|
|
171
|
+
userMsg(TS_14_25, "@alice", "(removed)", { deletedAt: MS_14_32 }),
|
|
172
|
+
]);
|
|
173
|
+
expect(out).toEqual([
|
|
174
|
+
textMsg("user", "[11/14/23 14:25 @alice — deleted 11/14/23 14:32]"),
|
|
175
|
+
]);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("delete takes precedence over edit (delete wins)", () => {
|
|
179
|
+
// A message that was edited at 14:30 and then deleted at 14:32
|
|
180
|
+
// should render as deleted, not edited — and content must be elided.
|
|
181
|
+
const out = renderSlackTranscript([
|
|
182
|
+
userMsg(TS_14_25, "@alice", "edited body", {
|
|
183
|
+
editedAt: MS_14_30,
|
|
184
|
+
deletedAt: MS_14_32,
|
|
185
|
+
}),
|
|
186
|
+
]);
|
|
187
|
+
expect(out).toEqual([
|
|
188
|
+
textMsg("user", "[11/14/23 14:25 @alice — deleted 11/14/23 14:32]"),
|
|
189
|
+
]);
|
|
190
|
+
const text0 = extractTagLineTexts(out)[0];
|
|
191
|
+
// No "edited" suffix should leak through.
|
|
192
|
+
expect(text0.includes("edited")).toBe(false);
|
|
193
|
+
// Content body must not appear.
|
|
194
|
+
expect(text0.includes("edited body")).toBe(false);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("deleted message preserves chronological ordering", () => {
|
|
198
|
+
// A deleted message in the middle of a transcript should still occupy
|
|
199
|
+
// its chronological slot — only the body is elided.
|
|
200
|
+
const out = renderSlackTranscript([
|
|
201
|
+
userMsg(TS_14_25, "@alice", "first"),
|
|
202
|
+
userMsg(TS_14_28, "@bob", "(removed)", { deletedAt: MS_14_30 }),
|
|
203
|
+
userMsg(TS_14_30, "@carol", "third"),
|
|
204
|
+
]);
|
|
205
|
+
expect(extractTagLineTexts(out)).toEqual([
|
|
206
|
+
"[11/14/23 14:25 @alice]: first",
|
|
207
|
+
"[11/14/23 14:28 @bob — deleted 11/14/23 14:30]",
|
|
208
|
+
"[11/14/23 14:30 @carol]: third",
|
|
209
|
+
]);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("renders reaction added", () => {
|
|
213
|
+
const alias = parentAlias(TS_14_25);
|
|
214
|
+
const out = renderSlackTranscript([
|
|
215
|
+
reactionMsg(TS_14_28, "@bob", "👍", TS_14_25, "added"),
|
|
216
|
+
]);
|
|
217
|
+
expect(out).toEqual([
|
|
218
|
+
textMsg("user", `[11/14/23 14:28 @bob reacted 👍 to ${alias}]`),
|
|
219
|
+
]);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("renders reaction removed", () => {
|
|
223
|
+
const alias = parentAlias(TS_14_25);
|
|
224
|
+
const out = renderSlackTranscript([
|
|
225
|
+
reactionMsg(TS_14_28, "@bob", "👍", TS_14_25, "removed"),
|
|
226
|
+
]);
|
|
227
|
+
expect(out).toEqual([
|
|
228
|
+
textMsg("user", `[11/14/23 14:28 @bob removed 👍 from ${alias}]`),
|
|
229
|
+
]);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("assistant-role message emits content with no tag-line wrapper", () => {
|
|
233
|
+
// Rationale: the `role` slot already conveys identity, and the
|
|
234
|
+
// assistant responds ~immediately after the triggering user message
|
|
235
|
+
// so the timestamp would add little beyond chronological adjacency.
|
|
236
|
+
// Keeping a bracketed tag on assistant rows caused the model to
|
|
237
|
+
// mimic the `[MM/DD/YY HH:MM]:` format as a literal prefix in new
|
|
238
|
+
// outbound Slack replies.
|
|
239
|
+
const out = renderSlackTranscript([
|
|
240
|
+
userMsg(TS_14_25, null, "yo 👋", { role: "assistant" }),
|
|
241
|
+
]);
|
|
242
|
+
expect(out).toEqual([textMsg("assistant", "yo 👋")]);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("omits sender label for user-role message with null senderLabel (no displayName)", () => {
|
|
246
|
+
const out = renderSlackTranscript([userMsg(TS_14_25, null, "yo")]);
|
|
247
|
+
expect(out).toEqual([textMsg("user", "[11/14/23 14:25]: yo")]);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("omits sender label on legacy user row with null senderLabel", () => {
|
|
251
|
+
const out = renderSlackTranscript([legacyMsg(MS_14_25, null, "hi")]);
|
|
252
|
+
expect(out).toEqual([textMsg("user", "[11/14/23 14:25]: hi")]);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("thread-reply assistant row emits content-only — no tag wrapper, no thread arrow", () => {
|
|
256
|
+
const out = renderSlackTranscript([
|
|
257
|
+
userMsg(TS_14_28, null, "got it", {
|
|
258
|
+
threadTs: TS_14_25,
|
|
259
|
+
role: "assistant",
|
|
260
|
+
}),
|
|
261
|
+
]);
|
|
262
|
+
expect(out).toEqual([textMsg("assistant", "got it")]);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("deleted assistant row collapses to the `[deleted]` sentinel", () => {
|
|
266
|
+
// Chronology must still be preserved — we emit a stable short sentinel
|
|
267
|
+
// rather than eliding the row entirely. The sentinel is intentionally
|
|
268
|
+
// different from the user-row `[MM/DD/YY — deleted MM/DD/YY]` form so
|
|
269
|
+
// the model has no timestamp pattern to mimic in new outbound replies.
|
|
270
|
+
const out = renderSlackTranscript([
|
|
271
|
+
userMsg(TS_14_25, null, "(removed)", {
|
|
272
|
+
deletedAt: MS_14_32,
|
|
273
|
+
role: "assistant",
|
|
274
|
+
}),
|
|
275
|
+
]);
|
|
276
|
+
expect(out).toEqual([textMsg("assistant", "[deleted]")]);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("edited assistant row emits the latest content verbatim — no edit suffix", () => {
|
|
280
|
+
const out = renderSlackTranscript([
|
|
281
|
+
userMsg(TS_14_25, null, "v2", {
|
|
282
|
+
editedAt: MS_14_30,
|
|
283
|
+
role: "assistant",
|
|
284
|
+
}),
|
|
285
|
+
]);
|
|
286
|
+
expect(out).toEqual([textMsg("assistant", "v2")]);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("reaction with null senderLabel falls back on role-derived subject", () => {
|
|
290
|
+
// Defensive: reactions always need a grammatical subject. If a caller
|
|
291
|
+
// accidentally passes null, the renderer falls back on a role-derived
|
|
292
|
+
// label so the tag line still parses.
|
|
293
|
+
const alias = parentAlias(TS_14_25);
|
|
294
|
+
const out = renderSlackTranscript([
|
|
295
|
+
reactionMsg(TS_14_28, null, "👍", TS_14_25, "added", "assistant"),
|
|
296
|
+
]);
|
|
297
|
+
expect(out).toEqual([
|
|
298
|
+
textMsg(
|
|
299
|
+
"assistant",
|
|
300
|
+
`[11/14/23 14:28 @assistant reacted 👍 to ${alias}]`,
|
|
301
|
+
),
|
|
302
|
+
]);
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// ── edited marker ────────────────────────────────────────────────────────────
|
|
307
|
+
|
|
308
|
+
describe("renderSlackTranscript — edited marker", () => {
|
|
309
|
+
test("deleted takes precedence over edited (no edit suffix on deleted line)", () => {
|
|
310
|
+
// A row may carry both editedAt and deletedAt if it was edited before
|
|
311
|
+
// being deleted. The deleted form takes precedence and the edited
|
|
312
|
+
// suffix must not appear.
|
|
313
|
+
const out = renderSlackTranscript([
|
|
314
|
+
userMsg(TS_14_25, "@alice", "(removed)", {
|
|
315
|
+
editedAt: MS_14_30,
|
|
316
|
+
deletedAt: MS_14_32,
|
|
317
|
+
}),
|
|
318
|
+
]);
|
|
319
|
+
expect(out).toEqual([
|
|
320
|
+
textMsg("user", "[11/14/23 14:25 @alice — deleted 11/14/23 14:32]"),
|
|
321
|
+
]);
|
|
322
|
+
expect(extractTagLineTexts(out)[0].includes("edited")).toBe(false);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("reaction rows do not render the edited marker even if metadata has editedAt", () => {
|
|
326
|
+
// The renderer must never apply the edited suffix to a reaction-kind row.
|
|
327
|
+
// We construct a reaction with an editedAt field set in metadata to
|
|
328
|
+
// confirm the reaction code path ignores it.
|
|
329
|
+
const reaction: RenderableSlackMessage = {
|
|
330
|
+
role: "user",
|
|
331
|
+
content: "",
|
|
332
|
+
senderLabel: "@bob",
|
|
333
|
+
createdAt: Number.parseFloat(TS_14_28) * 1000,
|
|
334
|
+
metadata: {
|
|
335
|
+
source: "slack",
|
|
336
|
+
channelId: CHANNEL,
|
|
337
|
+
channelTs: TS_14_28,
|
|
338
|
+
eventKind: "reaction",
|
|
339
|
+
reaction: {
|
|
340
|
+
emoji: "👍",
|
|
341
|
+
targetChannelTs: TS_14_25,
|
|
342
|
+
op: "added",
|
|
343
|
+
},
|
|
344
|
+
editedAt: MS_14_30,
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
const out = renderSlackTranscript([reaction]);
|
|
348
|
+
const alias = parentAlias(TS_14_25);
|
|
349
|
+
expect(out).toEqual([
|
|
350
|
+
textMsg("user", `[11/14/23 14:28 @bob reacted 👍 to ${alias}]`),
|
|
351
|
+
]);
|
|
352
|
+
expect(extractTagLineTexts(out)[0].includes("edited")).toBe(false);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test("editedAt of 0 (epoch) still renders as 00:00 marker", () => {
|
|
356
|
+
// Defensive: 0 is a valid (if unusual) timestamp and must not be
|
|
357
|
+
// skipped by a truthy check.
|
|
358
|
+
const out = renderSlackTranscript([
|
|
359
|
+
userMsg(TS_14_25, "@alice", "v2", { editedAt: 0 }),
|
|
360
|
+
]);
|
|
361
|
+
expect(out).toEqual([
|
|
362
|
+
textMsg("user", "[11/14/23 14:25 @alice, edited 01/01/70 00:00]: v2"),
|
|
363
|
+
]);
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// ── parent alias stability ───────────────────────────────────────────────────
|
|
368
|
+
|
|
369
|
+
describe("parentAlias", () => {
|
|
370
|
+
test("is stable across calls with the same ts", () => {
|
|
371
|
+
const a = parentAlias("1700000000.000100");
|
|
372
|
+
const b = parentAlias("1700000000.000100");
|
|
373
|
+
expect(a).toEqual(b);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test("differs across distinct ts values", () => {
|
|
377
|
+
const a = parentAlias("1700000000.000100");
|
|
378
|
+
const b = parentAlias("1700000000.000200");
|
|
379
|
+
expect(a).not.toEqual(b);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test("starts with M and is 7 chars long (M + 6 hex)", () => {
|
|
383
|
+
const a = parentAlias("1700000000.000100");
|
|
384
|
+
expect(a).toMatch(/^M[0-9a-f]{6}$/);
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// ── isReactionTagLine ────────────────────────────────────────────────────────
|
|
389
|
+
|
|
390
|
+
describe("isReactionTagLine", () => {
|
|
391
|
+
// Pinned to the exact shapes `renderReaction` and the overflow trailer
|
|
392
|
+
// produce. The helper is the public contract that lets consumers
|
|
393
|
+
// re-label the transcript without double-attributing reaction lines,
|
|
394
|
+
// so drift here silently breaks `buildActiveThreadBlockFromRenderable`.
|
|
395
|
+
const alias = parentAlias("1700000000.000100");
|
|
396
|
+
|
|
397
|
+
test("matches reaction-add line", () => {
|
|
398
|
+
expect(
|
|
399
|
+
isReactionTagLine(`[11/14/23 14:28 @bob reacted 👍 to ${alias}]`),
|
|
400
|
+
).toBe(true);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test("matches reaction-remove line", () => {
|
|
404
|
+
expect(
|
|
405
|
+
isReactionTagLine(`[11/14/23 14:28 @bob removed 👍 from ${alias}]`),
|
|
406
|
+
).toBe(true);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
test("matches overflow trailer line", () => {
|
|
410
|
+
expect(isReactionTagLine(`[…and 2 more reactions to ${alias}]`)).toBe(true);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test("does not match a regular message tag line", () => {
|
|
414
|
+
expect(isReactionTagLine("[11/14/23 14:25 @alice]: hi")).toBe(false);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
test("does not match content-only assistant output", () => {
|
|
418
|
+
expect(isReactionTagLine("on it. here's the answer")).toBe(false);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
test("does not match the `[deleted]` sentinel", () => {
|
|
422
|
+
expect(isReactionTagLine("[deleted]")).toBe(false);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test("does not match a user-deleted marker", () => {
|
|
426
|
+
expect(
|
|
427
|
+
isReactionTagLine("[11/14/23 14:25 @alice — deleted 11/14/23 14:32]"),
|
|
428
|
+
).toBe(false);
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// ── reaction cap ─────────────────────────────────────────────────────────────
|
|
433
|
+
|
|
434
|
+
describe("renderSlackTranscript — reaction cap", () => {
|
|
435
|
+
test("renders all reactions when below the default cap (5)", () => {
|
|
436
|
+
const messages: RenderableSlackMessage[] = [
|
|
437
|
+
userMsg(TS_14_25, "@alice", "hi"),
|
|
438
|
+
reactionMsg("1700000800.000001", "@u1", "👍", TS_14_25),
|
|
439
|
+
reactionMsg("1700000800.000002", "@u2", "🎉", TS_14_25),
|
|
440
|
+
reactionMsg("1700000800.000003", "@u3", "🔥", TS_14_25),
|
|
441
|
+
];
|
|
442
|
+
const out = renderSlackTranscript(messages);
|
|
443
|
+
expect(out.length).toBe(4);
|
|
444
|
+
expect(
|
|
445
|
+
extractTagLineTexts(out).some((t) => t.includes("more reactions")),
|
|
446
|
+
).toBe(false);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test("collapses excess reactions into a trailer line", () => {
|
|
450
|
+
const messages: RenderableSlackMessage[] = [
|
|
451
|
+
userMsg(TS_14_25, "@alice", "hi"),
|
|
452
|
+
reactionMsg("1700000800.000001", "@u1", "👍", TS_14_25),
|
|
453
|
+
reactionMsg("1700000800.000002", "@u2", "🎉", TS_14_25),
|
|
454
|
+
reactionMsg("1700000800.000003", "@u3", "🔥", TS_14_25),
|
|
455
|
+
reactionMsg("1700000800.000004", "@u4", "💯", TS_14_25),
|
|
456
|
+
reactionMsg("1700000800.000005", "@u5", "👏", TS_14_25),
|
|
457
|
+
reactionMsg("1700000800.000006", "@u6", "👀", TS_14_25),
|
|
458
|
+
reactionMsg("1700000800.000007", "@u7", "🚀", TS_14_25),
|
|
459
|
+
];
|
|
460
|
+
const out = renderSlackTranscript(messages);
|
|
461
|
+
// 1 message + 5 rendered reactions + 1 trailer.
|
|
462
|
+
expect(out.length).toBe(7);
|
|
463
|
+
const trailer = extractTagLineTexts(out)[out.length - 1];
|
|
464
|
+
expect(trailer).toMatch(/…and 2 more reactions to M[0-9a-f]{6}\]/);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
test("respects custom maxReactionsPerMessage", () => {
|
|
468
|
+
const messages: RenderableSlackMessage[] = [
|
|
469
|
+
userMsg(TS_14_25, "@alice", "hi"),
|
|
470
|
+
reactionMsg("1700000800.000001", "@u1", "👍", TS_14_25),
|
|
471
|
+
reactionMsg("1700000800.000002", "@u2", "🎉", TS_14_25),
|
|
472
|
+
reactionMsg("1700000800.000003", "@u3", "🔥", TS_14_25),
|
|
473
|
+
];
|
|
474
|
+
const out = renderSlackTranscript(messages, { maxReactionsPerMessage: 2 });
|
|
475
|
+
// 1 msg + 2 reactions + 1 trailer for 1 excess.
|
|
476
|
+
expect(out.length).toBe(4);
|
|
477
|
+
// Singular "reaction" when excess is exactly 1.
|
|
478
|
+
expect(extractTagLineTexts(out)[out.length - 1]).toMatch(
|
|
479
|
+
/…and 1 more reaction to M[0-9a-f]{6}\]/,
|
|
480
|
+
);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test("overflow trailer uses plural 'reactions' when excess > 1", () => {
|
|
484
|
+
const messages: RenderableSlackMessage[] = [
|
|
485
|
+
userMsg(TS_14_25, "@alice", "hi"),
|
|
486
|
+
reactionMsg("1700000800.000001", "@u1", "👍", TS_14_25),
|
|
487
|
+
reactionMsg("1700000800.000002", "@u2", "🎉", TS_14_25),
|
|
488
|
+
reactionMsg("1700000800.000003", "@u3", "🔥", TS_14_25),
|
|
489
|
+
reactionMsg("1700000800.000004", "@u4", "💯", TS_14_25),
|
|
490
|
+
];
|
|
491
|
+
const out = renderSlackTranscript(messages, { maxReactionsPerMessage: 2 });
|
|
492
|
+
// 1 msg + 2 reactions + 1 trailer for 2 excess.
|
|
493
|
+
expect(out.length).toBe(4);
|
|
494
|
+
expect(extractTagLineTexts(out)[out.length - 1]).toMatch(
|
|
495
|
+
/…and 2 more reactions to M[0-9a-f]{6}\]/,
|
|
496
|
+
);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
test("overflow trailer lands in chronological position, before later non-reaction messages", () => {
|
|
500
|
+
// Reactions overflow the cap, then a later message arrives. The trailer
|
|
501
|
+
// must be emitted at the point the overflow window closes — immediately
|
|
502
|
+
// before the later message — so chronology is preserved.
|
|
503
|
+
const alias = parentAlias(TS_14_25);
|
|
504
|
+
const messages: RenderableSlackMessage[] = [
|
|
505
|
+
userMsg(TS_14_25, "@alice", "hi"),
|
|
506
|
+
// Cap is 2 — first two reactions render inline.
|
|
507
|
+
reactionMsg("1699971950.000001", "@u1", "👍", TS_14_25), // 14:25:50
|
|
508
|
+
reactionMsg("1699971955.000002", "@u2", "🎉", TS_14_25), // 14:25:55
|
|
509
|
+
// Next two reactions overflow.
|
|
510
|
+
reactionMsg("1699971960.000003", "@u3", "🔥", TS_14_25), // 14:26
|
|
511
|
+
reactionMsg("1699971965.000004", "@u4", "💯", TS_14_25), // 14:26:05
|
|
512
|
+
// A later top-level message — trailer must land BEFORE this line.
|
|
513
|
+
userMsg(TS_14_30, "@bob", "later"),
|
|
514
|
+
];
|
|
515
|
+
const out = renderSlackTranscript(messages, { maxReactionsPerMessage: 2 });
|
|
516
|
+
expect(extractTagLineTexts(out)).toEqual([
|
|
517
|
+
"[11/14/23 14:25 @alice]: hi",
|
|
518
|
+
`[11/14/23 14:25 @u1 reacted 👍 to ${alias}]`,
|
|
519
|
+
`[11/14/23 14:25 @u2 reacted 🎉 to ${alias}]`,
|
|
520
|
+
`[…and 2 more reactions to ${alias}]`,
|
|
521
|
+
"[11/14/23 14:30 @bob]: later",
|
|
522
|
+
]);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
test("overflow trailer for one target flushes before reaction event on a different target", () => {
|
|
526
|
+
// Two independent reaction streams. The first target overflows, then a
|
|
527
|
+
// reaction arrives for a second target. The first target's trailer must
|
|
528
|
+
// close its window before the second target's reaction is emitted.
|
|
529
|
+
const parentA_ts = "1700000000.000001";
|
|
530
|
+
const parentB_ts = "1700000000.000002";
|
|
531
|
+
const aliasA = parentAlias(parentA_ts);
|
|
532
|
+
const aliasB = parentAlias(parentB_ts);
|
|
533
|
+
const messages: RenderableSlackMessage[] = [
|
|
534
|
+
userMsg(parentA_ts, "@alice", "A"),
|
|
535
|
+
userMsg(parentB_ts, "@bob", "B"),
|
|
536
|
+
// Overflow the cap on A.
|
|
537
|
+
reactionMsg("1700000100.000001", "@u1", "👍", parentA_ts),
|
|
538
|
+
reactionMsg("1700000100.000002", "@u2", "🎉", parentA_ts),
|
|
539
|
+
reactionMsg("1700000100.000003", "@u3", "🔥", parentA_ts), // excess 1
|
|
540
|
+
reactionMsg("1700000100.000004", "@u4", "💯", parentA_ts), // excess 2
|
|
541
|
+
// Reaction on B arrives chronologically after the overflow — A's
|
|
542
|
+
// trailer should flush here, before B's reaction renders.
|
|
543
|
+
reactionMsg("1700000100.000005", "@u5", "👏", parentB_ts),
|
|
544
|
+
];
|
|
545
|
+
const out = renderSlackTranscript(messages, { maxReactionsPerMessage: 2 });
|
|
546
|
+
expect(extractTagLineTexts(out)).toEqual([
|
|
547
|
+
"[11/14/23 22:13 @alice]: A",
|
|
548
|
+
"[11/14/23 22:13 @bob]: B",
|
|
549
|
+
`[11/14/23 22:15 @u1 reacted 👍 to ${aliasA}]`,
|
|
550
|
+
`[11/14/23 22:15 @u2 reacted 🎉 to ${aliasA}]`,
|
|
551
|
+
`[…and 2 more reactions to ${aliasA}]`,
|
|
552
|
+
`[11/14/23 22:15 @u5 reacted 👏 to ${aliasB}]`,
|
|
553
|
+
]);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
test("caps are tracked per-target message independently", () => {
|
|
557
|
+
const messages: RenderableSlackMessage[] = [
|
|
558
|
+
userMsg(TS_14_25, "@alice", "first"),
|
|
559
|
+
userMsg(TS_14_26, "@alice", "second"),
|
|
560
|
+
// 2 reactions on first
|
|
561
|
+
reactionMsg("1700000800.000001", "@u1", "👍", TS_14_25),
|
|
562
|
+
reactionMsg("1700000800.000002", "@u2", "🎉", TS_14_25),
|
|
563
|
+
// 2 reactions on second
|
|
564
|
+
reactionMsg("1700000800.000003", "@u3", "🔥", TS_14_26),
|
|
565
|
+
reactionMsg("1700000800.000004", "@u4", "💯", TS_14_26),
|
|
566
|
+
];
|
|
567
|
+
const out = renderSlackTranscript(messages, { maxReactionsPerMessage: 5 });
|
|
568
|
+
// 2 messages + 4 reactions, no trailers.
|
|
569
|
+
expect(out.length).toBe(6);
|
|
570
|
+
expect(
|
|
571
|
+
extractTagLineTexts(out).some((t) => t.includes("more reactions")),
|
|
572
|
+
).toBe(false);
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
// ── mixed message + reaction chronology ─────────────────────────────────────
|
|
577
|
+
|
|
578
|
+
describe("renderSlackTranscript — mixed message + reaction chronology", () => {
|
|
579
|
+
test("reaction renders inline at correct chronological position", () => {
|
|
580
|
+
// Order of events as they happened in time:
|
|
581
|
+
// 14:25 — alice posts the parent
|
|
582
|
+
// 14:26 — bob posts a follow-up message
|
|
583
|
+
// 14:28 — carol reacts to alice's parent
|
|
584
|
+
// 14:30 — dan posts another message
|
|
585
|
+
// Inputs are intentionally shuffled so the renderer must sort.
|
|
586
|
+
const aliasParent = parentAlias(TS_14_25);
|
|
587
|
+
const out = renderSlackTranscript([
|
|
588
|
+
reactionMsg(TS_14_28, "@carol", "👍", TS_14_25, "added"),
|
|
589
|
+
userMsg(TS_14_30, "@dan", "later"),
|
|
590
|
+
userMsg(TS_14_25, "@alice", "lunch?"),
|
|
591
|
+
userMsg(TS_14_26, "@bob", "yes"),
|
|
592
|
+
]);
|
|
593
|
+
expect(extractTagLineTexts(out)).toEqual([
|
|
594
|
+
"[11/14/23 14:25 @alice]: lunch?",
|
|
595
|
+
"[11/14/23 14:26 @bob]: yes",
|
|
596
|
+
`[11/14/23 14:28 @carol reacted 👍 to ${aliasParent}]`,
|
|
597
|
+
"[11/14/23 14:30 @dan]: later",
|
|
598
|
+
]);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
test("removed reactions interleave with messages by their own ts", () => {
|
|
602
|
+
// A reaction is added at 14:26 then removed at 14:30; bob posts a message
|
|
603
|
+
// at 14:28 in between. The "removed" line must land after bob's message,
|
|
604
|
+
// not collapsed beside the "added" line.
|
|
605
|
+
const aliasParent = parentAlias(TS_14_25);
|
|
606
|
+
const out = renderSlackTranscript([
|
|
607
|
+
userMsg(TS_14_25, "@alice", "lunch?"),
|
|
608
|
+
reactionMsg("1699971960.000010", "@carol", "👍", TS_14_25, "added"),
|
|
609
|
+
userMsg(TS_14_28, "@bob", "yes"),
|
|
610
|
+
reactionMsg(TS_14_30, "@carol", "👍", TS_14_25, "removed"),
|
|
611
|
+
]);
|
|
612
|
+
expect(extractTagLineTexts(out)).toEqual([
|
|
613
|
+
"[11/14/23 14:25 @alice]: lunch?",
|
|
614
|
+
`[11/14/23 14:26 @carol reacted 👍 to ${aliasParent}]`,
|
|
615
|
+
"[11/14/23 14:28 @bob]: yes",
|
|
616
|
+
`[11/14/23 14:30 @carol removed 👍 from ${aliasParent}]`,
|
|
617
|
+
]);
|
|
618
|
+
});
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
// ── sort stability ───────────────────────────────────────────────────────────
|
|
622
|
+
|
|
623
|
+
describe("renderSlackTranscript — sort", () => {
|
|
624
|
+
test("orders chronologically by channelTs", () => {
|
|
625
|
+
const out = renderSlackTranscript([
|
|
626
|
+
userMsg(TS_14_30, "@late", "later"),
|
|
627
|
+
userMsg(TS_14_25, "@early", "earlier"),
|
|
628
|
+
userMsg(TS_14_28, "@mid", "middle"),
|
|
629
|
+
]);
|
|
630
|
+
expect(extractTagLineTexts(out)).toEqual([
|
|
631
|
+
"[11/14/23 14:25 @early]: earlier",
|
|
632
|
+
"[11/14/23 14:28 @mid]: middle",
|
|
633
|
+
"[11/14/23 14:30 @late]: later",
|
|
634
|
+
]);
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
test("preserves input order when sort keys are identical (stable sort)", () => {
|
|
638
|
+
const sameTs = TS_14_25;
|
|
639
|
+
const out = renderSlackTranscript([
|
|
640
|
+
userMsg(sameTs, "@first", "1"),
|
|
641
|
+
userMsg(sameTs, "@second", "2"),
|
|
642
|
+
userMsg(sameTs, "@third", "3"),
|
|
643
|
+
]);
|
|
644
|
+
expect(extractTagLineTexts(out)).toEqual([
|
|
645
|
+
"[11/14/23 14:25 @first]: 1",
|
|
646
|
+
"[11/14/23 14:25 @second]: 2",
|
|
647
|
+
"[11/14/23 14:25 @third]: 3",
|
|
648
|
+
]);
|
|
649
|
+
});
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
// ── design brief scenarios ───────────────────────────────────────────────────
|
|
653
|
+
|
|
654
|
+
describe("renderSlackTranscript — four design-brief scenarios", () => {
|
|
655
|
+
// Setup: a top-level @alice message at 14:25; a sibling @carol top-level
|
|
656
|
+
// at 14:28; two replies in @alice's thread.
|
|
657
|
+
const aliceTopTs = TS_14_25;
|
|
658
|
+
const carolTopTs = TS_14_28;
|
|
659
|
+
const bobReply1Ts = "1699971960.000300"; // 14:26
|
|
660
|
+
const aliceReply2Ts = "1699972020.000400"; // 14:27
|
|
661
|
+
|
|
662
|
+
function baseFixture(): RenderableSlackMessage[] {
|
|
663
|
+
return [
|
|
664
|
+
userMsg(aliceTopTs, "@alice", "lunch?"),
|
|
665
|
+
userMsg(bobReply1Ts, "@bob", "yes!", { threadTs: aliceTopTs }),
|
|
666
|
+
userMsg(aliceReply2Ts, "@alice", "12:30 ok?", { threadTs: aliceTopTs }),
|
|
667
|
+
userMsg(carolTopTs, "@carol", "standup soon"),
|
|
668
|
+
];
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
test("scenario: reply in an existing thread", () => {
|
|
672
|
+
const replyTs = "1699972100.000500"; // 14:28:20 — after carol's top
|
|
673
|
+
const messages = [
|
|
674
|
+
...baseFixture(),
|
|
675
|
+
userMsg(replyTs, "@dan", "I'll join", { threadTs: aliceTopTs }),
|
|
676
|
+
];
|
|
677
|
+
const out = renderSlackTranscript(messages);
|
|
678
|
+
const aliceAlias = parentAlias(aliceTopTs);
|
|
679
|
+
expect(extractTagLineTexts(out)).toEqual([
|
|
680
|
+
"[11/14/23 14:25 @alice]: lunch?",
|
|
681
|
+
`[11/14/23 14:26 @bob → ${aliceAlias}]: yes!`,
|
|
682
|
+
`[11/14/23 14:27 @alice → ${aliceAlias}]: 12:30 ok?`,
|
|
683
|
+
"[11/14/23 14:28 @carol]: standup soon",
|
|
684
|
+
`[11/14/23 14:28 @dan → ${aliceAlias}]: I'll join`,
|
|
685
|
+
]);
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
test("scenario: reply to a top-level message (creating a new thread)", () => {
|
|
689
|
+
// @ed replies to @carol's top-level message; carol's top becomes a thread.
|
|
690
|
+
const replyTs = "1699972100.000600"; // 14:28:20
|
|
691
|
+
const messages = [
|
|
692
|
+
...baseFixture(),
|
|
693
|
+
userMsg(replyTs, "@ed", "joining now", { threadTs: carolTopTs }),
|
|
694
|
+
];
|
|
695
|
+
const out = renderSlackTranscript(messages);
|
|
696
|
+
const carolAlias = parentAlias(carolTopTs);
|
|
697
|
+
const texts = extractTagLineTexts(out);
|
|
698
|
+
// The reply tag points at carol's alias; carol's top stays untagged.
|
|
699
|
+
expect(texts[texts.length - 1]).toBe(
|
|
700
|
+
`[11/14/23 14:28 @ed → ${carolAlias}]: joining now`,
|
|
701
|
+
);
|
|
702
|
+
expect(texts[3]).toBe("[11/14/23 14:28 @carol]: standup soon");
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
test("scenario: reply to the most recent top-level message", () => {
|
|
706
|
+
// Same as above but emphasises the "last message" case.
|
|
707
|
+
const replyTs = "1699972110.000700"; // 14:28:30
|
|
708
|
+
const messages = [
|
|
709
|
+
...baseFixture(),
|
|
710
|
+
userMsg(replyTs, "@frank", "+1", { threadTs: carolTopTs }),
|
|
711
|
+
];
|
|
712
|
+
const out = renderSlackTranscript(messages);
|
|
713
|
+
const carolAlias = parentAlias(carolTopTs);
|
|
714
|
+
const texts = extractTagLineTexts(out);
|
|
715
|
+
expect(texts[texts.length - 1]).toBe(
|
|
716
|
+
`[11/14/23 14:28 @frank → ${carolAlias}]: +1`,
|
|
717
|
+
);
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
test("scenario: new top-level message (no threadTs)", () => {
|
|
721
|
+
const messages = [
|
|
722
|
+
...baseFixture(),
|
|
723
|
+
userMsg("1699972260.000800", "@gina", "anyone in office?"), // 14:31
|
|
724
|
+
];
|
|
725
|
+
const out = renderSlackTranscript(messages);
|
|
726
|
+
const texts = extractTagLineTexts(out);
|
|
727
|
+
// No arrow on the new top-level row.
|
|
728
|
+
expect(texts[texts.length - 1]).toBe(
|
|
729
|
+
"[11/14/23 14:31 @gina]: anyone in office?",
|
|
730
|
+
);
|
|
731
|
+
});
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
// ── mixed legacy + post-upgrade fixture ──────────────────────────────────────
|
|
735
|
+
|
|
736
|
+
describe("renderSlackTranscript — mixed legacy + post-upgrade", () => {
|
|
737
|
+
test("legacy rows render flat with no thread tag and intermix chronologically", () => {
|
|
738
|
+
const messages: RenderableSlackMessage[] = [
|
|
739
|
+
// Post-upgrade: 14:28 reply in alice's thread
|
|
740
|
+
userMsg("1699972080.000900", "@bob", "yes!", { threadTs: TS_14_25 }),
|
|
741
|
+
// Legacy row at 14:26 — should sort BETWEEN the 14:25 post-upgrade
|
|
742
|
+
// top-level and the 14:28 post-upgrade reply.
|
|
743
|
+
legacyMsg(1699971960_000, "@dana", "drive-by note"),
|
|
744
|
+
// Post-upgrade: 14:25 alice top-level
|
|
745
|
+
userMsg(TS_14_25, "@alice", "lunch?"),
|
|
746
|
+
];
|
|
747
|
+
const out = renderSlackTranscript(messages);
|
|
748
|
+
const alias = parentAlias(TS_14_25);
|
|
749
|
+
|
|
750
|
+
const texts = extractTagLineTexts(out);
|
|
751
|
+
expect(texts).toEqual([
|
|
752
|
+
"[11/14/23 14:25 @alice]: lunch?",
|
|
753
|
+
"[11/14/23 14:26 @dana]: drive-by note",
|
|
754
|
+
`[11/14/23 14:28 @bob → ${alias}]: yes!`,
|
|
755
|
+
]);
|
|
756
|
+
// Ensure the legacy row has no arrow.
|
|
757
|
+
expect(texts[1].includes("→")).toBe(false);
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
test("legacy assistant row carries assistant role and emits content verbatim", () => {
|
|
761
|
+
const out = renderSlackTranscript([
|
|
762
|
+
legacyMsg(MS_14_25, "@bot", "ack", "assistant"),
|
|
763
|
+
]);
|
|
764
|
+
expect(out).toEqual([textMsg("assistant", "ack")]);
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
test("preserves message role faithfully across mixed inputs", () => {
|
|
768
|
+
const out = renderSlackTranscript([
|
|
769
|
+
userMsg(TS_14_25, "@alice", "q?"),
|
|
770
|
+
userMsg(TS_14_26, "@bot", "a", { role: "assistant" }),
|
|
771
|
+
legacyMsg(MS_14_30, "@bot", "later legacy", "assistant"),
|
|
772
|
+
]);
|
|
773
|
+
expect(out.map((r) => r.role)).toEqual(["user", "assistant", "assistant"]);
|
|
774
|
+
});
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
// ── purity ────────────────────────────────────────────────────────────────────
|
|
778
|
+
|
|
779
|
+
describe("renderSlackTranscript — purity", () => {
|
|
780
|
+
test("does not mutate the input array or its elements", () => {
|
|
781
|
+
const original: RenderableSlackMessage[] = [
|
|
782
|
+
userMsg(TS_14_30, "@late", "later"),
|
|
783
|
+
userMsg(TS_14_25, "@early", "earlier"),
|
|
784
|
+
];
|
|
785
|
+
const snapshot = original.map((m) => ({ ...m, metadata: m.metadata }));
|
|
786
|
+
renderSlackTranscript(original);
|
|
787
|
+
expect(original.length).toBe(snapshot.length);
|
|
788
|
+
for (let i = 0; i < original.length; i++) {
|
|
789
|
+
expect(original[i].content).toBe(snapshot[i].content);
|
|
790
|
+
expect(original[i].senderLabel).toBe(snapshot[i].senderLabel);
|
|
791
|
+
expect(original[i].metadata).toBe(snapshot[i].metadata);
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
test("identical inputs produce identical outputs (deterministic)", () => {
|
|
796
|
+
const fixture: RenderableSlackMessage[] = [
|
|
797
|
+
userMsg(TS_14_25, "@alice", "hi"),
|
|
798
|
+
userMsg(TS_14_28, "@bob", "yo", { threadTs: TS_14_25 }),
|
|
799
|
+
reactionMsg(TS_14_30, "@carol", "👍", TS_14_25),
|
|
800
|
+
];
|
|
801
|
+
const a = renderSlackTranscript(fixture);
|
|
802
|
+
const b = renderSlackTranscript(fixture);
|
|
803
|
+
expect(a).toEqual(b);
|
|
804
|
+
});
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
// ── shape: Message[] / content-block structure ───────────────────────────────
|
|
808
|
+
|
|
809
|
+
describe("renderSlackTranscript — Message[] shape", () => {
|
|
810
|
+
test("empty input returns an empty array", () => {
|
|
811
|
+
expect(renderSlackTranscript([])).toEqual([]);
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
test("single text message returns one Message with one text content block", () => {
|
|
815
|
+
const out = renderSlackTranscript([userMsg(TS_14_25, "@alice", "hi")]);
|
|
816
|
+
expect(out).toEqual([
|
|
817
|
+
{
|
|
818
|
+
role: "user",
|
|
819
|
+
content: [{ type: "text", text: "[11/14/23 14:25 @alice]: hi" }],
|
|
820
|
+
},
|
|
821
|
+
]);
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
test("stable sort: messages with identical channelTs preserve input order", () => {
|
|
825
|
+
const sameTs = TS_14_25;
|
|
826
|
+
const out = renderSlackTranscript([
|
|
827
|
+
userMsg(sameTs, "@first", "1"),
|
|
828
|
+
userMsg(sameTs, "@second", "2"),
|
|
829
|
+
]);
|
|
830
|
+
expect(out).toEqual([
|
|
831
|
+
{
|
|
832
|
+
role: "user",
|
|
833
|
+
content: [{ type: "text", text: "[11/14/23 14:25 @first]: 1" }],
|
|
834
|
+
},
|
|
835
|
+
{
|
|
836
|
+
role: "user",
|
|
837
|
+
content: [{ type: "text", text: "[11/14/23 14:25 @second]: 2" }],
|
|
838
|
+
},
|
|
839
|
+
]);
|
|
840
|
+
});
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
// ── extractTagLineTexts helper ───────────────────────────────────────────────
|
|
844
|
+
|
|
845
|
+
describe("extractTagLineTexts", () => {
|
|
846
|
+
test("returns first text block text per message", () => {
|
|
847
|
+
const rendered: Message[] = [
|
|
848
|
+
{ role: "user", content: [{ type: "text", text: "line-a" }] },
|
|
849
|
+
{ role: "assistant", content: [{ type: "text", text: "line-b" }] },
|
|
850
|
+
];
|
|
851
|
+
expect(extractTagLineTexts(rendered)).toEqual(["line-a", "line-b"]);
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
test("returns empty string for a message with no text block", () => {
|
|
855
|
+
const rendered: Message[] = [
|
|
856
|
+
{ role: "user", content: [{ type: "text", text: "only text" }] },
|
|
857
|
+
// A message whose content has no text block at all (e.g. solely a
|
|
858
|
+
// tool_use/tool_result). The helper must emit "" rather than throw.
|
|
859
|
+
{
|
|
860
|
+
role: "assistant",
|
|
861
|
+
content: [
|
|
862
|
+
{
|
|
863
|
+
type: "tool_use",
|
|
864
|
+
id: "t1",
|
|
865
|
+
name: "noop",
|
|
866
|
+
input: {},
|
|
867
|
+
},
|
|
868
|
+
],
|
|
869
|
+
},
|
|
870
|
+
];
|
|
871
|
+
expect(extractTagLineTexts(rendered)).toEqual(["only text", ""]);
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
test("picks the first text block when multiple text blocks are present", () => {
|
|
875
|
+
const rendered: Message[] = [
|
|
876
|
+
{
|
|
877
|
+
role: "user",
|
|
878
|
+
content: [
|
|
879
|
+
{ type: "text", text: "first" },
|
|
880
|
+
{ type: "text", text: "second" },
|
|
881
|
+
],
|
|
882
|
+
},
|
|
883
|
+
];
|
|
884
|
+
expect(extractTagLineTexts(rendered)).toEqual(["first"]);
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
test("returns empty array for empty input", () => {
|
|
888
|
+
expect(extractTagLineTexts([])).toEqual([]);
|
|
889
|
+
});
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
// ── contentBlocks preservation ───────────────────────────────────────────────
|
|
893
|
+
|
|
894
|
+
describe("renderSlackTranscript — replayable content-block preservation", () => {
|
|
895
|
+
// When `contentBlocks` is populated, the renderer preserves replayable
|
|
896
|
+
// Anthropic blocks (tool_use, tool_result, thinking, redacted_thinking,
|
|
897
|
+
// image, file) verbatim alongside the tag line. Non-replayable blocks
|
|
898
|
+
// (ui_surface, server_tool_use, web_search_tool_result, unknown types) are
|
|
899
|
+
// stripped. Legacy rows (no contentBlocks field) render as a single text
|
|
900
|
+
// block.
|
|
901
|
+
|
|
902
|
+
test("[text, tool_use] assistant row preserves tool_use after tag line", () => {
|
|
903
|
+
// Assistant tool_use is paired with a follow-up user tool_result so the
|
|
904
|
+
// orphan-pair filter leaves both blocks intact.
|
|
905
|
+
const assistantRow: RenderableSlackMessage = {
|
|
906
|
+
...userMsg(TS_14_25, null, "looking it up", {
|
|
907
|
+
role: "assistant",
|
|
908
|
+
}),
|
|
909
|
+
contentBlocks: [
|
|
910
|
+
{ type: "text", text: "looking it up" },
|
|
911
|
+
{ type: "tool_use", id: "tu_1", name: "search", input: { q: "x" } },
|
|
912
|
+
],
|
|
913
|
+
};
|
|
914
|
+
const userRow: RenderableSlackMessage = {
|
|
915
|
+
...userMsg(TS_14_26, "@alice", ""),
|
|
916
|
+
contentBlocks: [
|
|
917
|
+
{ type: "tool_result", tool_use_id: "tu_1", content: "result text" },
|
|
918
|
+
],
|
|
919
|
+
};
|
|
920
|
+
const out = renderSlackTranscript([assistantRow, userRow]);
|
|
921
|
+
expect(out[0]).toEqual({
|
|
922
|
+
role: "assistant",
|
|
923
|
+
content: [
|
|
924
|
+
{ type: "text", text: "looking it up" },
|
|
925
|
+
{ type: "tool_use", id: "tu_1", name: "search", input: { q: "x" } },
|
|
926
|
+
],
|
|
927
|
+
});
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
test("[tool_result] user row emits only tool_result — no tag line", () => {
|
|
931
|
+
// Pair the user tool_result with a preceding assistant tool_use so the
|
|
932
|
+
// orphan-pair filter leaves the row intact; the assertion still pins
|
|
933
|
+
// the shape of the user row specifically (no tag line, single block).
|
|
934
|
+
const assistantRow: RenderableSlackMessage = {
|
|
935
|
+
...userMsg(TS_14_24, null, "", { role: "assistant" }),
|
|
936
|
+
contentBlocks: [
|
|
937
|
+
{ type: "tool_use", id: "tu_1", name: "search", input: {} },
|
|
938
|
+
],
|
|
939
|
+
};
|
|
940
|
+
const userRow: RenderableSlackMessage = {
|
|
941
|
+
...userMsg(TS_14_25, "@alice", ""),
|
|
942
|
+
contentBlocks: [
|
|
943
|
+
{ type: "tool_result", tool_use_id: "tu_1", content: "result text" },
|
|
944
|
+
],
|
|
945
|
+
};
|
|
946
|
+
const out = renderSlackTranscript([assistantRow, userRow]);
|
|
947
|
+
// Pin the second (user) row's shape — this is what the test is about.
|
|
948
|
+
expect(out[1]).toEqual({
|
|
949
|
+
role: "user",
|
|
950
|
+
content: [
|
|
951
|
+
{ type: "tool_result", tool_use_id: "tu_1", content: "result text" },
|
|
952
|
+
],
|
|
953
|
+
});
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
test("[thinking, text] assistant row preserves thinking before tag line (order preserved)", () => {
|
|
957
|
+
const base: RenderableSlackMessage = {
|
|
958
|
+
...userMsg(TS_14_25, null, "here's the answer", {
|
|
959
|
+
role: "assistant",
|
|
960
|
+
}),
|
|
961
|
+
contentBlocks: [
|
|
962
|
+
{ type: "thinking", thinking: "let me think", signature: "sig-abc" },
|
|
963
|
+
{ type: "text", text: "here's the answer" },
|
|
964
|
+
],
|
|
965
|
+
};
|
|
966
|
+
const out = renderSlackTranscript([base]);
|
|
967
|
+
expect(out).toEqual([
|
|
968
|
+
{
|
|
969
|
+
role: "assistant",
|
|
970
|
+
content: [
|
|
971
|
+
{ type: "thinking", thinking: "let me think", signature: "sig-abc" },
|
|
972
|
+
{ type: "text", text: "here's the answer" },
|
|
973
|
+
],
|
|
974
|
+
},
|
|
975
|
+
]);
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
test("[text, tool_use, tool_result] assistant row (degenerate) preserves order", () => {
|
|
979
|
+
// Degenerate but possible via the cleanAssistantContent path — rows
|
|
980
|
+
// that carry both tool_use and tool_result in the same message. The
|
|
981
|
+
// renderer passes them through in order.
|
|
982
|
+
const base: RenderableSlackMessage = {
|
|
983
|
+
...userMsg(TS_14_25, null, "doing a thing", {
|
|
984
|
+
role: "assistant",
|
|
985
|
+
}),
|
|
986
|
+
contentBlocks: [
|
|
987
|
+
{ type: "text", text: "doing a thing" },
|
|
988
|
+
{ type: "tool_use", id: "tu_A", name: "op", input: {} },
|
|
989
|
+
{ type: "tool_result", tool_use_id: "tu_A", content: "ok" },
|
|
990
|
+
],
|
|
991
|
+
};
|
|
992
|
+
const out = renderSlackTranscript([base]);
|
|
993
|
+
expect(out).toEqual([
|
|
994
|
+
{
|
|
995
|
+
role: "assistant",
|
|
996
|
+
content: [
|
|
997
|
+
{ type: "text", text: "doing a thing" },
|
|
998
|
+
{ type: "tool_use", id: "tu_A", name: "op", input: {} },
|
|
999
|
+
{ type: "tool_result", tool_use_id: "tu_A", content: "ok" },
|
|
1000
|
+
],
|
|
1001
|
+
},
|
|
1002
|
+
]);
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
test("[text, ui_surface] assistant row strips ui_surface — only content remains", () => {
|
|
1006
|
+
const base: RenderableSlackMessage = {
|
|
1007
|
+
...userMsg(TS_14_25, null, "reply body", { role: "assistant" }),
|
|
1008
|
+
contentBlocks: [
|
|
1009
|
+
{ type: "text", text: "reply body" },
|
|
1010
|
+
// ui_surface is local-only UI scaffolding and must not leak into
|
|
1011
|
+
// the replayable output. Typed as a generic shape here because
|
|
1012
|
+
// ui_surface is not part of the Anthropic ContentBlock union.
|
|
1013
|
+
{ type: "ui_surface", foo: "bar" } as unknown as never,
|
|
1014
|
+
] as never,
|
|
1015
|
+
};
|
|
1016
|
+
const out = renderSlackTranscript([base]);
|
|
1017
|
+
expect(out).toEqual([
|
|
1018
|
+
{
|
|
1019
|
+
role: "assistant",
|
|
1020
|
+
content: [{ type: "text", text: "reply body" }],
|
|
1021
|
+
},
|
|
1022
|
+
]);
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
test("[text, server_tool_use] assistant row strips server_tool_use (unknown to replay)", () => {
|
|
1026
|
+
const base: RenderableSlackMessage = {
|
|
1027
|
+
...userMsg(TS_14_25, null, "web search", { role: "assistant" }),
|
|
1028
|
+
contentBlocks: [
|
|
1029
|
+
{ type: "text", text: "web search" },
|
|
1030
|
+
{
|
|
1031
|
+
type: "server_tool_use",
|
|
1032
|
+
id: "st_1",
|
|
1033
|
+
name: "web_search",
|
|
1034
|
+
input: { q: "x" },
|
|
1035
|
+
},
|
|
1036
|
+
],
|
|
1037
|
+
};
|
|
1038
|
+
const out = renderSlackTranscript([base]);
|
|
1039
|
+
expect(out).toEqual([
|
|
1040
|
+
{
|
|
1041
|
+
role: "assistant",
|
|
1042
|
+
content: [{ type: "text", text: "web search" }],
|
|
1043
|
+
},
|
|
1044
|
+
]);
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
test("[image, text] user row preserves image before tag line (order preserved)", () => {
|
|
1048
|
+
const base: RenderableSlackMessage = {
|
|
1049
|
+
...userMsg(TS_14_25, "@alice", "check this out"),
|
|
1050
|
+
contentBlocks: [
|
|
1051
|
+
{
|
|
1052
|
+
type: "image",
|
|
1053
|
+
source: {
|
|
1054
|
+
type: "base64",
|
|
1055
|
+
media_type: "image/png",
|
|
1056
|
+
data: "base64data==",
|
|
1057
|
+
},
|
|
1058
|
+
},
|
|
1059
|
+
{ type: "text", text: "check this out" },
|
|
1060
|
+
],
|
|
1061
|
+
};
|
|
1062
|
+
const out = renderSlackTranscript([base]);
|
|
1063
|
+
expect(out).toEqual([
|
|
1064
|
+
{
|
|
1065
|
+
role: "user",
|
|
1066
|
+
content: [
|
|
1067
|
+
{
|
|
1068
|
+
type: "image",
|
|
1069
|
+
source: {
|
|
1070
|
+
type: "base64",
|
|
1071
|
+
media_type: "image/png",
|
|
1072
|
+
data: "base64data==",
|
|
1073
|
+
},
|
|
1074
|
+
},
|
|
1075
|
+
{ type: "text", text: "[11/14/23 14:25 @alice]: check this out" },
|
|
1076
|
+
],
|
|
1077
|
+
},
|
|
1078
|
+
]);
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
test("deleted row with [text, tool_use] contentBlocks emits only the deleted tag line", () => {
|
|
1082
|
+
const base: RenderableSlackMessage = {
|
|
1083
|
+
...userMsg(TS_14_25, "@alice", "(removed)", { deletedAt: MS_14_32 }),
|
|
1084
|
+
contentBlocks: [
|
|
1085
|
+
{ type: "text", text: "old body" },
|
|
1086
|
+
{ type: "tool_use", id: "tu_zombie", name: "op", input: {} },
|
|
1087
|
+
],
|
|
1088
|
+
};
|
|
1089
|
+
const out = renderSlackTranscript([base]);
|
|
1090
|
+
expect(out).toEqual([
|
|
1091
|
+
{
|
|
1092
|
+
role: "user",
|
|
1093
|
+
content: [
|
|
1094
|
+
{
|
|
1095
|
+
type: "text",
|
|
1096
|
+
text: "[11/14/23 14:25 @alice — deleted 11/14/23 14:32]",
|
|
1097
|
+
},
|
|
1098
|
+
],
|
|
1099
|
+
},
|
|
1100
|
+
]);
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
test("row with only non-replayable blocks emits fallback tag line annotated with what was stripped", () => {
|
|
1104
|
+
// Rows whose only content blocks are non-replayable (e.g. `server_tool_use`,
|
|
1105
|
+
// `ui_surface`) must still produce a turn so chronology and adjacent
|
|
1106
|
+
// tool_result context are preserved. `buildMessageContentBlocks` emits a
|
|
1107
|
+
// single fallback text block whose tag line names each stripped block's
|
|
1108
|
+
// type (and tool name, when available).
|
|
1109
|
+
const base: RenderableSlackMessage = {
|
|
1110
|
+
...userMsg(TS_14_25, null, "ran a web search", { role: "assistant" }),
|
|
1111
|
+
contentBlocks: [
|
|
1112
|
+
{
|
|
1113
|
+
type: "server_tool_use",
|
|
1114
|
+
id: "st_1",
|
|
1115
|
+
name: "web_search",
|
|
1116
|
+
input: { q: "x" },
|
|
1117
|
+
},
|
|
1118
|
+
{ type: "ui_surface", foo: "bar" } as unknown as never,
|
|
1119
|
+
] as never,
|
|
1120
|
+
};
|
|
1121
|
+
const out = renderSlackTranscript([base]);
|
|
1122
|
+
expect(out).toEqual([
|
|
1123
|
+
{
|
|
1124
|
+
role: "assistant",
|
|
1125
|
+
content: [
|
|
1126
|
+
{
|
|
1127
|
+
type: "text",
|
|
1128
|
+
text: "ran a web search [stripped non-replayable: server_tool_use(web_search), ui_surface]",
|
|
1129
|
+
},
|
|
1130
|
+
],
|
|
1131
|
+
},
|
|
1132
|
+
]);
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
test("legacy row (contentBlocks undefined) renders as single tag line — unchanged", () => {
|
|
1136
|
+
const base = userMsg(TS_14_25, "@alice", "legacy plain");
|
|
1137
|
+
// No `contentBlocks` field assigned — emulates a row whose JSON content
|
|
1138
|
+
// failed to parse or predates the plumbing.
|
|
1139
|
+
expect(base.contentBlocks).toBeUndefined();
|
|
1140
|
+
const out = renderSlackTranscript([base]);
|
|
1141
|
+
expect(out).toEqual([
|
|
1142
|
+
textMsg("user", "[11/14/23 14:25 @alice]: legacy plain"),
|
|
1143
|
+
]);
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
test("legacy row with empty contentBlocks array also falls back to single tag line", () => {
|
|
1147
|
+
const base: RenderableSlackMessage = {
|
|
1148
|
+
...userMsg(TS_14_25, "@alice", "empty blocks"),
|
|
1149
|
+
contentBlocks: [],
|
|
1150
|
+
};
|
|
1151
|
+
const out = renderSlackTranscript([base]);
|
|
1152
|
+
expect(out).toEqual([
|
|
1153
|
+
textMsg("user", "[11/14/23 14:25 @alice]: empty blocks"),
|
|
1154
|
+
]);
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
test("reaction row ignores contentBlocks and renders the single reaction tag line", () => {
|
|
1158
|
+
// Reactions go through the reaction path and never touch the
|
|
1159
|
+
// replayable-block preservation. Even if contentBlocks were somehow
|
|
1160
|
+
// populated on a reaction row, the tool blocks must not leak through.
|
|
1161
|
+
const reactionBase = reactionMsg(TS_14_28, "@bob", "👍", TS_14_25, "added");
|
|
1162
|
+
const withBlocks: RenderableSlackMessage = {
|
|
1163
|
+
...reactionBase,
|
|
1164
|
+
contentBlocks: [
|
|
1165
|
+
{ type: "tool_use", id: "tu_stray", name: "op", input: {} },
|
|
1166
|
+
],
|
|
1167
|
+
};
|
|
1168
|
+
const out = renderSlackTranscript([withBlocks]);
|
|
1169
|
+
const alias = parentAlias(TS_14_25);
|
|
1170
|
+
expect(out).toEqual([
|
|
1171
|
+
textMsg("user", `[11/14/23 14:28 @bob reacted 👍 to ${alias}]`),
|
|
1172
|
+
]);
|
|
1173
|
+
});
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
// ── orphan tool_use / tool_result filter ─────────────────────────────────────
|
|
1177
|
+
|
|
1178
|
+
describe("renderSlackTranscript — orphan tool_use / tool_result filter", () => {
|
|
1179
|
+
// A final safety pass strips any tool_use without a matching tool_result
|
|
1180
|
+
// (and vice versa) before returning. Messages that become empty after
|
|
1181
|
+
// filtering are dropped entirely so the caller never sees
|
|
1182
|
+
// `{role, content: []}`.
|
|
1183
|
+
|
|
1184
|
+
test("orphan tool_use is dropped; surrounding tag line survives", () => {
|
|
1185
|
+
// Assistant row has [text, tool_use] but no follower tool_result exists
|
|
1186
|
+
// anywhere in the transcript. The tool_use must be stripped; the tag
|
|
1187
|
+
// line (derived from the text block) stays.
|
|
1188
|
+
const base: RenderableSlackMessage = {
|
|
1189
|
+
...userMsg(TS_14_25, null, "looking it up", {
|
|
1190
|
+
role: "assistant",
|
|
1191
|
+
}),
|
|
1192
|
+
contentBlocks: [
|
|
1193
|
+
{ type: "text", text: "looking it up" },
|
|
1194
|
+
{
|
|
1195
|
+
type: "tool_use",
|
|
1196
|
+
id: "tu_orphan",
|
|
1197
|
+
name: "search",
|
|
1198
|
+
input: { q: "x" },
|
|
1199
|
+
},
|
|
1200
|
+
],
|
|
1201
|
+
};
|
|
1202
|
+
const out = renderSlackTranscript([base]);
|
|
1203
|
+
expect(out).toEqual([
|
|
1204
|
+
{
|
|
1205
|
+
role: "assistant",
|
|
1206
|
+
content: [{ type: "text", text: "looking it up" }],
|
|
1207
|
+
},
|
|
1208
|
+
]);
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
test("orphan tool_result is dropped; other content on the user row survives", () => {
|
|
1212
|
+
// User row with [tool_result (orphan), text]. The orphan tool_result is
|
|
1213
|
+
// stripped and the tag line derived from the text block survives.
|
|
1214
|
+
const base: RenderableSlackMessage = {
|
|
1215
|
+
...userMsg(TS_14_25, "@alice", "follow up"),
|
|
1216
|
+
contentBlocks: [
|
|
1217
|
+
{
|
|
1218
|
+
type: "tool_result",
|
|
1219
|
+
tool_use_id: "tu_missing",
|
|
1220
|
+
content: "stale result",
|
|
1221
|
+
},
|
|
1222
|
+
{ type: "text", text: "follow up" },
|
|
1223
|
+
],
|
|
1224
|
+
};
|
|
1225
|
+
const out = renderSlackTranscript([base]);
|
|
1226
|
+
expect(out).toEqual([
|
|
1227
|
+
{
|
|
1228
|
+
role: "user",
|
|
1229
|
+
content: [{ type: "text", text: "[11/14/23 14:25 @alice]: follow up" }],
|
|
1230
|
+
},
|
|
1231
|
+
]);
|
|
1232
|
+
});
|
|
1233
|
+
|
|
1234
|
+
test("fully-paired tool_use/tool_result — both preserved", () => {
|
|
1235
|
+
const assistantRow: RenderableSlackMessage = {
|
|
1236
|
+
...userMsg(TS_14_25, null, "running op", { role: "assistant" }),
|
|
1237
|
+
contentBlocks: [
|
|
1238
|
+
{ type: "text", text: "running op" },
|
|
1239
|
+
{ type: "tool_use", id: "tu_paired", name: "op", input: { a: 1 } },
|
|
1240
|
+
],
|
|
1241
|
+
};
|
|
1242
|
+
const userRow: RenderableSlackMessage = {
|
|
1243
|
+
...userMsg(TS_14_26, "@alice", ""),
|
|
1244
|
+
contentBlocks: [
|
|
1245
|
+
{
|
|
1246
|
+
type: "tool_result",
|
|
1247
|
+
tool_use_id: "tu_paired",
|
|
1248
|
+
content: "ok",
|
|
1249
|
+
},
|
|
1250
|
+
],
|
|
1251
|
+
};
|
|
1252
|
+
const out = renderSlackTranscript([assistantRow, userRow]);
|
|
1253
|
+
expect(out).toEqual([
|
|
1254
|
+
{
|
|
1255
|
+
role: "assistant",
|
|
1256
|
+
content: [
|
|
1257
|
+
{ type: "text", text: "running op" },
|
|
1258
|
+
{ type: "tool_use", id: "tu_paired", name: "op", input: { a: 1 } },
|
|
1259
|
+
],
|
|
1260
|
+
},
|
|
1261
|
+
{
|
|
1262
|
+
role: "user",
|
|
1263
|
+
content: [
|
|
1264
|
+
{ type: "tool_result", tool_use_id: "tu_paired", content: "ok" },
|
|
1265
|
+
],
|
|
1266
|
+
},
|
|
1267
|
+
]);
|
|
1268
|
+
});
|
|
1269
|
+
|
|
1270
|
+
test("message that becomes empty after filtering is dropped entirely", () => {
|
|
1271
|
+
// Pure tool-only user row whose tool_result has no matching tool_use.
|
|
1272
|
+
// After filtering the row is empty and must NOT be emitted as
|
|
1273
|
+
// `{role, content: []}` — it must be dropped so downstream consumers
|
|
1274
|
+
// never see an empty-content message.
|
|
1275
|
+
const orphanResultRow: RenderableSlackMessage = {
|
|
1276
|
+
...userMsg(TS_14_25, "@alice", ""),
|
|
1277
|
+
contentBlocks: [
|
|
1278
|
+
{
|
|
1279
|
+
type: "tool_result",
|
|
1280
|
+
tool_use_id: "tu_missing",
|
|
1281
|
+
content: "stale",
|
|
1282
|
+
},
|
|
1283
|
+
],
|
|
1284
|
+
};
|
|
1285
|
+
// A normal neighbour row to confirm we don't accidentally drop it too.
|
|
1286
|
+
const neighbour: RenderableSlackMessage = userMsg(TS_14_26, "@bob", "hi");
|
|
1287
|
+
const out = renderSlackTranscript([orphanResultRow, neighbour]);
|
|
1288
|
+
expect(out).toEqual([textMsg("user", "[11/14/23 14:26 @bob]: hi")]);
|
|
1289
|
+
// Sanity: the output contains no {role, content: []} placeholder.
|
|
1290
|
+
for (const m of out) {
|
|
1291
|
+
expect(m.content.length).toBeGreaterThan(0);
|
|
1292
|
+
}
|
|
1293
|
+
});
|
|
1294
|
+
|
|
1295
|
+
test("filter is idempotent: re-rendering the same input yields the same output", () => {
|
|
1296
|
+
// The function signature is `renderSlackTranscript(RenderableSlackMessage[])
|
|
1297
|
+
// -> Message[]`. Idempotence here means: rendering the same input twice
|
|
1298
|
+
// produces the same output. A mixed fixture exercises the paired path,
|
|
1299
|
+
// the orphan-tool_use drop path, and the orphan-tool_result drop path
|
|
1300
|
+
// in a single run.
|
|
1301
|
+
const fixture: RenderableSlackMessage[] = [
|
|
1302
|
+
// Paired tool call.
|
|
1303
|
+
{
|
|
1304
|
+
...userMsg(TS_14_25, null, "running op", { role: "assistant" }),
|
|
1305
|
+
contentBlocks: [
|
|
1306
|
+
{ type: "text", text: "running op" },
|
|
1307
|
+
{ type: "tool_use", id: "tu_paired", name: "op", input: {} },
|
|
1308
|
+
],
|
|
1309
|
+
},
|
|
1310
|
+
{
|
|
1311
|
+
...userMsg(TS_14_26, "@alice", ""),
|
|
1312
|
+
contentBlocks: [
|
|
1313
|
+
{ type: "tool_result", tool_use_id: "tu_paired", content: "ok" },
|
|
1314
|
+
],
|
|
1315
|
+
},
|
|
1316
|
+
// Orphan tool_use on the assistant side.
|
|
1317
|
+
{
|
|
1318
|
+
...userMsg(TS_14_28, null, "looking", { role: "assistant" }),
|
|
1319
|
+
contentBlocks: [
|
|
1320
|
+
{ type: "text", text: "looking" },
|
|
1321
|
+
{ type: "tool_use", id: "tu_orphan", name: "op", input: {} },
|
|
1322
|
+
],
|
|
1323
|
+
},
|
|
1324
|
+
// Orphan tool_result on the user side.
|
|
1325
|
+
{
|
|
1326
|
+
...userMsg(TS_14_30, "@alice", "stray"),
|
|
1327
|
+
contentBlocks: [
|
|
1328
|
+
{
|
|
1329
|
+
type: "tool_result",
|
|
1330
|
+
tool_use_id: "tu_missing",
|
|
1331
|
+
content: "stale",
|
|
1332
|
+
},
|
|
1333
|
+
{ type: "text", text: "stray" },
|
|
1334
|
+
],
|
|
1335
|
+
},
|
|
1336
|
+
];
|
|
1337
|
+
const a = renderSlackTranscript(fixture);
|
|
1338
|
+
const b = renderSlackTranscript(fixture);
|
|
1339
|
+
expect(a).toEqual(b);
|
|
1340
|
+
|
|
1341
|
+
// And confirm the expected shape explicitly so the idempotence claim is
|
|
1342
|
+
// grounded in the actual filter behaviour (paired kept, orphans stripped).
|
|
1343
|
+
expect(a).toEqual([
|
|
1344
|
+
{
|
|
1345
|
+
role: "assistant",
|
|
1346
|
+
content: [
|
|
1347
|
+
{ type: "text", text: "running op" },
|
|
1348
|
+
{ type: "tool_use", id: "tu_paired", name: "op", input: {} },
|
|
1349
|
+
],
|
|
1350
|
+
},
|
|
1351
|
+
{
|
|
1352
|
+
role: "user",
|
|
1353
|
+
content: [
|
|
1354
|
+
{ type: "tool_result", tool_use_id: "tu_paired", content: "ok" },
|
|
1355
|
+
],
|
|
1356
|
+
},
|
|
1357
|
+
{
|
|
1358
|
+
role: "assistant",
|
|
1359
|
+
content: [{ type: "text", text: "looking" }],
|
|
1360
|
+
},
|
|
1361
|
+
{
|
|
1362
|
+
role: "user",
|
|
1363
|
+
content: [{ type: "text", text: "[11/14/23 14:30 @alice]: stray" }],
|
|
1364
|
+
},
|
|
1365
|
+
]);
|
|
1366
|
+
});
|
|
1367
|
+
|
|
1368
|
+
test("filter does not touch thinking, image, file, or text blocks", () => {
|
|
1369
|
+
const base: RenderableSlackMessage = {
|
|
1370
|
+
...userMsg(TS_14_25, null, "here you go", { role: "assistant" }),
|
|
1371
|
+
contentBlocks: [
|
|
1372
|
+
{ type: "thinking", thinking: "ponder", signature: "sig" },
|
|
1373
|
+
{
|
|
1374
|
+
type: "image",
|
|
1375
|
+
source: {
|
|
1376
|
+
type: "base64",
|
|
1377
|
+
media_type: "image/png",
|
|
1378
|
+
data: "b64==",
|
|
1379
|
+
},
|
|
1380
|
+
},
|
|
1381
|
+
{
|
|
1382
|
+
type: "file",
|
|
1383
|
+
source: {
|
|
1384
|
+
type: "base64",
|
|
1385
|
+
media_type: "application/pdf",
|
|
1386
|
+
data: "pdfbase64==",
|
|
1387
|
+
filename: "doc.pdf",
|
|
1388
|
+
},
|
|
1389
|
+
},
|
|
1390
|
+
{ type: "text", text: "here you go" },
|
|
1391
|
+
],
|
|
1392
|
+
};
|
|
1393
|
+
const out = renderSlackTranscript([base]);
|
|
1394
|
+
expect(out).toEqual([
|
|
1395
|
+
{
|
|
1396
|
+
role: "assistant",
|
|
1397
|
+
content: [
|
|
1398
|
+
{ type: "thinking", thinking: "ponder", signature: "sig" },
|
|
1399
|
+
{
|
|
1400
|
+
type: "image",
|
|
1401
|
+
source: {
|
|
1402
|
+
type: "base64",
|
|
1403
|
+
media_type: "image/png",
|
|
1404
|
+
data: "b64==",
|
|
1405
|
+
},
|
|
1406
|
+
},
|
|
1407
|
+
{
|
|
1408
|
+
type: "file",
|
|
1409
|
+
source: {
|
|
1410
|
+
type: "base64",
|
|
1411
|
+
media_type: "application/pdf",
|
|
1412
|
+
data: "pdfbase64==",
|
|
1413
|
+
filename: "doc.pdf",
|
|
1414
|
+
},
|
|
1415
|
+
},
|
|
1416
|
+
{ type: "text", text: "here you go" },
|
|
1417
|
+
],
|
|
1418
|
+
},
|
|
1419
|
+
]);
|
|
1420
|
+
});
|
|
1421
|
+
});
|