discoclaw 0.1.0
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/.context/README.md +42 -0
- package/.context/architecture.md +58 -0
- package/.context/bot-setup.md +24 -0
- package/.context/dev.md +230 -0
- package/.context/discord.md +144 -0
- package/.context/memory.md +257 -0
- package/.context/ops.md +59 -0
- package/.context/pa-safety.md +47 -0
- package/.context/pa.md +118 -0
- package/.context/project.md +43 -0
- package/.context/runtime.md +253 -0
- package/.context/tasks.md +71 -0
- package/.context/tools.md +75 -0
- package/.env.example +88 -0
- package/.env.example.full +378 -0
- package/LICENSE +21 -0
- package/README.md +220 -0
- package/dist/beads/auto-tag.js +2 -0
- package/dist/beads/auto-tag.test.js +62 -0
- package/dist/beads/bd-cli.js +9 -0
- package/dist/beads/bd-cli.test.js +495 -0
- package/dist/beads/bead-hooks-cli.js +149 -0
- package/dist/beads/bead-sync-cli.js +5 -0
- package/dist/beads/bead-sync-cli.test.js +72 -0
- package/dist/beads/bead-sync-coordinator.js +4 -0
- package/dist/beads/bead-sync-coordinator.test.js +239 -0
- package/dist/beads/bead-sync-watcher.js +2 -0
- package/dist/beads/bead-sync-watcher.test.js +96 -0
- package/dist/beads/bead-sync.js +7 -0
- package/dist/beads/bead-sync.test.js +876 -0
- package/dist/beads/bead-thread-cache.js +8 -0
- package/dist/beads/bead-thread-cache.test.js +91 -0
- package/dist/beads/discord-sync.js +18 -0
- package/dist/beads/discord-sync.test.js +782 -0
- package/dist/beads/find-bead-by-thread.test.js +36 -0
- package/dist/beads/forum-guard.js +2 -0
- package/dist/beads/forum-guard.test.js +204 -0
- package/dist/beads/initialize.js +3 -0
- package/dist/beads/initialize.test.js +304 -0
- package/dist/beads/types.js +10 -0
- package/dist/cli/daemon-installer.js +225 -0
- package/dist/cli/daemon-installer.test.js +289 -0
- package/dist/cli/index.js +42 -0
- package/dist/cli/init-wizard.js +374 -0
- package/dist/cli/init-wizard.test.js +191 -0
- package/dist/config.js +385 -0
- package/dist/config.test.js +589 -0
- package/dist/cron/auto-tag.js +100 -0
- package/dist/cron/auto-tag.test.js +91 -0
- package/dist/cron/cadence.js +74 -0
- package/dist/cron/cadence.test.js +53 -0
- package/dist/cron/cron-sync-coordinator.js +66 -0
- package/dist/cron/cron-sync-coordinator.test.js +118 -0
- package/dist/cron/cron-sync.js +165 -0
- package/dist/cron/cron-sync.test.js +228 -0
- package/dist/cron/cron-tag-map-watcher.js +128 -0
- package/dist/cron/cron-tag-map-watcher.test.js +155 -0
- package/dist/cron/default-timezone.js +23 -0
- package/dist/cron/default-timezone.test.js +30 -0
- package/dist/cron/discord-sync.js +205 -0
- package/dist/cron/discord-sync.test.js +353 -0
- package/dist/cron/executor.js +303 -0
- package/dist/cron/executor.test.js +614 -0
- package/dist/cron/forum-sync.js +347 -0
- package/dist/cron/forum-sync.test.js +539 -0
- package/dist/cron/job-lock.js +164 -0
- package/dist/cron/job-lock.test.js +178 -0
- package/dist/cron/parser.js +68 -0
- package/dist/cron/parser.test.js +115 -0
- package/dist/cron/run-control.js +24 -0
- package/dist/cron/run-control.test.js +27 -0
- package/dist/cron/run-stats.js +265 -0
- package/dist/cron/run-stats.test.js +160 -0
- package/dist/cron/scheduler.js +97 -0
- package/dist/cron/scheduler.test.js +112 -0
- package/dist/cron/tag-map.js +47 -0
- package/dist/cron/tag-map.test.js +64 -0
- package/dist/cron/types.js +1 -0
- package/dist/discoclaw-plan-format.test.js +137 -0
- package/dist/discoclaw-recipe-format.test.js +137 -0
- package/dist/discord/abort-registry.js +70 -0
- package/dist/discord/action-categories.js +36 -0
- package/dist/discord/action-types.js +1 -0
- package/dist/discord/action-utils.js +58 -0
- package/dist/discord/action-utils.test.js +58 -0
- package/dist/discord/actions-beads.js +1 -0
- package/dist/discord/actions-beads.test.js +372 -0
- package/dist/discord/actions-bot-profile.js +107 -0
- package/dist/discord/actions-bot-profile.test.js +138 -0
- package/dist/discord/actions-channels.js +427 -0
- package/dist/discord/actions-channels.test.js +697 -0
- package/dist/discord/actions-config.js +173 -0
- package/dist/discord/actions-config.test.js +322 -0
- package/dist/discord/actions-crons.js +586 -0
- package/dist/discord/actions-crons.test.js +499 -0
- package/dist/discord/actions-defer.js +60 -0
- package/dist/discord/actions-defer.test.js +134 -0
- package/dist/discord/actions-forge.js +134 -0
- package/dist/discord/actions-forge.test.js +206 -0
- package/dist/discord/actions-guild.js +301 -0
- package/dist/discord/actions-guild.test.js +386 -0
- package/dist/discord/actions-memory.js +106 -0
- package/dist/discord/actions-memory.test.js +248 -0
- package/dist/discord/actions-messaging.js +401 -0
- package/dist/discord/actions-messaging.test.js +738 -0
- package/dist/discord/actions-moderation.js +65 -0
- package/dist/discord/actions-moderation.test.js +88 -0
- package/dist/discord/actions-plan.js +445 -0
- package/dist/discord/actions-plan.test.js +610 -0
- package/dist/discord/actions-poll.js +38 -0
- package/dist/discord/actions-poll.test.js +93 -0
- package/dist/discord/actions-tasks.js +3 -0
- package/dist/discord/actions-tasks.test.js +418 -0
- package/dist/discord/actions.js +600 -0
- package/dist/discord/actions.test.js +522 -0
- package/dist/discord/allowed-mentions.js +3 -0
- package/dist/discord/allowed-mentions.test.js +17 -0
- package/dist/discord/allowlist.js +29 -0
- package/dist/discord/allowlist.test.js +24 -0
- package/dist/discord/audit-handler.js +191 -0
- package/dist/discord/audit-handler.test.js +361 -0
- package/dist/discord/bot.js +141 -0
- package/dist/discord/channel-context.js +181 -0
- package/dist/discord/defer-scheduler.js +45 -0
- package/dist/discord/destructive-confirmation.js +128 -0
- package/dist/discord/destructive-confirmation.test.js +49 -0
- package/dist/discord/discord-plan-auto-implement.test.js +18 -0
- package/dist/discord/durable-memory.js +145 -0
- package/dist/discord/durable-memory.test.js +281 -0
- package/dist/discord/durable-write-queue.js +4 -0
- package/dist/discord/file-download.js +308 -0
- package/dist/discord/file-download.test.js +303 -0
- package/dist/discord/forge-audit-verdict.js +140 -0
- package/dist/discord/forge-auto-implement.js +80 -0
- package/dist/discord/forge-auto-implement.test.js +110 -0
- package/dist/discord/forge-commands.js +698 -0
- package/dist/discord/forge-commands.test.js +1606 -0
- package/dist/discord/forge-plan-registry.js +68 -0
- package/dist/discord/forge-plan-registry.test.js +127 -0
- package/dist/discord/forum-count-sync.js +130 -0
- package/dist/discord/forum-count-sync.test.js +200 -0
- package/dist/discord/health-command.js +98 -0
- package/dist/discord/health-command.test.js +195 -0
- package/dist/discord/help-command.js +22 -0
- package/dist/discord/help-command.test.js +49 -0
- package/dist/discord/image-download.js +201 -0
- package/dist/discord/image-download.test.js +499 -0
- package/dist/discord/inflight-replies.js +228 -0
- package/dist/discord/inflight-replies.test.js +295 -0
- package/dist/discord/json-extract.js +110 -0
- package/dist/discord/keyed-queue.js +22 -0
- package/dist/discord/memory-commands.js +85 -0
- package/dist/discord/memory-commands.test.js +159 -0
- package/dist/discord/memory-timing.integration.test.js +159 -0
- package/dist/discord/message-coordinator.js +2347 -0
- package/dist/discord/message-coordinator.onboarding.test.js +183 -0
- package/dist/discord/message-coordinator.plan-run.test.js +264 -0
- package/dist/discord/message-history.js +53 -0
- package/dist/discord/message-history.test.js +95 -0
- package/dist/discord/models-command.js +59 -0
- package/dist/discord/models-command.test.js +150 -0
- package/dist/discord/nickname.test.js +76 -0
- package/dist/discord/onboarding-completion.js +55 -0
- package/dist/discord/onboarding-completion.test.js +176 -0
- package/dist/discord/output-common.js +178 -0
- package/dist/discord/output-common.test.js +198 -0
- package/dist/discord/output-utils.js +156 -0
- package/dist/discord/parse-identity-name.test.js +129 -0
- package/dist/discord/plan-commands.js +612 -0
- package/dist/discord/plan-commands.test.js +1622 -0
- package/dist/discord/plan-manager.js +1491 -0
- package/dist/discord/plan-manager.test.js +2380 -0
- package/dist/discord/plan-parser.js +110 -0
- package/dist/discord/plan-parser.test.js +63 -0
- package/dist/discord/plan-run-phase-start.js +20 -0
- package/dist/discord/plan-run-phase-start.test.js +29 -0
- package/dist/discord/platform-message.js +45 -0
- package/dist/discord/platform-message.test.js +110 -0
- package/dist/discord/prompt-common.js +240 -0
- package/dist/discord/prompt-common.test.js +423 -0
- package/dist/discord/reaction-handler.js +691 -0
- package/dist/discord/reaction-handler.test.js +1574 -0
- package/dist/discord/reaction-prompts.js +118 -0
- package/dist/discord/reaction-prompts.test.js +253 -0
- package/dist/discord/reply-reference.js +66 -0
- package/dist/discord/reply-reference.test.js +125 -0
- package/dist/discord/restart-command.js +143 -0
- package/dist/discord/restart-command.test.js +196 -0
- package/dist/discord/runtime-utils.js +43 -0
- package/dist/discord/runtime-utils.test.js +112 -0
- package/dist/discord/session-key.js +7 -0
- package/dist/discord/session-key.test.js +13 -0
- package/dist/discord/shortterm-memory.js +166 -0
- package/dist/discord/shortterm-memory.test.js +345 -0
- package/dist/discord/shutdown-context.js +122 -0
- package/dist/discord/shutdown-context.test.js +279 -0
- package/dist/discord/startup-profile.test.js +214 -0
- package/dist/discord/status-channel.js +190 -0
- package/dist/discord/status-channel.test.js +282 -0
- package/dist/discord/status-command.js +206 -0
- package/dist/discord/status-command.test.js +341 -0
- package/dist/discord/streaming-progress.js +107 -0
- package/dist/discord/streaming-progress.test.js +93 -0
- package/dist/discord/summarizer.js +89 -0
- package/dist/discord/summarizer.test.js +245 -0
- package/dist/discord/system-bootstrap.js +396 -0
- package/dist/discord/system-bootstrap.test.js +724 -0
- package/dist/discord/thread-context.js +169 -0
- package/dist/discord/thread-context.test.js +386 -0
- package/dist/discord/tool-aware-queue.js +116 -0
- package/dist/discord/tool-aware-queue.test.js +180 -0
- package/dist/discord/update-command.js +127 -0
- package/dist/discord/update-command.test.js +275 -0
- package/dist/discord/user-errors.js +40 -0
- package/dist/discord/user-errors.test.js +31 -0
- package/dist/discord/user-turn-to-durable.js +111 -0
- package/dist/discord/user-turn-to-durable.test.js +273 -0
- package/dist/discord-followup.test.js +677 -0
- package/dist/discord.channel-context.test.js +95 -0
- package/dist/discord.fail-closed.test.js +199 -0
- package/dist/discord.health-command.integration.test.js +140 -0
- package/dist/discord.js +190 -0
- package/dist/discord.prompt-context.test.js +1431 -0
- package/dist/discord.render.test.js +621 -0
- package/dist/discord.status-wiring.test.js +187 -0
- package/dist/engine/claudeCli.js +137 -0
- package/dist/engine/types.js +1 -0
- package/dist/group-queue.js +25 -0
- package/dist/health/credential-check.js +175 -0
- package/dist/health/credential-check.test.js +401 -0
- package/dist/health/startup-healing.js +139 -0
- package/dist/health/startup-healing.test.js +298 -0
- package/dist/identity.js +36 -0
- package/dist/index.js +1378 -0
- package/dist/logging/logger-like.js +1 -0
- package/dist/observability/memory-sampler.js +51 -0
- package/dist/observability/memory-sampler.test.js +93 -0
- package/dist/observability/metrics.js +88 -0
- package/dist/observability/metrics.test.js +42 -0
- package/dist/onboarding/onboarding-flow.js +246 -0
- package/dist/onboarding/onboarding-flow.test.js +238 -0
- package/dist/onboarding/onboarding-writer.js +102 -0
- package/dist/onboarding/onboarding-writer.test.js +143 -0
- package/dist/pidlock.js +187 -0
- package/dist/pidlock.test.js +128 -0
- package/dist/pipeline/engine.js +206 -0
- package/dist/pipeline/engine.test.js +771 -0
- package/dist/root-policy.js +21 -0
- package/dist/root-policy.test.js +55 -0
- package/dist/runtime/claude-code-cli.js +35 -0
- package/dist/runtime/claude-code-cli.test.js +1199 -0
- package/dist/runtime/cli-adapter.js +584 -0
- package/dist/runtime/cli-output-parsers.js +108 -0
- package/dist/runtime/cli-shared.js +96 -0
- package/dist/runtime/cli-shared.test.js +104 -0
- package/dist/runtime/cli-strategy.js +6 -0
- package/dist/runtime/codex-cli.js +16 -0
- package/dist/runtime/codex-cli.test.js +862 -0
- package/dist/runtime/concurrency-limit.js +80 -0
- package/dist/runtime/concurrency-limit.test.js +137 -0
- package/dist/runtime/gemini-cli.js +16 -0
- package/dist/runtime/gemini-cli.test.js +413 -0
- package/dist/runtime/long-running-process.js +415 -0
- package/dist/runtime/long-running-process.test.js +318 -0
- package/dist/runtime/model-smoke-helpers.js +160 -0
- package/dist/runtime/model-smoke.test.js +194 -0
- package/dist/runtime/model-tiers.js +33 -0
- package/dist/runtime/model-tiers.test.js +65 -0
- package/dist/runtime/openai-auth.js +151 -0
- package/dist/runtime/openai-auth.test.js +361 -0
- package/dist/runtime/openai-compat.js +178 -0
- package/dist/runtime/openai-compat.test.js +449 -0
- package/dist/runtime/process-pool.js +93 -0
- package/dist/runtime/process-pool.test.js +148 -0
- package/dist/runtime/registry.js +15 -0
- package/dist/runtime/registry.test.js +47 -0
- package/dist/runtime/session-scanner.js +186 -0
- package/dist/runtime/session-scanner.test.js +257 -0
- package/dist/runtime/strategies/claude-strategy.js +193 -0
- package/dist/runtime/strategies/codex-strategy.js +161 -0
- package/dist/runtime/strategies/gemini-strategy.js +64 -0
- package/dist/runtime/strategies/template-strategy.js +85 -0
- package/dist/runtime/tool-capabilities.js +27 -0
- package/dist/runtime/tool-capabilities.test.js +24 -0
- package/dist/runtime/tool-labels.js +48 -0
- package/dist/runtime/types.js +2 -0
- package/dist/sessionManager.js +47 -0
- package/dist/sessions.js +18 -0
- package/dist/tasks/architecture-contract.js +33 -0
- package/dist/tasks/architecture-contract.test.js +90 -0
- package/dist/tasks/auto-tag.js +50 -0
- package/dist/tasks/auto-tag.test.js +64 -0
- package/dist/tasks/bd-cli.js +164 -0
- package/dist/tasks/bd-cli.test.js +359 -0
- package/dist/tasks/bead-sync.js +1 -0
- package/dist/tasks/context-summary.js +27 -0
- package/dist/tasks/discord-sync.js +3 -0
- package/dist/tasks/discord-sync.test.js +685 -0
- package/dist/tasks/discord-types.js +4 -0
- package/dist/tasks/find-task-by-thread.test.js +36 -0
- package/dist/tasks/forum-guard.js +81 -0
- package/dist/tasks/forum-guard.test.js +192 -0
- package/dist/tasks/initialize.js +77 -0
- package/dist/tasks/initialize.test.js +263 -0
- package/dist/tasks/logger-types.js +1 -0
- package/dist/tasks/metrics-types.js +3 -0
- package/dist/tasks/migrate.js +33 -0
- package/dist/tasks/migrate.test.js +156 -0
- package/dist/tasks/path-defaults.js +67 -0
- package/dist/tasks/path-defaults.test.js +73 -0
- package/dist/tasks/runtime-types.js +1 -0
- package/dist/tasks/service.js +33 -0
- package/dist/tasks/service.test.js +51 -0
- package/dist/tasks/store.js +238 -0
- package/dist/tasks/store.test.js +417 -0
- package/dist/tasks/sync-context.js +1 -0
- package/dist/tasks/sync-contract.js +24 -0
- package/dist/tasks/sync-contract.test.js +25 -0
- package/dist/tasks/sync-coordinator-metrics.js +41 -0
- package/dist/tasks/sync-coordinator-retries.js +71 -0
- package/dist/tasks/sync-coordinator.js +96 -0
- package/dist/tasks/sync-coordinator.test.js +501 -0
- package/dist/tasks/sync-types.js +1 -0
- package/dist/tasks/sync-watcher.js +27 -0
- package/dist/tasks/sync-watcher.test.js +92 -0
- package/dist/tasks/tag-map.js +36 -0
- package/dist/tasks/tag-map.test.js +54 -0
- package/dist/tasks/task-action-contract.js +16 -0
- package/dist/tasks/task-action-contract.test.js +16 -0
- package/dist/tasks/task-action-executor.js +18 -0
- package/dist/tasks/task-action-executor.test.js +420 -0
- package/dist/tasks/task-action-mutation-helpers.js +17 -0
- package/dist/tasks/task-action-mutations.js +151 -0
- package/dist/tasks/task-action-prompt.js +62 -0
- package/dist/tasks/task-action-read-ops.js +73 -0
- package/dist/tasks/task-action-runner-types.js +1 -0
- package/dist/tasks/task-action-thread-sync.js +82 -0
- package/dist/tasks/task-actions.js +3 -0
- package/dist/tasks/task-cli.js +227 -0
- package/dist/tasks/task-context.js +1 -0
- package/dist/tasks/task-lifecycle.js +46 -0
- package/dist/tasks/task-lifecycle.test.js +35 -0
- package/dist/tasks/task-sync-apply-plan.js +95 -0
- package/dist/tasks/task-sync-apply-types.js +12 -0
- package/dist/tasks/task-sync-apply.js +319 -0
- package/dist/tasks/task-sync-cli.js +89 -0
- package/dist/tasks/task-sync-cli.test.js +70 -0
- package/dist/tasks/task-sync-engine.js +88 -0
- package/dist/tasks/task-sync-engine.test.js +934 -0
- package/dist/tasks/task-sync-phase-apply.js +171 -0
- package/dist/tasks/task-sync-pipeline.js +2 -0
- package/dist/tasks/task-sync-pipeline.test.js +265 -0
- package/dist/tasks/task-sync-reconcile-plan.js +182 -0
- package/dist/tasks/task-sync-reconcile.js +144 -0
- package/dist/tasks/task-sync.js +56 -0
- package/dist/tasks/task-sync.test.js +86 -0
- package/dist/tasks/thread-cache.js +42 -0
- package/dist/tasks/thread-cache.test.js +89 -0
- package/dist/tasks/thread-contracts.test.js +711 -0
- package/dist/tasks/thread-forum-ops.js +68 -0
- package/dist/tasks/thread-helpers.js +86 -0
- package/dist/tasks/thread-helpers.test.js +33 -0
- package/dist/tasks/thread-lifecycle-ops.js +144 -0
- package/dist/tasks/thread-ops-shared.js +21 -0
- package/dist/tasks/thread-ops.js +2 -0
- package/dist/tasks/types.js +20 -0
- package/dist/tasks/types.test.js +60 -0
- package/dist/test-setup.js +11 -0
- package/dist/test-setup.test.js +42 -0
- package/dist/transport/types.js +1 -0
- package/dist/validate.js +41 -0
- package/dist/validate.test.js +94 -0
- package/dist/version.js +15 -0
- package/dist/version.test.js +31 -0
- package/dist/webhook/server.js +199 -0
- package/dist/webhook/server.test.js +460 -0
- package/dist/workspace-bootstrap.js +135 -0
- package/dist/workspace-bootstrap.test.js +514 -0
- package/dist/workspace-permissions.js +134 -0
- package/dist/workspace-permissions.test.js +181 -0
- package/package.json +74 -0
- package/scripts/cron/cron-tag-map.json +9 -0
- package/scripts/tasks/tag-map.json +10 -0
- package/systemd/discoclaw.service +19 -0
- package/templates/recipes/integration.discoclaw-recipe.md +171 -0
- package/templates/workspace/AGENTS.md +217 -0
- package/templates/workspace/BOOTSTRAP.md +1 -0
- package/templates/workspace/HEARTBEAT.md +10 -0
- package/templates/workspace/IDENTITY.md +16 -0
- package/templates/workspace/MEMORY.md +24 -0
- package/templates/workspace/SOUL.md +52 -0
- package/templates/workspace/TOOLS.md +304 -0
- package/templates/workspace/USER.md +37 -0
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { renderDiscordTail, renderActivityTail, formatBoldLabel, splitDiscord, truncateCodeBlocks, thinkingLabel, selectStreamingOutput, formatElapsed, } from './discord.js';
|
|
3
|
+
const ZWS = '\u200b';
|
|
4
|
+
/** Extract the content lines between the opening and closing fences (for renderDiscordTail). */
|
|
5
|
+
function contentLines(rendered) {
|
|
6
|
+
const lines = rendered.split('\n');
|
|
7
|
+
// First line is "```text", last line is "```".
|
|
8
|
+
return lines.slice(1, -1);
|
|
9
|
+
}
|
|
10
|
+
/** Extract the bold label line from renderActivityTail output. */
|
|
11
|
+
function activityBoldLabel(rendered) {
|
|
12
|
+
return rendered.split('\n')[0];
|
|
13
|
+
}
|
|
14
|
+
/** Extract the code block content lines from renderActivityTail output (skips bold line + fence). */
|
|
15
|
+
function activityContentLines(rendered) {
|
|
16
|
+
const lines = rendered.split('\n');
|
|
17
|
+
// Line 0: **label**, Line 1: ```text, Lines 2..N-1: content, Line N: ```
|
|
18
|
+
return lines.slice(2, -1);
|
|
19
|
+
}
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// renderDiscordTail
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
describe('renderDiscordTail', () => {
|
|
24
|
+
it('empty string → 8 ZWS lines', () => {
|
|
25
|
+
const out = renderDiscordTail('');
|
|
26
|
+
const lines = contentLines(out);
|
|
27
|
+
expect(lines).toHaveLength(8);
|
|
28
|
+
expect(lines.every((l) => l === ZWS)).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
it('single line → 7 ZWS + 1 content line', () => {
|
|
31
|
+
const out = renderDiscordTail('hello');
|
|
32
|
+
const lines = contentLines(out);
|
|
33
|
+
expect(lines).toHaveLength(8);
|
|
34
|
+
expect(lines.slice(0, 7).every((l) => l === ZWS)).toBe(true);
|
|
35
|
+
expect(lines[7]).toBe('hello');
|
|
36
|
+
});
|
|
37
|
+
it('exactly 8 lines → no padding', () => {
|
|
38
|
+
const input = Array.from({ length: 8 }, (_, i) => `line${i}`).join('\n');
|
|
39
|
+
const out = renderDiscordTail(input);
|
|
40
|
+
const lines = contentLines(out);
|
|
41
|
+
expect(lines).toHaveLength(8);
|
|
42
|
+
expect(lines[0]).toBe('line0');
|
|
43
|
+
expect(lines[7]).toBe('line7');
|
|
44
|
+
});
|
|
45
|
+
it('more than 8 lines → only last 8', () => {
|
|
46
|
+
const input = Array.from({ length: 12 }, (_, i) => `line${i}`).join('\n');
|
|
47
|
+
const out = renderDiscordTail(input);
|
|
48
|
+
const lines = contentLines(out);
|
|
49
|
+
expect(lines).toHaveLength(8);
|
|
50
|
+
expect(lines[0]).toBe('line4');
|
|
51
|
+
expect(lines[7]).toBe('line11');
|
|
52
|
+
});
|
|
53
|
+
it('triple backticks in input are escaped', () => {
|
|
54
|
+
const out = renderDiscordTail('before\n```code```\nafter');
|
|
55
|
+
expect(out).not.toContain('```code```');
|
|
56
|
+
// The escaped form replaces ``` with ``\`
|
|
57
|
+
expect(out).toContain('``\\`code``\\`');
|
|
58
|
+
});
|
|
59
|
+
it('CRLF normalized to LF', () => {
|
|
60
|
+
const out = renderDiscordTail('line1\r\nline2\r\nline3');
|
|
61
|
+
const lines = contentLines(out);
|
|
62
|
+
expect(lines).toHaveLength(8);
|
|
63
|
+
expect(lines[5]).toBe('line1');
|
|
64
|
+
expect(lines[6]).toBe('line2');
|
|
65
|
+
expect(lines[7]).toBe('line3');
|
|
66
|
+
});
|
|
67
|
+
it('empty lines in input are filtered out', () => {
|
|
68
|
+
const out = renderDiscordTail('a\n\nb\n\nc');
|
|
69
|
+
const lines = contentLines(out);
|
|
70
|
+
expect(lines).toHaveLength(8);
|
|
71
|
+
// Only non-empty lines kept: a, b, c
|
|
72
|
+
expect(lines[5]).toBe('a');
|
|
73
|
+
expect(lines[6]).toBe('b');
|
|
74
|
+
expect(lines[7]).toBe('c');
|
|
75
|
+
});
|
|
76
|
+
it('custom maxLines is respected', () => {
|
|
77
|
+
const out = renderDiscordTail('hello', 4);
|
|
78
|
+
const lines = contentLines(out);
|
|
79
|
+
expect(lines).toHaveLength(4);
|
|
80
|
+
expect(lines[3]).toBe('hello');
|
|
81
|
+
});
|
|
82
|
+
it('maxLines = 0 → slice(-0) returns all non-empty lines (1 line for single-word input)', () => {
|
|
83
|
+
// slice(-0) === slice(0) in JS, so all filtered lines are kept.
|
|
84
|
+
// The while loop condition (tail.length < 0) never fires → no padding.
|
|
85
|
+
const out = renderDiscordTail('hello', 0);
|
|
86
|
+
const lines = contentLines(out);
|
|
87
|
+
expect(lines).toHaveLength(1);
|
|
88
|
+
expect(lines[0]).toBe('hello');
|
|
89
|
+
});
|
|
90
|
+
it('maxLines = 1 → one content line', () => {
|
|
91
|
+
const out = renderDiscordTail('a\nb\nc', 1);
|
|
92
|
+
const lines = contentLines(out);
|
|
93
|
+
expect(lines).toHaveLength(1);
|
|
94
|
+
expect(lines[0]).toBe('c');
|
|
95
|
+
});
|
|
96
|
+
it('wraps in ```text fences', () => {
|
|
97
|
+
const out = renderDiscordTail('hi');
|
|
98
|
+
expect(out.startsWith('```text\n')).toBe(true);
|
|
99
|
+
expect(out.endsWith('\n```')).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
it('null/undefined input treated as empty', () => {
|
|
102
|
+
// The function uses String(text ?? ''), so null/undefined should work.
|
|
103
|
+
const out = renderDiscordTail(null);
|
|
104
|
+
const lines = contentLines(out);
|
|
105
|
+
expect(lines).toHaveLength(8);
|
|
106
|
+
expect(lines.every((l) => l === ZWS)).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
it('long lines are truncated to maxWidth with ellipsis', () => {
|
|
109
|
+
const long = 'x'.repeat(100);
|
|
110
|
+
const out = renderDiscordTail(long, 8, 72);
|
|
111
|
+
const lines = contentLines(out);
|
|
112
|
+
expect(lines[7].length).toBe(72);
|
|
113
|
+
expect(lines[7].endsWith('\u2026')).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
it('lines at or under maxWidth are not truncated', () => {
|
|
116
|
+
const exact = 'y'.repeat(72);
|
|
117
|
+
const out = renderDiscordTail(exact, 8, 72);
|
|
118
|
+
const lines = contentLines(out);
|
|
119
|
+
expect(lines[7]).toBe(exact);
|
|
120
|
+
});
|
|
121
|
+
it('ZWS padding lines are not affected by maxWidth', () => {
|
|
122
|
+
const out = renderDiscordTail('short', 8, 10);
|
|
123
|
+
const lines = contentLines(out);
|
|
124
|
+
// All padding lines should still be ZWS.
|
|
125
|
+
expect(lines.slice(0, 7).every((l) => l === ZWS)).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// renderActivityTail
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
describe('renderActivityTail', () => {
|
|
132
|
+
it('normal label → bold above, 8 ZWS in block', () => {
|
|
133
|
+
const out = renderActivityTail('(working...)');
|
|
134
|
+
expect(activityBoldLabel(out)).toBe('**(working...)**');
|
|
135
|
+
const lines = activityContentLines(out);
|
|
136
|
+
expect(lines).toHaveLength(8);
|
|
137
|
+
expect(lines.every((l) => l === ZWS)).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
it('label with triple backticks is escaped in bold context', () => {
|
|
140
|
+
const out = renderActivityTail('reading ```file```');
|
|
141
|
+
// Backticks are escaped in the bold label
|
|
142
|
+
expect(activityBoldLabel(out)).toBe('**reading \\`\\`\\`file\\`\\`\\`**');
|
|
143
|
+
// Triple backticks in the code block body are also escaped
|
|
144
|
+
expect(out).not.toContain('```file```');
|
|
145
|
+
// Code block content is all ZWS
|
|
146
|
+
const lines = activityContentLines(out);
|
|
147
|
+
expect(lines).toHaveLength(8);
|
|
148
|
+
expect(lines.every((l) => l === ZWS)).toBe(true);
|
|
149
|
+
});
|
|
150
|
+
it('label with newline uses only first non-empty line', () => {
|
|
151
|
+
const out = renderActivityTail('first\nsecond\nthird');
|
|
152
|
+
expect(activityBoldLabel(out)).toBe('**first**');
|
|
153
|
+
const lines = activityContentLines(out);
|
|
154
|
+
expect(lines).toHaveLength(8);
|
|
155
|
+
expect(lines.every((l) => l === ZWS)).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
it('label that is only newlines → empty bold, 8 ZWS lines', () => {
|
|
158
|
+
const out = renderActivityTail('\n');
|
|
159
|
+
expect(activityBoldLabel(out)).toBe('****');
|
|
160
|
+
const lines = activityContentLines(out);
|
|
161
|
+
expect(lines).toHaveLength(8);
|
|
162
|
+
expect(lines.every((l) => l === ZWS)).toBe(true);
|
|
163
|
+
});
|
|
164
|
+
it('custom maxLines is respected', () => {
|
|
165
|
+
const out = renderActivityTail('label', 4);
|
|
166
|
+
expect(activityBoldLabel(out)).toBe('**label**');
|
|
167
|
+
const lines = activityContentLines(out);
|
|
168
|
+
expect(lines).toHaveLength(4);
|
|
169
|
+
expect(lines.every((l) => l === ZWS)).toBe(true);
|
|
170
|
+
});
|
|
171
|
+
it('maxLines = 0 → bold label, empty code block', () => {
|
|
172
|
+
const out = renderActivityTail('label', 0);
|
|
173
|
+
expect(activityBoldLabel(out)).toBe('**label**');
|
|
174
|
+
// The join of zero lines produces '' between the fences, yielding one empty line
|
|
175
|
+
const lines = activityContentLines(out);
|
|
176
|
+
expect(lines).toHaveLength(1);
|
|
177
|
+
expect(lines[0]).toBe('');
|
|
178
|
+
});
|
|
179
|
+
it('maxLines = 1 → bold label, one ZWS line in block', () => {
|
|
180
|
+
const out = renderActivityTail('label', 1);
|
|
181
|
+
expect(activityBoldLabel(out)).toBe('**label**');
|
|
182
|
+
const lines = activityContentLines(out);
|
|
183
|
+
expect(lines).toHaveLength(1);
|
|
184
|
+
expect(lines[0]).toBe(ZWS);
|
|
185
|
+
});
|
|
186
|
+
it('starts with bold label, then ```text fences', () => {
|
|
187
|
+
const out = renderActivityTail('hi');
|
|
188
|
+
expect(out.startsWith('**hi**\n```text\n')).toBe(true);
|
|
189
|
+
expect(out.endsWith('\n```')).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
it('long label is truncated to maxWidth with ellipsis', () => {
|
|
192
|
+
const long = 'z'.repeat(100);
|
|
193
|
+
const out = renderActivityTail(long, 8, 72);
|
|
194
|
+
const bold = activityBoldLabel(out);
|
|
195
|
+
// Bold wraps: **...truncated...**
|
|
196
|
+
// The truncated label is 55 chars + ellipsis = 56 chars, then escaped
|
|
197
|
+
expect(bold.startsWith('**')).toBe(true);
|
|
198
|
+
expect(bold.endsWith('**')).toBe(true);
|
|
199
|
+
expect(bold).toContain('\u2026');
|
|
200
|
+
});
|
|
201
|
+
it('label at or under maxWidth is not truncated', () => {
|
|
202
|
+
const exact = 'a'.repeat(72);
|
|
203
|
+
const out = renderActivityTail(exact, 8, 72);
|
|
204
|
+
const bold = activityBoldLabel(out);
|
|
205
|
+
expect(bold).toBe(`**${exact}**`);
|
|
206
|
+
});
|
|
207
|
+
it('markdown special chars in label are escaped', () => {
|
|
208
|
+
const out = renderActivityTail('*bold* _italic_ ~strike~');
|
|
209
|
+
expect(activityBoldLabel(out)).toBe('**\\*bold\\* \\_italic\\_ \\~strike\\~**');
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// thinkingLabel
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
describe('thinkingLabel', () => {
|
|
216
|
+
it('tick 0 → Thinking.', () => {
|
|
217
|
+
expect(thinkingLabel(0)).toBe('Thinking.');
|
|
218
|
+
});
|
|
219
|
+
it('tick 1 → Thinking..', () => {
|
|
220
|
+
expect(thinkingLabel(1)).toBe('Thinking..');
|
|
221
|
+
});
|
|
222
|
+
it('tick 2 → Thinking...', () => {
|
|
223
|
+
expect(thinkingLabel(2)).toBe('Thinking...');
|
|
224
|
+
});
|
|
225
|
+
it('tick 3 → Thinking (no dots)', () => {
|
|
226
|
+
expect(thinkingLabel(3)).toBe('Thinking');
|
|
227
|
+
});
|
|
228
|
+
it('tick 4 → wraps back to Thinking.', () => {
|
|
229
|
+
expect(thinkingLabel(4)).toBe('Thinking.');
|
|
230
|
+
});
|
|
231
|
+
it('tick 7 → Thinking (no dots)', () => {
|
|
232
|
+
expect(thinkingLabel(7)).toBe('Thinking');
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
// formatElapsed
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
describe('formatElapsed', () => {
|
|
239
|
+
it('0ms → (0s)', () => {
|
|
240
|
+
expect(formatElapsed(0)).toBe('(0s)');
|
|
241
|
+
});
|
|
242
|
+
it('sub-second (500ms) → (0s)', () => {
|
|
243
|
+
expect(formatElapsed(500)).toBe('(0s)');
|
|
244
|
+
});
|
|
245
|
+
it('1s (1000ms) → (1s)', () => {
|
|
246
|
+
expect(formatElapsed(1000)).toBe('(1s)');
|
|
247
|
+
});
|
|
248
|
+
it('30s (30000ms) → (30s)', () => {
|
|
249
|
+
expect(formatElapsed(30000)).toBe('(30s)');
|
|
250
|
+
});
|
|
251
|
+
it('59s (59000ms) → (59s)', () => {
|
|
252
|
+
expect(formatElapsed(59000)).toBe('(59s)');
|
|
253
|
+
});
|
|
254
|
+
it('59999ms rounds down → (59s)', () => {
|
|
255
|
+
expect(formatElapsed(59999)).toBe('(59s)');
|
|
256
|
+
});
|
|
257
|
+
it('exactly 60s (60000ms) → (1m0s)', () => {
|
|
258
|
+
expect(formatElapsed(60000)).toBe('(1m0s)');
|
|
259
|
+
});
|
|
260
|
+
it('90s (90000ms) → (1m30s)', () => {
|
|
261
|
+
expect(formatElapsed(90000)).toBe('(1m30s)');
|
|
262
|
+
});
|
|
263
|
+
it('125s (125000ms) → (2m5s)', () => {
|
|
264
|
+
expect(formatElapsed(125000)).toBe('(2m5s)');
|
|
265
|
+
});
|
|
266
|
+
it('large value — 1 hour (3600000ms) → (60m0s)', () => {
|
|
267
|
+
expect(formatElapsed(3600000)).toBe('(60m0s)');
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
// formatBoldLabel
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
describe('formatBoldLabel', () => {
|
|
274
|
+
it('wraps label in bold', () => {
|
|
275
|
+
expect(formatBoldLabel('hello')).toBe('**hello**');
|
|
276
|
+
});
|
|
277
|
+
it('escapes markdown special chars', () => {
|
|
278
|
+
expect(formatBoldLabel('*bold* _ital_')).toBe('**\\*bold\\* \\_ital\\_**');
|
|
279
|
+
});
|
|
280
|
+
it('truncates long labels with ellipsis', () => {
|
|
281
|
+
const long = 'z'.repeat(100);
|
|
282
|
+
const out = formatBoldLabel(long, 72);
|
|
283
|
+
// ** + 72 chars + ** = total; inner content is 71 chars + ellipsis
|
|
284
|
+
expect(out).toBe(`**${'z'.repeat(71)}\u2026**`);
|
|
285
|
+
});
|
|
286
|
+
it('labels at maxWidth are not truncated', () => {
|
|
287
|
+
const exact = 'a'.repeat(72);
|
|
288
|
+
expect(formatBoldLabel(exact, 72)).toBe(`**${exact}**`);
|
|
289
|
+
});
|
|
290
|
+
it('uses first non-empty line from multi-line input', () => {
|
|
291
|
+
expect(formatBoldLabel('first\nsecond\nthird')).toBe('**first**');
|
|
292
|
+
});
|
|
293
|
+
it('newline-only input → empty bold', () => {
|
|
294
|
+
expect(formatBoldLabel('\n\n')).toBe('****');
|
|
295
|
+
});
|
|
296
|
+
it('escapes backticks', () => {
|
|
297
|
+
expect(formatBoldLabel('reading ```file```')).toBe('**reading \\`\\`\\`file\\`\\`\\`**');
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
// selectStreamingOutput
|
|
302
|
+
// ---------------------------------------------------------------------------
|
|
303
|
+
describe('selectStreamingOutput', () => {
|
|
304
|
+
it('deltaText wins over all others and shows thinking label above', () => {
|
|
305
|
+
const out = selectStreamingOutput({
|
|
306
|
+
deltaText: 'streaming text',
|
|
307
|
+
activityLabel: 'Reading file...',
|
|
308
|
+
finalText: 'final answer',
|
|
309
|
+
statusTick: 0,
|
|
310
|
+
});
|
|
311
|
+
// Should be bold thinking label + code block (renderDiscordTail)
|
|
312
|
+
expect(out).toContain('**Thinking.**');
|
|
313
|
+
expect(out).toContain('```text');
|
|
314
|
+
expect(out).toContain('streaming text');
|
|
315
|
+
});
|
|
316
|
+
it('deltaText thinking label animates with statusTick', () => {
|
|
317
|
+
const out2 = selectStreamingOutput({
|
|
318
|
+
deltaText: 'hello',
|
|
319
|
+
activityLabel: '',
|
|
320
|
+
finalText: '',
|
|
321
|
+
statusTick: 2,
|
|
322
|
+
});
|
|
323
|
+
expect(out2).toContain('**Thinking...**');
|
|
324
|
+
expect(out2).toContain('hello');
|
|
325
|
+
});
|
|
326
|
+
it('activityLabel wins over finalText and default', () => {
|
|
327
|
+
const out = selectStreamingOutput({
|
|
328
|
+
deltaText: '',
|
|
329
|
+
activityLabel: 'Reading file...',
|
|
330
|
+
finalText: 'final answer',
|
|
331
|
+
statusTick: 0,
|
|
332
|
+
});
|
|
333
|
+
// Should be bold + code block (renderActivityTail)
|
|
334
|
+
expect(out).toContain('**Reading file...**');
|
|
335
|
+
expect(out).toContain('```text');
|
|
336
|
+
});
|
|
337
|
+
it('finalText wins over default thinking', () => {
|
|
338
|
+
const out = selectStreamingOutput({
|
|
339
|
+
deltaText: '',
|
|
340
|
+
activityLabel: '',
|
|
341
|
+
finalText: 'final answer',
|
|
342
|
+
statusTick: 0,
|
|
343
|
+
});
|
|
344
|
+
expect(out).toContain('```text');
|
|
345
|
+
expect(out).toContain('final answer');
|
|
346
|
+
expect(out).not.toContain('**');
|
|
347
|
+
});
|
|
348
|
+
it('empty deltaText/activityLabel/finalText → returns thinking label', () => {
|
|
349
|
+
const out = selectStreamingOutput({
|
|
350
|
+
deltaText: '',
|
|
351
|
+
activityLabel: '',
|
|
352
|
+
finalText: '',
|
|
353
|
+
statusTick: 2,
|
|
354
|
+
});
|
|
355
|
+
// tick 2 → "Thinking..."
|
|
356
|
+
expect(out).toContain('**Thinking...**');
|
|
357
|
+
expect(out).toContain('```text');
|
|
358
|
+
});
|
|
359
|
+
it('thinking label tick advances correctly', () => {
|
|
360
|
+
const out0 = selectStreamingOutput({ deltaText: '', activityLabel: '', finalText: '', statusTick: 0 });
|
|
361
|
+
const out1 = selectStreamingOutput({ deltaText: '', activityLabel: '', finalText: '', statusTick: 1 });
|
|
362
|
+
const out3 = selectStreamingOutput({ deltaText: '', activityLabel: '', finalText: '', statusTick: 3 });
|
|
363
|
+
expect(out0).toContain('**Thinking.**');
|
|
364
|
+
expect(out1).toContain('**Thinking..**');
|
|
365
|
+
expect(out3).toContain('**Thinking**');
|
|
366
|
+
});
|
|
367
|
+
// -- showPreview: false (delay streaming code block) --
|
|
368
|
+
it('showPreview=false + deltaText → label only (no code block)', () => {
|
|
369
|
+
const out = selectStreamingOutput({
|
|
370
|
+
deltaText: 'streaming text',
|
|
371
|
+
activityLabel: '',
|
|
372
|
+
finalText: '',
|
|
373
|
+
statusTick: 1,
|
|
374
|
+
showPreview: false,
|
|
375
|
+
});
|
|
376
|
+
expect(out).toBe('**Thinking..**');
|
|
377
|
+
expect(out).not.toContain('```');
|
|
378
|
+
expect(out).not.toContain('streaming text');
|
|
379
|
+
});
|
|
380
|
+
it('showPreview=false + activityLabel → label only (no code block)', () => {
|
|
381
|
+
const out = selectStreamingOutput({
|
|
382
|
+
deltaText: '',
|
|
383
|
+
activityLabel: 'Reading file...',
|
|
384
|
+
finalText: '',
|
|
385
|
+
statusTick: 0,
|
|
386
|
+
showPreview: false,
|
|
387
|
+
});
|
|
388
|
+
expect(out).toBe('**Reading file...**');
|
|
389
|
+
expect(out).not.toContain('```');
|
|
390
|
+
});
|
|
391
|
+
it('showPreview=false + finalText → still renders code block (bypasses gate)', () => {
|
|
392
|
+
const out = selectStreamingOutput({
|
|
393
|
+
deltaText: '',
|
|
394
|
+
activityLabel: '',
|
|
395
|
+
finalText: 'final answer',
|
|
396
|
+
statusTick: 0,
|
|
397
|
+
showPreview: false,
|
|
398
|
+
});
|
|
399
|
+
expect(out).toContain('```text');
|
|
400
|
+
expect(out).toContain('final answer');
|
|
401
|
+
});
|
|
402
|
+
it('showPreview=false + no content → thinking label only (no code block)', () => {
|
|
403
|
+
const out = selectStreamingOutput({
|
|
404
|
+
deltaText: '',
|
|
405
|
+
activityLabel: '',
|
|
406
|
+
finalText: '',
|
|
407
|
+
statusTick: 2,
|
|
408
|
+
showPreview: false,
|
|
409
|
+
});
|
|
410
|
+
expect(out).toBe('**Thinking...**');
|
|
411
|
+
expect(out).not.toContain('```');
|
|
412
|
+
});
|
|
413
|
+
it('showPreview=true (default) is unchanged', () => {
|
|
414
|
+
const out = selectStreamingOutput({
|
|
415
|
+
deltaText: 'data',
|
|
416
|
+
activityLabel: '',
|
|
417
|
+
finalText: '',
|
|
418
|
+
statusTick: 0,
|
|
419
|
+
});
|
|
420
|
+
expect(out).toContain('```text');
|
|
421
|
+
expect(out).toContain('data');
|
|
422
|
+
});
|
|
423
|
+
// -- elapsed prefix --
|
|
424
|
+
it('elapsedMs prefixes thinking label when deltaText is present', () => {
|
|
425
|
+
const out = selectStreamingOutput({
|
|
426
|
+
deltaText: 'streaming text',
|
|
427
|
+
activityLabel: '',
|
|
428
|
+
finalText: '',
|
|
429
|
+
statusTick: 0,
|
|
430
|
+
elapsedMs: 42000,
|
|
431
|
+
});
|
|
432
|
+
expect(out).toContain('**(42s) Thinking.**');
|
|
433
|
+
expect(out).toContain('streaming text');
|
|
434
|
+
});
|
|
435
|
+
it('elapsedMs prefixes activity label', () => {
|
|
436
|
+
const out = selectStreamingOutput({
|
|
437
|
+
deltaText: '',
|
|
438
|
+
activityLabel: 'Reading file...',
|
|
439
|
+
finalText: '',
|
|
440
|
+
statusTick: 0,
|
|
441
|
+
elapsedMs: 72000,
|
|
442
|
+
});
|
|
443
|
+
expect(out).toContain('**(1m12s) Reading file...**');
|
|
444
|
+
});
|
|
445
|
+
it('elapsedMs prefixes default thinking label (no deltaText/activityLabel/finalText)', () => {
|
|
446
|
+
const out = selectStreamingOutput({
|
|
447
|
+
deltaText: '',
|
|
448
|
+
activityLabel: '',
|
|
449
|
+
finalText: '',
|
|
450
|
+
statusTick: 1,
|
|
451
|
+
elapsedMs: 5000,
|
|
452
|
+
});
|
|
453
|
+
expect(out).toContain('**(5s) Thinking..**');
|
|
454
|
+
});
|
|
455
|
+
it('no elapsed prefix on final-text-only output', () => {
|
|
456
|
+
const out = selectStreamingOutput({
|
|
457
|
+
deltaText: '',
|
|
458
|
+
activityLabel: '',
|
|
459
|
+
finalText: 'done',
|
|
460
|
+
statusTick: 0,
|
|
461
|
+
elapsedMs: 30000,
|
|
462
|
+
});
|
|
463
|
+
expect(out).toContain('done');
|
|
464
|
+
expect(out).not.toContain('(30s)');
|
|
465
|
+
expect(out).not.toContain('**');
|
|
466
|
+
});
|
|
467
|
+
it('no prefix when elapsedMs is omitted (backward compatibility)', () => {
|
|
468
|
+
const out = selectStreamingOutput({
|
|
469
|
+
deltaText: 'hello',
|
|
470
|
+
activityLabel: '',
|
|
471
|
+
finalText: '',
|
|
472
|
+
statusTick: 0,
|
|
473
|
+
});
|
|
474
|
+
expect(out).toContain('**Thinking.**');
|
|
475
|
+
// No (Xs) token present
|
|
476
|
+
expect(out).not.toMatch(/\(\d+s\)/);
|
|
477
|
+
expect(out).not.toMatch(/\(\d+m\d+s\)/);
|
|
478
|
+
});
|
|
479
|
+
it('showPreview=false + elapsedMs: prefix renders in thinking label', () => {
|
|
480
|
+
const out = selectStreamingOutput({
|
|
481
|
+
deltaText: 'streaming text',
|
|
482
|
+
activityLabel: '',
|
|
483
|
+
finalText: '',
|
|
484
|
+
statusTick: 0,
|
|
485
|
+
showPreview: false,
|
|
486
|
+
elapsedMs: 42000,
|
|
487
|
+
});
|
|
488
|
+
expect(out).toBe('**(42s) Thinking.**');
|
|
489
|
+
expect(out).not.toContain('```');
|
|
490
|
+
});
|
|
491
|
+
it('showPreview=false + elapsedMs: prefix renders in activity label', () => {
|
|
492
|
+
const out = selectStreamingOutput({
|
|
493
|
+
deltaText: '',
|
|
494
|
+
activityLabel: 'Reading file...',
|
|
495
|
+
finalText: '',
|
|
496
|
+
statusTick: 0,
|
|
497
|
+
showPreview: false,
|
|
498
|
+
elapsedMs: 90000,
|
|
499
|
+
});
|
|
500
|
+
expect(out).toBe('**(1m30s) Reading file...**');
|
|
501
|
+
expect(out).not.toContain('```');
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
// ---------------------------------------------------------------------------
|
|
505
|
+
// splitDiscord
|
|
506
|
+
// ---------------------------------------------------------------------------
|
|
507
|
+
describe('splitDiscord', () => {
|
|
508
|
+
it('short text → single chunk', () => {
|
|
509
|
+
const chunks = splitDiscord('Hello world');
|
|
510
|
+
expect(chunks).toEqual(['Hello world']);
|
|
511
|
+
});
|
|
512
|
+
it('text under limit returns as-is', () => {
|
|
513
|
+
const text = 'a'.repeat(100);
|
|
514
|
+
const chunks = splitDiscord(text, 200);
|
|
515
|
+
expect(chunks).toHaveLength(1);
|
|
516
|
+
expect(chunks[0]).toBe(text);
|
|
517
|
+
});
|
|
518
|
+
it('long text → multiple chunks, each ≤ limit', () => {
|
|
519
|
+
const lines = Array.from({ length: 100 }, (_, i) => `line-${i}-${'x'.repeat(30)}`);
|
|
520
|
+
const text = lines.join('\n');
|
|
521
|
+
const limit = 200;
|
|
522
|
+
const chunks = splitDiscord(text, limit);
|
|
523
|
+
expect(chunks.length).toBeGreaterThan(1);
|
|
524
|
+
for (const chunk of chunks) {
|
|
525
|
+
expect(chunk.length).toBeLessThanOrEqual(limit);
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
it('never exceeds limit when re-opening fenced code blocks', () => {
|
|
529
|
+
const limit = 20;
|
|
530
|
+
const longish = 'x'.repeat(15); // <= limit, but too long once the ```js header is re-opened.
|
|
531
|
+
const text = `\`\`\`js\n${longish}\n\`\`\``;
|
|
532
|
+
const chunks = splitDiscord(text, limit);
|
|
533
|
+
for (const chunk of chunks)
|
|
534
|
+
expect(chunk.length).toBeLessThanOrEqual(limit);
|
|
535
|
+
});
|
|
536
|
+
it('fenced code blocks are closed/reopened across chunk boundaries', () => {
|
|
537
|
+
const codeLines = Array.from({ length: 50 }, (_, i) => ` code line ${i}`);
|
|
538
|
+
const text = '```js\n' + codeLines.join('\n') + '\n```';
|
|
539
|
+
const chunks = splitDiscord(text, 200);
|
|
540
|
+
expect(chunks.length).toBeGreaterThan(1);
|
|
541
|
+
// First chunk should start with the fence opener.
|
|
542
|
+
expect(chunks[0]).toContain('```js');
|
|
543
|
+
// All mid-chunks that are inside the fence should have fence markers.
|
|
544
|
+
for (let i = 0; i < chunks.length - 1; i++) {
|
|
545
|
+
const trimmed = chunks[i].trimEnd();
|
|
546
|
+
// Chunks inside a fence should end with ``` (fence close).
|
|
547
|
+
if (trimmed.includes('```js') || (i > 0 && !chunks[i].startsWith('```'))) {
|
|
548
|
+
// At least verify it's valid markdown (no assertion needed; coverage is the goal).
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
it('normalizes CRLF to LF', () => {
|
|
553
|
+
const chunks = splitDiscord('a\r\nb\r\nc');
|
|
554
|
+
expect(chunks).toEqual(['a\nb\nc']);
|
|
555
|
+
});
|
|
556
|
+
it('empty chunks are filtered out', () => {
|
|
557
|
+
const chunks = splitDiscord('hello');
|
|
558
|
+
for (const chunk of chunks) {
|
|
559
|
+
expect(chunk.trim().length).toBeGreaterThan(0);
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
it('single line longer than limit is hard-split', () => {
|
|
563
|
+
const line = 'x'.repeat(300);
|
|
564
|
+
const chunks = splitDiscord(line, 100);
|
|
565
|
+
expect(chunks.length).toBeGreaterThan(1);
|
|
566
|
+
// Reassembled should equal the original.
|
|
567
|
+
expect(chunks.join('')).toBe(line);
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
// ---------------------------------------------------------------------------
|
|
571
|
+
// truncateCodeBlocks
|
|
572
|
+
// ---------------------------------------------------------------------------
|
|
573
|
+
describe('truncateCodeBlocks', () => {
|
|
574
|
+
it('short block → unchanged', () => {
|
|
575
|
+
const text = '```js\nline1\nline2\nline3\n```';
|
|
576
|
+
expect(truncateCodeBlocks(text, 10)).toBe(text);
|
|
577
|
+
});
|
|
578
|
+
it('long block → truncated with omission message', () => {
|
|
579
|
+
const lines = Array.from({ length: 30 }, (_, i) => `line${i}`);
|
|
580
|
+
const text = '```\n' + lines.join('\n') + '\n```';
|
|
581
|
+
const result = truncateCodeBlocks(text, 10);
|
|
582
|
+
expect(result).toContain('lines omitted');
|
|
583
|
+
// Should keep some top and bottom lines.
|
|
584
|
+
expect(result).toContain('line0');
|
|
585
|
+
expect(result).toContain('line29');
|
|
586
|
+
// Middle lines should be gone.
|
|
587
|
+
expect(result).not.toContain('line15');
|
|
588
|
+
});
|
|
589
|
+
it('keeps first/last lines of truncated block', () => {
|
|
590
|
+
const lines = Array.from({ length: 40 }, (_, i) => `L${i}`);
|
|
591
|
+
const text = '```py\n' + lines.join('\n') + '\n```';
|
|
592
|
+
const result = truncateCodeBlocks(text, 10);
|
|
593
|
+
// keepTop = ceil(10/2) = 5, keepBottom = floor(10/2) = 5
|
|
594
|
+
for (let i = 0; i < 5; i++)
|
|
595
|
+
expect(result).toContain(`L${i}`);
|
|
596
|
+
for (let i = 35; i < 40; i++)
|
|
597
|
+
expect(result).toContain(`L${i}`);
|
|
598
|
+
// Omitted count: 40 - 5 - 5 = 30
|
|
599
|
+
expect(result).toContain('30 lines omitted');
|
|
600
|
+
});
|
|
601
|
+
it('text without code blocks → unchanged', () => {
|
|
602
|
+
const text = 'Hello world\nNo code here.';
|
|
603
|
+
expect(truncateCodeBlocks(text, 5)).toBe(text);
|
|
604
|
+
});
|
|
605
|
+
it('block exactly at maxLines → unchanged', () => {
|
|
606
|
+
const lines = Array.from({ length: 10 }, (_, i) => `line${i}`);
|
|
607
|
+
const text = '```\n' + lines.join('\n') + '\n```';
|
|
608
|
+
expect(truncateCodeBlocks(text, 10)).toBe(text);
|
|
609
|
+
});
|
|
610
|
+
it('multiple code blocks truncated independently', () => {
|
|
611
|
+
const longBlock = Array.from({ length: 25 }, (_, i) => `a${i}`).join('\n');
|
|
612
|
+
const shortBlock = 'x\ny';
|
|
613
|
+
const text = `before\n\`\`\`\n${longBlock}\n\`\`\`\nmiddle\n\`\`\`\n${shortBlock}\n\`\`\`\nafter`;
|
|
614
|
+
const result = truncateCodeBlocks(text, 10);
|
|
615
|
+
expect(result).toContain('lines omitted');
|
|
616
|
+
// Short block should be unchanged.
|
|
617
|
+
expect(result).toContain('x\ny');
|
|
618
|
+
expect(result).toContain('before');
|
|
619
|
+
expect(result).toContain('after');
|
|
620
|
+
});
|
|
621
|
+
});
|