import { test } from "node:test"; import assert from "node:assert/strict"; import { SEARCH_IN_DEFAULT, applySearchIn, canonicalHash, isGroup, isLeaf, newGroup, newLeaf, searchInReadsNothing, searchInUnderTags, stringifyRoot, type GroupNode, type LeafNode, type SearchIn, } from "./searchQuery"; // The "Search in" rewrite (release 16, slice CK): what a "transcripts" leaf // reads is the row's business, a leaf asked for by name is not. const T_ONLY: SearchIn = { transcripts: true, posts: false, chat: false }; function tree(...children: GroupNode["children"]): GroupNode { return newGroup({ id: "root", children }); } test("the default row with no posts corpus changes nothing — the very same root", () => { const root = tree(newLeaf({ id: "a", query: "alpha" })); const r = applySearchIn(root, T_ONLY); assert.equal(r.root, root); assert.equal(r.origin.size, 0); assert.equal(r.unionOf.size, 0); }); test("the ruling's default is Transcripts and Posts on, Live chat off", () => { assert.deepEqual(SEARCH_IN_DEFAULT, { transcripts: true, posts: true, chat: false, }); }); test("Transcripts + Live chat: the leaf becomes OR(transcripts, chat) under derived ids", () => { const root = tree(newLeaf({ id: "a", query: "alpha" })); const r = applySearchIn(root, { transcripts: true, posts: false, chat: true }); const [or] = r.root.children; assert.ok(isGroup(or)); assert.equal(or.id, "a~in"); assert.equal(or.op, "OR"); assert.equal(or.negate, false); assert.deepEqual( or.children.map((c) => [c.id, (c as LeafNode).scope, (c as LeafNode).query]), [ ["a~transcripts", "transcripts", "alpha"], ["a~chat", "chat", "alpha"], ], ); assert.deepEqual( [...r.origin], [ ["a~transcripts", "a"], ["a~chat", "a"], ], ); assert.deepEqual([...r.unionOf], [["a", "a~in"]]); // The root keeps its id; nothing is mutated. assert.equal(r.root.id, "root"); assert.equal(root.children[0].id, "a"); }); test("Live chat only: the same leaf, scope chat, same id — nothing to fold", () => { const root = tree(newLeaf({ id: "a", query: "alpha", useRegex: true })); const r = applySearchIn(root, { transcripts: false, posts: false, chat: true }); const [leaf] = r.root.children; assert.ok(isLeaf(leaf)); assert.equal(leaf.id, "a"); assert.equal(leaf.scope, "chat"); assert.equal(leaf.useRegex, true); assert.equal(r.origin.size, 0); assert.equal(r.unionOf.size, 0); }); test("Posts only: the same leaf, scope posts", () => { const root = tree(newLeaf({ id: "a", query: "alpha" })); const r = applySearchIn(root, { transcripts: false, posts: true, chat: false }); const [leaf] = r.root.children; assert.ok(isLeaf(leaf)); assert.equal(leaf.scope, "posts"); assert.equal(leaf.id, "a"); }); test("all three: one copy per kind, in a fixed order", () => { const root = tree(newLeaf({ id: "a", query: "alpha" })); const r = applySearchIn(root, { transcripts: true, posts: true, chat: true }); const [or] = r.root.children; assert.ok(isGroup(or)); assert.deepEqual( or.children.map((c) => (c as LeafNode).scope), ["transcripts", "posts", "chat"], ); assert.equal(r.origin.size, 3); }); test("nothing ticked: the tree is left as it is (it reads its transcripts)", () => { const root = tree(newLeaf({ id: "a", query: "alpha" })); const nothing = { transcripts: false, posts: false, chat: false }; assert.ok(searchInReadsNothing(nothing)); assert.ok(!searchInReadsNothing(T_ONLY)); assert.ok(!searchInReadsNothing({ transcripts: false, posts: false, chat: true })); assert.equal(applySearchIn(root, nothing).root, root); }); test("a leaf asked for by name, and an empty leaf, are not the row's business", () => { const byName = ["chat", "posts", "metadata", "description", "tags"] as const; const root = tree( ...byName.map((scope) => newLeaf({ id: scope, query: "alpha", scope })), newLeaf({ id: "empty", query: " " }), ); const r = applySearchIn(root, { transcripts: false, posts: true, chat: true }); assert.equal(r.root, root, "no leaf changed, so the root is the same object"); }); test("a negated leaf reads NOT of the union: the OR sits inside a negated AND", () => { const root = tree( newLeaf({ id: "a", query: "alpha", negate: true, contributeHits: false }), ); const r = applySearchIn(root, { transcripts: true, posts: false, chat: true }); const [not] = r.root.children; assert.ok(isGroup(not)); assert.equal(not.id, "a~not"); assert.equal(not.op, "AND"); assert.equal(not.negate, true); const [or] = not.children; assert.ok(isGroup(or)); assert.equal(or.id, "a~in"); assert.equal(or.negate, false, "the union itself is never negated"); for (const c of or.children) { assert.ok(isLeaf(c)); assert.equal(c.negate, false); assert.equal(c.contributeHits, false, "the copy keeps contributeHits"); } assert.deepEqual([...r.unionOf], [["a", "a~in"]]); }); test("nested: only the transcripts leaf is rewritten; untouched subtrees keep identity", () => { const untouched = newGroup({ id: "g2", op: "OR", children: [newLeaf({ id: "m", query: "title", scope: "metadata" })], }); const inner = newGroup({ id: "g1", op: "AND", children: [newLeaf({ id: "t", query: "beta" })], }); const root = tree(untouched, inner); const before = stringifyRoot(root); const hash = canonicalHash(root); const r = applySearchIn(root, { transcripts: true, posts: true, chat: false }); assert.equal(r.root.children[0], untouched); const g1 = r.root.children[1]; assert.ok(isGroup(g1)); assert.equal(g1.id, "g1"); assert.equal(g1.children[0].id, "t~in"); // The visitor's tree — its URL form and its hash — is untouched. assert.equal(stringifyRoot(root), before); assert.equal(canonicalHash(root), hash); }); test("two transcripts leaves each get their own copies", () => { const root = tree( newLeaf({ id: "a", query: "alpha" }), newLeaf({ id: "b", query: "beta" }), ); const r = applySearchIn(root, { transcripts: true, posts: true, chat: false }); assert.deepEqual( r.root.children.map((c) => c.id), ["a~in", "b~in"], ); assert.deepEqual( [...r.origin.values()].sort(), ["a", "a", "b", "b"], ); }); test("under a tag filter the posts copy is left out, unless posts are all the row reads", () => { const all = { transcripts: true, posts: true, chat: true }; assert.equal(searchInUnderTags(all, false), all, "no tag filter: unchanged"); assert.deepEqual(searchInUnderTags(all, true), { ...all, posts: false }); assert.deepEqual( searchInUnderTags({ transcripts: false, posts: true, chat: true }, true), { transcripts: false, posts: false, chat: true }, ); // Posts alone stay: the copy reads an empty scope (no results), rather than // the row reading nothing and the leaf falling back to its transcripts. const postsOnly = { transcripts: false, posts: true, chat: false }; assert.equal(searchInUnderTags(postsOnly, true), postsOnly); const noPosts = { transcripts: true, posts: false, chat: false }; assert.equal(searchInUnderTags(noPosts, true), noPosts); });