119 lines
4.3 KiB
TypeScript
119 lines
4.3 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import {
|
|
HINT_STORAGE_KEY,
|
|
PROMPT_TOKEN_BUDGET,
|
|
fitHintToBudget,
|
|
loadHint,
|
|
normalizeHint,
|
|
saveHint,
|
|
} from "../src/lib/hint";
|
|
|
|
/** One "token" per UTF-16 unit: deterministic, monotone, easy to reason about. */
|
|
const charEncode = (text: string) => [...text].map((c) => c.charCodeAt(0));
|
|
const charDecode = (ids: number[]) => String.fromCharCode(...ids);
|
|
|
|
function mapStorage(initial: Record<string, string> = {}) {
|
|
const map = new Map(Object.entries(initial));
|
|
return {
|
|
getItem: (key: string) => map.get(key) ?? null,
|
|
setItem: (key: string, value: string) => void map.set(key, value),
|
|
map,
|
|
};
|
|
}
|
|
|
|
const throwingStorage = {
|
|
getItem(): string | null {
|
|
throw new Error("denied");
|
|
},
|
|
setItem(): void {
|
|
throw new Error("denied");
|
|
},
|
|
};
|
|
|
|
describe("normalizeHint", () => {
|
|
test("one term per line becomes a comma-separated list", () => {
|
|
expect(normalizeHint(" Maxwell\n\n Voices of the Void \n")).toBe(
|
|
"Maxwell, Voices of the Void",
|
|
);
|
|
});
|
|
|
|
test("a comma-separated list is unchanged", () => {
|
|
expect(normalizeHint("Maxwell, Voices of the Void")).toBe("Maxwell, Voices of the Void");
|
|
});
|
|
|
|
test("control characters are removed and whitespace-only means no hint", () => {
|
|
expect(normalizeHint("Max\u0000well\u0007")).toBe("Maxwell");
|
|
expect(normalizeHint(" \n\t \r\n")).toBe("");
|
|
});
|
|
|
|
test("Windows line endings and tabs are handled", () => {
|
|
expect(normalizeHint("Århus\r\nÆrø\tFestuge")).toBe("Århus, Ærø Festuge");
|
|
});
|
|
});
|
|
|
|
describe("hint storage", () => {
|
|
test("round-trips through storage", () => {
|
|
const storage = mapStorage();
|
|
expect(loadHint(storage)).toBe("");
|
|
saveHint("Maxwell\nVoid", storage);
|
|
expect(storage.map.get(HINT_STORAGE_KEY)).toBe("Maxwell\nVoid");
|
|
expect(loadHint(storage)).toBe("Maxwell\nVoid");
|
|
});
|
|
|
|
test("unavailable storage never throws", () => {
|
|
expect(loadHint(throwingStorage)).toBe("");
|
|
expect(() => saveHint("x", throwingStorage)).not.toThrow();
|
|
expect(loadHint(null)).toBe("");
|
|
});
|
|
});
|
|
|
|
describe("fitHintToBudget (D-048 truncation rule)", () => {
|
|
test("empty hint means no prompt", () => {
|
|
expect(fitHintToBudget(" \n", charEncode)).toEqual([]);
|
|
});
|
|
|
|
test("a hint within budget is encoded as a space-led sentence ending in a full stop", () => {
|
|
// Measured (D-048): an unterminated list prompt suppresses punctuation
|
|
// and triggers repetition loops on smaller models; a full stop does not.
|
|
expect(fitHintToBudget("Maxwell\nVoices of the Void", charEncode)).toEqual(
|
|
charEncode(" Maxwell, Voices of the Void."),
|
|
);
|
|
});
|
|
|
|
test("a hint already ending in . ? or ! gets no extra full stop", () => {
|
|
expect(charDecode(fitHintToBudget("Is it Maxwell?", charEncode))).toBe(" Is it Maxwell?");
|
|
expect(charDecode(fitHintToBudget("Dr. Bao.", charEncode))).toBe(" Dr. Bao.");
|
|
});
|
|
|
|
test("over budget: keeps whole leading words, drops a dangling comma, ends in a full stop", () => {
|
|
expect(charDecode(fitHintToBudget("alpha, beta gamma", charEncode, 10))).toBe(" alpha.");
|
|
expect(charDecode(fitHintToBudget("alpha, beta gamma", charEncode, 13))).toBe(" alpha, beta.");
|
|
});
|
|
|
|
test("a single word longer than the budget is cut at the budget", () => {
|
|
const ids = fitHintToBudget("x".repeat(50), charEncode, 10);
|
|
expect(ids).toEqual(charEncode(" xxxxxxxxx"));
|
|
expect(ids.length).toBe(10);
|
|
});
|
|
|
|
test("a very long hint fits the 120-token budget and is a word prefix of the hint", () => {
|
|
const words = Array.from({ length: 5000 }, (_, i) => `term${i}`);
|
|
const hint = words.join(", ");
|
|
const ids = fitHintToBudget(hint, charEncode);
|
|
expect(PROMPT_TOKEN_BUDGET).toBe(120);
|
|
expect(ids.length).toBeLessThanOrEqual(120);
|
|
expect(ids.length).toBeGreaterThan(100);
|
|
const kept = charDecode(ids);
|
|
// Ends on a whole term plus the full stop: the hint continues with a separator there.
|
|
expect(kept.endsWith(".")).toBe(true);
|
|
const body = kept.slice(0, -1);
|
|
expect(` ${hint}`.startsWith(body)).toBe(true);
|
|
expect([",", " "]).toContain(` ${hint}`.charAt(body.length));
|
|
});
|
|
|
|
test("is deterministic", () => {
|
|
const hint = "Maxwell, ".repeat(200);
|
|
expect(fitHintToBudget(hint, charEncode)).toEqual(fitHintToBudget(hint, charEncode));
|
|
});
|
|
});
|