110 lines
3.7 KiB
TypeScript
110 lines
3.7 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 with Whisper's leading space", () => {
|
|
expect(fitHintToBudget("Maxwell\nVoices of the Void", charEncode)).toEqual(
|
|
charEncode(" Maxwell, Voices of the Void"),
|
|
);
|
|
});
|
|
|
|
test("over budget: keeps whole leading words and drops a dangling comma", () => {
|
|
expect(charDecode(fitHintToBudget("alpha, beta gamma", charEncode, 10))).toBe(" alpha");
|
|
});
|
|
|
|
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 223-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(223);
|
|
expect(ids.length).toBeLessThanOrEqual(223);
|
|
expect(ids.length).toBeGreaterThan(200);
|
|
const kept = charDecode(ids);
|
|
expect(` ${hint}`.startsWith(kept)).toBe(true);
|
|
// Ends on a whole term: the next character of the hint is a separator.
|
|
expect(kept.endsWith(",")).toBe(false);
|
|
expect([",", " "]).toContain(` ${hint}`.charAt(kept.length));
|
|
});
|
|
|
|
test("is deterministic", () => {
|
|
const hint = "Maxwell, ".repeat(200);
|
|
expect(fitHintToBudget(hint, charEncode)).toEqual(fitHintToBudget(hint, charEncode));
|
|
});
|
|
});
|