Files
llm-wiki/packages/graph-engine/test/helpers.test.ts
T
2026-07-12 21:26:08 +08:00

288 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
splitLabelGraphemes,
labelCharWidth,
measureLabelWidth,
truncateLabel,
cardDims,
createSafeStorage
} from "../src/model";
const LABEL_CJK_WIDTH = 15;
const LABEL_LATIN_WIDTH = 8.5;
const LABEL_MIN_WIDTH = 72;
const LABEL_MAX_WIDTH = 180;
const LABEL_ELLIPSIS = "…";
async function loadHelpersWithoutSegmenter() {
const originalIntl = globalThis.Intl;
Object.defineProperty(globalThis, "Intl", { value: {}, configurable: true });
try {
return await import(`../src/model/legacy-helpers.ts?fallback=${Date.now()}`);
} finally {
Object.defineProperty(globalThis, "Intl", { value: originalIntl, configurable: true });
}
}
// --- splitLabelGraphemes ---
describe("splitLabelGraphemes", () => {
it("splits empty string", () => {
assert.deepEqual(splitLabelGraphemes(""), []);
});
it("splits ASCII", () => {
assert.deepEqual(splitLabelGraphemes("abc"), ["a", "b", "c"]);
});
it("splits CJK characters", () => {
assert.deepEqual(splitLabelGraphemes("中文"), ["中", "文"]);
});
it("does not corrupt emoji with surrogate pairs", () => {
const result = splitLabelGraphemes("a👨‍👩‍👧‍👦b");
assert.ok(result.includes("👨‍👩‍👧‍👦"), "family emoji kept as single grapheme");
assert.equal(result[0], "a");
assert.equal(result[result.length - 1], "b");
});
it("does not corrupt surrogate pairs", () => {
const result = splitLabelGraphemes("𠮷");
// Whether it's 1 grapheme (with Intl.Segmenter) or split into code points,
// the result must not contain unmatched surrogate halves
assert.ok(!/[\uD800-\uDBFF](?![\uDC00-\uDFFF])/.test(result.join("")));
});
it("falls back when Intl.Segmenter is unavailable", async () => {
const fallbackHelpers = await loadHelpersWithoutSegmenter();
assert.deepEqual(Array.from(fallbackHelpers.splitLabelGraphemes("abc")), ["a", "b", "c"]);
assert.deepEqual(Array.from(fallbackHelpers.splitLabelGraphemes("中文")), ["中", "文"]);
assert.deepEqual(Array.from(fallbackHelpers.splitLabelGraphemes("👨‍👩‍👧‍👦")), ["👨‍👩‍👧‍👦"]);
const truncated = fallbackHelpers.truncateLabel("节点A👨‍👩‍👧‍👦AlphaBeta超长标签" + "超".repeat(20), 120);
assert.equal(truncated.truncated, true);
assert.ok(truncated.text.endsWith(LABEL_ELLIPSIS));
assert.ok(
!/\uD800(?![\uDC00-\uDFFF])|(?:^|[^\uD800-\uDBFF])[\uDC00-\uDFFF]/.test(truncated.text),
"fallback path keeps surrogate pairs intact"
);
const emojiBoundary = fallbackHelpers.truncateLabel("节点A👨‍👩‍👧‍👦AlphaBeta超长标签" + "超".repeat(20), 90);
assert.ok(emojiBoundary.text.endsWith(LABEL_ELLIPSIS));
assert.ok(
!emojiBoundary.text.startsWith("") && !emojiBoundary.text.includes("" + LABEL_ELLIPSIS),
"fallback path should not cut through a ZWJ sequence"
);
assert.ok(
!emojiBoundary.text.includes("👨‍") || emojiBoundary.text.includes("👨‍👩‍👧‍👦"),
"fallback path should keep the family emoji intact if it is included"
);
});
});
// --- labelCharWidth ---
describe("labelCharWidth", () => {
it("returns CJK width for CJK character", () => {
assert.equal(labelCharWidth("中"), LABEL_CJK_WIDTH);
});
it("returns Latin width for Latin character", () => {
assert.equal(labelCharWidth("a"), LABEL_LATIN_WIDTH);
});
it("returns Latin width for digit", () => {
assert.equal(labelCharWidth("5"), LABEL_LATIN_WIDTH);
});
it("returns Latin width for punctuation", () => {
assert.equal(labelCharWidth("-"), LABEL_LATIN_WIDTH);
});
});
// --- measureLabelWidth ---
describe("measureLabelWidth", () => {
it("returns 0 for empty array", () => {
assert.equal(measureLabelWidth([]), 0);
});
it("returns correct width for single grapheme", () => {
assert.equal(measureLabelWidth(["a"]), LABEL_LATIN_WIDTH);
});
it("sums mixed CJK and Latin widths", () => {
const width = measureLabelWidth(["中", "a", "文"]);
assert.equal(width, LABEL_CJK_WIDTH * 2 + LABEL_LATIN_WIDTH);
});
});
// --- truncateLabel ---
describe("truncateLabel", () => {
it("handles empty string", () => {
const r = truncateLabel("", 100);
assert.equal(r.text, "");
assert.equal(r.truncated, false);
});
it("handles null", () => {
const r = truncateLabel(null, 100);
assert.equal(r.text, "");
assert.equal(r.truncated, false);
});
it("handles undefined", () => {
const r = truncateLabel(undefined, 100);
assert.equal(r.text, "");
assert.equal(r.truncated, false);
});
it("does not truncate short label", () => {
const r = truncateLabel("短标签", 120);
assert.equal(r.truncated, false);
assert.equal(r.text, "短标签");
});
it("truncates long label with ellipsis", () => {
const longLabel = "超".repeat(30);
const r = truncateLabel(longLabel, 100);
assert.equal(r.truncated, true);
assert.ok(r.text.endsWith(LABEL_ELLIPSIS));
});
it("does not corrupt emoji when truncating", () => {
const r = truncateLabel("节点A👨‍👩‍👧‍👦AlphaBeta超长标签" + "超".repeat(20), 120);
assert.equal(r.truncated, true);
assert.ok(!r.text.includes("undefined"));
assert.ok(
!/\uD800(?![\uDC00-\uDFFF])|(?:^|[^\uD800-\uDBFF])[\uDC00-\uDFFF]/.test(r.text),
"no unmatched surrogate halves"
);
});
it("handles CJK + Latin mix", () => {
const r = truncateLabel("中文English混合标签" + "超".repeat(20), 120);
assert.equal(r.truncated, true);
assert.ok(r.text.endsWith(LABEL_ELLIPSIS));
});
it("respects maxWidth exactly at boundary", () => {
const label = "a".repeat(10);
const r = truncateLabel(label, 1000);
assert.equal(r.truncated, false);
assert.equal(r.text, label);
});
});
// --- cardDims ---
describe("cardDims", () => {
it("returns dimensions for short label", () => {
const r = cardDims({ id: "1", label: "短", type: "entity" });
assert.ok(r.w >= LABEL_MIN_WIDTH);
assert.ok(r.w <= LABEL_MAX_WIDTH);
assert.equal(r.h, 36);
});
it("caps width at LABEL_MAX_WIDTH for long label", () => {
const r = cardDims({ id: "1", label: "超".repeat(30), type: "entity" });
assert.equal(r.w, LABEL_MAX_WIDTH);
});
it("enforces LABEL_MIN_WIDTH for empty label", () => {
const r = cardDims({ id: "1", label: "", type: "entity" });
assert.equal(r.w, LABEL_MIN_WIDTH);
});
it("taller for topic type", () => {
const r = cardDims({ id: "1", label: "T", type: "topic" });
assert.equal(r.h, 40);
});
it("shorter for source type", () => {
const r = cardDims({ id: "1", label: "S", type: "source" });
assert.equal(r.h, 32);
});
it("returns dimensions for generic entity", () => {
const r = cardDims({ id: "1", label: "X", type: "entity" });
assert.ok(r.w > 0);
assert.ok(r.h > 0);
});
});
// --- module export ---
describe("module export", () => {
it("exports helpers from the model entry", async () => {
const model = await import("../src/model");
assert.equal(typeof model.truncateLabel, "function");
assert.equal(typeof model.cardDims, "function");
assert.equal(typeof model.createSafeStorage, "function");
});
it("keeps legacy renderer exports available from the package entry", async () => {
const entry = await import("../src");
const render = await import("../src/render");
assert.equal(typeof entry.createGraphRenderer, "function");
assert.equal(entry.createStaticGraphRenderer, entry.createGraphRenderer);
assert.equal(typeof entry.createStaticGraphRenderer, "function");
assert.equal(typeof render.createGraphRenderer, "function");
assert.equal(render.createStaticGraphRenderer, render.createGraphRenderer);
assert.equal(typeof render.createStaticGraphRenderer, "function");
});
});
// --- createSafeStorage ---
describe("createSafeStorage", () => {
it("gets and sets normally", () => {
const store = {};
const storage = createSafeStorage({
getItem: (k) => store[k],
setItem: (k, v) => { store[k] = v; }
});
storage.set("k", "v");
assert.equal(storage.get("k"), "v");
});
it("returns null when get throws", () => {
const logs = [];
const storage = createSafeStorage({
getItem: () => { throw new Error("boom"); },
setItem: () => {}
}, (...args) => logs.push(args));
assert.equal(storage.get("k"), null);
assert.equal(logs.length, 1);
});
it("swallows set errors", () => {
const logs = [];
const storage = createSafeStorage({
getItem: () => null,
setItem: () => { throw new Error("boom"); }
}, (...args) => logs.push(args));
storage.set("k", "v");
assert.equal(logs.length, 1);
});
it("handles null logger", () => {
const storage = createSafeStorage({
getItem: () => { throw new Error("boom"); },
setItem: () => { throw new Error("boom"); }
}, null);
assert.equal(storage.get("k"), null);
storage.set("k", "v");
});
it("handles null storage", () => {
const storage = createSafeStorage(null, null);
assert.equal(storage.get("k"), null);
storage.set("k", "v");
});
});