coding-friend-cli 1.16.0 → 1.17.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 (75) hide show
  1. package/README.md +12 -0
  2. package/dist/{chunk-D4EWPGBL.js → chunk-C5LYVVEI.js} +1 -1
  3. package/dist/{chunk-X5WEODUD.js → chunk-CYQU33FY.js} +1 -0
  4. package/dist/{chunk-QNLL3ZDF.js → chunk-G6CEEMAR.js} +3 -3
  5. package/dist/{chunk-4DB4XTSL.js → chunk-KTX4MGMR.js} +15 -1
  6. package/dist/{chunk-KJUGTLPQ.js → chunk-YO6JKGR3.js} +38 -2
  7. package/dist/{config-AIZJJ5D2.js → config-LZFXXOI4.js} +276 -14
  8. package/dist/{dev-WJ5QQ35B.js → dev-R3IYWZ3M.js} +2 -2
  9. package/dist/{disable-JDVOQNZG.js → disable-R6K5YJN4.js} +2 -2
  10. package/dist/{enable-JBJ4Q2S7.js → enable-HF4PYVJN.js} +2 -2
  11. package/dist/{host-NA7LZ4HX.js → host-SYZH3FVC.js} +4 -4
  12. package/dist/index.js +78 -18
  13. package/dist/{init-FZ3GG53E.js → init-YK6YRTOT.js} +102 -6
  14. package/dist/{install-I3GOS56Q.js → install-Q4PWEU43.js} +4 -4
  15. package/dist/{mcp-DLS3J6QJ.js → mcp-TBEDYELW.js} +4 -4
  16. package/dist/memory-7RM67ZLS.js +668 -0
  17. package/dist/postinstall.js +1 -1
  18. package/dist/{session-E3CZJJZQ.js → session-H4XW2WXH.js} +1 -1
  19. package/dist/{statusline-6HQCDWBD.js → statusline-6Y2EBAFQ.js} +1 -1
  20. package/dist/{uninstall-JN5YIKKM.js → uninstall-3PSUDGI4.js} +3 -3
  21. package/dist/{update-OWS4IJTG.js → update-WL6SFGGO.js} +4 -4
  22. package/lib/cf-memory/CHANGELOG.md +15 -0
  23. package/lib/cf-memory/README.md +284 -0
  24. package/lib/cf-memory/package-lock.json +2790 -0
  25. package/lib/cf-memory/package.json +31 -0
  26. package/lib/cf-memory/scripts/migrate-frontmatter.ts +134 -0
  27. package/lib/cf-memory/src/__tests__/daemon-e2e.test.ts +223 -0
  28. package/lib/cf-memory/src/__tests__/daemon.test.ts +407 -0
  29. package/lib/cf-memory/src/__tests__/dedup.test.ts +103 -0
  30. package/lib/cf-memory/src/__tests__/embeddings.test.ts +292 -0
  31. package/lib/cf-memory/src/__tests__/lazy-install.test.ts +210 -0
  32. package/lib/cf-memory/src/__tests__/markdown-backend.test.ts +410 -0
  33. package/lib/cf-memory/src/__tests__/migration.test.ts +255 -0
  34. package/lib/cf-memory/src/__tests__/migrations.test.ts +288 -0
  35. package/lib/cf-memory/src/__tests__/minisearch-backend.test.ts +262 -0
  36. package/lib/cf-memory/src/__tests__/ollama.test.ts +48 -0
  37. package/lib/cf-memory/src/__tests__/schema.test.ts +128 -0
  38. package/lib/cf-memory/src/__tests__/search.test.ts +115 -0
  39. package/lib/cf-memory/src/__tests__/temporal-decay.test.ts +54 -0
  40. package/lib/cf-memory/src/__tests__/tier.test.ts +293 -0
  41. package/lib/cf-memory/src/__tests__/tools.test.ts +83 -0
  42. package/lib/cf-memory/src/backends/markdown.ts +318 -0
  43. package/lib/cf-memory/src/backends/minisearch.ts +203 -0
  44. package/lib/cf-memory/src/backends/sqlite/embeddings.ts +286 -0
  45. package/lib/cf-memory/src/backends/sqlite/index.ts +549 -0
  46. package/lib/cf-memory/src/backends/sqlite/migrations.ts +188 -0
  47. package/lib/cf-memory/src/backends/sqlite/schema.ts +120 -0
  48. package/lib/cf-memory/src/backends/sqlite/search.ts +296 -0
  49. package/lib/cf-memory/src/bin/cf-memory.ts +2 -0
  50. package/lib/cf-memory/src/daemon/entry.ts +99 -0
  51. package/lib/cf-memory/src/daemon/process.ts +220 -0
  52. package/lib/cf-memory/src/daemon/server.ts +166 -0
  53. package/lib/cf-memory/src/daemon/watcher.ts +90 -0
  54. package/lib/cf-memory/src/index.ts +45 -0
  55. package/lib/cf-memory/src/lib/backend.ts +23 -0
  56. package/lib/cf-memory/src/lib/daemon-client.ts +163 -0
  57. package/lib/cf-memory/src/lib/dedup.ts +80 -0
  58. package/lib/cf-memory/src/lib/lazy-install.ts +274 -0
  59. package/lib/cf-memory/src/lib/ollama.ts +76 -0
  60. package/lib/cf-memory/src/lib/temporal-decay.ts +19 -0
  61. package/lib/cf-memory/src/lib/tier.ts +107 -0
  62. package/lib/cf-memory/src/lib/types.ts +109 -0
  63. package/lib/cf-memory/src/resources/index.ts +62 -0
  64. package/lib/cf-memory/src/server.ts +20 -0
  65. package/lib/cf-memory/src/tools/delete.ts +38 -0
  66. package/lib/cf-memory/src/tools/list.ts +38 -0
  67. package/lib/cf-memory/src/tools/retrieve.ts +52 -0
  68. package/lib/cf-memory/src/tools/search.ts +47 -0
  69. package/lib/cf-memory/src/tools/store.ts +70 -0
  70. package/lib/cf-memory/src/tools/update.ts +62 -0
  71. package/lib/cf-memory/tsconfig.json +15 -0
  72. package/lib/cf-memory/vitest.config.ts +7 -0
  73. package/lib/learn-host/CHANGELOG.md +4 -0
  74. package/lib/learn-host/package.json +1 -1
  75. package/package.json +1 -1
@@ -0,0 +1,288 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ getSchemaVersion,
4
+ migrate,
5
+ applyPragmas,
6
+ getMetadata,
7
+ setMetadata,
8
+ checkEmbeddingMismatch,
9
+ type DatabaseLike,
10
+ } from "../backends/sqlite/migrations.js";
11
+ import { SCHEMA_VERSION } from "../backends/sqlite/schema.js";
12
+ import { DEFAULT_TRANSFORMERS_MODEL } from "../backends/sqlite/embeddings.js";
13
+
14
+ /**
15
+ * In-memory mock database for testing schema logic without better-sqlite3.
16
+ *
17
+ * Tracks SQL statements and supports basic prepare/get/run.
18
+ * Note: The `exec` method here is better-sqlite3's SQL execution,
19
+ * NOT child_process.exec — there is no shell involvement.
20
+ */
21
+ function createMockDb(): DatabaseLike & {
22
+ statements: string[];
23
+ tables: Map<string, Array<Record<string, unknown>>>;
24
+ } {
25
+ const statements: string[] = [];
26
+ const tables = new Map<string, Array<Record<string, unknown>>>();
27
+
28
+ return {
29
+ statements,
30
+ tables,
31
+ // better-sqlite3 Database.exec() — runs SQL, not shell commands
32
+ exec(sql: string) {
33
+ statements.push(sql);
34
+
35
+ // Simulate table creation and data insertion
36
+ if (sql.includes("INSERT INTO schema_version")) {
37
+ const match = sql.match(/VALUES\s*\((\d+)\)/);
38
+ if (match) {
39
+ tables.set("schema_version", [{ version: parseInt(match[1], 10) }]);
40
+ }
41
+ }
42
+ },
43
+ pragma(pragma: string) {
44
+ statements.push(`PRAGMA ${pragma}`);
45
+ return undefined;
46
+ },
47
+ prepare(sql: string) {
48
+ return {
49
+ get(..._params: unknown[]) {
50
+ if (sql.includes("schema_version")) {
51
+ const rows = tables.get("schema_version");
52
+ return rows?.[0];
53
+ }
54
+ return undefined;
55
+ },
56
+ run(...params: unknown[]) {
57
+ statements.push(sql);
58
+ // Simulate schema_version UPDATE from migrations
59
+ if (sql.includes("UPDATE schema_version SET version")) {
60
+ const version = params[0] as number;
61
+ tables.set("schema_version", [{ version }]);
62
+ }
63
+ return {};
64
+ },
65
+ };
66
+ },
67
+ };
68
+ }
69
+
70
+ describe("getSchemaVersion()", () => {
71
+ it("returns 0 when table doesn't exist", () => {
72
+ const db = createMockDb();
73
+ // Override prepare to throw (simulating missing table)
74
+ db.prepare = () => ({
75
+ get() {
76
+ throw new Error("no such table: schema_version");
77
+ },
78
+ run() {
79
+ return {};
80
+ },
81
+ });
82
+
83
+ expect(getSchemaVersion(db)).toBe(0);
84
+ });
85
+
86
+ it("returns version from schema_version table", () => {
87
+ const db = createMockDb();
88
+ db.tables.set("schema_version", [{ version: 1 }]);
89
+ expect(getSchemaVersion(db)).toBe(1);
90
+ });
91
+ });
92
+
93
+ describe("applyPragmas()", () => {
94
+ it("runs all PRAGMA settings", () => {
95
+ const db = createMockDb();
96
+ applyPragmas(db);
97
+
98
+ expect(db.statements.some((s) => s.includes("journal_mode"))).toBe(true);
99
+ expect(db.statements.some((s) => s.includes("synchronous"))).toBe(true);
100
+ expect(db.statements.some((s) => s.includes("cache_size"))).toBe(true);
101
+ });
102
+ });
103
+
104
+ describe("migrate()", () => {
105
+ it("applies schema from version 0", () => {
106
+ const db = createMockDb();
107
+ const result = migrate(db);
108
+
109
+ expect(result.version).toBe(SCHEMA_VERSION);
110
+ expect(result.migrated).toBe(true);
111
+
112
+ // Should have run schema creation SQL
113
+ expect(db.statements.some((s) => s.includes("CREATE TABLE"))).toBe(true);
114
+ expect(db.statements.some((s) => s.includes("memories_fts"))).toBe(true);
115
+ });
116
+
117
+ it("is idempotent — running twice produces same result", () => {
118
+ const db = createMockDb();
119
+
120
+ const first = migrate(db);
121
+ expect(first.migrated).toBe(true);
122
+
123
+ // Now schema_version exists with version 1
124
+ const second = migrate(db);
125
+ expect(second.version).toBe(SCHEMA_VERSION);
126
+ expect(second.migrated).toBe(false);
127
+ });
128
+
129
+ it("skips migration when already at current version", () => {
130
+ const db = createMockDb();
131
+ db.tables.set("schema_version", [{ version: SCHEMA_VERSION }]);
132
+
133
+ const result = migrate(db);
134
+ expect(result.migrated).toBe(false);
135
+ expect(result.version).toBe(SCHEMA_VERSION);
136
+ });
137
+
138
+ it("migrates from v1 to v2 creating metadata table", () => {
139
+ const db = createMockDb();
140
+ // Simulate existing v1 database
141
+ db.tables.set("schema_version", [{ version: 1 }]);
142
+
143
+ const result = migrate(db);
144
+ expect(result.version).toBe(SCHEMA_VERSION);
145
+ expect(result.migrated).toBe(true);
146
+
147
+ // Should have created metadata table
148
+ expect(
149
+ db.statements.some((s) =>
150
+ s.includes("CREATE TABLE IF NOT EXISTS metadata"),
151
+ ),
152
+ ).toBe(true);
153
+ });
154
+ });
155
+
156
+ /**
157
+ * Enhanced mock database that supports metadata table operations.
158
+ */
159
+ function createMetadataMockDb(): DatabaseLike & {
160
+ metadata: Map<string, string>;
161
+ } {
162
+ const metadata = new Map<string, string>();
163
+
164
+ return {
165
+ metadata,
166
+ // better-sqlite3 Database.exec() - runs SQL, not shell commands
167
+ exec(_sql: string) {
168
+ // Handle metadata table creation (no-op in mock)
169
+ },
170
+ pragma() {
171
+ return undefined;
172
+ },
173
+ prepare(sql: string) {
174
+ return {
175
+ get(...params: unknown[]) {
176
+ if (sql.includes("SELECT value FROM metadata WHERE key = ?")) {
177
+ const key = params[0] as string;
178
+ const val = metadata.get(key);
179
+ return val !== undefined ? { value: val } : undefined;
180
+ }
181
+ return undefined;
182
+ },
183
+ run(...params: unknown[]) {
184
+ if (sql.includes("INSERT OR REPLACE INTO metadata")) {
185
+ const key = params[0] as string;
186
+ const value = params[1] as string;
187
+ metadata.set(key, value);
188
+ }
189
+ return {};
190
+ },
191
+ };
192
+ },
193
+ };
194
+ }
195
+
196
+ describe("getMetadata() and setMetadata()", () => {
197
+ it("returns null for missing key", () => {
198
+ const db = createMetadataMockDb();
199
+ expect(getMetadata(db, "nonexistent")).toBeNull();
200
+ });
201
+
202
+ it("stores and retrieves a value", () => {
203
+ const db = createMetadataMockDb();
204
+ setMetadata(db, "embedding_model", "nomic-embed-text");
205
+ expect(getMetadata(db, "embedding_model")).toBe("nomic-embed-text");
206
+ });
207
+
208
+ it("overwrites existing value", () => {
209
+ const db = createMetadataMockDb();
210
+ setMetadata(db, "embedding_dims", "384");
211
+ setMetadata(db, "embedding_dims", "768");
212
+ expect(getMetadata(db, "embedding_dims")).toBe("768");
213
+ });
214
+ });
215
+
216
+ describe("checkEmbeddingMismatch()", () => {
217
+ it("returns mismatched=false when model and dims match", () => {
218
+ const db = createMetadataMockDb();
219
+ setMetadata(db, "embedding_model", DEFAULT_TRANSFORMERS_MODEL);
220
+ setMetadata(db, "embedding_dims", "384");
221
+
222
+ const result = checkEmbeddingMismatch(db, DEFAULT_TRANSFORMERS_MODEL, 384);
223
+ expect(result.mismatched).toBe(false);
224
+ expect(result.storedModel).toBe(DEFAULT_TRANSFORMERS_MODEL);
225
+ expect(result.storedDims).toBe(384);
226
+ expect(result.currentModel).toBe(DEFAULT_TRANSFORMERS_MODEL);
227
+ expect(result.currentDims).toBe(384);
228
+ });
229
+
230
+ it("returns mismatched=true when model changed", () => {
231
+ const db = createMetadataMockDb();
232
+ setMetadata(db, "embedding_model", DEFAULT_TRANSFORMERS_MODEL);
233
+ setMetadata(db, "embedding_dims", "384");
234
+
235
+ const result = checkEmbeddingMismatch(db, "nomic-embed-text", 768);
236
+ expect(result.mismatched).toBe(true);
237
+ expect(result.storedModel).toBe(DEFAULT_TRANSFORMERS_MODEL);
238
+ expect(result.storedDims).toBe(384);
239
+ expect(result.currentModel).toBe("nomic-embed-text");
240
+ expect(result.currentDims).toBe(768);
241
+ });
242
+
243
+ it("returns mismatched=true when only dims changed", () => {
244
+ const db = createMetadataMockDb();
245
+ setMetadata(db, "embedding_model", "custom-model");
246
+ setMetadata(db, "embedding_dims", "384");
247
+
248
+ const result = checkEmbeddingMismatch(db, "custom-model", 768);
249
+ expect(result.mismatched).toBe(true);
250
+ });
251
+
252
+ it("returns mismatched=false when no metadata exists (fresh db)", () => {
253
+ const db = createMetadataMockDb();
254
+ // No metadata set -- fresh database
255
+ const result = checkEmbeddingMismatch(db, "all-MiniLM-L6-v2", 384);
256
+ expect(result.mismatched).toBe(false);
257
+ });
258
+
259
+ it("returns mismatched=false when only partial metadata exists", () => {
260
+ const db = createMetadataMockDb();
261
+ // Only model set, no dims -- treat as fresh
262
+ setMetadata(db, "embedding_model", DEFAULT_TRANSFORMERS_MODEL);
263
+ const result = checkEmbeddingMismatch(db, DEFAULT_TRANSFORMERS_MODEL, 384);
264
+ expect(result.mismatched).toBe(false);
265
+ });
266
+
267
+ it("v1→v2 migration default matches DEFAULT_TRANSFORMERS_MODEL (no false positive)", () => {
268
+ // Regression: migration used to store "all-MiniLM-L6-v2" but pipeline
269
+ // resolves to "Xenova/all-MiniLM-L6-v2", causing a spurious mismatch warning
270
+ const db = createMockDb();
271
+ // Simulate fresh v0 database → migrate to v2
272
+ migrate(db);
273
+
274
+ // The migration should have stored the full qualified model name
275
+ // that matches what EmbeddingPipeline.modelName returns
276
+ const metaDb = createMetadataMockDb();
277
+ // Simulate what migrateV1ToV2 stores
278
+ setMetadata(metaDb, "embedding_model", "Xenova/all-MiniLM-L6-v2");
279
+ setMetadata(metaDb, "embedding_dims", "384");
280
+
281
+ const result = checkEmbeddingMismatch(
282
+ metaDb,
283
+ DEFAULT_TRANSFORMERS_MODEL,
284
+ 384,
285
+ );
286
+ expect(result.mismatched).toBe(false);
287
+ });
288
+ });
@@ -0,0 +1,262 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { mkdirSync, rmSync } from "fs";
3
+ import { join } from "path";
4
+ import { tmpdir } from "os";
5
+ import { MiniSearchBackend } from "../backends/minisearch.js";
6
+ import type { StoreInput } from "../lib/types.js";
7
+
8
+ let testDir: string;
9
+ let backend: MiniSearchBackend;
10
+ let counter = 0;
11
+
12
+ beforeEach(() => {
13
+ testDir = join(tmpdir(), `cf-memory-mini-test-${Date.now()}-${++counter}`);
14
+ mkdirSync(testDir, { recursive: true });
15
+ backend = new MiniSearchBackend(testDir);
16
+ });
17
+
18
+ afterEach(() => {
19
+ rmSync(testDir, { recursive: true, force: true });
20
+ });
21
+
22
+ const sampleInput: StoreInput = {
23
+ title: "API Authentication Pattern",
24
+ description: "Auth module uses JWT tokens stored in httpOnly cookies",
25
+ type: "fact",
26
+ tags: ["auth", "jwt", "api"],
27
+ content: "# API Auth\n\nThe project uses JWT tokens with refresh rotation.",
28
+ };
29
+
30
+ describe("MiniSearchBackend", () => {
31
+ describe("store()", () => {
32
+ it("stores a memory and indexes it", async () => {
33
+ const memory = await backend.store(sampleInput);
34
+ expect(memory.id).toBe("features/api-authentication-pattern");
35
+ expect(memory.frontmatter.title).toBe("API Authentication Pattern");
36
+
37
+ // Verify it's searchable
38
+ const results = await backend.search({ query: "authentication" });
39
+ expect(results.length).toBeGreaterThan(0);
40
+ expect(results[0].memory.frontmatter.title).toContain("Authentication");
41
+ });
42
+ });
43
+
44
+ describe("search()", () => {
45
+ beforeEach(async () => {
46
+ await backend.store(sampleInput);
47
+ await backend.store({
48
+ title: "Database Migration Guide",
49
+ description: "How to run database migrations with Prisma",
50
+ type: "procedure",
51
+ tags: ["database", "prisma", "migration"],
52
+ content: "# Migrations\n\nRun npx prisma migrate dev.",
53
+ });
54
+ await backend.store({
55
+ title: "CORS Bug Fix",
56
+ description: "Fixed CORS issue on /api/upload endpoint",
57
+ type: "episode",
58
+ tags: ["cors", "api", "bug"],
59
+ content: "# CORS Fix\n\nAdded missing Origin header to allowlist.",
60
+ });
61
+ });
62
+
63
+ it("finds by title match", async () => {
64
+ const results = await backend.search({ query: "authentication" });
65
+ expect(results.length).toBeGreaterThan(0);
66
+ expect(results[0].memory.frontmatter.title).toContain("Authentication");
67
+ });
68
+
69
+ it("finds by tag match", async () => {
70
+ const results = await backend.search({ query: "prisma" });
71
+ expect(results.length).toBeGreaterThan(0);
72
+ expect(results[0].memory.frontmatter.title).toBe(
73
+ "Database Migration Guide",
74
+ );
75
+ });
76
+
77
+ it("finds by content match", async () => {
78
+ const results = await backend.search({ query: "allowlist" });
79
+ expect(results.length).toBeGreaterThan(0);
80
+ expect(results[0].memory.frontmatter.title).toContain("CORS");
81
+ });
82
+
83
+ it("fuzzy search finds results that substring grep misses", async () => {
84
+ // Typo: "autentication" (missing 'h')
85
+ const results = await backend.search({ query: "autentication" });
86
+ expect(results.length).toBeGreaterThan(0);
87
+ expect(results[0].memory.frontmatter.title).toContain("Authentication");
88
+ });
89
+
90
+ it("BM25 ranking: both exact and partial find the right result", async () => {
91
+ const exactResults = await backend.search({ query: "authentication" });
92
+ const partialResults = await backend.search({ query: "auth" });
93
+
94
+ // Both should find the auth memory
95
+ expect(exactResults.length).toBeGreaterThan(0);
96
+ expect(partialResults.length).toBeGreaterThan(0);
97
+
98
+ const exactHit = exactResults.find((r) =>
99
+ r.memory.frontmatter.title.includes("Authentication"),
100
+ );
101
+ const partialHit = partialResults.find((r) =>
102
+ r.memory.frontmatter.title.includes("Authentication"),
103
+ );
104
+
105
+ expect(exactHit).toBeDefined();
106
+ expect(partialHit).toBeDefined();
107
+ // Both should have positive scores
108
+ expect(exactHit!.score).toBeGreaterThan(0);
109
+ expect(partialHit!.score).toBeGreaterThan(0);
110
+ });
111
+
112
+ it("boost: title/tags matches rank above content-only matches", async () => {
113
+ // "api" is in both title/tags of CORS and content of auth
114
+ const results = await backend.search({ query: "api" });
115
+ expect(results.length).toBeGreaterThanOrEqual(2);
116
+ // The first result should have matched on title or tags, not just content
117
+ expect(results[0].matchedOn.length).toBeGreaterThan(0);
118
+ });
119
+
120
+ it("filters by type", async () => {
121
+ const results = await backend.search({
122
+ query: "api",
123
+ type: "episode",
124
+ });
125
+ for (const r of results) {
126
+ expect(r.memory.frontmatter.type).toBe("episode");
127
+ }
128
+ });
129
+
130
+ it("filters by tags", async () => {
131
+ const results = await backend.search({
132
+ query: "api",
133
+ tags: ["cors"],
134
+ });
135
+ expect(results.length).toBe(1);
136
+ expect(results[0].memory.frontmatter.title).toContain("CORS");
137
+ });
138
+
139
+ it("respects limit", async () => {
140
+ const results = await backend.search({ query: "api", limit: 1 });
141
+ expect(results.length).toBe(1);
142
+ });
143
+
144
+ it("returns empty for no matches", async () => {
145
+ const results = await backend.search({
146
+ query: "zzz_nonexistent_zzz",
147
+ });
148
+ expect(results).toEqual([]);
149
+ });
150
+
151
+ it("empty query returns list results", async () => {
152
+ const results = await backend.search({ query: "" });
153
+ expect(results.length).toBe(3);
154
+ });
155
+ });
156
+
157
+ describe("index rebuild after mutations", () => {
158
+ it("search finds newly stored memory", async () => {
159
+ await backend.store(sampleInput);
160
+
161
+ const before = await backend.search({ query: "migration" });
162
+ expect(before.length).toBe(0);
163
+
164
+ await backend.store({
165
+ title: "DB Migration",
166
+ description: "How to migrate",
167
+ type: "procedure",
168
+ tags: ["db"],
169
+ content: "Migration steps.",
170
+ });
171
+
172
+ const after = await backend.search({ query: "migration" });
173
+ expect(after.length).toBeGreaterThan(0);
174
+ });
175
+
176
+ it("search reflects updates", async () => {
177
+ const memory = await backend.store(sampleInput);
178
+
179
+ await backend.update({
180
+ id: memory.id,
181
+ title: "Updated Authentication Pattern v2",
182
+ });
183
+
184
+ const results = await backend.search({ query: "v2" });
185
+ expect(results.length).toBeGreaterThan(0);
186
+ expect(results[0].memory.frontmatter.title).toContain("v2");
187
+ });
188
+
189
+ it("search excludes deleted memory", async () => {
190
+ const memory = await backend.store(sampleInput);
191
+
192
+ const before = await backend.search({ query: "authentication" });
193
+ expect(before.length).toBe(1);
194
+
195
+ await backend.delete(memory.id);
196
+
197
+ const after = await backend.search({ query: "authentication" });
198
+ expect(after.length).toBe(0);
199
+ });
200
+
201
+ it("rebuild() refreshes the full index", async () => {
202
+ await backend.store(sampleInput);
203
+ await backend.store({
204
+ title: "Second Memory",
205
+ description: "Test",
206
+ type: "fact",
207
+ tags: [],
208
+ content: "Content",
209
+ });
210
+
211
+ await backend.rebuild();
212
+
213
+ const results = await backend.search({ query: "authentication" });
214
+ expect(results.length).toBe(1);
215
+ });
216
+ });
217
+
218
+ describe("CRUD cycle", () => {
219
+ it("store → retrieve → update → delete", async () => {
220
+ const uniqueInput = {
221
+ ...sampleInput,
222
+ title: `CRUD Test Memory ${Date.now()}`,
223
+ };
224
+ const stored = await backend.store(uniqueInput);
225
+ expect(stored.id).toBeTruthy();
226
+
227
+ const retrieved = await backend.retrieve(stored.id);
228
+ expect(retrieved).not.toBeNull();
229
+ expect(retrieved!.frontmatter.title).toBe(uniqueInput.title);
230
+
231
+ const updated = await backend.update({
232
+ id: stored.id,
233
+ title: "Updated Auth",
234
+ });
235
+ expect(updated!.frontmatter.title).toBe("Updated Auth");
236
+
237
+ const deleted = await backend.delete(stored.id);
238
+ expect(deleted).toBe(true);
239
+
240
+ const gone = await backend.retrieve(stored.id);
241
+ expect(gone).toBeNull();
242
+ });
243
+ });
244
+
245
+ describe("stats()", () => {
246
+ it("returns correct counts", async () => {
247
+ await backend.store(sampleInput);
248
+ await backend.store({
249
+ title: "A Bug",
250
+ description: "Bug fix",
251
+ type: "episode",
252
+ tags: [],
253
+ content: "Bug",
254
+ });
255
+
256
+ const stats = await backend.stats();
257
+ expect(stats.total).toBe(2);
258
+ expect(stats.byType.fact).toBe(1);
259
+ expect(stats.byType.episode).toBe(1);
260
+ });
261
+ });
262
+ });
@@ -0,0 +1,48 @@
1
+ import { describe, it, expect, vi, afterEach } from "vitest";
2
+ import {
3
+ isOllamaRunning,
4
+ listOllamaModels,
5
+ hasOllamaEmbeddingModel,
6
+ detectEmbeddingProvider,
7
+ } from "../lib/ollama.js";
8
+
9
+ afterEach(() => {
10
+ vi.restoreAllMocks();
11
+ });
12
+
13
+ describe("isOllamaRunning()", () => {
14
+ it("returns false when Ollama is not running", async () => {
15
+ // Use a port that's very unlikely to be in use
16
+ const result = await isOllamaRunning("http://localhost:19999");
17
+ expect(result).toBe(false);
18
+ });
19
+
20
+ it("handles network errors gracefully", async () => {
21
+ const result = await isOllamaRunning("http://invalid-host-xyz:11434");
22
+ expect(result).toBe(false);
23
+ });
24
+ });
25
+
26
+ describe("listOllamaModels()", () => {
27
+ it("returns empty array when Ollama is not running", async () => {
28
+ const models = await listOllamaModels("http://localhost:19999");
29
+ expect(models).toEqual([]);
30
+ });
31
+ });
32
+
33
+ describe("hasOllamaEmbeddingModel()", () => {
34
+ it("returns false when Ollama is not running", async () => {
35
+ const has = await hasOllamaEmbeddingModel(
36
+ "all-minilm:l6-v2",
37
+ "http://localhost:19999",
38
+ );
39
+ expect(has).toBe(false);
40
+ });
41
+ });
42
+
43
+ describe("detectEmbeddingProvider()", () => {
44
+ it("falls back to transformers when Ollama is not available", async () => {
45
+ const result = await detectEmbeddingProvider("http://localhost:19999");
46
+ expect(result.provider).toBe("transformers");
47
+ });
48
+ });