1import { describe, expect, it } from "vitest";
2
3import { buildIndexDocs, chunkDocId, dedupeToParent } from "./indexer.js";
4import type { GrantSnapshot, IndexDocInput } from "./types.js";
5
6const grants: GrantSnapshot = { public: false, workspace: true, user: false, teams: ["t1"], users: ["u1"] };
7
8function input(partial: Partial<IndexDocInput> & Pick<IndexDocInput, "docId" | "type" | "id" | "body">): IndexDocInput {
9 return {
10 workspaceId: "ws1",
11 title: "T",
12 grants,
13 archived: false,
14 url: "/ws1/x",
15 createdAt: "2025-01-01T00:00:00Z",
16 updatedAt: "2025-01-02T00:00:00Z",
17 ...partial,
18 };
19}
20
21describe("chunkDocId", () => {
22 it("appends #n to the parent id", () => {
23 expect(chunkDocId("page:pg_1", 0)).toBe("page:pg_1#0");
24 expect(chunkDocId("page:pg_1", 7)).toBe("page:pg_1#7");
25 });
26});
27
28describe("buildIndexDocs", () => {
29 it("indexes a scalar type (record) as a single doc", () => {
30 const docs = buildIndexDocs(input({ docId: "record:r1", type: "record", id: "r1", body: "any length at all..." }));
31 expect(docs).toHaveLength(1);
32 expect(docs[0]!.docId).toBe("record:r1");
33 expect(docs[0]!.parentDocId).toBeUndefined();
34 });
35
36 it("indexes a short page body as the parent doc itself (no #0)", () => {
37 const docs = buildIndexDocs(input({ docId: "page:pg_1", type: "page", id: "pg_1", body: "short body" }));
38 expect(docs).toHaveLength(1);
39 expect(docs[0]!.docId).toBe("page:pg_1");
40 });
41
42 it("expands a long page into chunked docs with parentDocId", () => {
43 const body = Array.from({ length: 30 }, (_, i) => `Paragraph ${i} with some text. ${"word ".repeat(20).trim()}`).join(
44 "\n\n",
45 );
46 const docs = buildIndexDocs(
47 input({ docId: "page:pg_1", type: "page", id: "pg_1", body, vector: [0.1, 0.2] }),
48 { maxChars: 300, overlapChars: 0 },
49 );
50 expect(docs.length).toBeGreaterThan(1);
51 for (const [i, doc] of docs.entries()) {
52 expect(doc.docId).toBe(`page:pg_1#${i}`);
53 expect(doc.parentDocId).toBe("page:pg_1");
54 expect(doc.chunkIndex).toBe(i);
55 expect(doc.grants).toBe(grants);
56 expect(doc.body.length).toBeLessThanOrEqual(300);
57 }
58
59 expect(docs[0]!.vector).toEqual([0.1, 0.2]);
60 expect(docs[1]!.vector).toBeUndefined();
61 });
62
63 it("keeps grants/workspace/url on every chunk", () => {
64 const body = Array.from({ length: 30 }, (_, i) => `Para ${i} ${"filler ".repeat(15).trim()}`).join("\n\n");
65 const docs = buildIndexDocs(input({ docId: "page:pg_9", type: "page", id: "pg_9", body }), {
66 maxChars: 200,
67 overlapChars: 0,
68 });
69 for (const doc of docs) {
70 expect(doc.workspaceId).toBe("ws1");
71 expect(doc.url).toBe("/ws1/x");
72 expect(doc.grants.workspace).toBe(true);
73 }
74 });
75});
76
77describe("dedupeToParent", () => {
78 it("keeps the better-scoring of chunk vs parent and records the matched chunk", () => {
79 const out = dedupeToParent([
80 { docId: "page:pg_1#2", parentDocId: "page:pg_1", score: 5 },
81 { docId: "page:pg_1", score: 3 },
82 { docId: "page:pg_2#0", parentDocId: "page:pg_2", score: 9 },
83 ]);
84 expect(out).toHaveLength(2);
85 expect(out[0]).toEqual({ docId: "page:pg_2", score: 9, matchedChunk: "page:pg_2#0" });
86 expect(out[1]).toEqual({ docId: "page:pg_1", score: 5, matchedChunk: "page:pg_1#2" });
87 });
88
89 it("keeps the parent when it outranks its own chunks", () => {
90 const out = dedupeToParent([
91 { docId: "page:pg_1#0", parentDocId: "page:pg_1", score: 2 },
92 { docId: "page:pg_1", score: 7 },
93 ]);
94 expect(out).toHaveLength(1);
95 expect(out[0]).toEqual({ docId: "page:pg_1", score: 7, matchedChunk: undefined });
96 });
97
98 it("sorts by descending score", () => {
99 const out = dedupeToParent([
100 { docId: "page:a", score: 1 },
101 { docId: "page:b", score: 4 },
102 { docId: "page:c", score: 2 },
103 ]);
104 expect(out.map((d) => d.docId)).toEqual(["page:b", "page:c", "page:a"]);
105 });
106});
107