Files
openclaw/extensions/telegram/src/progress-summary.test.ts
Peter Lindsey 16a07774a9 feat(telegram): collapse summary bar + multi-line lane rendering
Port the dev-beta2 progress-summary work onto the compositor-era dispatch:

- Post a one-line activity digest (🧠 N thoughts · 💬 N notes · 🛠️ N tool
  calls · ⏱️ Ns, Discord parity) as a durable message when the progress
  window collapses at end-of-turn, before the final answer. Counted
  per-burst channel-side; only window-streamed activity feeds the bar
  (durable rv/verbose items never do — persistent message XOR bar count),
  no bar for error finals or turns where the window never rendered.
- Render multi-line commentary with its line structure intact (per-line
  markdown conversion, joined as line breaks) instead of collapsing to
  one run-on line; per-line conversion also keeps block markdown (setext
  headings) from forming across lines.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 18:38:39 -07:00

149 lines
5.6 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
createTelegramProgressSummaryTracker,
formatTelegramProgressSummaryLine,
} from "./progress-summary.js";
describe("formatTelegramProgressSummaryLine", () => {
it("renders all three lanes plus elapsed, mirroring Discord content", () => {
expect(
formatTelegramProgressSummaryLine(
{ reasoningSteps: 3, commentaryNotes: 2, toolCalls: 4 },
21_000,
),
).toBe("🧠 3 thoughts · 💬 2 notes · 🛠️ 4 tool calls · ⏱️ 21s");
});
it("uses singular nouns for a count of one", () => {
expect(
formatTelegramProgressSummaryLine(
{ reasoningSteps: 1, commentaryNotes: 1, toolCalls: 1 },
1_000,
),
).toBe("🧠 1 thought · 💬 1 note · 🛠️ 1 tool call · ⏱️ 1s");
});
it("omits lanes with a zero count", () => {
expect(
formatTelegramProgressSummaryLine(
{ reasoningSteps: 0, commentaryNotes: 0, toolCalls: 2 },
5_400,
),
).toBe("🛠️ 2 tool calls · ⏱️ 5s");
});
it("returns undefined when there is nothing to summarize (no degenerate elapsed-only line)", () => {
expect(
formatTelegramProgressSummaryLine(
{ reasoningSteps: 0, commentaryNotes: 0, toolCalls: 0 },
9_000,
),
).toBeUndefined();
});
it("floors elapsed at 1 second", () => {
expect(
formatTelegramProgressSummaryLine({ reasoningSteps: 0, commentaryNotes: 0, toolCalls: 1 }, 0),
).toBe("🛠️ 1 tool call · ⏱️ 1s");
});
it("rounds elapsed to the nearest second", () => {
expect(
formatTelegramProgressSummaryLine(
{ reasoningSteps: 0, commentaryNotes: 0, toolCalls: 1 },
21_600,
),
).toBe("🛠️ 1 tool call · ⏱️ 22s");
});
});
describe("createTelegramProgressSummaryTracker", () => {
it("counts a window reasoning burst once when closed by a tool call", () => {
const t = createTelegramProgressSummaryTracker();
t.noteReasoningActivity();
t.noteReasoningActivity(); // same burst, deltas
t.noteToolCall();
expect(t.counts()).toEqual({ reasoningSteps: 1, commentaryNotes: 0, toolCalls: 1 });
});
it("counts a trailing open burst at the summary flush (counts())", () => {
const t = createTelegramProgressSummaryTracker();
t.noteReasoningActivity();
t.noteToolCall();
t.noteReasoningActivity(); // trailing burst, no end event
expect(t.counts()).toEqual({ reasoningSteps: 2, commentaryNotes: 0, toolCalls: 1 });
});
it("closes a burst on an explicit reasoning-end event", () => {
const t = createTelegramProgressSummaryTracker();
t.noteReasoningActivity();
t.closeReasoningBurst();
t.closeReasoningBurst(); // idempotent, no double count
expect(t.counts()).toEqual({ reasoningSteps: 1, commentaryNotes: 0, toolCalls: 0 });
});
it("keeps one burst open across re-fires of the same note id", () => {
const t = createTelegramProgressSummaryTracker();
t.noteCommentary("a", "first");
t.noteCommentary("a", "first (delta)");
t.noteCommentary("b", "second");
expect(t.counts()).toEqual({ reasoningSteps: 0, commentaryNotes: 2, toolCalls: 0 });
});
it("counts same-id commentary notes separated by tool boundaries as N, not 1 (D3)", () => {
const t = createTelegramProgressSummaryTracker();
// The anthropic core re-uses the turn-local id "commentary-0" for EVERY note
// in a turn; a tool follows each note, so each note's burst closes at its tool
// before the next opens. An id-Set dedup collapsed these to 1 — the D3 bug.
t.noteCommentary("commentary-0", "about to run date");
t.noteToolCall();
t.noteCommentary("commentary-0", "about to list files");
t.noteToolCall();
t.noteCommentary("commentary-0", "about to run uptime");
t.noteToolCall();
expect(t.counts()).toEqual({ reasoningSteps: 0, commentaryNotes: 3, toolCalls: 3 });
});
it("a tool call closes an open commentary burst (counts it once)", () => {
const t = createTelegramProgressSummaryTracker();
t.noteCommentary("commentary-0", "narration");
t.noteToolCall();
expect(t.counts()).toEqual({ reasoningSteps: 0, commentaryNotes: 1, toolCalls: 1 });
});
it("dedupes id-less commentary by repeated text but counts new text", () => {
const t = createTelegramProgressSummaryTracker();
t.noteCommentary(undefined, "note");
t.noteCommentary(undefined, "note");
t.noteCommentary(undefined, "another");
expect(t.counts()).toEqual({ reasoningSteps: 0, commentaryNotes: 2, toolCalls: 0 });
});
it("ignores empty/whitespace id-less commentary", () => {
const t = createTelegramProgressSummaryTracker();
t.noteCommentary(undefined, " ");
t.noteCommentary();
expect(t.hasActivity()).toBe(false);
expect(t.counts()).toEqual({ reasoningSteps: 0, commentaryNotes: 0, toolCalls: 0 });
});
it("hasActivity reflects an open burst before it is counted", () => {
const t = createTelegramProgressSummaryTracker();
expect(t.hasActivity()).toBe(false);
t.noteReasoningActivity();
expect(t.hasActivity()).toBe(true);
});
it("produces a faithful end-to-end deepseek-style summary (2 bursts closed by tools + trailing)", () => {
const t = createTelegramProgressSummaryTracker();
// burst 1 → tool → burst 2 → tool → trailing burst flushed at summary
t.noteReasoningActivity();
t.noteToolCall();
t.noteReasoningActivity();
t.noteToolCall();
t.noteReasoningActivity();
const line = formatTelegramProgressSummaryLine(t.counts(), 21_000);
expect(line).toBe("🧠 3 thoughts · 🛠️ 2 tool calls · ⏱️ 21s");
});
});