Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

index-existing-repo.test.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

index-existing-repo.test.tsBlame161 lines · 1 contributor
5d13cf5ccantynz-alt1/**
2 * Repositories that arrive without a push must still be indexed.
3 *
4 * indexChangedFiles() is called from exactly ONE place — the git post-receive
5 * hook. That is correct for pushes and wrong for every other way a repository
6 * comes into existence:
7 *
8 * - bulk import CLONES (git clone fires no post-receive hook)
9 * - POST /new + first-repo-scaffold writes via the git plumbing API
10 * - forks copy the bare repo directly
11 *
12 * So both onboarding paths built in this session produced repositories where
13 * gluecron_semantic_search, repo chat and "ask this repo" returned nothing.
14 * Silently — an empty index is indistinguishable from "no matches", which is
15 * why it survived. It was found while wiring the agent-first path, which
16 * depends entirely on that index being populated.
17 *
18 * Deps are injected rather than mock.module()'d: ../git/repository is imported
19 * by dozens of test files and a module mock leaks across the whole run.
20 */
21
22import { describe, expect, it } from "bun:test";
23import { readFileSync } from "fs";
24
25const SRC = readFileSync("src/lib/index-existing-repo.ts", "utf8");
26
27type Entry = { name: string; type: string; path?: string };
28
29function treeFrom(map: Record<string, Entry[]>) {
30 return async (_o: string, _r: string, _ref: string, path?: string) =>
31 map[path ?? ""] ?? [];
32}
33
34describe("tree walking", () => {
35 it("collects blobs across nested directories", async () => {
36 const captured: string[][] = [];
37 const { indexExistingRepo } = await import("../lib/index-existing-repo");
38 const r = await indexExistingRepo(
39 { repositoryId: "r1", owner: "a", repoName: "b", ref: "main", commitSha: "sha" },
40 {
41 getTree: treeFrom({
42 "": [
43 { name: "index.ts", type: "blob", path: "index.ts" },
44 { name: "src", type: "tree", path: "src" },
45 ],
46 src: [{ name: "app.ts", type: "blob", path: "src/app.ts" }],
47 }),
48 indexChangedFiles: async (a) => {
49 captured.push(a.changedPaths);
50 return { indexed: a.changedPaths.length, skipped: 0, model: "test" };
51 },
52 }
53 );
54 expect(r.indexed).toBe(2);
55 expect(captured[0].sort()).toEqual(["index.ts", "src/app.ts"]);
56 });
57
58 it("skips vendor and build directories", async () => {
59 let seen: string[] = [];
60 const { indexExistingRepo } = await import("../lib/index-existing-repo");
61 await indexExistingRepo(
62 { repositoryId: "r1", owner: "a", repoName: "b", ref: "main", commitSha: "sha" },
63 {
64 getTree: treeFrom({
65 "": [
66 { name: "node_modules", type: "tree", path: "node_modules" },
67 { name: "dist", type: "tree", path: "dist" },
68 { name: "keep.ts", type: "blob", path: "keep.ts" },
69 ],
70 node_modules: [{ name: "junk.js", type: "blob", path: "node_modules/junk.js" }],
71 dist: [{ name: "out.js", type: "blob", path: "dist/out.js" }],
72 }),
73 indexChangedFiles: async (a) => {
74 seen = a.changedPaths;
75 return { indexed: a.changedPaths.length, skipped: 0, model: "test" };
76 },
77 }
78 );
79 expect(seen).toEqual(["keep.ts"]);
80 });
81
82 it("delegates filtering to indexChangedFiles rather than duplicating it", () => {
83 // isCodeFile, the per-push cap and the embedding model all live in
84 // semantic-index.ts. Re-implementing them here would let scaffolded and
85 // pushed repos drift apart in what they index.
86 //
87 // Comments stripped: the source explains this delegation in prose that
88 // names the very symbol being asserted absent.
89 const code = SRC.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
90 expect(code).toContain("indexChangedFiles");
91 expect(code).not.toContain("isCodeFile");
92 });
93});
94
95describe("fault isolation", () => {
96 it("never throws when the tree read fails", async () => {
97 const { indexExistingRepo } = await import("../lib/index-existing-repo");
98 const r = await indexExistingRepo(
99 { repositoryId: "r1", owner: "a", repoName: "b", ref: "main", commitSha: "sha" },
100 {
101 getTree: async () => {
102 throw new Error("disk on fire");
103 },
104 indexChangedFiles: async () => ({ indexed: 0, skipped: 0, model: "test" }),
105 }
106 );
107 // A single unreadable directory must not abandon the index, and an
108 // unindexed repo must never fail an import or a repo creation.
109 expect(r.indexed).toBe(0);
110 expect(r.error).toBeUndefined();
111 });
112
113 it("reports an embedding failure instead of throwing", async () => {
114 const { indexExistingRepo } = await import("../lib/index-existing-repo");
115 const r = await indexExistingRepo(
116 { repositoryId: "r1", owner: "a", repoName: "b", ref: "main", commitSha: "sha" },
117 {
118 getTree: treeFrom({ "": [{ name: "a.ts", type: "blob", path: "a.ts" }] }),
119 indexChangedFiles: async () => {
120 throw new Error("embedder down");
121 },
122 }
123 );
124 expect(r.indexed).toBe(0);
125 expect(r.error).toContain("embedder down");
126 });
127
128 it("returns zero for an empty repository without calling the embedder", async () => {
129 let called = false;
130 const { indexExistingRepo } = await import("../lib/index-existing-repo");
131 const r = await indexExistingRepo(
132 { repositoryId: "r1", owner: "a", repoName: "b", ref: "main", commitSha: "sha" },
133 {
134 getTree: treeFrom({}),
135 indexChangedFiles: async () => {
136 called = true;
137 return { indexed: 0, skipped: 0, model: "test" };
138 },
139 }
140 );
141 expect(r.indexed).toBe(0);
142 expect(called).toBe(false);
143 });
144});
145
146describe("both onboarding paths index", () => {
147 it("bulk import indexes each repo", () => {
148 const mig = readFileSync("src/lib/migration-onboarding.ts", "utf8");
149 expect(mig).toContain("indexExistingRepo");
150 expect(mig).toContain("filesIndexed");
151 });
152
153 it("new-repo scaffold indexes after committing", () => {
154 const scaffold = readFileSync("src/lib/first-repo-scaffold.ts", "utf8");
155 expect(scaffold).toContain("indexExistingRepo");
156 // Must come after the commits — there is nothing to index before them.
157 expect(scaffold.indexOf("createOrUpdateFileOnBranch")).toBeLessThan(
158 scaffold.lastIndexOf("indexExistingRepo")
159 );
160 });
161});