145 lines
4.9 KiB
TypeScript
145 lines
4.9 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { chunkDiscordText } from "./chunk.js";
|
|
|
|
function countLines(text: string) {
|
|
return text.split("\n").length;
|
|
}
|
|
|
|
function hasBalancedFences(chunk: string) {
|
|
let open: { markerChar: string; markerLen: number } | null = null;
|
|
for (const line of chunk.split("\n")) {
|
|
const match = line.match(/^( {0,3})(`{3,}|~{3,})(.*)$/);
|
|
if (!match) continue;
|
|
const marker = match[2];
|
|
if (!open) {
|
|
open = { markerChar: marker[0], markerLen: marker.length };
|
|
continue;
|
|
}
|
|
if (open.markerChar === marker[0] && marker.length >= open.markerLen) {
|
|
open = null;
|
|
}
|
|
}
|
|
return open === null;
|
|
}
|
|
|
|
describe("chunkDiscordText", () => {
|
|
it("splits tall messages even when under 2000 chars", () => {
|
|
const text = Array.from({ length: 45 }, (_, i) => `line-${i + 1}`).join("\n");
|
|
expect(text.length).toBeLessThan(2000);
|
|
|
|
const chunks = chunkDiscordText(text, { maxChars: 2000, maxLines: 20 });
|
|
expect(chunks.length).toBeGreaterThan(1);
|
|
for (const chunk of chunks) {
|
|
expect(countLines(chunk)).toBeLessThanOrEqual(20);
|
|
}
|
|
});
|
|
|
|
it("keeps fenced code blocks balanced across chunks", () => {
|
|
const body = Array.from({ length: 30 }, (_, i) => `console.log(${i});`).join("\n");
|
|
const text = `Here is code:\n\n\`\`\`js\n${body}\n\`\`\`\n\nDone.`;
|
|
|
|
const chunks = chunkDiscordText(text, { maxChars: 2000, maxLines: 10 });
|
|
expect(chunks.length).toBeGreaterThan(1);
|
|
|
|
for (const chunk of chunks) {
|
|
expect(hasBalancedFences(chunk)).toBe(true);
|
|
expect(chunk.length).toBeLessThanOrEqual(2000);
|
|
}
|
|
|
|
expect(chunks[0]).toContain("```js");
|
|
expect(chunks.at(-1)).toContain("Done.");
|
|
});
|
|
|
|
it("reserves space for closing fences when chunking", () => {
|
|
const body = "a".repeat(120);
|
|
const text = `\`\`\`txt\n${body}\n\`\`\``;
|
|
|
|
const chunks = chunkDiscordText(text, { maxChars: 50, maxLines: 50 });
|
|
expect(chunks.length).toBeGreaterThan(1);
|
|
for (const chunk of chunks) {
|
|
expect(chunk.length).toBeLessThanOrEqual(50);
|
|
expect(hasBalancedFences(chunk)).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("preserves whitespace when splitting long lines", () => {
|
|
const text = Array.from({ length: 40 }, () => "word").join(" ");
|
|
const chunks = chunkDiscordText(text, { maxChars: 20, maxLines: 50 });
|
|
expect(chunks.length).toBeGreaterThan(1);
|
|
expect(chunks.join("")).toBe(text);
|
|
});
|
|
|
|
it("preserves mixed whitespace across chunk boundaries", () => {
|
|
const text = "alpha beta\tgamma delta epsilon zeta";
|
|
const chunks = chunkDiscordText(text, { maxChars: 12, maxLines: 50 });
|
|
expect(chunks.length).toBeGreaterThan(1);
|
|
expect(chunks.join("")).toBe(text);
|
|
});
|
|
|
|
it("keeps leading whitespace when splitting long lines", () => {
|
|
const text = " indented line with words that force splits";
|
|
const chunks = chunkDiscordText(text, { maxChars: 14, maxLines: 50 });
|
|
expect(chunks.length).toBeGreaterThan(1);
|
|
expect(chunks.join("")).toBe(text);
|
|
});
|
|
|
|
it("keeps reasoning italics balanced across chunks", () => {
|
|
const body = Array.from({ length: 25 }, (_, i) => `${i + 1}. line`).join("\n");
|
|
const text = `Reasoning:\n_${body}_`;
|
|
|
|
const chunks = chunkDiscordText(text, { maxLines: 10, maxChars: 2000 });
|
|
expect(chunks.length).toBeGreaterThan(1);
|
|
|
|
for (const chunk of chunks) {
|
|
// Each chunk should have balanced italics markers (even count).
|
|
const count = (chunk.match(/_/g) || []).length;
|
|
expect(count % 2).toBe(0);
|
|
}
|
|
|
|
// Ensure italics reopen on subsequent chunks
|
|
expect(chunks[0]).toContain("_1. line");
|
|
// Second chunk should reopen italics at the start
|
|
expect(chunks[1].trimStart().startsWith("_")).toBe(true);
|
|
});
|
|
|
|
it("keeps reasoning italics balanced when chunks split by char limit", () => {
|
|
const longLine = "This is a very long reasoning line that forces char splits.";
|
|
const body = Array.from({ length: 5 }, () => longLine).join("\n");
|
|
const text = `Reasoning:\n_${body}_`;
|
|
|
|
const chunks = chunkDiscordText(text, { maxChars: 80, maxLines: 50 });
|
|
expect(chunks.length).toBeGreaterThan(1);
|
|
|
|
for (const chunk of chunks) {
|
|
const underscoreCount = (chunk.match(/_/g) || []).length;
|
|
expect(underscoreCount % 2).toBe(0);
|
|
}
|
|
});
|
|
|
|
it("reopens italics while preserving leading whitespace on following chunk", () => {
|
|
const body = [
|
|
"1. line",
|
|
"2. line",
|
|
"3. line",
|
|
"4. line",
|
|
"5. line",
|
|
"6. line",
|
|
"7. line",
|
|
"8. line",
|
|
"9. line",
|
|
"10. line",
|
|
" 11. indented line",
|
|
"12. line",
|
|
].join("\n");
|
|
const text = `Reasoning:\n_${body}_`;
|
|
|
|
const chunks = chunkDiscordText(text, { maxLines: 10, maxChars: 2000 });
|
|
expect(chunks.length).toBeGreaterThan(1);
|
|
|
|
const second = chunks[1];
|
|
expect(second.startsWith("_")).toBe(true);
|
|
expect(second).toContain(" 11. indented line");
|
|
});
|
|
});
|