disclaw/tests/unit/split-for-discord.test.ts
Nick Tabeling 62ea507974
Some checks failed
CI / build-and-test (ubuntu-latest) (pull_request) Has been cancelled
CI / build-and-test (windows-latest) (pull_request) Has been cancelled
CI / lint (pull_request) Has been cancelled
fix(DIS-105): prevent fence closure from pushing chunks beyond 2000-char hard limit
The flush() helper now enforces the Discord hard limit by hard-splitting
any chunk that exceeds 2000 chars (e.g. when "\n```" was appended after
the soft-limit check). Also reserves 4 chars of overhead in the
soft-limit check when inside a fenced code block.

Adds a regression test that verifies no chunk exceeds 2000 chars even
when the fence closure is appended at the limit boundary.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 10:13:58 +02:00

120 lines
4.7 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { splitForDiscord } from "../../src/discord/split";
const DISCORD_HARD_LIMIT = 2000;
describe("splitForDiscord", () => {
// (a) Short message below limit → returned as single unchanged chunk
it("returns a single chunk for short messages", () => {
const text = "Hello, world!";
const result = splitForDiscord(text);
expect(result).toHaveLength(1);
expect(result[0]).toBe(text);
});
// (b) Text exactly at the limit → single chunk
it("returns a single chunk when text is exactly at the limit", () => {
const text = "a".repeat(1900);
const result = splitForDiscord(text, 1900);
expect(result).toHaveLength(1);
expect(result[0]).toBe(text);
});
// (c) Long plain text → split at line boundaries
it("splits long plain text at line boundaries", () => {
const line = "x".repeat(100);
// 25 lines = 25*100 + 24 newlines = 2524 chars → must split with limit 1900
const text = Array.from({ length: 25 }, () => line).join("\n");
const result = splitForDiscord(text, 1900);
expect(result.length).toBeGreaterThan(1);
// Reassembled text must equal original
expect(result.join("\n")).toBe(text);
});
// (d) Code block spanning a split → fence is closed and reopened with lang tag
it("closes and reopens fenced code blocks across chunk boundaries", () => {
const preamble = "Here is some code:\n";
const fenceOpen = "```typescript\n";
// Fill enough code lines to force a split inside the fence
const codeLine = "const x = 1; // " + "y".repeat(80) + "\n";
const codeLines = Array.from({ length: 20 }, () => codeLine).join("");
const fenceClose = "```\n";
const epilogue = "End of message.";
const text = preamble + fenceOpen + codeLines + fenceClose + epilogue;
const result = splitForDiscord(text, 1900);
expect(result.length).toBeGreaterThan(1);
// Every chunk that starts mid-fence must open with ```typescript
// Every chunk that ends mid-fence must close with ```
for (let i = 0; i < result.length - 1; i++) {
const chunk = result[i];
// Count fence toggles to determine if we end inside a fence
const fenceToggles = (chunk.match(/^```/gm) ?? []).length;
if (fenceToggles % 2 === 1) {
// Odd number means we opened but didn't close — the splitter must close it
expect(chunk.trimEnd()).toMatch(/```$/);
// And the next chunk must reopen with the same language
expect(result[i + 1]).toMatch(/^```typescript/);
}
}
});
// (e) No chunk exceeds 2000 characters
it("never produces a chunk exceeding 2000 characters", () => {
const line = "z".repeat(150);
const text = Array.from({ length: 50 }, () => line).join("\n");
const result = splitForDiscord(text, 1900);
for (const chunk of result) {
expect(chunk.length).toBeLessThanOrEqual(DISCORD_HARD_LIMIT);
}
});
// (f) Empty input → no crash, returns consistent value
it("handles empty input without crashing", () => {
expect(() => splitForDiscord("")).not.toThrow();
const result = splitForDiscord("");
expect(Array.isArray(result)).toBe(true);
});
// Additional: fence language tag is preserved correctly
it("preserves the fence language tag when reopening after a split", () => {
const limit = 200;
const header = "Intro\n";
const fenceOpen = "```python\n";
// Enough lines to exceed 200 chars inside the fence
const codeLine = "print('hello')\n";
const codeLines = Array.from({ length: 20 }, () => codeLine).join("");
const fenceClose = "```";
const text = header + fenceOpen + codeLines + fenceClose;
const result = splitForDiscord(text, limit);
// Find the first chunk that ends with ``` (closed fence)
const closingChunk = result.find(
(c, i) => i < result.length - 1 && c.trimEnd().endsWith("```")
);
expect(closingChunk).toBeDefined();
// The chunk following a mid-fence close should start with ```python
const idx = result.indexOf(closingChunk!);
expect(result[idx + 1]).toMatch(/^```python/);
});
// Regression: fence closure "\n```" must not push a chunk beyond the 2000 hard limit
it("does not exceed 2000 chars even when fence closure is appended at limit boundary", () => {
const fenceOpen = "```typescript\n";
// Line long enough that current is near 2000 when fence closure is added
const longLine = "x".repeat(1984);
const nextLine = "y".repeat(10);
const fenceClose = "\n```";
const text = fenceOpen + longLine + "\n" + nextLine + fenceClose;
const result = splitForDiscord(text, 2000);
for (const chunk of result) {
expect(chunk.length).toBeLessThanOrEqual(DISCORD_HARD_LIMIT);
}
});
});