@teammates/recall 0.6.1 → 0.6.3

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.
@@ -1,337 +1,337 @@
1
- import { mkdir, rm, writeFile } from "node:fs/promises";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
- import { Indexer } from "./indexer.js";
6
-
7
- // Stub embeddings — we don't want to load the real model in tests
8
- class StubEmbeddings {
9
- readonly maxTokens = 256;
10
- async createEmbeddings(inputs: string | string[]) {
11
- const texts = Array.isArray(inputs) ? inputs : [inputs];
12
- return {
13
- status: "success" as const,
14
- output: texts.map(() => new Array(384).fill(0).map(() => Math.random())),
15
- };
16
- }
17
- }
18
-
19
- // Create an Indexer with stubbed embeddings
20
- function createIndexer(teammatesDir: string): Indexer {
21
- const indexer = new Indexer({ teammatesDir });
22
- // Swap out the real embeddings with our stub
23
- (indexer as any)._embeddings = new StubEmbeddings();
24
- return indexer;
25
- }
26
-
27
- let testDir: string;
28
-
29
- beforeEach(async () => {
30
- testDir = join(
31
- tmpdir(),
32
- `recall-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
33
- );
34
- await mkdir(testDir, { recursive: true });
35
- });
36
-
37
- afterEach(async () => {
38
- await rm(testDir, { recursive: true, force: true });
39
- });
40
-
41
- describe("Indexer", () => {
42
- describe("discoverTeammates", () => {
43
- it("finds directories containing SOUL.md", async () => {
44
- const beacon = join(testDir, "beacon");
45
- const scribe = join(testDir, "scribe");
46
- const notTeammate = join(testDir, "random");
47
-
48
- await mkdir(beacon, { recursive: true });
49
- await mkdir(scribe, { recursive: true });
50
- await mkdir(notTeammate, { recursive: true });
51
-
52
- await writeFile(join(beacon, "SOUL.md"), "# Beacon");
53
- await writeFile(join(scribe, "SOUL.md"), "# Scribe");
54
- // notTeammate has no SOUL.md
55
-
56
- const indexer = createIndexer(testDir);
57
- const teammates = await indexer.discoverTeammates();
58
-
59
- expect(teammates).toContain("beacon");
60
- expect(teammates).toContain("scribe");
61
- expect(teammates).not.toContain("random");
62
- });
63
-
64
- it("ignores dot-prefixed directories", async () => {
65
- const hidden = join(testDir, ".tmp");
66
- await mkdir(hidden, { recursive: true });
67
- await writeFile(join(hidden, "SOUL.md"), "# Hidden");
68
-
69
- const indexer = createIndexer(testDir);
70
- const teammates = await indexer.discoverTeammates();
71
-
72
- expect(teammates).not.toContain(".tmp");
73
- expect(teammates).toHaveLength(0);
74
- });
75
-
76
- it("returns empty array when no teammates exist", async () => {
77
- const indexer = createIndexer(testDir);
78
- const teammates = await indexer.discoverTeammates();
79
- expect(teammates).toEqual([]);
80
- });
81
- });
82
-
83
- describe("collectFiles", () => {
84
- it("collects WISDOM.md", async () => {
85
- const beacon = join(testDir, "beacon");
86
- await mkdir(beacon, { recursive: true });
87
- await writeFile(join(beacon, "WISDOM.md"), "# Wisdom");
88
-
89
- const indexer = createIndexer(testDir);
90
- const { files } = await indexer.collectFiles("beacon");
91
-
92
- expect(files).toHaveLength(1);
93
- expect(files[0].uri).toBe("beacon/WISDOM.md");
94
- });
95
-
96
- it("collects typed memory files from memory/", async () => {
97
- const memDir = join(testDir, "beacon", "memory");
98
- await mkdir(memDir, { recursive: true });
99
- await writeFile(join(memDir, "feedback_testing.md"), "# Feedback");
100
- await writeFile(join(memDir, "project_goals.md"), "# Goals");
101
-
102
- const indexer = createIndexer(testDir);
103
- const { files } = await indexer.collectFiles("beacon");
104
-
105
- const uris = files.map((f) => f.uri);
106
- expect(uris).toContain("beacon/memory/feedback_testing.md");
107
- expect(uris).toContain("beacon/memory/project_goals.md");
108
- });
109
-
110
- it("includes older daily logs but skips today's", async () => {
111
- const memDir = join(testDir, "beacon", "memory");
112
- await mkdir(memDir, { recursive: true });
113
- const today = new Date().toISOString().slice(0, 10);
114
- await writeFile(join(memDir, `${today}.md`), "# Today");
115
- await writeFile(join(memDir, "2026-03-14.md"), "# Day 1");
116
- await writeFile(join(memDir, "2026-03-15.md"), "# Day 2");
117
- await writeFile(join(memDir, "feedback_testing.md"), "# Feedback");
118
-
119
- const indexer = createIndexer(testDir);
120
- const { files } = await indexer.collectFiles("beacon");
121
-
122
- const uris = files.map((f) => f.uri);
123
- expect(uris).not.toContain(`beacon/memory/${today}.md`);
124
- expect(uris).toContain("beacon/memory/2026-03-14.md");
125
- expect(uris).toContain("beacon/memory/2026-03-15.md");
126
- expect(uris).toContain("beacon/memory/feedback_testing.md");
127
- });
128
-
129
- it("collects weekly summaries from memory/weekly/", async () => {
130
- const weeklyDir = join(testDir, "beacon", "memory", "weekly");
131
- await mkdir(weeklyDir, { recursive: true });
132
- await writeFile(join(weeklyDir, "2026-W10.md"), "# Week 10");
133
- await writeFile(join(weeklyDir, "2026-W11.md"), "# Week 11");
134
-
135
- const indexer = createIndexer(testDir);
136
- const { files } = await indexer.collectFiles("beacon");
137
-
138
- const uris = files.map((f) => f.uri);
139
- expect(uris).toContain("beacon/memory/weekly/2026-W10.md");
140
- expect(uris).toContain("beacon/memory/weekly/2026-W11.md");
141
- });
142
-
143
- it("collects monthly summaries from memory/monthly/", async () => {
144
- const monthlyDir = join(testDir, "beacon", "memory", "monthly");
145
- await mkdir(monthlyDir, { recursive: true });
146
- await writeFile(join(monthlyDir, "2025-12.md"), "# Dec 2025");
147
-
148
- const indexer = createIndexer(testDir);
149
- const { files } = await indexer.collectFiles("beacon");
150
-
151
- const uris = files.map((f) => f.uri);
152
- expect(uris).toContain("beacon/memory/monthly/2025-12.md");
153
- });
154
-
155
- it("skips non-md files", async () => {
156
- const memDir = join(testDir, "beacon", "memory");
157
- await mkdir(memDir, { recursive: true });
158
- await writeFile(join(memDir, "notes.txt"), "not markdown");
159
- await writeFile(join(memDir, "feedback_test.md"), "# Feedback");
160
-
161
- const indexer = createIndexer(testDir);
162
- const { files } = await indexer.collectFiles("beacon");
163
-
164
- expect(files).toHaveLength(1);
165
- expect(files[0].uri).toBe("beacon/memory/feedback_test.md");
166
- });
167
-
168
- it("returns empty files when teammate has no content", async () => {
169
- await mkdir(join(testDir, "beacon"), { recursive: true });
170
-
171
- const indexer = createIndexer(testDir);
172
- const { files } = await indexer.collectFiles("beacon");
173
- expect(files).toEqual([]);
174
- });
175
- });
176
-
177
- describe("indexPath", () => {
178
- it("returns correct path under teammate directory", () => {
179
- const indexer = createIndexer(testDir);
180
- const p = indexer.indexPath("beacon");
181
- expect(p).toBe(join(testDir, "beacon", ".index"));
182
- });
183
- });
184
-
185
- describe("indexTeammate", () => {
186
- it("creates an index and returns file count", async () => {
187
- const beacon = join(testDir, "beacon");
188
- const memDir = join(beacon, "memory");
189
- await mkdir(memDir, { recursive: true });
190
- await writeFile(join(beacon, "WISDOM.md"), "# Wisdom content");
191
- await writeFile(join(memDir, "feedback_test.md"), "# Feedback content");
192
-
193
- const indexer = createIndexer(testDir);
194
- const count = await indexer.indexTeammate("beacon");
195
-
196
- expect(count).toBe(2);
197
- });
198
-
199
- it("returns 0 when no files to index", async () => {
200
- await mkdir(join(testDir, "beacon"), { recursive: true });
201
-
202
- const indexer = createIndexer(testDir);
203
- const count = await indexer.indexTeammate("beacon");
204
- expect(count).toBe(0);
205
- });
206
-
207
- it("skips empty files", async () => {
208
- const beacon = join(testDir, "beacon");
209
- await mkdir(beacon, { recursive: true });
210
- await writeFile(join(beacon, "WISDOM.md"), " "); // whitespace only
211
-
212
- const indexer = createIndexer(testDir);
213
- const count = await indexer.indexTeammate("beacon");
214
- expect(count).toBe(0);
215
- });
216
- });
217
-
218
- describe("indexAll", () => {
219
- it("indexes all discovered teammates", async () => {
220
- const beacon = join(testDir, "beacon");
221
- const scribe = join(testDir, "scribe");
222
- await mkdir(beacon, { recursive: true });
223
- await mkdir(scribe, { recursive: true });
224
- await writeFile(join(beacon, "SOUL.md"), "# Beacon");
225
- await writeFile(join(beacon, "WISDOM.md"), "# Beacon wisdom");
226
- await writeFile(join(scribe, "SOUL.md"), "# Scribe");
227
-
228
- const indexer = createIndexer(testDir);
229
- const results = await indexer.indexAll();
230
-
231
- expect(results.get("beacon")).toBe(1); // WISDOM.md only (SOUL.md not collected)
232
- expect(results.get("scribe")).toBe(0); // no indexable files
233
- });
234
- });
235
-
236
- describe("upsertFile", () => {
237
- it("upserts a single file into a new index", async () => {
238
- const beacon = join(testDir, "beacon");
239
- await mkdir(beacon, { recursive: true });
240
- const filePath = join(beacon, "WISDOM.md");
241
- await writeFile(filePath, "# Upsert test wisdom");
242
-
243
- const indexer = createIndexer(testDir);
244
- await indexer.upsertFile("beacon", filePath);
245
-
246
- // Verify index was created by syncing (which reads the index)
247
- const count = await indexer.syncTeammate("beacon");
248
- expect(count).toBeGreaterThanOrEqual(1);
249
- });
250
-
251
- it("upserts into an existing index without rebuilding", async () => {
252
- const beacon = join(testDir, "beacon");
253
- const memDir = join(beacon, "memory");
254
- await mkdir(memDir, { recursive: true });
255
- await writeFile(join(beacon, "WISDOM.md"), "# Wisdom");
256
-
257
- const indexer = createIndexer(testDir);
258
- // Build initial index
259
- await indexer.indexTeammate("beacon");
260
-
261
- // Upsert a new file
262
- const newFile = join(memDir, "feedback_test.md");
263
- await writeFile(newFile, "# New feedback content");
264
- await indexer.upsertFile("beacon", newFile);
265
-
266
- // Sync should see both files
267
- const count = await indexer.syncTeammate("beacon");
268
- expect(count).toBe(2);
269
- });
270
-
271
- it("skips empty files", async () => {
272
- const beacon = join(testDir, "beacon");
273
- await mkdir(beacon, { recursive: true });
274
- const filePath = join(beacon, "WISDOM.md");
275
- await writeFile(filePath, " "); // whitespace only
276
-
277
- const indexer = createIndexer(testDir);
278
- // Should not throw, just skip
279
- await indexer.upsertFile("beacon", filePath);
280
- });
281
- });
282
-
283
- describe("syncAll", () => {
284
- it("syncs all discovered teammates", async () => {
285
- const beacon = join(testDir, "beacon");
286
- const scribe = join(testDir, "scribe");
287
- await mkdir(beacon, { recursive: true });
288
- await mkdir(scribe, { recursive: true });
289
- await writeFile(join(beacon, "SOUL.md"), "# Beacon");
290
- await writeFile(join(beacon, "WISDOM.md"), "# Beacon wisdom");
291
- await writeFile(join(scribe, "SOUL.md"), "# Scribe");
292
- await writeFile(join(scribe, "WISDOM.md"), "# Scribe wisdom");
293
-
294
- const indexer = createIndexer(testDir);
295
- const results = await indexer.syncAll();
296
-
297
- expect(results.get("beacon")).toBe(1);
298
- expect(results.get("scribe")).toBe(1);
299
- });
300
-
301
- it("returns empty map when no teammates exist", async () => {
302
- const indexer = createIndexer(testDir);
303
- const results = await indexer.syncAll();
304
- expect(results.size).toBe(0);
305
- });
306
- });
307
-
308
- describe("syncTeammate", () => {
309
- it("falls back to full index when no index exists", async () => {
310
- const beacon = join(testDir, "beacon");
311
- await mkdir(beacon, { recursive: true });
312
- await writeFile(join(beacon, "WISDOM.md"), "# Wisdom");
313
-
314
- const indexer = createIndexer(testDir);
315
- const count = await indexer.syncTeammate("beacon");
316
- expect(count).toBe(1);
317
- });
318
-
319
- it("upserts files into existing index", async () => {
320
- const beacon = join(testDir, "beacon");
321
- const memDir = join(beacon, "memory");
322
- await mkdir(memDir, { recursive: true });
323
- await writeFile(join(beacon, "WISDOM.md"), "# Wisdom");
324
-
325
- const indexer = createIndexer(testDir);
326
- // First build the index
327
- await indexer.indexTeammate("beacon");
328
-
329
- // Add a new file
330
- await writeFile(join(memDir, "project_goals.md"), "# Goals");
331
-
332
- // Sync should pick up the new file
333
- const count = await indexer.syncTeammate("beacon");
334
- expect(count).toBe(2); // WISDOM + project_goals
335
- });
336
- });
337
- });
1
+ import { mkdir, rm, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
+ import { Indexer } from "./indexer.js";
6
+
7
+ // Stub embeddings — we don't want to load the real model in tests
8
+ class StubEmbeddings {
9
+ readonly maxTokens = 256;
10
+ async createEmbeddings(inputs: string | string[]) {
11
+ const texts = Array.isArray(inputs) ? inputs : [inputs];
12
+ return {
13
+ status: "success" as const,
14
+ output: texts.map(() => new Array(384).fill(0).map(() => Math.random())),
15
+ };
16
+ }
17
+ }
18
+
19
+ // Create an Indexer with stubbed embeddings
20
+ function createIndexer(teammatesDir: string): Indexer {
21
+ const indexer = new Indexer({ teammatesDir });
22
+ // Swap out the real embeddings with our stub
23
+ (indexer as any)._embeddings = new StubEmbeddings();
24
+ return indexer;
25
+ }
26
+
27
+ let testDir: string;
28
+
29
+ beforeEach(async () => {
30
+ testDir = join(
31
+ tmpdir(),
32
+ `recall-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
33
+ );
34
+ await mkdir(testDir, { recursive: true });
35
+ });
36
+
37
+ afterEach(async () => {
38
+ await rm(testDir, { recursive: true, force: true });
39
+ });
40
+
41
+ describe("Indexer", () => {
42
+ describe("discoverTeammates", () => {
43
+ it("finds directories containing SOUL.md", async () => {
44
+ const beacon = join(testDir, "beacon");
45
+ const scribe = join(testDir, "scribe");
46
+ const notTeammate = join(testDir, "random");
47
+
48
+ await mkdir(beacon, { recursive: true });
49
+ await mkdir(scribe, { recursive: true });
50
+ await mkdir(notTeammate, { recursive: true });
51
+
52
+ await writeFile(join(beacon, "SOUL.md"), "# Beacon");
53
+ await writeFile(join(scribe, "SOUL.md"), "# Scribe");
54
+ // notTeammate has no SOUL.md
55
+
56
+ const indexer = createIndexer(testDir);
57
+ const teammates = await indexer.discoverTeammates();
58
+
59
+ expect(teammates).toContain("beacon");
60
+ expect(teammates).toContain("scribe");
61
+ expect(teammates).not.toContain("random");
62
+ });
63
+
64
+ it("ignores dot-prefixed directories", async () => {
65
+ const hidden = join(testDir, ".tmp");
66
+ await mkdir(hidden, { recursive: true });
67
+ await writeFile(join(hidden, "SOUL.md"), "# Hidden");
68
+
69
+ const indexer = createIndexer(testDir);
70
+ const teammates = await indexer.discoverTeammates();
71
+
72
+ expect(teammates).not.toContain(".tmp");
73
+ expect(teammates).toHaveLength(0);
74
+ });
75
+
76
+ it("returns empty array when no teammates exist", async () => {
77
+ const indexer = createIndexer(testDir);
78
+ const teammates = await indexer.discoverTeammates();
79
+ expect(teammates).toEqual([]);
80
+ });
81
+ });
82
+
83
+ describe("collectFiles", () => {
84
+ it("collects WISDOM.md", async () => {
85
+ const beacon = join(testDir, "beacon");
86
+ await mkdir(beacon, { recursive: true });
87
+ await writeFile(join(beacon, "WISDOM.md"), "# Wisdom");
88
+
89
+ const indexer = createIndexer(testDir);
90
+ const { files } = await indexer.collectFiles("beacon");
91
+
92
+ expect(files).toHaveLength(1);
93
+ expect(files[0].uri).toBe("beacon/WISDOM.md");
94
+ });
95
+
96
+ it("collects typed memory files from memory/", async () => {
97
+ const memDir = join(testDir, "beacon", "memory");
98
+ await mkdir(memDir, { recursive: true });
99
+ await writeFile(join(memDir, "feedback_testing.md"), "# Feedback");
100
+ await writeFile(join(memDir, "project_goals.md"), "# Goals");
101
+
102
+ const indexer = createIndexer(testDir);
103
+ const { files } = await indexer.collectFiles("beacon");
104
+
105
+ const uris = files.map((f) => f.uri);
106
+ expect(uris).toContain("beacon/memory/feedback_testing.md");
107
+ expect(uris).toContain("beacon/memory/project_goals.md");
108
+ });
109
+
110
+ it("includes older daily logs but skips today's", async () => {
111
+ const memDir = join(testDir, "beacon", "memory");
112
+ await mkdir(memDir, { recursive: true });
113
+ const today = new Date().toISOString().slice(0, 10);
114
+ await writeFile(join(memDir, `${today}.md`), "# Today");
115
+ await writeFile(join(memDir, "2026-03-14.md"), "# Day 1");
116
+ await writeFile(join(memDir, "2026-03-15.md"), "# Day 2");
117
+ await writeFile(join(memDir, "feedback_testing.md"), "# Feedback");
118
+
119
+ const indexer = createIndexer(testDir);
120
+ const { files } = await indexer.collectFiles("beacon");
121
+
122
+ const uris = files.map((f) => f.uri);
123
+ expect(uris).not.toContain(`beacon/memory/${today}.md`);
124
+ expect(uris).toContain("beacon/memory/2026-03-14.md");
125
+ expect(uris).toContain("beacon/memory/2026-03-15.md");
126
+ expect(uris).toContain("beacon/memory/feedback_testing.md");
127
+ });
128
+
129
+ it("collects weekly summaries from memory/weekly/", async () => {
130
+ const weeklyDir = join(testDir, "beacon", "memory", "weekly");
131
+ await mkdir(weeklyDir, { recursive: true });
132
+ await writeFile(join(weeklyDir, "2026-W10.md"), "# Week 10");
133
+ await writeFile(join(weeklyDir, "2026-W11.md"), "# Week 11");
134
+
135
+ const indexer = createIndexer(testDir);
136
+ const { files } = await indexer.collectFiles("beacon");
137
+
138
+ const uris = files.map((f) => f.uri);
139
+ expect(uris).toContain("beacon/memory/weekly/2026-W10.md");
140
+ expect(uris).toContain("beacon/memory/weekly/2026-W11.md");
141
+ });
142
+
143
+ it("collects monthly summaries from memory/monthly/", async () => {
144
+ const monthlyDir = join(testDir, "beacon", "memory", "monthly");
145
+ await mkdir(monthlyDir, { recursive: true });
146
+ await writeFile(join(monthlyDir, "2025-12.md"), "# Dec 2025");
147
+
148
+ const indexer = createIndexer(testDir);
149
+ const { files } = await indexer.collectFiles("beacon");
150
+
151
+ const uris = files.map((f) => f.uri);
152
+ expect(uris).toContain("beacon/memory/monthly/2025-12.md");
153
+ });
154
+
155
+ it("skips non-md files", async () => {
156
+ const memDir = join(testDir, "beacon", "memory");
157
+ await mkdir(memDir, { recursive: true });
158
+ await writeFile(join(memDir, "notes.txt"), "not markdown");
159
+ await writeFile(join(memDir, "feedback_test.md"), "# Feedback");
160
+
161
+ const indexer = createIndexer(testDir);
162
+ const { files } = await indexer.collectFiles("beacon");
163
+
164
+ expect(files).toHaveLength(1);
165
+ expect(files[0].uri).toBe("beacon/memory/feedback_test.md");
166
+ });
167
+
168
+ it("returns empty files when teammate has no content", async () => {
169
+ await mkdir(join(testDir, "beacon"), { recursive: true });
170
+
171
+ const indexer = createIndexer(testDir);
172
+ const { files } = await indexer.collectFiles("beacon");
173
+ expect(files).toEqual([]);
174
+ });
175
+ });
176
+
177
+ describe("indexPath", () => {
178
+ it("returns correct path under teammate directory", () => {
179
+ const indexer = createIndexer(testDir);
180
+ const p = indexer.indexPath("beacon");
181
+ expect(p).toBe(join(testDir, "beacon", ".index"));
182
+ });
183
+ });
184
+
185
+ describe("indexTeammate", () => {
186
+ it("creates an index and returns file count", async () => {
187
+ const beacon = join(testDir, "beacon");
188
+ const memDir = join(beacon, "memory");
189
+ await mkdir(memDir, { recursive: true });
190
+ await writeFile(join(beacon, "WISDOM.md"), "# Wisdom content");
191
+ await writeFile(join(memDir, "feedback_test.md"), "# Feedback content");
192
+
193
+ const indexer = createIndexer(testDir);
194
+ const count = await indexer.indexTeammate("beacon");
195
+
196
+ expect(count).toBe(2);
197
+ });
198
+
199
+ it("returns 0 when no files to index", async () => {
200
+ await mkdir(join(testDir, "beacon"), { recursive: true });
201
+
202
+ const indexer = createIndexer(testDir);
203
+ const count = await indexer.indexTeammate("beacon");
204
+ expect(count).toBe(0);
205
+ });
206
+
207
+ it("skips empty files", async () => {
208
+ const beacon = join(testDir, "beacon");
209
+ await mkdir(beacon, { recursive: true });
210
+ await writeFile(join(beacon, "WISDOM.md"), " "); // whitespace only
211
+
212
+ const indexer = createIndexer(testDir);
213
+ const count = await indexer.indexTeammate("beacon");
214
+ expect(count).toBe(0);
215
+ });
216
+ });
217
+
218
+ describe("indexAll", () => {
219
+ it("indexes all discovered teammates", async () => {
220
+ const beacon = join(testDir, "beacon");
221
+ const scribe = join(testDir, "scribe");
222
+ await mkdir(beacon, { recursive: true });
223
+ await mkdir(scribe, { recursive: true });
224
+ await writeFile(join(beacon, "SOUL.md"), "# Beacon");
225
+ await writeFile(join(beacon, "WISDOM.md"), "# Beacon wisdom");
226
+ await writeFile(join(scribe, "SOUL.md"), "# Scribe");
227
+
228
+ const indexer = createIndexer(testDir);
229
+ const results = await indexer.indexAll();
230
+
231
+ expect(results.get("beacon")).toBe(1); // WISDOM.md only (SOUL.md not collected)
232
+ expect(results.get("scribe")).toBe(0); // no indexable files
233
+ });
234
+ });
235
+
236
+ describe("upsertFile", () => {
237
+ it("upserts a single file into a new index", async () => {
238
+ const beacon = join(testDir, "beacon");
239
+ await mkdir(beacon, { recursive: true });
240
+ const filePath = join(beacon, "WISDOM.md");
241
+ await writeFile(filePath, "# Upsert test wisdom");
242
+
243
+ const indexer = createIndexer(testDir);
244
+ await indexer.upsertFile("beacon", filePath);
245
+
246
+ // Verify index was created by syncing (which reads the index)
247
+ const count = await indexer.syncTeammate("beacon");
248
+ expect(count).toBeGreaterThanOrEqual(1);
249
+ });
250
+
251
+ it("upserts into an existing index without rebuilding", async () => {
252
+ const beacon = join(testDir, "beacon");
253
+ const memDir = join(beacon, "memory");
254
+ await mkdir(memDir, { recursive: true });
255
+ await writeFile(join(beacon, "WISDOM.md"), "# Wisdom");
256
+
257
+ const indexer = createIndexer(testDir);
258
+ // Build initial index
259
+ await indexer.indexTeammate("beacon");
260
+
261
+ // Upsert a new file
262
+ const newFile = join(memDir, "feedback_test.md");
263
+ await writeFile(newFile, "# New feedback content");
264
+ await indexer.upsertFile("beacon", newFile);
265
+
266
+ // Sync should see both files
267
+ const count = await indexer.syncTeammate("beacon");
268
+ expect(count).toBe(2);
269
+ });
270
+
271
+ it("skips empty files", async () => {
272
+ const beacon = join(testDir, "beacon");
273
+ await mkdir(beacon, { recursive: true });
274
+ const filePath = join(beacon, "WISDOM.md");
275
+ await writeFile(filePath, " "); // whitespace only
276
+
277
+ const indexer = createIndexer(testDir);
278
+ // Should not throw, just skip
279
+ await indexer.upsertFile("beacon", filePath);
280
+ });
281
+ });
282
+
283
+ describe("syncAll", () => {
284
+ it("syncs all discovered teammates", async () => {
285
+ const beacon = join(testDir, "beacon");
286
+ const scribe = join(testDir, "scribe");
287
+ await mkdir(beacon, { recursive: true });
288
+ await mkdir(scribe, { recursive: true });
289
+ await writeFile(join(beacon, "SOUL.md"), "# Beacon");
290
+ await writeFile(join(beacon, "WISDOM.md"), "# Beacon wisdom");
291
+ await writeFile(join(scribe, "SOUL.md"), "# Scribe");
292
+ await writeFile(join(scribe, "WISDOM.md"), "# Scribe wisdom");
293
+
294
+ const indexer = createIndexer(testDir);
295
+ const results = await indexer.syncAll();
296
+
297
+ expect(results.get("beacon")).toBe(1);
298
+ expect(results.get("scribe")).toBe(1);
299
+ });
300
+
301
+ it("returns empty map when no teammates exist", async () => {
302
+ const indexer = createIndexer(testDir);
303
+ const results = await indexer.syncAll();
304
+ expect(results.size).toBe(0);
305
+ });
306
+ });
307
+
308
+ describe("syncTeammate", () => {
309
+ it("falls back to full index when no index exists", async () => {
310
+ const beacon = join(testDir, "beacon");
311
+ await mkdir(beacon, { recursive: true });
312
+ await writeFile(join(beacon, "WISDOM.md"), "# Wisdom");
313
+
314
+ const indexer = createIndexer(testDir);
315
+ const count = await indexer.syncTeammate("beacon");
316
+ expect(count).toBe(1);
317
+ });
318
+
319
+ it("upserts files into existing index", async () => {
320
+ const beacon = join(testDir, "beacon");
321
+ const memDir = join(beacon, "memory");
322
+ await mkdir(memDir, { recursive: true });
323
+ await writeFile(join(beacon, "WISDOM.md"), "# Wisdom");
324
+
325
+ const indexer = createIndexer(testDir);
326
+ // First build the index
327
+ await indexer.indexTeammate("beacon");
328
+
329
+ // Add a new file
330
+ await writeFile(join(memDir, "project_goals.md"), "# Goals");
331
+
332
+ // Sync should pick up the new file
333
+ const count = await indexer.syncTeammate("beacon");
334
+ expect(count).toBe(2); // WISDOM + project_goals
335
+ });
336
+ });
337
+ });