@vellumai/assistant 0.6.3 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +273 -10
- package/Dockerfile +2 -3
- package/bun.lock +5 -13
- package/docs/backup-troubleshooting.md +52 -0
- package/docs/browser-use-architecture-phase2.md +174 -0
- package/docs/stt-provider-onboarding.md +120 -0
- package/knip.json +12 -2
- package/node_modules/@vellumai/ces-contracts/bun.lock +8 -6
- package/node_modules/@vellumai/ces-contracts/package.json +3 -3
- package/openapi.yaml +982 -72
- package/package.json +4 -6
- package/scripts/generate-openapi.ts +0 -1
- package/scripts/test.sh +73 -18
- package/src/__tests__/agent-image-optimize.test.ts +28 -0
- package/src/__tests__/agent-loop.test.ts +123 -0
- package/src/__tests__/anthropic-provider.test.ts +263 -10
- package/src/__tests__/auto-analysis-end-to-end.test.ts +550 -0
- package/src/__tests__/auto-analysis-prompt.test.ts +50 -0
- package/src/__tests__/browser-fill-credential.test.ts +11 -0
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
- package/src/__tests__/browser-skill-endstate.test.ts +31 -7
- package/src/__tests__/btw-routes.test.ts +7 -0
- package/src/__tests__/call-controller.test.ts +581 -20
- package/src/__tests__/catalog-files.test.ts +138 -0
- package/src/__tests__/channel-invite-transport.test.ts +2 -2
- package/src/__tests__/channel-readiness-routes.test.ts +16 -20
- package/src/__tests__/channel-readiness-service.test.ts +12 -7
- package/src/__tests__/checker.test.ts +157 -10
- package/src/__tests__/clawhub-files.test.ts +347 -0
- package/src/__tests__/commit-message-enrichment-service.test.ts +36 -19
- package/src/__tests__/config-analysis.test.ts +100 -0
- package/src/__tests__/config-schema.test.ts +1013 -66
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +339 -0
- package/src/__tests__/config-watcher.test.ts +43 -8
- package/src/__tests__/contact-store-user-file.test.ts +512 -0
- package/src/__tests__/contacts-write.test.ts +197 -0
- package/src/__tests__/context-window-manager.test.ts +88 -0
- package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop.test.ts +98 -2
- package/src/__tests__/conversation-confirmation-signals.test.ts +135 -0
- package/src/__tests__/conversation-error.test.ts +70 -0
- package/src/__tests__/conversation-history-web-search.test.ts +11 -4
- package/src/__tests__/conversation-init.benchmark.test.ts +6 -1
- package/src/__tests__/conversation-launcher-skill-regression.test.ts +51 -0
- package/src/__tests__/conversation-list-source.test.ts +145 -0
- package/src/__tests__/conversation-pre-run-repair.test.ts +2 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
- package/src/__tests__/conversation-queue.test.ts +901 -60
- package/src/__tests__/conversation-routes-disk-view.test.ts +270 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +55 -0
- package/src/__tests__/conversation-skill-tools.test.ts +7 -4
- package/src/__tests__/conversation-slash-commands.test.ts +33 -0
- package/src/__tests__/conversation-slash-queue.test.ts +89 -18
- package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
- package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +226 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
- package/src/__tests__/credential-health-service.test.ts +352 -0
- package/src/__tests__/credential-security-invariants.test.ts +5 -3
- package/src/__tests__/credential-vault-unit.test.ts +379 -3
- package/src/__tests__/credentials-cli.test.ts +40 -16
- package/src/__tests__/cross-provider-web-search.test.ts +146 -35
- package/src/__tests__/deterministic-verification-control-plane.test.ts +10 -1
- package/src/__tests__/device-id.test.ts +112 -0
- package/src/__tests__/docker-signing-key-bootstrap.test.ts +167 -4
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -3
- package/src/__tests__/email-html-renderer.test.ts +71 -0
- package/src/__tests__/email-invite-adapter.test.ts +36 -32
- package/src/__tests__/emit-event-signal.test.ts +71 -0
- package/src/__tests__/extension-id-sync-guard.test.ts +75 -8
- package/src/__tests__/fixtures/mock-chrome-extension.ts +11 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +206 -1
- package/src/__tests__/gateway-only-guard.test.ts +0 -1
- package/src/__tests__/gemini-provider.test.ts +64 -0
- package/src/__tests__/get-skill-detail-audit.test.ts +325 -0
- package/src/__tests__/gmail-archive-fallback.test.ts +193 -0
- package/src/__tests__/gmail-archive-gate.test.ts +246 -0
- package/src/__tests__/gmail-preferences.test.ts +117 -0
- package/src/__tests__/headless-browser-interactions.test.ts +43 -0
- package/src/__tests__/headless-browser-mode.test.ts +614 -0
- package/src/__tests__/headless-browser-navigate.test.ts +142 -5
- package/src/__tests__/headless-browser-read-tools.test.ts +11 -0
- package/src/__tests__/headless-browser-snapshot.test.ts +10 -0
- package/src/__tests__/heartbeat-service.test.ts +70 -17
- package/src/__tests__/home-state-routes.test.ts +162 -0
- package/src/__tests__/host-bash-proxy.test.ts +0 -5
- package/src/__tests__/host-browser-e2e-cloud.test.ts +138 -4
- package/src/__tests__/host-browser-e2e-self-hosted.test.ts +4 -4
- package/src/__tests__/host-browser-ws-events-e2e.test.ts +103 -0
- package/src/__tests__/host-cu-proxy.test.ts +0 -5
- package/src/__tests__/identity-intro-cache.test.ts +40 -10
- package/src/__tests__/init-feature-flag-overrides.test.ts +38 -112
- package/src/__tests__/jobs-store-upsert-debounced.test.ts +141 -0
- package/src/__tests__/llm-context-normalization.test.ts +488 -0
- package/src/__tests__/llm-context-route-provider.test.ts +86 -5
- package/src/__tests__/llm-usage-store.test.ts +363 -0
- package/src/__tests__/media-stream-output.test.ts +555 -0
- package/src/__tests__/media-stream-parser.test.ts +374 -0
- package/src/__tests__/media-stream-server-integration.test.ts +1234 -0
- package/src/__tests__/media-stream-stt-session.test.ts +588 -0
- package/src/__tests__/media-turn-detector.test.ts +440 -0
- package/src/__tests__/message-queue.test.ts +125 -0
- package/src/__tests__/migration-export-http.test.ts +6 -6
- package/src/__tests__/migration-import-commit-http.test.ts +8 -6
- package/src/__tests__/migration-import-preflight-http.test.ts +6 -5
- package/src/__tests__/migration-validate-http.test.ts +3 -3
- package/src/__tests__/mock-gateway-ipc.ts +151 -0
- package/src/__tests__/model-intents.test.ts +2 -2
- package/src/__tests__/oauth-apps-routes.test.ts +1 -0
- package/src/__tests__/oauth-cli.test.ts +2 -0
- package/src/__tests__/oauth-connect-orchestrator.test.ts +2 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +1 -0
- package/src/__tests__/oauth-providers-routes.test.ts +2 -0
- package/src/__tests__/oauth-store.test.ts +85 -0
- package/src/__tests__/oauth2-gateway-transport.test.ts +249 -6
- package/src/__tests__/onboarding-template-contract.test.ts +6 -13
- package/src/__tests__/openai-provider.test.ts +176 -0
- package/src/__tests__/openai-responses-cutover-guard.test.ts +184 -0
- package/src/__tests__/openai-responses-provider.test.ts +1105 -0
- package/src/__tests__/openrouter-token-estimation.test.ts +100 -0
- package/src/__tests__/outlook-unsubscribe.test.ts +31 -2
- package/src/__tests__/persona-resolver.test.ts +251 -0
- package/src/__tests__/platform-bash-auto-approve.test.ts +4 -0
- package/src/__tests__/platform.test.ts +92 -1
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +47 -0
- package/src/__tests__/prechat-onboarding-contract.test.ts +267 -0
- package/src/__tests__/pricing.test.ts +174 -0
- package/src/__tests__/qdrant-manager.test.ts +29 -8
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +194 -0
- package/src/__tests__/relationship-state-contract.test.ts +175 -0
- package/src/__tests__/relay-server.test.ts +423 -5
- package/src/__tests__/search-skills-unified.test.ts +118 -0
- package/src/__tests__/secret-scanner-executor.test.ts +4 -0
- package/src/__tests__/secure-keys.test.ts +107 -0
- package/src/__tests__/send-endpoint-busy.test.ts +5 -1
- package/src/__tests__/sequence-store.test.ts +1 -1
- package/src/__tests__/server-history-render.test.ts +49 -0
- package/src/__tests__/settings-routes.test.ts +201 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +1 -0
- package/src/__tests__/skills-file-content-endpoint.test.ts +276 -145
- package/src/__tests__/skills-files-catalog-fallback.test.ts +381 -93
- package/src/__tests__/skills.test.ts +5 -2
- package/src/__tests__/skillssh-files.test.ts +446 -0
- package/src/__tests__/slack-block-formatting.test.ts +110 -0
- package/src/__tests__/slack-channel-config.test.ts +564 -1
- package/src/__tests__/stt-catalog-parity.test.ts +282 -0
- package/src/__tests__/stt-stream-session.test.ts +535 -0
- package/src/__tests__/system-prompt.test.ts +112 -26
- package/src/__tests__/telephony-stt-routing.test.ts +329 -0
- package/src/__tests__/terminal-tools.test.ts +18 -7
- package/src/__tests__/test-preload.ts +18 -0
- package/src/__tests__/test-support/browser-skill-harness.ts +4 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +9 -5
- package/src/__tests__/tool-executor-shell-integration.test.ts +4 -0
- package/src/__tests__/tool-executor.test.ts +33 -24
- package/src/__tests__/tool-result-truncation.test.ts +36 -0
- package/src/__tests__/trust-store.test.ts +7 -1
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -1
- package/src/__tests__/tts-catalog-parity.test.ts +345 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +512 -114
- package/src/__tests__/twilio-routes.test.ts +376 -0
- package/src/__tests__/unicode.test.ts +293 -0
- package/src/__tests__/update-bulletin-format.test.ts +59 -0
- package/src/__tests__/update-bulletin.test.ts +206 -5
- package/src/__tests__/usage-routes.test.ts +25 -4
- package/src/__tests__/user-reference.test.ts +46 -61
- package/src/__tests__/verification-control-plane-policy.test.ts +4 -0
- package/src/__tests__/voice-config-update.test.ts +403 -0
- package/src/__tests__/voice-quality.test.ts +434 -19
- package/src/__tests__/workspace-heartbeat-service.test.ts +7 -0
- package/src/__tests__/workspace-migration-033-stt-service-explicit-config.test.ts +547 -0
- package/src/__tests__/workspace-migration-034-remove-calls-voice-transcription-provider.test.ts +596 -0
- package/src/__tests__/workspace-migration-drop-user-md.test.ts +368 -0
- package/src/__tests__/workspace-migration-meets.test.ts +244 -0
- package/src/__tests__/workspace-migration-seed-device-id.test.ts +14 -20
- package/src/__tests__/workspace-policy.test.ts +2 -0
- package/src/agent/image-optimize.ts +24 -12
- package/src/agent/loop.ts +43 -3
- package/src/backup/__tests__/backup-key.test.ts +152 -0
- package/src/backup/__tests__/backup-worker.test.ts +767 -0
- package/src/backup/__tests__/list-snapshots.test.ts +87 -0
- package/src/backup/__tests__/local-writer.test.ts +218 -0
- package/src/backup/__tests__/offsite-writer.test.ts +641 -0
- package/src/backup/__tests__/paths.test.ts +300 -0
- package/src/backup/__tests__/restore.test.ts +498 -0
- package/src/backup/__tests__/snapshot-lock.test.ts +352 -0
- package/src/backup/__tests__/stream-crypt.test.ts +228 -0
- package/src/backup/backup-key.ts +137 -0
- package/src/backup/backup-worker.ts +459 -0
- package/src/backup/list-snapshots.ts +147 -0
- package/src/backup/local-writer.ts +133 -0
- package/src/backup/offsite-writer.ts +222 -0
- package/src/backup/paths.ts +226 -0
- package/src/backup/restore.ts +322 -0
- package/src/backup/snapshot-lock.ts +431 -0
- package/src/backup/stream-crypt.ts +263 -0
- package/src/bundler/package-resolver.ts +4 -0
- package/src/calls/audio-store.ts +11 -5
- package/src/calls/call-controller.ts +226 -71
- package/src/calls/call-domain.ts +9 -0
- package/src/calls/call-speech-output.ts +190 -0
- package/src/calls/call-transport.ts +77 -0
- package/src/calls/media-stream-audio-transcode.ts +173 -0
- package/src/calls/media-stream-output.ts +660 -0
- package/src/calls/media-stream-parser.ts +300 -0
- package/src/calls/media-stream-protocol.ts +166 -0
- package/src/calls/media-stream-server.ts +592 -0
- package/src/calls/media-stream-stt-session.ts +460 -0
- package/src/calls/media-turn-detector.ts +230 -0
- package/src/calls/relay-server.ts +90 -75
- package/src/calls/resolve-call-tts-provider.ts +136 -0
- package/src/calls/telephony-stt-routing.ts +145 -0
- package/src/calls/tts-call-strategy.ts +161 -0
- package/src/calls/tts-text-sanitizer.ts +32 -16
- package/src/calls/twilio-routes.ts +281 -17
- package/src/calls/voice-quality.ts +78 -35
- package/src/calls/voice-session-bridge.ts +8 -1
- package/src/channels/types.ts +16 -0
- package/src/cli/__tests__/run-assistant-command.ts +11 -1
- package/src/cli/commands/__tests__/backup.test.ts +1165 -0
- package/src/cli/commands/__tests__/domain-register.test.ts +234 -0
- package/src/cli/commands/__tests__/domain-status.test.ts +132 -0
- package/src/cli/commands/__tests__/email-attachment.test.ts +422 -0
- package/src/cli/commands/__tests__/email-download.test.ts +16 -1
- package/src/cli/commands/__tests__/email-list.test.ts +22 -4
- package/src/cli/commands/__tests__/email-register.test.ts +4 -4
- package/src/cli/commands/__tests__/email-send.test.ts +37 -4
- package/src/cli/commands/__tests__/email-status.test.ts +5 -1
- package/src/cli/commands/__tests__/email-unregister.test.ts +34 -5
- package/src/cli/commands/backup.ts +993 -0
- package/src/cli/commands/conversations.ts +77 -0
- package/src/cli/commands/credentials.ts +0 -1
- package/src/cli/commands/domain.ts +210 -0
- package/src/cli/commands/email.ts +255 -3
- package/src/cli/commands/oauth/__tests__/connect.test.ts +12 -0
- package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +1 -0
- package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -0
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -0
- package/src/cli/commands/oauth/mode.ts +12 -3
- package/src/cli/commands/oauth/providers.ts +15 -0
- package/src/cli/commands/oauth/shared.ts +2 -1
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +4 -9
- package/src/cli/commands/platform/__tests__/connect.test.ts +6 -0
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +6 -0
- package/src/cli/program.ts +30 -4
- package/src/config/__tests__/backup-schema.test.ts +134 -0
- package/src/config/assistant-feature-flags.ts +61 -62
- package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +37 -1
- package/src/config/bundled-skills/browser/SKILL.md +30 -5
- package/src/config/bundled-skills/browser/TOOLS.json +123 -0
- package/src/config/bundled-skills/browser/tools/browser-attach.ts +12 -0
- package/src/config/bundled-skills/browser/tools/browser-detach.ts +12 -0
- package/src/config/bundled-skills/browser/tools/browser-status.ts +12 -0
- package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +17 -0
- package/src/config/bundled-skills/contacts/SKILL.md +2 -2
- package/src/config/bundled-skills/gmail/SKILL.md +53 -7
- package/src/config/bundled-skills/gmail/TOOLS.json +33 -3
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +116 -9
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +138 -11
- package/src/config/bundled-skills/gmail/tools/gmail-preferences-tool.ts +59 -0
- package/src/config/bundled-skills/gmail/tools/gmail-preferences.ts +82 -0
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +113 -17
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -2
- package/src/config/bundled-skills/media-processing/SKILL.md +3 -9
- package/src/config/bundled-skills/media-processing/TOOLS.json +1 -6
- package/src/config/bundled-skills/media-processing/__tests__/audio-transcribe.test.ts +125 -0
- package/src/config/bundled-skills/media-processing/__tests__/extract-keyframes.test.ts +181 -0
- package/src/config/bundled-skills/media-processing/__tests__/preprocess-audio.test.ts +141 -0
- package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +32 -87
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +8 -4
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +0 -10
- package/src/config/bundled-skills/messaging/SKILL.md +3 -3
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
- package/src/config/bundled-skills/outlook/SKILL.md +2 -2
- package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +2 -2
- package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
- package/src/config/bundled-skills/phone-calls/references/CONFIG.md +27 -18
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +3 -3
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +26 -22
- package/src/config/bundled-skills/slack/SKILL.md +1 -0
- package/src/config/bundled-skills/transcribe/SKILL.md +9 -14
- package/src/config/bundled-skills/transcribe/TOOLS.json +2 -7
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +256 -0
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +38 -188
- package/src/config/bundled-tool-registry.ts +8 -0
- package/src/config/env-registry.ts +24 -0
- package/src/config/env.ts +34 -10
- package/src/config/feature-flag-registry.json +46 -14
- package/src/config/loader.ts +26 -12
- package/src/config/schema.ts +35 -10
- package/src/config/schemas/__tests__/stt.test.ts +43 -0
- package/src/config/schemas/analysis.ts +51 -0
- package/src/config/schemas/backup.ts +72 -0
- package/src/config/schemas/calls.ts +1 -26
- package/src/config/schemas/elevenlabs.ts +0 -59
- package/src/config/schemas/filing.ts +47 -7
- package/src/config/schemas/heartbeat.ts +27 -5
- package/src/config/schemas/host-browser.ts +47 -1
- package/src/config/schemas/inference.ts +1 -1
- package/src/config/schemas/memory-lifecycle.ts +14 -2
- package/src/config/schemas/services.ts +44 -0
- package/src/config/schemas/stt.ts +59 -0
- package/src/config/schemas/tts.ts +230 -0
- package/src/config/schemas/updates.ts +14 -0
- package/src/config/skills.ts +4 -0
- package/src/config/types.ts +4 -0
- package/src/contacts/contact-store.ts +56 -11
- package/src/contacts/contacts-write.ts +38 -1
- package/src/context/post-turn-tool-result-truncation.ts +3 -2
- package/src/context/tool-result-truncation.ts +2 -1
- package/src/context/window-manager.ts +45 -12
- package/src/credential-execution/executable-discovery.ts +12 -2
- package/src/credential-execution/process-manager.ts +33 -2
- package/src/credential-health/credential-health-service.ts +366 -0
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +324 -0
- package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +497 -0
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +17 -8
- package/src/daemon/__tests__/lifecycle-startup-ordering.test.ts +127 -0
- package/src/daemon/config-watcher.ts +99 -5
- package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
- package/src/daemon/conversation-agent-loop.ts +101 -24
- package/src/daemon/conversation-error.ts +11 -0
- package/src/daemon/conversation-history.ts +40 -6
- package/src/daemon/conversation-launch.ts +220 -0
- package/src/daemon/conversation-lifecycle.ts +59 -9
- package/src/daemon/conversation-messaging.ts +37 -3
- package/src/daemon/conversation-notifiers.ts +5 -0
- package/src/daemon/conversation-process.ts +581 -19
- package/src/daemon/conversation-queue-manager.ts +24 -0
- package/src/daemon/conversation-runtime-assembly.ts +11 -1
- package/src/daemon/conversation-slash.ts +36 -0
- package/src/daemon/conversation-surfaces.ts +94 -4
- package/src/daemon/conversation-tool-setup.ts +25 -0
- package/src/daemon/conversation-usage.ts +7 -4
- package/src/daemon/conversation.ts +86 -28
- package/src/daemon/handlers/config-slack-channel.ts +269 -94
- package/src/daemon/handlers/conversations.ts +4 -1
- package/src/daemon/handlers/shared.ts +22 -0
- package/src/daemon/handlers/skills.ts +321 -77
- package/src/daemon/host-browser-proxy.ts +2 -1
- package/src/daemon/lifecycle.ts +122 -25
- package/src/daemon/message-protocol.ts +6 -0
- package/src/daemon/message-types/conversations.ts +34 -1
- package/src/daemon/message-types/home.ts +40 -0
- package/src/daemon/message-types/meet.ts +143 -0
- package/src/daemon/message-types/messages.ts +14 -0
- package/src/daemon/message-types/schedules.ts +34 -2
- package/src/daemon/message-types/skills.ts +16 -0
- package/src/daemon/message-types/surfaces.ts +2 -0
- package/src/daemon/server.ts +347 -2
- package/src/daemon/shutdown-handlers.ts +32 -4
- package/src/daemon/shutdown-registry.ts +40 -0
- package/src/daemon/tool-side-effects.ts +9 -0
- package/src/email/html-renderer.ts +76 -0
- package/src/heartbeat/heartbeat-service.ts +93 -7
- package/src/home/__tests__/assistant-feed-authoring.test.ts +156 -0
- package/src/home/__tests__/emit-feed-event.test.ts +169 -0
- package/src/home/__tests__/feed-scheduler.test.ts +194 -0
- package/src/home/__tests__/feed-types.test.ts +275 -0
- package/src/home/__tests__/feed-writer.test.ts +688 -0
- package/src/home/__tests__/phase5-exit-criteria.test.ts +212 -0
- package/src/home/__tests__/platform-gmail-digest.test.ts +222 -0
- package/src/home/__tests__/progress-formula.test.ts +213 -0
- package/src/home/__tests__/relationship-state-writer.test.ts +740 -0
- package/src/home/__tests__/rollup-producer.test.ts +398 -0
- package/src/home/assistant-feed-authoring.ts +124 -0
- package/src/home/emit-feed-event.ts +158 -0
- package/src/home/feed-scheduler.ts +247 -0
- package/src/home/feed-types.ts +181 -0
- package/src/home/feed-writer.ts +469 -0
- package/src/home/platform-gmail-digest.ts +163 -0
- package/src/home/progress-formula.ts +86 -0
- package/src/home/relationship-state-writer.ts +824 -0
- package/src/home/relationship-state.ts +143 -0
- package/src/home/rollup-producer.ts +384 -0
- package/src/hooks/runner.ts +7 -0
- package/src/inbound/platform-callback-registration.ts +12 -3
- package/src/inbound/public-ingress-urls.ts +12 -0
- package/src/instrument.ts +1 -1
- package/src/ipc/__tests__/cli-ipc.test.ts +200 -0
- package/src/ipc/cli-client.ts +151 -0
- package/src/ipc/cli-server.ts +234 -0
- package/src/ipc/gateway-client.ts +180 -0
- package/src/ipc/routes/index.ts +5 -0
- package/src/ipc/routes/wake-conversation.ts +19 -0
- package/src/memory/__tests__/auto-analysis-enqueue.test.ts +356 -0
- package/src/memory/__tests__/auto-analysis-guard.test.ts +57 -0
- package/src/memory/__tests__/conversation-analyze-job.test.ts +232 -0
- package/src/memory/__tests__/find-analysis-conversation.test.ts +196 -0
- package/src/memory/app-store.ts +1 -1
- package/src/memory/attachments-store.ts +70 -0
- package/src/memory/auto-analysis-enqueue.ts +127 -0
- package/src/memory/auto-analysis-guard.ts +27 -0
- package/src/memory/cleanup-schedule-state.ts +37 -0
- package/src/memory/conversation-analyze-job.ts +73 -0
- package/src/memory/conversation-crud.ts +99 -0
- package/src/memory/conversation-disk-view.ts +7 -0
- package/src/memory/conversation-group-migration.ts +34 -2
- package/src/memory/conversation-queries.ts +6 -5
- package/src/memory/db-init.ts +6 -0
- package/src/memory/db-maintenance.ts +108 -0
- package/src/memory/db.ts +1 -0
- package/src/memory/graph/conversation-graph-memory.ts +15 -0
- package/src/memory/graph/extraction.test.ts +23 -0
- package/src/memory/graph/extraction.ts +8 -0
- package/src/memory/graph/retriever.ts +27 -18
- package/src/memory/graph/scoring.test.ts +186 -0
- package/src/memory/graph/scoring.ts +31 -1
- package/src/memory/graph/tools.ts +1 -1
- package/src/memory/group-crud.ts +6 -1
- package/src/memory/indexer.ts +95 -16
- package/src/memory/job-handlers/cleanup.ts +11 -8
- package/src/memory/job-handlers/conversation-starters.ts +16 -10
- package/src/memory/jobs-store.ts +64 -4
- package/src/memory/jobs-worker.ts +22 -9
- package/src/memory/llm-usage-store.ts +92 -56
- package/src/memory/migrations/219-oauth-providers-token-exchange-body-format.ts +15 -0
- package/src/memory/migrations/220-normalize-user-file-by-principal.ts +190 -0
- package/src/memory/migrations/221-conversations-archived-at.ts +16 -0
- package/src/memory/migrations/index.ts +6 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/qdrant-manager.ts +43 -16
- package/src/memory/schema/conversations.ts +2 -0
- package/src/memory/schema/oauth.ts +3 -0
- package/src/memory/usage-buckets.ts +396 -0
- package/src/messaging/providers/gmail/client.ts +57 -6
- package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +282 -0
- package/src/messaging/providers/slack/adapter.ts +143 -38
- package/src/messaging/providers/slack/client.ts +16 -0
- package/src/messaging/providers/slack/types.ts +4 -0
- package/src/notifications/decision-engine.ts +3 -3
- package/src/notifications/signal.ts +5 -0
- package/src/oauth/__tests__/identity-verifier.test.ts +1 -0
- package/src/oauth/byo-connection.test.ts +18 -1
- package/src/oauth/byo-connection.ts +3 -1
- package/src/oauth/connect-orchestrator.ts +2 -0
- package/src/oauth/connection-resolver.ts +6 -2
- package/src/oauth/connection.ts +2 -0
- package/src/oauth/oauth-store.ts +9 -0
- package/src/oauth/platform-connection.test.ts +98 -0
- package/src/oauth/platform-connection.ts +52 -31
- package/src/oauth/seed-providers.ts +7 -0
- package/src/permissions/checker.ts +16 -6
- package/src/permissions/defaults.ts +49 -1
- package/src/permissions/trust-store.ts +3 -3
- package/src/permissions/workspace-policy.ts +3 -0
- package/src/platform/client.test.ts +10 -0
- package/src/platform/sync-identity.ts +129 -0
- package/src/prompts/persona-resolver.ts +126 -2
- package/src/prompts/system-prompt.ts +59 -18
- package/src/prompts/templates/BOOTSTRAP.md +5 -5
- package/src/prompts/templates/SOUL.md +3 -1
- package/src/prompts/templates/UPDATES.md +12 -0
- package/src/prompts/templates/channels/slack.md +20 -0
- package/src/prompts/update-bulletin-format.ts +26 -9
- package/src/prompts/update-bulletin.ts +34 -23
- package/src/prompts/user-reference.ts +20 -17
- package/src/providers/__tests__/provider-secret-catalog.test.ts +42 -0
- package/src/providers/anthropic/client.ts +157 -61
- package/src/providers/fireworks/client.ts +2 -2
- package/src/providers/gemini/client.ts +9 -1
- package/src/providers/model-catalog.ts +6 -0
- package/src/providers/model-intents.ts +4 -4
- package/src/providers/ollama/client.ts +2 -2
- package/src/providers/openai/chat-completions-provider.ts +474 -0
- package/src/providers/openai/client.ts +25 -440
- package/src/providers/openai/responses-provider.ts +502 -0
- package/src/providers/openrouter/client.ts +101 -4
- package/src/providers/provider-secret-catalog.ts +139 -0
- package/src/providers/registry.ts +2 -2
- package/src/providers/retry.ts +14 -3
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +251 -0
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +828 -0
- package/src/providers/speech-to-text/deepgram-realtime.test.ts +980 -0
- package/src/providers/speech-to-text/deepgram-realtime.ts +767 -0
- package/src/providers/speech-to-text/deepgram.test.ts +332 -0
- package/src/providers/speech-to-text/deepgram.ts +115 -0
- package/src/providers/speech-to-text/google-gemini-live-stream.test.ts +743 -0
- package/src/providers/speech-to-text/google-gemini-live-stream.ts +625 -0
- package/src/providers/speech-to-text/google-gemini.test.ts +226 -0
- package/src/providers/speech-to-text/google-gemini.ts +101 -0
- package/src/providers/speech-to-text/openai-whisper-stream.test.ts +564 -0
- package/src/providers/speech-to-text/openai-whisper-stream.ts +381 -0
- package/src/providers/speech-to-text/openai-whisper.test.ts +1 -37
- package/src/providers/speech-to-text/openai-whisper.ts +63 -33
- package/src/providers/speech-to-text/provider-catalog.ts +306 -0
- package/src/providers/speech-to-text/resolve.ts +386 -6
- package/src/providers/types.ts +9 -0
- package/src/runtime/AGENTS.md +43 -1
- package/src/runtime/__tests__/agent-wake.test.ts +831 -0
- package/src/runtime/__tests__/runtime-mode.test.ts +62 -0
- package/src/runtime/__tests__/slack-block-formatting.test.ts +481 -0
- package/src/runtime/agent-wake.ts +512 -0
- package/src/runtime/auth/__tests__/route-policy.test.ts +40 -0
- package/src/runtime/auth/route-policy.ts +30 -5
- package/src/runtime/auth/token-service.ts +56 -1
- package/src/runtime/btw-sidechain.ts +2 -0
- package/src/runtime/capability-tokens.ts +10 -10
- package/src/runtime/channel-invite-transport.ts +1 -1
- package/src/runtime/channel-invite-transports/email.ts +14 -6
- package/src/runtime/channel-readiness-service.ts +12 -22
- package/src/runtime/chrome-extension-registry.ts +38 -2
- package/src/runtime/http-server.ts +395 -10
- package/src/runtime/http-types.ts +6 -2
- package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +36 -0
- package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +360 -0
- package/src/runtime/migrations/migration-transport.ts +1 -0
- package/src/runtime/migrations/migration-wizard.ts +1 -0
- package/src/runtime/migrations/vbundle-import-analyzer.ts +77 -1
- package/src/runtime/migrations/vbundle-importer.ts +34 -0
- package/src/runtime/pending-interactions.ts +0 -11
- package/src/runtime/routes/__tests__/backup-routes.test.ts +967 -0
- package/src/runtime/routes/__tests__/home-feed-routes.test.ts +507 -0
- package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +208 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +406 -0
- package/src/runtime/routes/__tests__/tts-routes.test.ts +474 -0
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +148 -17
- package/src/runtime/routes/app-management-routes.ts +12 -18
- package/src/runtime/routes/attachment-routes.test.ts +9 -3
- package/src/runtime/routes/attachment-routes.ts +216 -17
- package/src/runtime/routes/backup-routes.ts +519 -0
- package/src/runtime/routes/browser-extension-pair-routes.ts +82 -23
- package/src/runtime/routes/btw-routes.ts +8 -6
- package/src/runtime/routes/contact-routes.test.ts +298 -0
- package/src/runtime/routes/contact-routes.ts +132 -5
- package/src/runtime/routes/conversation-analysis-routes.ts +22 -142
- package/src/runtime/routes/conversation-management-routes.ts +115 -0
- package/src/runtime/routes/conversation-routes.ts +367 -146
- package/src/runtime/routes/filing-routes.ts +93 -0
- package/src/runtime/routes/home-feed-routes.ts +334 -0
- package/src/runtime/routes/home-state-routes.ts +138 -0
- package/src/runtime/routes/host-browser-routes.ts +3 -14
- package/src/runtime/routes/identity-intro-cache.ts +7 -3
- package/src/runtime/routes/identity-routes.ts +3 -17
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +46 -39
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +15 -15
- package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +137 -0
- package/src/runtime/routes/integrations/slack/__tests__/share.test.ts +179 -0
- package/src/runtime/routes/integrations/slack/channel.ts +11 -3
- package/src/runtime/routes/integrations/slack/share.ts +45 -7
- package/src/runtime/routes/llm-context-normalization.ts +303 -0
- package/src/runtime/routes/memory-item-routes.test.ts +3 -2
- package/src/runtime/routes/migration-routes.ts +40 -5
- package/src/runtime/routes/settings-routes.ts +22 -5
- package/src/runtime/routes/skills-routes.ts +76 -7
- package/src/runtime/routes/stt-routes.ts +233 -0
- package/src/runtime/routes/surface-action-routes.ts +41 -2
- package/src/runtime/routes/tts-routes.ts +108 -24
- package/src/runtime/routes/usage-routes.ts +30 -2
- package/src/runtime/routes/user-route-dispatcher.ts +50 -5
- package/src/runtime/routes/user-routes.ts +13 -1
- package/src/runtime/routes/work-items-routes.ts +8 -1
- package/src/runtime/runtime-mode.ts +33 -0
- package/src/runtime/services/__tests__/analyze-conversation.test.ts +444 -0
- package/src/runtime/services/__tests__/analyze-deps-singleton.test.ts +67 -0
- package/src/runtime/services/__tests__/auto-analysis-prompt.test.ts +53 -0
- package/src/runtime/services/__tests__/manual-analysis-prompt.test.ts +41 -0
- package/src/runtime/services/analyze-conversation.ts +344 -0
- package/src/runtime/services/analyze-deps-singleton.ts +32 -0
- package/src/runtime/services/auto-analysis-prompt.ts +55 -0
- package/src/runtime/skill-route-registry.ts +49 -0
- package/src/runtime/slack-block-formatting.ts +437 -10
- package/src/schedule/scheduler.ts +50 -0
- package/src/security/oauth2.ts +26 -4
- package/src/security/secure-keys.ts +25 -2
- package/src/security/token-manager.ts +8 -0
- package/src/sequence/engine.ts +23 -0
- package/src/sequence/types.ts +1 -1
- package/src/skills/catalog-files.ts +64 -2
- package/src/skills/category-inference.ts +122 -0
- package/src/skills/clawhub-files.ts +213 -0
- package/src/skills/clawhub.ts +84 -23
- package/src/skills/skill-file-provider.ts +40 -0
- package/src/skills/skillssh-files.ts +395 -0
- package/src/skills/skillssh-registry.ts +4 -4
- package/src/stt/__tests__/daemon-batch-transcriber.test.ts +392 -0
- package/src/stt/__tests__/types.test.ts +89 -0
- package/src/stt/daemon-batch-transcriber.ts +195 -0
- package/src/stt/stt-stream-session.ts +499 -0
- package/src/stt/types.ts +330 -0
- package/src/stt/wav-encoder.test.ts +373 -0
- package/src/stt/wav-encoder.ts +175 -0
- package/src/subagent/manager.ts +38 -14
- package/src/tools/browser/__tests__/browser-mode.test.ts +119 -0
- package/src/tools/browser/__tests__/browser-status.test.ts +123 -0
- package/src/tools/browser/browser-execution.ts +1163 -23
- package/src/tools/browser/browser-manager.ts +45 -0
- package/src/tools/browser/browser-mode-constants.ts +12 -0
- package/src/tools/browser/browser-mode.ts +92 -0
- package/src/tools/browser/browser-status-constants.ts +33 -0
- package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +393 -0
- package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +29 -0
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +1648 -32
- package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +264 -0
- package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +183 -17
- package/src/tools/browser/cdp-client/cdp-inspect-client.ts +254 -21
- package/src/tools/browser/cdp-client/errors.ts +15 -0
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +39 -16
- package/src/tools/browser/cdp-client/factory.ts +797 -87
- package/src/tools/browser/cdp-client/index.ts +16 -2
- package/src/tools/browser/cdp-client/types.ts +68 -0
- package/src/tools/credentials/vault.ts +35 -6
- package/src/tools/network/web-fetch.ts +5 -2
- package/src/tools/network/web-search.ts +5 -2
- package/src/tools/shared/shell-output.ts +3 -1
- package/src/tools/side-effects.ts +2 -0
- package/src/tools/skills/sandbox-runner.ts +3 -2
- package/src/tools/terminal/safe-env.ts +10 -2
- package/src/tools/terminal/shell.ts +15 -4
- package/src/tools/tool-manifest.ts +21 -0
- package/src/tools/types.ts +17 -0
- package/src/tools/ui-surface/definitions.ts +6 -1
- package/src/tts/__tests__/provider-adapters.test.ts +834 -0
- package/src/tts/__tests__/provider-catalog-consistency.test.ts +196 -0
- package/src/tts/__tests__/provider-catalog.test.ts +183 -0
- package/src/tts/__tests__/provider-registry.test.ts +90 -0
- package/src/tts/provider-catalog.ts +201 -0
- package/src/tts/provider-registry.ts +73 -0
- package/src/tts/providers/deepgram-provider.ts +219 -0
- package/src/tts/providers/elevenlabs-provider.ts +211 -0
- package/src/tts/providers/fish-audio-provider.ts +183 -0
- package/src/tts/providers/index.ts +42 -0
- package/src/tts/providers/register-builtins.ts +130 -0
- package/src/tts/synthesize-text.ts +110 -0
- package/src/tts/tts-config-resolver.ts +78 -0
- package/src/tts/types.ts +153 -0
- package/src/types/onboarding-context.ts +7 -0
- package/src/util/abort-reasons.ts +58 -0
- package/src/util/device-id.ts +32 -16
- package/src/util/errors.ts +9 -1
- package/src/util/platform.ts +54 -10
- package/src/util/pricing.ts +66 -3
- package/src/util/spawn.ts +1 -1
- package/src/util/truncate.ts +4 -2
- package/src/util/unicode.ts +201 -0
- package/src/version.ts +19 -24
- package/src/watcher/engine.ts +23 -0
- package/src/watcher/watcher-store.ts +31 -0
- package/src/workspace/migrations/003-seed-device-id.ts +9 -3
- package/src/workspace/migrations/017-seed-persona-dirs.ts +68 -4
- package/src/workspace/migrations/029-seed-pkb.ts +1 -1
- package/src/workspace/migrations/031-drop-user-md.ts +317 -0
- package/src/workspace/migrations/031-llm-log-retention-zero-to-null.ts +73 -0
- package/src/workspace/migrations/032-tts-provider-unification.ts +227 -0
- package/src/workspace/migrations/033-stt-service-explicit-config.ts +122 -0
- package/src/workspace/migrations/034-remove-calls-voice-transcription-provider.ts +215 -0
- package/src/workspace/migrations/035-seed-slack-channel-persona.ts +50 -0
- package/src/workspace/migrations/036-update-pkb-index-bar.ts +37 -0
- package/src/workspace/migrations/037-create-meets-dir.ts +61 -0
- package/src/workspace/migrations/registry.ts +16 -0
- package/src/workspace/top-level-renderer.ts +13 -1
- package/src/workspace/turn-commit.ts +31 -0
- package/src/__tests__/email-cli.test.ts +0 -297
- package/src/__tests__/email-service-config-fallback.test.ts +0 -102
- package/src/cli/commands/browser-relay.ts +0 -466
- package/src/email/guardrails.ts +0 -221
- package/src/email/provider.ts +0 -117
- package/src/email/providers/agentmail.ts +0 -361
- package/src/email/providers/index.ts +0 -65
- package/src/email/service.ts +0 -384
- package/src/email/types.ts +0 -126
- package/src/prompts/templates/USER.md +0 -13
- package/src/providers/speech-to-text/types.ts +0 -17
- package/src/runtime/routes/browser-cdp-routes.ts +0 -229
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `getDaemonRuntimeMode()` — ensures the helper reflects
|
|
3
|
+
* `IS_CONTAINERIZED` environment state via the existing truthy-check
|
|
4
|
+
* semantics shared with `getIsContainerized()`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
8
|
+
|
|
9
|
+
import { getDaemonRuntimeMode } from "../runtime-mode.js";
|
|
10
|
+
|
|
11
|
+
describe("getDaemonRuntimeMode", () => {
|
|
12
|
+
let savedIsContainerized: string | undefined;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
savedIsContainerized = process.env.IS_CONTAINERIZED;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
if (savedIsContainerized === undefined) {
|
|
20
|
+
delete process.env.IS_CONTAINERIZED;
|
|
21
|
+
} else {
|
|
22
|
+
process.env.IS_CONTAINERIZED = savedIsContainerized;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("returns 'docker' when IS_CONTAINERIZED=true", () => {
|
|
27
|
+
process.env.IS_CONTAINERIZED = "true";
|
|
28
|
+
expect(getDaemonRuntimeMode()).toBe("docker");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("returns 'docker' when IS_CONTAINERIZED=1", () => {
|
|
32
|
+
// Matches the existing truthy-check in getIsContainerized(), which
|
|
33
|
+
// treats "1" as equivalent to "true" for boolean env flags.
|
|
34
|
+
process.env.IS_CONTAINERIZED = "1";
|
|
35
|
+
expect(getDaemonRuntimeMode()).toBe("docker");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("returns 'bare-metal' when IS_CONTAINERIZED is unset", () => {
|
|
39
|
+
delete process.env.IS_CONTAINERIZED;
|
|
40
|
+
expect(getDaemonRuntimeMode()).toBe("bare-metal");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("returns 'bare-metal' when IS_CONTAINERIZED=false", () => {
|
|
44
|
+
process.env.IS_CONTAINERIZED = "false";
|
|
45
|
+
expect(getDaemonRuntimeMode()).toBe("bare-metal");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("returns 'bare-metal' when IS_CONTAINERIZED=0", () => {
|
|
49
|
+
process.env.IS_CONTAINERIZED = "0";
|
|
50
|
+
expect(getDaemonRuntimeMode()).toBe("bare-metal");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("returns 'bare-metal' when IS_CONTAINERIZED is an empty string", () => {
|
|
54
|
+
process.env.IS_CONTAINERIZED = "";
|
|
55
|
+
expect(getDaemonRuntimeMode()).toBe("bare-metal");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("returns 'bare-metal' for arbitrary non-truthy values", () => {
|
|
59
|
+
process.env.IS_CONTAINERIZED = "yes";
|
|
60
|
+
expect(getDaemonRuntimeMode()).toBe("bare-metal");
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for `splitLongTextSegment` — the pure helper that slices a
|
|
3
|
+
* string into Slack-section-sized chunks while preferring natural
|
|
4
|
+
* boundaries (paragraph → newline → sentence → hard slice).
|
|
5
|
+
*
|
|
6
|
+
* Also covers `textToSlackBlocks` integration for the long-text splitting
|
|
7
|
+
* path.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, test } from "bun:test";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
SLACK_SECTION_MAX_CHARS,
|
|
14
|
+
splitCodeSegmentContent,
|
|
15
|
+
splitLongTextSegment,
|
|
16
|
+
textToSlackBlocks,
|
|
17
|
+
} from "../slack-block-formatting.js";
|
|
18
|
+
|
|
19
|
+
describe("splitLongTextSegment", () => {
|
|
20
|
+
test("returns single-element array for text under the limit", () => {
|
|
21
|
+
const text = "short message";
|
|
22
|
+
const chunks = splitLongTextSegment(text);
|
|
23
|
+
expect(chunks).toEqual([text]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("returns single-element array for text exactly at the limit", () => {
|
|
27
|
+
const text = "a".repeat(SLACK_SECTION_MAX_CHARS);
|
|
28
|
+
const chunks = splitLongTextSegment(text);
|
|
29
|
+
expect(chunks).toEqual([text]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("splits 5000-char paragraph-only text into ≥ 2 chunks under the limit and reconstructs input", () => {
|
|
33
|
+
// Build enough paragraphs (~50 chars each + "\n\n" separators) to
|
|
34
|
+
// comfortably exceed 5000 chars.
|
|
35
|
+
const paragraphs: string[] = [];
|
|
36
|
+
for (let i = 0; i < 120; i++) {
|
|
37
|
+
paragraphs.push(`Paragraph number ${i} with filler content here.`);
|
|
38
|
+
}
|
|
39
|
+
const text = paragraphs.join("\n\n");
|
|
40
|
+
expect(text.length).toBeGreaterThanOrEqual(5000);
|
|
41
|
+
|
|
42
|
+
const chunks = splitLongTextSegment(text);
|
|
43
|
+
|
|
44
|
+
expect(chunks.length).toBeGreaterThanOrEqual(2);
|
|
45
|
+
for (const chunk of chunks) {
|
|
46
|
+
expect(chunk.length).toBeLessThanOrEqual(SLACK_SECTION_MAX_CHARS);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Joining chunks with empty string should recover all non-whitespace
|
|
50
|
+
// content (the helper trims chunk boundaries, so inter-chunk "\n\n"
|
|
51
|
+
// separators may be collapsed). Compare whitespace-stripped.
|
|
52
|
+
const rejoined = chunks.join("").replace(/\s+/g, "");
|
|
53
|
+
const original = text.replace(/\s+/g, "");
|
|
54
|
+
expect(rejoined).toBe(original);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("splits on paragraph boundary rather than mid-sentence", () => {
|
|
58
|
+
// Two paragraphs where a paragraph split is available inside the
|
|
59
|
+
// first window. Use maxChars large enough that the first paragraph
|
|
60
|
+
// fits, but both together don't.
|
|
61
|
+
const firstParagraph = "a".repeat(2000);
|
|
62
|
+
const secondParagraph = "b".repeat(2000);
|
|
63
|
+
const text = `${firstParagraph}\n\n${secondParagraph}`;
|
|
64
|
+
|
|
65
|
+
const chunks = splitLongTextSegment(text);
|
|
66
|
+
|
|
67
|
+
expect(chunks.length).toBe(2);
|
|
68
|
+
expect(chunks[0]).toBe(firstParagraph);
|
|
69
|
+
expect(chunks[1]).toBe(secondParagraph);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("splits text with no paragraph or sentence boundaries via hard slice", () => {
|
|
73
|
+
const text = "x".repeat(10_000);
|
|
74
|
+
const chunks = splitLongTextSegment(text);
|
|
75
|
+
|
|
76
|
+
expect(chunks.length).toBeGreaterThanOrEqual(
|
|
77
|
+
Math.ceil(10_000 / SLACK_SECTION_MAX_CHARS),
|
|
78
|
+
);
|
|
79
|
+
for (const chunk of chunks) {
|
|
80
|
+
expect(chunk.length).toBeLessThanOrEqual(SLACK_SECTION_MAX_CHARS);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// No content lost.
|
|
84
|
+
expect(chunks.join("")).toBe(text);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("respects custom maxChars parameter", () => {
|
|
88
|
+
const text = "a".repeat(100);
|
|
89
|
+
const chunks = splitLongTextSegment(text, 30);
|
|
90
|
+
|
|
91
|
+
expect(chunks.length).toBeGreaterThanOrEqual(Math.ceil(100 / 30));
|
|
92
|
+
for (const chunk of chunks) {
|
|
93
|
+
expect(chunk.length).toBeLessThanOrEqual(30);
|
|
94
|
+
}
|
|
95
|
+
expect(chunks.join("")).toBe(text);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("prefers sentence boundary when no paragraph or newline is available", () => {
|
|
99
|
+
const sentenceA = "This is sentence A. ".repeat(100); // ~2000 chars
|
|
100
|
+
const sentenceB = "This is sentence B. ".repeat(100); // ~2000 chars
|
|
101
|
+
const text = sentenceA + sentenceB;
|
|
102
|
+
|
|
103
|
+
const chunks = splitLongTextSegment(text);
|
|
104
|
+
|
|
105
|
+
expect(chunks.length).toBeGreaterThanOrEqual(2);
|
|
106
|
+
for (const chunk of chunks) {
|
|
107
|
+
expect(chunk.length).toBeLessThanOrEqual(SLACK_SECTION_MAX_CHARS);
|
|
108
|
+
// Each chunk should end with a period (sentence-aligned split).
|
|
109
|
+
expect(chunk.endsWith(".")).toBe(true);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("prefers single newline over sentence boundary when no paragraph is present", () => {
|
|
114
|
+
const lineA = "a".repeat(1500);
|
|
115
|
+
const lineB = "b".repeat(1500);
|
|
116
|
+
const text = `${lineA}\n${lineB}`;
|
|
117
|
+
|
|
118
|
+
const chunks = splitLongTextSegment(text);
|
|
119
|
+
|
|
120
|
+
expect(chunks.length).toBe(2);
|
|
121
|
+
expect(chunks[0]).toBe(lineA);
|
|
122
|
+
expect(chunks[1]).toBe(lineB);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("returns input unchanged when maxChars is non-positive (avoids infinite loop)", () => {
|
|
126
|
+
const text = "a".repeat(100);
|
|
127
|
+
expect(splitLongTextSegment(text, 0)).toEqual([text]);
|
|
128
|
+
expect(splitLongTextSegment(text, -1)).toEqual([text]);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("plain `<` in technical prose does not protect trailing sentence boundaries from being used as split points", () => {
|
|
132
|
+
// `computeMrkdwnSpans` only treats `<` as a link span start when
|
|
133
|
+
// followed by a recognized Slack link/mention prefix. A plain `<` in
|
|
134
|
+
// prose like `a < b. Another sentence. ...` must not create a
|
|
135
|
+
// protected span that extends to the window edge, or every `. `
|
|
136
|
+
// boundary after it would be rejected, forcing a mid-word hard slice.
|
|
137
|
+
const sentence = "a < b. ";
|
|
138
|
+
// Repeat enough to comfortably exceed maxChars so the splitter must
|
|
139
|
+
// pick a boundary inside the window.
|
|
140
|
+
const text = sentence.repeat(500);
|
|
141
|
+
expect(text.length).toBeGreaterThan(SLACK_SECTION_MAX_CHARS);
|
|
142
|
+
|
|
143
|
+
const chunks = splitLongTextSegment(text);
|
|
144
|
+
|
|
145
|
+
expect(chunks.length).toBeGreaterThanOrEqual(2);
|
|
146
|
+
for (const chunk of chunks) {
|
|
147
|
+
expect(chunk.length).toBeLessThanOrEqual(SLACK_SECTION_MAX_CHARS);
|
|
148
|
+
// Sentence-aligned split: chunks should end on a sentence terminator
|
|
149
|
+
// (trailing space is trimmed), not mid-token.
|
|
150
|
+
expect(chunk.endsWith(".")).toBe(true);
|
|
151
|
+
}
|
|
152
|
+
// No content lost (ignoring inter-chunk whitespace trimming).
|
|
153
|
+
expect(chunks.join(" ").replace(/\s+/g, " ").trim()).toBe(
|
|
154
|
+
text.replace(/\s+/g, " ").trim(),
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("protects `<scheme://...>` link spans for schemes beyond the http/https whitelist", () => {
|
|
159
|
+
// `markdownToMrkdwn` wraps any markdown link target in `<url|text>`,
|
|
160
|
+
// including schemes like ftp, ssh, or custom app schemes. The splitter
|
|
161
|
+
// must recognize those as protected spans so it does not bisect the
|
|
162
|
+
// URL token when deciding where to cut a long chunk.
|
|
163
|
+
const filler = "lorem ipsum dolor sit amet. ".repeat(200);
|
|
164
|
+
const longUrl =
|
|
165
|
+
"ftp://example.com/" + "segment/".repeat(30) + "final-path";
|
|
166
|
+
const linkToken = `<${longUrl}|download>`;
|
|
167
|
+
const text = filler + linkToken + " " + filler;
|
|
168
|
+
expect(text.length).toBeGreaterThan(SLACK_SECTION_MAX_CHARS);
|
|
169
|
+
|
|
170
|
+
const chunks = splitLongTextSegment(text);
|
|
171
|
+
|
|
172
|
+
expect(chunks.length).toBeGreaterThanOrEqual(2);
|
|
173
|
+
for (const chunk of chunks) {
|
|
174
|
+
expect(chunk.length).toBeLessThanOrEqual(SLACK_SECTION_MAX_CHARS);
|
|
175
|
+
// If the splitter ever landed inside the `<...>` token, some chunk
|
|
176
|
+
// would contain a `<` without the matching `>` (or vice versa).
|
|
177
|
+
const opens = (chunk.match(/</g) ?? []).length;
|
|
178
|
+
const closes = (chunk.match(/>/g) ?? []).length;
|
|
179
|
+
expect(opens).toBe(closes);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("splitCodeSegmentContent", () => {
|
|
185
|
+
test("returns single-element array when content fits in one fenced section", () => {
|
|
186
|
+
const content = "short\ncode\nblock";
|
|
187
|
+
expect(splitCodeSegmentContent(content, "js")).toEqual([content]);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("splits on line boundaries so each chunk + fence fits the section limit", () => {
|
|
191
|
+
const lines: string[] = [];
|
|
192
|
+
for (let i = 0; i < 300; i++) {
|
|
193
|
+
lines.push(`line ${i} with some filler`);
|
|
194
|
+
}
|
|
195
|
+
const content = lines.join("\n");
|
|
196
|
+
const lang = "javascript";
|
|
197
|
+
const chunks = splitCodeSegmentContent(content, lang);
|
|
198
|
+
|
|
199
|
+
expect(chunks.length).toBeGreaterThanOrEqual(2);
|
|
200
|
+
const overhead = 3 + lang.length + 1 + 1 + 3;
|
|
201
|
+
for (const chunk of chunks) {
|
|
202
|
+
expect(chunk.length + overhead).toBeLessThanOrEqual(
|
|
203
|
+
SLACK_SECTION_MAX_CHARS,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
// All lines preserved in order.
|
|
207
|
+
expect(chunks.join("\n")).toBe(content);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("hard-slices a single line longer than the budget", () => {
|
|
211
|
+
const content = "x".repeat(10_000);
|
|
212
|
+
const chunks = splitCodeSegmentContent(content, "");
|
|
213
|
+
expect(chunks.length).toBeGreaterThanOrEqual(2);
|
|
214
|
+
const overhead = 3 + 0 + 1 + 1 + 3;
|
|
215
|
+
for (const chunk of chunks) {
|
|
216
|
+
expect(chunk.length + overhead).toBeLessThanOrEqual(
|
|
217
|
+
SLACK_SECTION_MAX_CHARS,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
expect(chunks.join("")).toBe(content);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe("textToSlackBlocks long-text splitting", () => {
|
|
225
|
+
test("5000-char prose input produces multiple section blocks, each ≤ 3000 chars", () => {
|
|
226
|
+
// Build ≥ 5000 chars of prose with paragraph boundaries so the splitter
|
|
227
|
+
// has natural cut points.
|
|
228
|
+
const paragraphs: string[] = [];
|
|
229
|
+
for (let i = 0; i < 120; i++) {
|
|
230
|
+
paragraphs.push(`Paragraph number ${i} with filler content here.`);
|
|
231
|
+
}
|
|
232
|
+
const text = paragraphs.join("\n\n");
|
|
233
|
+
expect(text.length).toBeGreaterThanOrEqual(5000);
|
|
234
|
+
|
|
235
|
+
const blocks = textToSlackBlocks(text);
|
|
236
|
+
expect(blocks).toBeDefined();
|
|
237
|
+
|
|
238
|
+
const sectionBlocks = blocks!.filter((b) => b.type === "section");
|
|
239
|
+
expect(sectionBlocks.length).toBeGreaterThanOrEqual(2);
|
|
240
|
+
|
|
241
|
+
for (const block of sectionBlocks) {
|
|
242
|
+
// Cast: filter() narrows to section blocks but TS may not
|
|
243
|
+
// follow the discriminant narrowing through filter.
|
|
244
|
+
const section = block as { type: "section"; text: { text: string } };
|
|
245
|
+
expect(section.text.text.length).toBeLessThanOrEqual(3000);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("2000-char prose input (under the limit) produces a single section block", () => {
|
|
250
|
+
const text = "a".repeat(2000);
|
|
251
|
+
const blocks = textToSlackBlocks(text);
|
|
252
|
+
expect(blocks).toBeDefined();
|
|
253
|
+
expect(blocks!.length).toBe(1);
|
|
254
|
+
expect(blocks![0].type).toBe("section");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("does not split a markdown link across section blocks when the link text contains a sentence boundary near maxChars", () => {
|
|
258
|
+
// Craft prose so the last `. ` (sentence boundary) inside the first
|
|
259
|
+
// `SLACK_SECTION_MAX_CHARS` window falls INSIDE a markdown link's text.
|
|
260
|
+
// If `splitLongTextSegment` ran on raw markdown, it would cut the link
|
|
261
|
+
// in half — chunk 1 would end with `[First sentence. ` and chunk 2 would
|
|
262
|
+
// start with `Second sentence](https://example.com)`, leaving orphan
|
|
263
|
+
// `](` tokens that never get converted to Slack mrkdwn and leak raw
|
|
264
|
+
// markdown to Slack.
|
|
265
|
+
//
|
|
266
|
+
// By transforming to mrkdwn FIRST (so the link becomes
|
|
267
|
+
// `<url|First sentence. Second sentence>`), the splitter can no longer
|
|
268
|
+
// land on the `. ` inside the `|...>` span in a way that produces orphan
|
|
269
|
+
// markdown tokens — and `](` never appears in any chunk.
|
|
270
|
+
const linkMarkdown =
|
|
271
|
+
"[First sentence. Second sentence](https://example.com)";
|
|
272
|
+
// Fill the first window with sentence-delimited filler so there is a
|
|
273
|
+
// valid `. ` split point available and the link straddles the boundary.
|
|
274
|
+
const prefix = "Filler sentence. ".repeat(170); // ≈ 2890 chars
|
|
275
|
+
// Pad the remainder so the total length comfortably exceeds the limit.
|
|
276
|
+
const suffix = " Trailing sentence. ".repeat(100);
|
|
277
|
+
const text = prefix + linkMarkdown + suffix;
|
|
278
|
+
expect(text.length).toBeGreaterThan(SLACK_SECTION_MAX_CHARS);
|
|
279
|
+
|
|
280
|
+
const blocks = textToSlackBlocks(text);
|
|
281
|
+
expect(blocks).toBeDefined();
|
|
282
|
+
|
|
283
|
+
const sectionBlocks = blocks!.filter((b) => b.type === "section") as Array<{
|
|
284
|
+
type: "section";
|
|
285
|
+
text: { type: string; text: string };
|
|
286
|
+
}>;
|
|
287
|
+
expect(sectionBlocks.length).toBeGreaterThanOrEqual(2);
|
|
288
|
+
|
|
289
|
+
for (const section of sectionBlocks) {
|
|
290
|
+
// Every section block must be well-formed Slack mrkdwn: no raw
|
|
291
|
+
// markdown-link tokens should leak through.
|
|
292
|
+
expect(section.text.text).not.toContain("](");
|
|
293
|
+
expect(section.text.text).not.toContain("[First sentence");
|
|
294
|
+
// No orphan `**` bold markers.
|
|
295
|
+
expect(section.text.text).not.toContain("**");
|
|
296
|
+
// Section must respect Slack's 3000-char ceiling.
|
|
297
|
+
expect(section.text.text.length).toBeLessThanOrEqual(3000);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// The link should be rendered exactly once as Slack mrkdwn across all
|
|
301
|
+
// blocks — not duplicated, not broken apart.
|
|
302
|
+
const combined = sectionBlocks.map((s) => s.text.text).join("\n");
|
|
303
|
+
expect(combined).toContain(
|
|
304
|
+
"<https://example.com|First sentence. Second sentence>",
|
|
305
|
+
);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("does not bisect a `<url|text>` span when its internal `. ` is the latest sentence delimiter in the window", () => {
|
|
309
|
+
// Construct a window where the only `. ` candidate inside the maxChars
|
|
310
|
+
// window lies INSIDE the converted link span. A naive splitter would
|
|
311
|
+
// pick that `. ` and emit chunk 1 ending with `<url|First sentence.`
|
|
312
|
+
// (unclosed `<`) and chunk 2 starting with `Second sentence>` (orphan
|
|
313
|
+
// `>`). Span-aware splitting must reject that boundary and either back
|
|
314
|
+
// up to before the span or hard-slice safely.
|
|
315
|
+
const filler = "a".repeat(2770);
|
|
316
|
+
const linkMarkdown = "[First sentence. Second sentence](https://example.com)";
|
|
317
|
+
const text = filler + linkMarkdown + " trailing content here. ".repeat(40);
|
|
318
|
+
expect(text.length).toBeGreaterThan(SLACK_SECTION_MAX_CHARS);
|
|
319
|
+
|
|
320
|
+
const blocks = textToSlackBlocks(text);
|
|
321
|
+
expect(blocks).toBeDefined();
|
|
322
|
+
const sectionBlocks = blocks!.filter((b) => b.type === "section") as Array<{
|
|
323
|
+
type: "section";
|
|
324
|
+
text: { type: string; text: string };
|
|
325
|
+
}>;
|
|
326
|
+
|
|
327
|
+
for (const section of sectionBlocks) {
|
|
328
|
+
// Each chunk must have balanced `<...>` tokens — no unclosed `<` or
|
|
329
|
+
// orphan `>` from a bisected span.
|
|
330
|
+
const opens = (section.text.text.match(/</g) ?? []).length;
|
|
331
|
+
const closes = (section.text.text.match(/>/g) ?? []).length;
|
|
332
|
+
expect(opens).toBe(closes);
|
|
333
|
+
expect(section.text.text).not.toContain("](");
|
|
334
|
+
expect(section.text.text.length).toBeLessThanOrEqual(3000);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const combined = sectionBlocks.map((s) => s.text.text).join("\n");
|
|
338
|
+
expect(combined).toContain(
|
|
339
|
+
"<https://example.com|First sentence. Second sentence>",
|
|
340
|
+
);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("does not bisect a `<url|text>` span that straddles the maxChars cutoff with no in-window delimiters", () => {
|
|
344
|
+
// Pathological prefix with no natural boundaries: the splitter would
|
|
345
|
+
// hard-slice at maxChars, which falls inside the converted link token.
|
|
346
|
+
// Span-aware hard-slice must back up to the start of the span.
|
|
347
|
+
const filler = "a".repeat(2793);
|
|
348
|
+
const linkMarkdown = "[link text here](https://example.com/path)";
|
|
349
|
+
const text = filler + " " + linkMarkdown + " trailing";
|
|
350
|
+
expect(text.length).toBeGreaterThan(SLACK_SECTION_MAX_CHARS);
|
|
351
|
+
|
|
352
|
+
const blocks = textToSlackBlocks(text);
|
|
353
|
+
expect(blocks).toBeDefined();
|
|
354
|
+
const sectionBlocks = blocks!.filter((b) => b.type === "section") as Array<{
|
|
355
|
+
type: "section";
|
|
356
|
+
text: { type: string; text: string };
|
|
357
|
+
}>;
|
|
358
|
+
|
|
359
|
+
for (const section of sectionBlocks) {
|
|
360
|
+
const opens = (section.text.text.match(/</g) ?? []).length;
|
|
361
|
+
const closes = (section.text.text.match(/>/g) ?? []).length;
|
|
362
|
+
expect(opens).toBe(closes);
|
|
363
|
+
expect(section.text.text).not.toContain("](");
|
|
364
|
+
expect(section.text.text.length).toBeLessThanOrEqual(3000);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const combined = sectionBlocks.map((s) => s.text.text).join("\n");
|
|
368
|
+
expect(combined).toContain(
|
|
369
|
+
"<https://example.com/path|link text here>",
|
|
370
|
+
);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test("does not split a **bold** span across section blocks when the bold text contains a sentence boundary near maxChars", () => {
|
|
374
|
+
// Same shape as the link-straddle test, but for `**bold**`. If splitting
|
|
375
|
+
// ran on raw markdown, the splitter would land on a `. ` inside the bold
|
|
376
|
+
// span, leaving chunk 1 with `**First sentence. ` (orphan `**` opener)
|
|
377
|
+
// and chunk 2 with `Second sentence**` (orphan `**` closer). Both
|
|
378
|
+
// chunks' mrkdwn regexes would fail to match, leaking raw `**` tokens.
|
|
379
|
+
const boldMarkdown = "**First sentence. Second sentence**";
|
|
380
|
+
const prefix = "Filler sentence. ".repeat(170); // ≈ 2890 chars
|
|
381
|
+
const suffix = " Trailing sentence. ".repeat(100);
|
|
382
|
+
const text = prefix + boldMarkdown + suffix;
|
|
383
|
+
expect(text.length).toBeGreaterThan(SLACK_SECTION_MAX_CHARS);
|
|
384
|
+
|
|
385
|
+
const blocks = textToSlackBlocks(text);
|
|
386
|
+
expect(blocks).toBeDefined();
|
|
387
|
+
|
|
388
|
+
const sectionBlocks = blocks!.filter((b) => b.type === "section") as Array<{
|
|
389
|
+
type: "section";
|
|
390
|
+
text: { type: string; text: string };
|
|
391
|
+
}>;
|
|
392
|
+
expect(sectionBlocks.length).toBeGreaterThanOrEqual(2);
|
|
393
|
+
|
|
394
|
+
for (const section of sectionBlocks) {
|
|
395
|
+
// No orphan `**` bold markers should survive.
|
|
396
|
+
expect(section.text.text).not.toContain("**");
|
|
397
|
+
// No orphan link tokens either (defense in depth — this test focuses
|
|
398
|
+
// on bold, but the shared fix should protect both).
|
|
399
|
+
expect(section.text.text).not.toContain("](");
|
|
400
|
+
expect(section.text.text.length).toBeLessThanOrEqual(3000);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// The bold span should be rendered exactly once as Slack single-asterisk
|
|
404
|
+
// bold across all blocks.
|
|
405
|
+
const combined = sectionBlocks.map((s) => s.text.text).join("\n");
|
|
406
|
+
expect(combined).toContain("*First sentence. Second sentence*");
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
test("oversize code block is split into multiple fenced section blocks, each ≤ 3000 chars", () => {
|
|
410
|
+
// Build a code block whose content exceeds a single Slack section's limit.
|
|
411
|
+
// Include newlines so the splitter has line boundaries to use.
|
|
412
|
+
const codeLines: string[] = [];
|
|
413
|
+
for (let i = 0; i < 300; i++) {
|
|
414
|
+
codeLines.push(`const value${i} = "filler content on line ${i}";`);
|
|
415
|
+
}
|
|
416
|
+
const codeBody = codeLines.join("\n");
|
|
417
|
+
expect(codeBody.length).toBeGreaterThan(SLACK_SECTION_MAX_CHARS);
|
|
418
|
+
const text = "```javascript\n" + codeBody + "\n```";
|
|
419
|
+
|
|
420
|
+
const blocks = textToSlackBlocks(text);
|
|
421
|
+
expect(blocks).toBeDefined();
|
|
422
|
+
|
|
423
|
+
const sectionBlocks = blocks!.filter((b) => b.type === "section") as Array<{
|
|
424
|
+
type: "section";
|
|
425
|
+
text: { type: string; text: string };
|
|
426
|
+
}>;
|
|
427
|
+
expect(sectionBlocks.length).toBeGreaterThanOrEqual(2);
|
|
428
|
+
|
|
429
|
+
for (const section of sectionBlocks) {
|
|
430
|
+
// Each chunk must start and end with a fence and fit inside Slack's limit.
|
|
431
|
+
expect(section.text.text.startsWith("```javascript\n")).toBe(true);
|
|
432
|
+
expect(section.text.text.endsWith("\n```")).toBe(true);
|
|
433
|
+
expect(section.text.text.length).toBeLessThanOrEqual(3000);
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
test("4000-char paragraph followed by header and second paragraph preserves ordering", () => {
|
|
438
|
+
const firstParagraph = "a".repeat(4000);
|
|
439
|
+
const secondParagraph = "short second paragraph.";
|
|
440
|
+
const text = `${firstParagraph}\n\n# My Header\n\n${secondParagraph}`;
|
|
441
|
+
|
|
442
|
+
const blocks = textToSlackBlocks(text);
|
|
443
|
+
expect(blocks).toBeDefined();
|
|
444
|
+
|
|
445
|
+
// Locate the header block — it must exist and not be absorbed into
|
|
446
|
+
// the long-text split.
|
|
447
|
+
const headerIndices = blocks!
|
|
448
|
+
.map((b, i) => (b.type === "header" ? i : -1))
|
|
449
|
+
.filter((i) => i >= 0);
|
|
450
|
+
expect(headerIndices.length).toBe(1);
|
|
451
|
+
const headerIndex = headerIndices[0];
|
|
452
|
+
|
|
453
|
+
const headerBlock = blocks![headerIndex] as {
|
|
454
|
+
type: "header";
|
|
455
|
+
text: { text: string };
|
|
456
|
+
};
|
|
457
|
+
expect(headerBlock.text.text).toBe("My Header");
|
|
458
|
+
|
|
459
|
+
// Before the header: at least one section block (from the 4000-char
|
|
460
|
+
// paragraph, which should split into ≥ 2 sections).
|
|
461
|
+
const beforeHeader = blocks!.slice(0, headerIndex);
|
|
462
|
+
const sectionsBeforeHeader = beforeHeader.filter(
|
|
463
|
+
(b) => b.type === "section",
|
|
464
|
+
);
|
|
465
|
+
expect(sectionsBeforeHeader.length).toBeGreaterThanOrEqual(2);
|
|
466
|
+
|
|
467
|
+
// After the header: at least one section block (the second paragraph).
|
|
468
|
+
const afterHeader = blocks!.slice(headerIndex + 1);
|
|
469
|
+
const sectionsAfterHeader = afterHeader.filter(
|
|
470
|
+
(b) => b.type === "section",
|
|
471
|
+
);
|
|
472
|
+
expect(sectionsAfterHeader.length).toBeGreaterThanOrEqual(1);
|
|
473
|
+
|
|
474
|
+
// Every section block stays under Slack's 3000-char ceiling.
|
|
475
|
+
for (const block of blocks!) {
|
|
476
|
+
if (block.type === "section") {
|
|
477
|
+
expect(block.text.text.length).toBeLessThanOrEqual(3000);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
});
|