Files
gitmost/apps/client/src/features/editor/components/slash-menu/menu-items.suggestions.test.ts
claude_code 3d4ad664b3 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>
2026-06-21 19:10:27 +03:00

85 lines
3.5 KiB
TypeScript

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");
});
});