Files
whisper_web/tests/hint.test.ts
T

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-WS2a 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-WS2a): 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));
});
});