@indigoai-us/hq-cloud 6.14.49 → 6.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/.github/workflows/unreleased-commits-nag.yml +256 -0
  2. package/dist/bin/sync-runner-watch-loop.d.ts +12 -0
  3. package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
  4. package/dist/bin/sync-runner-watch-loop.js +153 -25
  5. package/dist/bin/sync-runner-watch-loop.js.map +1 -1
  6. package/dist/bin/sync-runner-watch-routes.d.ts +17 -0
  7. package/dist/bin/sync-runner-watch-routes.d.ts.map +1 -1
  8. package/dist/bin/sync-runner-watch-routes.js +11 -0
  9. package/dist/bin/sync-runner-watch-routes.js.map +1 -1
  10. package/dist/bin/sync-runner-watch-routes.test.d.ts +2 -0
  11. package/dist/bin/sync-runner-watch-routes.test.d.ts.map +1 -0
  12. package/dist/bin/sync-runner-watch-routes.test.js +51 -0
  13. package/dist/bin/sync-runner-watch-routes.test.js.map +1 -0
  14. package/dist/bin/sync-runner.d.ts +11 -12
  15. package/dist/bin/sync-runner.d.ts.map +1 -1
  16. package/dist/bin/sync-runner.js.map +1 -1
  17. package/dist/bin/sync-runner.test.js +173 -52
  18. package/dist/bin/sync-runner.test.js.map +1 -1
  19. package/dist/cli/reindex-knowledge.d.ts +7 -0
  20. package/dist/cli/reindex-knowledge.d.ts.map +1 -0
  21. package/dist/cli/reindex-knowledge.js +389 -0
  22. package/dist/cli/reindex-knowledge.js.map +1 -0
  23. package/dist/cli/reindex-knowledge.test.d.ts +12 -0
  24. package/dist/cli/reindex-knowledge.test.d.ts.map +1 -0
  25. package/dist/cli/reindex-knowledge.test.js +260 -0
  26. package/dist/cli/reindex-knowledge.test.js.map +1 -0
  27. package/dist/cli/reindex.d.ts.map +1 -1
  28. package/dist/cli/reindex.js +8 -0
  29. package/dist/cli/reindex.js.map +1 -1
  30. package/dist/cli/rescue-classify-ordering.test.js +8 -2
  31. package/dist/cli/rescue-classify-ordering.test.js.map +1 -1
  32. package/dist/cli/rescue-core.d.ts +85 -0
  33. package/dist/cli/rescue-core.d.ts.map +1 -1
  34. package/dist/cli/rescue-core.js +390 -1
  35. package/dist/cli/rescue-core.js.map +1 -1
  36. package/dist/cli/rescue-settings-reconcile.test.d.ts +2 -0
  37. package/dist/cli/rescue-settings-reconcile.test.d.ts.map +1 -0
  38. package/dist/cli/rescue-settings-reconcile.test.js +523 -0
  39. package/dist/cli/rescue-settings-reconcile.test.js.map +1 -0
  40. package/dist/index.d.ts +1 -1
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +1 -1
  43. package/dist/index.js.map +1 -1
  44. package/dist/journal.d.ts.map +1 -1
  45. package/dist/journal.js +81 -51
  46. package/dist/journal.js.map +1 -1
  47. package/dist/journal.test.js +11 -19
  48. package/dist/journal.test.js.map +1 -1
  49. package/dist/sync/event-sync.d.ts +8 -25
  50. package/dist/sync/event-sync.d.ts.map +1 -1
  51. package/dist/sync/event-sync.js +23 -48
  52. package/dist/sync/event-sync.js.map +1 -1
  53. package/dist/sync/event-sync.test.js +6 -32
  54. package/dist/sync/event-sync.test.js.map +1 -1
  55. package/dist/sync/feature-flags.d.ts +20 -117
  56. package/dist/sync/feature-flags.d.ts.map +1 -1
  57. package/dist/sync/feature-flags.js +24 -147
  58. package/dist/sync/feature-flags.js.map +1 -1
  59. package/dist/sync/feature-flags.test.js +15 -61
  60. package/dist/sync/feature-flags.test.js.map +1 -1
  61. package/dist/sync/index.d.ts +1 -1
  62. package/dist/sync/index.d.ts.map +1 -1
  63. package/dist/sync/index.js +1 -1
  64. package/dist/sync/index.js.map +1 -1
  65. package/dist/sync/lease-client.d.ts +67 -0
  66. package/dist/sync/lease-client.d.ts.map +1 -0
  67. package/dist/sync/lease-client.js +158 -0
  68. package/dist/sync/lease-client.js.map +1 -0
  69. package/dist/sync/lease-client.test.d.ts +2 -0
  70. package/dist/sync/lease-client.test.d.ts.map +1 -0
  71. package/dist/sync/lease-client.test.js +119 -0
  72. package/dist/sync/lease-client.test.js.map +1 -0
  73. package/dist/sync/push-receiver.d.ts +8 -19
  74. package/dist/sync/push-receiver.d.ts.map +1 -1
  75. package/dist/sync/push-receiver.js +6 -50
  76. package/dist/sync/push-receiver.js.map +1 -1
  77. package/dist/sync/push-receiver.test.js +6 -57
  78. package/dist/sync/push-receiver.test.js.map +1 -1
  79. package/dist/sync/realtime-rollout.d.ts +85 -0
  80. package/dist/sync/realtime-rollout.d.ts.map +1 -0
  81. package/dist/sync/realtime-rollout.js +206 -0
  82. package/dist/sync/realtime-rollout.js.map +1 -0
  83. package/dist/sync/realtime-rollout.test.d.ts +2 -0
  84. package/dist/sync/realtime-rollout.test.d.ts.map +1 -0
  85. package/dist/sync/realtime-rollout.test.js +71 -0
  86. package/dist/sync/realtime-rollout.test.js.map +1 -0
  87. package/dist/sync/state-store.d.ts +40 -0
  88. package/dist/sync/state-store.d.ts.map +1 -0
  89. package/dist/sync/state-store.js +680 -0
  90. package/dist/sync/state-store.js.map +1 -0
  91. package/dist/sync/state-store.test.d.ts +2 -0
  92. package/dist/sync/state-store.test.d.ts.map +1 -0
  93. package/dist/sync/state-store.test.js +159 -0
  94. package/dist/sync/state-store.test.js.map +1 -0
  95. package/dist/types.d.ts +12 -0
  96. package/dist/types.d.ts.map +1 -1
  97. package/package.json +1 -1
  98. package/src/bin/sync-runner-watch-loop.ts +176 -32
  99. package/src/bin/sync-runner-watch-routes.test.ts +71 -0
  100. package/src/bin/sync-runner-watch-routes.ts +27 -0
  101. package/src/bin/sync-runner.test.ts +188 -53
  102. package/src/bin/sync-runner.ts +11 -12
  103. package/src/cli/reindex-knowledge.test.ts +307 -0
  104. package/src/cli/reindex-knowledge.ts +450 -0
  105. package/src/cli/reindex.ts +9 -0
  106. package/src/cli/rescue-classify-ordering.test.ts +9 -3
  107. package/src/cli/rescue-core.ts +459 -1
  108. package/src/cli/rescue-settings-reconcile.test.ts +637 -0
  109. package/src/index.ts +2 -4
  110. package/src/journal.test.ts +19 -21
  111. package/src/journal.ts +87 -54
  112. package/src/sync/event-sync.test.ts +9 -44
  113. package/src/sync/event-sync.ts +26 -52
  114. package/src/sync/feature-flags.test.ts +22 -72
  115. package/src/sync/feature-flags.ts +32 -199
  116. package/src/sync/index.ts +2 -4
  117. package/src/sync/lease-client.test.ts +128 -0
  118. package/src/sync/lease-client.ts +207 -0
  119. package/src/sync/push-receiver.test.ts +6 -58
  120. package/src/sync/push-receiver.ts +9 -74
  121. package/src/sync/realtime-rollout.test.ts +86 -0
  122. package/src/sync/realtime-rollout.ts +262 -0
  123. package/src/sync/state-store.test.ts +194 -0
  124. package/src/sync/state-store.ts +727 -0
  125. package/src/types.ts +14 -0
  126. package/test/e2e/sync/transient-company-leg.test.ts +8 -4
@@ -0,0 +1,307 @@
1
+ /**
2
+ * Tests for the legacy knowledge-repo materialization pass (`hq reindex`).
3
+ *
4
+ * Each test builds a temp HQ tree with the layout under test, runs
5
+ * materializeLegacyKnowledgeRepos() (or full reindex() for the integration
6
+ * case), and asserts observable filesystem outcomes: canonical knowledge
7
+ * paths become real directories with their content (and history) inline, and
8
+ * fully-migrated legacy repos under repos/ are removed — while supported
9
+ * layouts (pack mounts, embedded repos, in-tree aliases) are untouched.
10
+ */
11
+
12
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
13
+ import { spawnSync } from "child_process";
14
+ import * as fs from "fs";
15
+ import * as path from "path";
16
+ import * as os from "os";
17
+ import { materializeLegacyKnowledgeRepos } from "./reindex-knowledge.js";
18
+ import { reindex } from "./reindex.js";
19
+
20
+ let root: string;
21
+
22
+ function git(dir: string, ...args: string[]): string {
23
+ const res = spawnSync("git", ["-C", dir, ...args], { encoding: "utf8" });
24
+ expect(res.status, `git ${args.join(" ")} in ${dir}: ${res.stderr}`).toBe(0);
25
+ return res.stdout.trim();
26
+ }
27
+
28
+ /** Init a standalone git repo at `dir` with one committed file per `files`. */
29
+ function makeRepo(dir: string, files: Record<string, string>): void {
30
+ fs.mkdirSync(dir, { recursive: true });
31
+ git(dir, "init", "-q");
32
+ git(dir, "config", "user.email", "test@example.com");
33
+ git(dir, "config", "user.name", "test");
34
+ for (const [rel, content] of Object.entries(files)) {
35
+ const abs = path.join(dir, rel);
36
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
37
+ fs.writeFileSync(abs, content);
38
+ }
39
+ git(dir, "add", "-A");
40
+ git(dir, "commit", "-qm", "init");
41
+ }
42
+
43
+ function isRealDir(p: string): boolean {
44
+ const st = fs.lstatSync(p);
45
+ return st.isDirectory() && !st.isSymbolicLink();
46
+ }
47
+
48
+ beforeEach(() => {
49
+ root = fs.mkdtempSync(path.join(os.tmpdir(), "hq-reindex-knowledge-"));
50
+ fs.mkdirSync(path.join(root, "companies"), { recursive: true });
51
+ });
52
+
53
+ afterEach(() => {
54
+ fs.rmSync(root, { recursive: true, force: true });
55
+ });
56
+
57
+ describe("materializeLegacyKnowledgeRepos", () => {
58
+ it("materializes a company knowledge symlink and removes the legacy repo", () => {
59
+ const legacy = path.join(root, "repos", "private", "knowledge-acme");
60
+ makeRepo(legacy, { "notes.md": "# Acme notes\n", "sub/deep.md": "deep\n" });
61
+ fs.mkdirSync(path.join(root, "companies", "acme"), { recursive: true });
62
+ const link = path.join(root, "companies", "acme", "knowledge");
63
+ fs.symlinkSync(path.join("..", "..", "repos", "private", "knowledge-acme"), link);
64
+
65
+ materializeLegacyKnowledgeRepos(root);
66
+
67
+ expect(isRealDir(link)).toBe(true);
68
+ expect(fs.readFileSync(path.join(link, "notes.md"), "utf8")).toBe("# Acme notes\n");
69
+ expect(fs.readFileSync(path.join(link, "sub", "deep.md"), "utf8")).toBe("deep\n");
70
+ // History preserved as a supported embedded repo at the canonical path.
71
+ expect(fs.existsSync(path.join(link, ".git"))).toBe(true);
72
+ expect(git(link, "log", "--oneline")).toContain("init");
73
+ // Legacy repo fully materialized → removed.
74
+ expect(fs.existsSync(legacy)).toBe(false);
75
+ });
76
+
77
+ it("pulls the legacy repo before copying so the inline content is current", () => {
78
+ const upstream = path.join(root, "upstream.git");
79
+ makeRepo(upstream, { "notes.md": "v1\n" });
80
+ const legacy = path.join(root, "repos", "public", "knowledge-team");
81
+ fs.mkdirSync(path.dirname(legacy), { recursive: true });
82
+ git(path.dirname(legacy), "clone", "-q", upstream, legacy);
83
+ // Upstream advances after the clone.
84
+ fs.writeFileSync(path.join(upstream, "notes.md"), "v2\n");
85
+ git(upstream, "commit", "-aqm", "v2");
86
+
87
+ fs.mkdirSync(path.join(root, "core", "knowledge", "public"), { recursive: true });
88
+ const link = path.join(root, "core", "knowledge", "public", "team");
89
+ fs.symlinkSync(legacy, link);
90
+
91
+ materializeLegacyKnowledgeRepos(root);
92
+
93
+ expect(isRealDir(link)).toBe(true);
94
+ expect(fs.readFileSync(path.join(link, "notes.md"), "utf8")).toBe("v2\n");
95
+ expect(fs.existsSync(legacy)).toBe(false);
96
+ });
97
+
98
+ it("keeps a legacy repo whose knowledge is only a subdirectory, and warns", () => {
99
+ const legacy = path.join(root, "repos", "public", "ralph-methodology");
100
+ makeRepo(legacy, { "docs/guide.md": "guide\n", "src/code.ts": "code\n" });
101
+ fs.mkdirSync(path.join(root, "core", "knowledge", "public"), { recursive: true });
102
+ const link = path.join(root, "core", "knowledge", "public", "Ralph");
103
+ fs.symlinkSync(path.join(legacy, "docs"), link);
104
+
105
+ materializeLegacyKnowledgeRepos(root);
106
+
107
+ expect(isRealDir(link)).toBe(true);
108
+ expect(fs.readFileSync(path.join(link, "guide.md"), "utf8")).toBe("guide\n");
109
+ // The repo also holds non-knowledge content → left in place.
110
+ expect(fs.existsSync(path.join(legacy, "src", "code.ts"))).toBe(true);
111
+ });
112
+
113
+ it("materializes every alias of one repo before removing it", () => {
114
+ const legacy = path.join(root, "repos", "public", "knowledge-hq-core");
115
+ makeRepo(legacy, { "README.md": "hq\n" });
116
+ fs.mkdirSync(path.join(root, "core", "knowledge", "public"), { recursive: true });
117
+ const canonical = path.join(root, "core", "knowledge", "public", "hq-core");
118
+ const alias = path.join(root, "core", "knowledge", "public", "hq");
119
+ fs.symlinkSync(legacy, canonical);
120
+ fs.symlinkSync(legacy, alias);
121
+
122
+ materializeLegacyKnowledgeRepos(root);
123
+
124
+ expect(isRealDir(canonical)).toBe(true);
125
+ expect(isRealDir(alias)).toBe(true);
126
+ expect(fs.readFileSync(path.join(canonical, "README.md"), "utf8")).toBe("hq\n");
127
+ expect(fs.readFileSync(path.join(alias, "README.md"), "utf8")).toBe("hq\n");
128
+ expect(fs.existsSync(legacy)).toBe(false);
129
+ });
130
+
131
+ it("migrates a personal knowledge entry symlinked outside repos/ but keeps the external repo", () => {
132
+ const external = path.join(root, "..", path.basename(root) + "-external-knowledge");
133
+ makeRepo(external, { "me.md": "personal\n" });
134
+ try {
135
+ fs.mkdirSync(path.join(root, "personal", "knowledge"), { recursive: true });
136
+ const link = path.join(root, "personal", "knowledge", "me");
137
+ fs.symlinkSync(external, link);
138
+
139
+ materializeLegacyKnowledgeRepos(root);
140
+
141
+ expect(isRealDir(link)).toBe(true);
142
+ expect(fs.readFileSync(path.join(link, "me.md"), "utf8")).toBe("personal\n");
143
+ // Outside this HQ's repos/ → never deleted automatically.
144
+ expect(fs.existsSync(path.join(external, "me.md"))).toBe(true);
145
+ } finally {
146
+ fs.rmSync(external, { recursive: true, force: true });
147
+ }
148
+ });
149
+
150
+ it("leaves pack mounts, embedded repos, in-tree aliases, and broken links alone", () => {
151
+ // Pack mount: core/knowledge/public/<x> → core/packages/<pack>/knowledge.
152
+ const packKnowledge = path.join(root, "core", "packages", "hq-pack-demo", "knowledge");
153
+ fs.mkdirSync(packKnowledge, { recursive: true });
154
+ fs.writeFileSync(path.join(packKnowledge, "pack.md"), "pack\n");
155
+ fs.mkdirSync(path.join(root, "core", "knowledge", "public"), { recursive: true });
156
+ const packLink = path.join(root, "core", "knowledge", "public", "demo-pack");
157
+ fs.symlinkSync(packKnowledge, packLink);
158
+
159
+ // Supported embedded repo at the canonical path.
160
+ const embedded = path.join(root, "companies", "beta", "knowledge");
161
+ makeRepo(embedded, { "beta.md": "beta\n" });
162
+
163
+ // In-tree alias: symlink to a plain directory inside this HQ tree.
164
+ const inTree = path.join(root, "core", "knowledge", "public", "shared-notes");
165
+ fs.mkdirSync(inTree, { recursive: true });
166
+ fs.writeFileSync(path.join(inTree, "a.md"), "a\n");
167
+ const aliasLink = path.join(root, "core", "knowledge", "public", "notes-alias");
168
+ fs.symlinkSync(inTree, aliasLink);
169
+
170
+ // Broken symlink.
171
+ const broken = path.join(root, "core", "knowledge", "public", "gone");
172
+ fs.symlinkSync(path.join(root, "does-not-exist"), broken);
173
+
174
+ materializeLegacyKnowledgeRepos(root);
175
+
176
+ expect(fs.lstatSync(packLink).isSymbolicLink()).toBe(true);
177
+ expect(isRealDir(embedded)).toBe(true);
178
+ expect(fs.existsSync(path.join(embedded, ".git"))).toBe(true);
179
+ expect(fs.lstatSync(aliasLink).isSymbolicLink()).toBe(true);
180
+ expect(fs.lstatSync(broken).isSymbolicLink()).toBe(true);
181
+ });
182
+
183
+ it("copies internal relative symlinks verbatim so they survive legacy-repo removal", () => {
184
+ const legacy = path.join(root, "repos", "private", "knowledge-linked");
185
+ makeRepo(legacy, { "b.md": "content\n" });
186
+ fs.symlinkSync("./b.md", path.join(legacy, "a.md"));
187
+ git(legacy, "add", "-A");
188
+ git(legacy, "commit", "-qm", "add link");
189
+ fs.mkdirSync(path.join(root, "companies", "linked"), { recursive: true });
190
+ const link = path.join(root, "companies", "linked", "knowledge");
191
+ fs.symlinkSync(legacy, link);
192
+
193
+ materializeLegacyKnowledgeRepos(root);
194
+
195
+ expect(isRealDir(link)).toBe(true);
196
+ expect(fs.existsSync(legacy)).toBe(false);
197
+ // The internal link must still be relative — an absolute rewrite would
198
+ // point into the (now removed) legacy repo.
199
+ expect(fs.readlinkSync(path.join(link, "a.md"))).toBe("./b.md");
200
+ expect(fs.readFileSync(path.join(link, "a.md"), "utf8")).toBe("content\n");
201
+ });
202
+
203
+ it("leaves an in-HQ mirror link to an embedded personal repo for the mirror cleanup", () => {
204
+ // Retired-release shape: core/knowledge/<x> → ../../personal/knowledge/<x>
205
+ // where the personal side is an embedded git repo. That link is the
206
+ // mirror-cleanup pass's to prune — materializing it would duplicate the
207
+ // knowledge as a real directory.
208
+ const personalRepo = path.join(root, "personal", "knowledge", "notes");
209
+ makeRepo(personalRepo, { "n.md": "notes\n" });
210
+ fs.mkdirSync(path.join(root, "core", "knowledge"), { recursive: true });
211
+ const mirror = path.join(root, "core", "knowledge", "notes");
212
+ fs.symlinkSync(path.join("..", "..", "personal", "knowledge", "notes"), mirror);
213
+
214
+ materializeLegacyKnowledgeRepos(root);
215
+
216
+ expect(fs.lstatSync(mirror).isSymbolicLink()).toBe(true);
217
+ expect(isRealDir(personalRepo)).toBe(true);
218
+ });
219
+
220
+ it("keeps git-ignored local files out of the copy and preserves the legacy repo", () => {
221
+ const legacy = path.join(root, "repos", "private", "knowledge-secrets");
222
+ makeRepo(legacy, { "notes.md": "notes\n", ".gitignore": "credentials.json\n" });
223
+ fs.writeFileSync(path.join(legacy, "credentials.json"), "{\"secret\":true}\n");
224
+ fs.mkdirSync(path.join(root, "companies", "sec"), { recursive: true });
225
+ const link = path.join(root, "companies", "sec", "knowledge");
226
+ fs.symlinkSync(legacy, link);
227
+
228
+ materializeLegacyKnowledgeRepos(root);
229
+
230
+ expect(isRealDir(link)).toBe(true);
231
+ expect(fs.readFileSync(path.join(link, "notes.md"), "utf8")).toBe("notes\n");
232
+ // The repo-ignored local file must not land under a cloud-synced path…
233
+ expect(fs.existsSync(path.join(link, "credentials.json"))).toBe(false);
234
+ // …and the legacy repo survives so that local-only file is never destroyed.
235
+ expect(fs.existsSync(path.join(legacy, "credentials.json"))).toBe(true);
236
+ });
237
+
238
+ it("migrates a linked git worktree without its .git pointer and keeps the worktree", () => {
239
+ const mainRepo = path.join(root, "repos", "private", "knowledge-main");
240
+ makeRepo(mainRepo, { "w.md": "worktree\n" });
241
+ const worktree = path.join(root, "repos", "private", "knowledge-wt");
242
+ git(mainRepo, "worktree", "add", "-q", worktree);
243
+ fs.mkdirSync(path.join(root, "companies", "wt"), { recursive: true });
244
+ const link = path.join(root, "companies", "wt", "knowledge");
245
+ fs.symlinkSync(worktree, link);
246
+
247
+ materializeLegacyKnowledgeRepos(root);
248
+
249
+ expect(isRealDir(link)).toBe(true);
250
+ expect(fs.readFileSync(path.join(link, "w.md"), "utf8")).toBe("worktree\n");
251
+ // The gitdir pointer file must not be copied (it references metadata in
252
+ // the main repo), and the worktree stays for `git worktree remove`.
253
+ expect(fs.existsSync(path.join(link, ".git"))).toBe(false);
254
+ expect(fs.existsSync(worktree)).toBe(true);
255
+ expect(fs.existsSync(mainRepo)).toBe(true);
256
+ });
257
+
258
+ it("honors the HQ_REINDEX_SKIP_KNOWLEDGE_MIGRATION escape hatch", () => {
259
+ const legacy = path.join(root, "repos", "private", "knowledge-x");
260
+ makeRepo(legacy, { "x.md": "x\n" });
261
+ fs.mkdirSync(path.join(root, "companies", "x"), { recursive: true });
262
+ const link = path.join(root, "companies", "x", "knowledge");
263
+ fs.symlinkSync(legacy, link);
264
+
265
+ process.env.HQ_REINDEX_SKIP_KNOWLEDGE_MIGRATION = "1";
266
+ try {
267
+ materializeLegacyKnowledgeRepos(root);
268
+ } finally {
269
+ delete process.env.HQ_REINDEX_SKIP_KNOWLEDGE_MIGRATION;
270
+ }
271
+
272
+ expect(fs.lstatSync(link).isSymbolicLink()).toBe(true);
273
+ expect(fs.existsSync(legacy)).toBe(true);
274
+ });
275
+
276
+ it("is idempotent — a second run over a migrated tree changes nothing", () => {
277
+ const legacy = path.join(root, "repos", "private", "knowledge-acme");
278
+ makeRepo(legacy, { "notes.md": "notes\n" });
279
+ fs.mkdirSync(path.join(root, "companies", "acme"), { recursive: true });
280
+ const link = path.join(root, "companies", "acme", "knowledge");
281
+ fs.symlinkSync(legacy, link);
282
+
283
+ materializeLegacyKnowledgeRepos(root);
284
+ const before = fs.readdirSync(link).sort();
285
+ materializeLegacyKnowledgeRepos(root);
286
+
287
+ expect(isRealDir(link)).toBe(true);
288
+ expect(fs.readdirSync(link).sort()).toEqual(before);
289
+ });
290
+
291
+ it("runs as part of reindex() against a legacy tree", () => {
292
+ // Minimal reindex-able HQ tree plus one legacy knowledge symlink.
293
+ fs.mkdirSync(path.join(root, ".claude", "skills"), { recursive: true });
294
+ const legacy = path.join(root, "repos", "private", "knowledge-gamma");
295
+ makeRepo(legacy, { "g.md": "gamma\n" });
296
+ fs.mkdirSync(path.join(root, "companies", "gamma"), { recursive: true });
297
+ const link = path.join(root, "companies", "gamma", "knowledge");
298
+ fs.symlinkSync(legacy, link);
299
+
300
+ const result = reindex({ repoRoot: root, skipLock: true });
301
+
302
+ expect(result.status).toBe(0);
303
+ expect(isRealDir(link)).toBe(true);
304
+ expect(fs.readFileSync(path.join(link, "g.md"), "utf8")).toBe("gamma\n");
305
+ expect(fs.existsSync(legacy)).toBe(false);
306
+ });
307
+ });