test(refactor-tail): extract pure cores + cover collab/share/ai-chat/client gate
Batches 6-9: behaviour-preserving extractions of testable pure cores plus the tests they unblock, and a fix for the broken client test environment. Full suites green: server 113 suites / 1117 + 1 todo, client 30 files / 338. client (R0 infra): - vitest.setup.ts: in-memory localStorage/sessionStorage Storage stub wired via setupFiles. Unblocks menu-items.gating.test.ts (was 9 failing) -> client suite fully green. + menu-items.suggestions.test.ts (getSuggestionItems filter/sort). share: - extract buildShareMetaHtml (share-seo.util.ts) from the SEO controller; tests for reflected-XSS escaping in <title>/og/twitter meta, noindex, truncation; extractPageSlugId; updateAttachmentAttr; prepareContentForShare comment-strip (anonymous-viewer metadata-leak guard). ai-chat (security extractions): - selectAccessibleHits: CASL post-filter for semantic search (restricted page in an accessible space must NOT leak to the agent). - validateResolvedAddresses: SSRF connect-time guard (block if ANY resolved address is private). - resolveAudioFormat: mime whitelist (dead `?? 'webm'` fallback dropped, set unchanged). + mcp-servers toView header-leak guard, MCP tool namespacing. collaboration (data-loss area): - extract computeHistoryJob (pins the "agent delay MUST stay 0" invariant) and resolveSource. Integration: onAuthenticate read-only matrix (collab auth bypass), HistoryProcessor (contributor restore on save failure), onStoreDocument Approach-A boundary snapshot (human revision pinned before agent overwrite). Reviewed (APPROVE WITH SUGGESTIONS): extractions behaviour-preserving, security tests mutation-resistant. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { getSuggestionItems } from "./menu-items";
|
||||
|
||||
// Coverage for the filter/sort half of `getSuggestionItems` (distinct from the
|
||||
// HTML-embed gating suite). A slash query is matched against each item three
|
||||
// ways — fuzzy on the title, substring on the description, and substring on the
|
||||
// searchTerms — and matched items are sorted so title-substring hits float to
|
||||
// the top of their group. We also cover `excludeItems`.
|
||||
//
|
||||
// `getSuggestionItems` -> `isHtmlEmbedFeatureEnabled` reads the persisted
|
||||
// `currentUser` localStorage entry, so a working in-memory Storage stub is a
|
||||
// prerequisite (installed by vitest.setup.ts). We persist a `currentUser` with
|
||||
// the HTML-embed toggle OFF (the production default) so the gated "HTML embed"
|
||||
// item never leaks into these non-HTML queries.
|
||||
|
||||
const KEY = "currentUser";
|
||||
|
||||
function flatTitles(groups: ReturnType<typeof getSuggestionItems>): string[] {
|
||||
return Object.values(groups)
|
||||
.flat()
|
||||
.map((item) => item.title);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
// Default workspace state: HTML-embed feature OFF (matches production default).
|
||||
localStorage.setItem(KEY, JSON.stringify({ workspace: { settings: {} } }));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
describe("getSuggestionItems — filter and sort", () => {
|
||||
it("fuzzy-matches a title (non-contiguous characters)", () => {
|
||||
// "tdo" is not a substring of "to-do list" but matches fuzzily (t..d..o).
|
||||
const titles = flatTitles(getSuggestionItems({ query: "tdo" }));
|
||||
expect(titles).toContain("To-do list");
|
||||
});
|
||||
|
||||
it("matches via the description when the title does not match", () => {
|
||||
// "numbering" only appears in the description "Create a list with numbering.",
|
||||
// not in the "Numbered list" title nor its searchTerms.
|
||||
const titles = flatTitles(getSuggestionItems({ query: "numbering" }));
|
||||
expect(titles).toContain("Numbered list");
|
||||
});
|
||||
|
||||
it("matches via searchTerms when title and description do not match", () => {
|
||||
// "blockquote" is only present in the "Quote" item's searchTerms.
|
||||
const titles = flatTitles(getSuggestionItems({ query: "blockquote" }));
|
||||
expect(titles).toContain("Quote");
|
||||
});
|
||||
|
||||
it("sorts title-substring matches before non-title (description) matches", () => {
|
||||
// For "page": several titles contain "page" (e.g. "Page break"), while
|
||||
// "Synced block" matches only through its description (".. across pages.").
|
||||
// The sort tie-break must place every title hit ahead of the non-title hit.
|
||||
const titles = flatTitles(getSuggestionItems({ query: "page" }));
|
||||
|
||||
const syncedIndex = titles.indexOf("Synced block");
|
||||
const pageBreakIndex = titles.indexOf("Page break");
|
||||
|
||||
// Sanity: both items survived the filter for this query.
|
||||
expect(syncedIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(pageBreakIndex).toBeGreaterThanOrEqual(0);
|
||||
|
||||
// The title match ("Page break") sorts before the description-only match.
|
||||
expect(pageBreakIndex).toBeLessThan(syncedIndex);
|
||||
});
|
||||
|
||||
it("removes a named item via excludeItems", () => {
|
||||
const withBullet = flatTitles(getSuggestionItems({ query: "list" }));
|
||||
expect(withBullet).toContain("Bullet list");
|
||||
|
||||
const withoutBullet = flatTitles(
|
||||
getSuggestionItems({
|
||||
query: "list",
|
||||
excludeItems: new Set(["Bullet list"]),
|
||||
}),
|
||||
);
|
||||
expect(withoutBullet).not.toContain("Bullet list");
|
||||
// Other "list" matches remain unaffected by the exclusion.
|
||||
expect(withoutBullet).toContain("Numbered list");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user