Additive test coverage across server, editor-ext, client and mcp. #192 — AiChatService.stream integration (Section 3, against real Postgres): - new apps/server/test/integration/ai-chat-stream.int-spec.ts drives the real streamText through a seeded ai/test MockLanguageModelV3 and a real Node ServerResponse, covering: onError persists an assistant error record (status 'error' + partial answer + provider cause in metadata); external MCP client closed exactly once on BOTH onFinish and onError; anti-tamper — history is rebuilt from the DB transcript, not from body.messages. #206 — red-team findings (most already fixed+tested in #212): - mdrt-2 (UNFIXED, data loss): turndown.dataloss.test.ts documents that pageBreak / transclusionReference / mention are silently dropped on Markdown export (characterization + it.fails for the desired survive-export contract). - persist-6 (UNFIXED, data loss): persistence-store.spec.ts adds an it.failing documenting that a momentarily-empty live doc overwrites non-empty content (left unfixed — a store-side empty-guard is a behaviour change). #204 — test-strategy plan, highest-priority subset: - Phase 1: mcp-clients.lease.spec.ts covers the external MCP client lease/refcount/eviction lifecycle (leak / premature-close / double-close). - Phase 2 data-integrity pure functions: editor-ext table-utils (transpose/moveRow/convert round-trip) and math tokenizer false-positive guard; client emoji-menu (+ it.fails for the unguarded localStorage JSON.parse bug), sort-cells, normalizeTableColumnWidths; mcp htmlEmbed/ pageBreak markdown data-loss + footnote-diff; server export getInternalLinkPageName extensionless-path bug — FIXED (small/clear) + tested. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
101 lines
3.2 KiB
TypeScript
101 lines
3.2 KiB
TypeScript
import { describe, it, expect, beforeEach } from "vitest";
|
|
import {
|
|
sortFrequentlyUsedEmoji,
|
|
getFrequentlyUsedEmoji,
|
|
LOCAL_STORAGE_FREQUENT_KEY,
|
|
} from "./utils";
|
|
|
|
describe("sortFrequentlyUsedEmoji", () => {
|
|
it("orders known emoji by descending usage count", async () => {
|
|
const result = await sortFrequentlyUsedEmoji({
|
|
rocket: 1,
|
|
joy: 9,
|
|
heart_eyes: 5,
|
|
});
|
|
expect(result.map((e) => e.id)).toEqual(["joy", "heart_eyes", "rocket"]);
|
|
});
|
|
|
|
it("caps the result at the top 5 most frequent", async () => {
|
|
const result = await sortFrequentlyUsedEmoji({
|
|
rocket: 1,
|
|
joy: 2,
|
|
heart_eyes: 3,
|
|
grinning: 4,
|
|
laughing: 5,
|
|
scream: 6,
|
|
sweat_smile: 7,
|
|
});
|
|
expect(result).toHaveLength(5);
|
|
// Highest counts retained, lowest (rocket:1, joy:2) dropped.
|
|
expect(result.map((e) => e.id)).toEqual([
|
|
"sweat_smile",
|
|
"scream",
|
|
"laughing",
|
|
"grinning",
|
|
"heart_eyes",
|
|
]);
|
|
});
|
|
|
|
it("drops ids that have no matching emoji in the index", async () => {
|
|
const result = await sortFrequentlyUsedEmoji({
|
|
__definitely_not_a_real_emoji_id__: 100,
|
|
rocket: 1,
|
|
});
|
|
expect(result.map((e) => e.id)).toEqual(["rocket"]);
|
|
});
|
|
|
|
it("maps each entry to its native glyph and a command", async () => {
|
|
const [entry] = await sortFrequentlyUsedEmoji({ rocket: 5 });
|
|
expect(entry.id).toBe("rocket");
|
|
expect(typeof entry.emoji).toBe("string");
|
|
expect(entry.emoji.length).toBeGreaterThan(0);
|
|
expect(typeof entry.command).toBe("function");
|
|
});
|
|
|
|
it("returns an empty list for empty input", async () => {
|
|
expect(await sortFrequentlyUsedEmoji({})).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("getFrequentlyUsedEmoji", () => {
|
|
beforeEach(() => {
|
|
localStorage.clear();
|
|
});
|
|
|
|
it("falls back to the default map when nothing is stored", () => {
|
|
const result = getFrequentlyUsedEmoji();
|
|
expect(result["+1"]).toBe(10);
|
|
expect(result["rocket"]).toBe(1);
|
|
});
|
|
|
|
it("parses a valid stored JSON map", () => {
|
|
localStorage.setItem(
|
|
LOCAL_STORAGE_FREQUENT_KEY,
|
|
JSON.stringify({ rocket: 42 }),
|
|
);
|
|
expect(getFrequentlyUsedEmoji()).toEqual({ rocket: 42 });
|
|
});
|
|
|
|
// BUG (issue #204, Phase 2): getFrequentlyUsedEmoji() does an unprotected
|
|
// JSON.parse() of the raw localStorage value. A corrupt value (e.g. truncated
|
|
// by a crash, or written by another tab/extension) makes the emoji menu throw
|
|
// on open instead of degrading gracefully to the default set.
|
|
//
|
|
// Documented with it.fails: this asserts the DESIRED behavior (return a sane
|
|
// default, never throw). It currently FAILS because the function throws —
|
|
// flip to `it()` once utils.ts guards the JSON.parse.
|
|
it.fails(
|
|
"should degrade to a sane default on corrupt localStorage (currently throws)",
|
|
() => {
|
|
localStorage.setItem(LOCAL_STORAGE_FREQUENT_KEY, "{not valid json");
|
|
let result: Record<string, number> | undefined;
|
|
expect(() => {
|
|
result = getFrequentlyUsedEmoji();
|
|
}).not.toThrow();
|
|
// Should hand back a usable, non-empty map rather than nothing.
|
|
expect(result).toBeTruthy();
|
|
expect(Object.keys(result ?? {}).length).toBeGreaterThan(0);
|
|
},
|
|
);
|
|
});
|