Add splitForDiscord() in src/discord/split.ts. Fences are properly closed and reopened across chunk boundaries. Router uses splitForDiscord instead of internal splitMessage(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
104 lines
4.1 KiB
TypeScript
104 lines
4.1 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/);
|
|
});
|
|
});
|