@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.
- package/dist/cli.js +23 -23
- package/dist/index.d.ts +1 -1
- package/dist/query-expansion.js +136 -15
- package/package.json +1 -1
- package/src/cli.test.ts +324 -324
- package/src/cli.ts +407 -407
- package/src/embeddings.ts +56 -56
- package/src/index.ts +11 -11
- package/src/indexer.test.ts +337 -337
- package/src/indexer.ts +260 -260
- package/src/memory-index.ts +3 -1
- package/src/query-expansion.test.ts +9 -3
- package/src/query-expansion.ts +136 -15
- package/src/search.test.ts +3 -9
- package/src/search.ts +244 -244
package/src/indexer.test.ts
CHANGED
|
@@ -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
|
+
});
|