coding-friend-cli 1.36.4 → 1.37.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.
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Regression tests for lazy DB creation in SqliteBackend.
3
+ *
4
+ * Verifies that constructing a SqliteBackend (or performing read operations)
5
+ * on a project with no existing memories does NOT create the DB file or dir,
6
+ * and that the first write (store) creates the DB and the item is findable.
7
+ */
8
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
9
+ import { mkdirSync, rmSync, existsSync } from "fs";
10
+ import { join } from "path";
11
+ import { tmpdir } from "os";
12
+ import { areSqliteDepsAvailable } from "../lib/lazy-install.js";
13
+ import { SqliteBackend } from "../backends/sqlite/index.js";
14
+ import { createBackendForTier } from "../lib/tier.js";
15
+ import type { StoreInput } from "../lib/types.js";
16
+
17
+ let testDir: string;
18
+ let dbPath: string;
19
+ let docsDir: string;
20
+ let counter = 0;
21
+
22
+ beforeEach(() => {
23
+ testDir = join(
24
+ tmpdir(),
25
+ `cf-memory-lazy-db-test-${Date.now()}-${++counter}`,
26
+ );
27
+ mkdirSync(testDir, { recursive: true });
28
+ docsDir = join(testDir, "docs", "memory");
29
+ dbPath = join(testDir, "db.sqlite");
30
+ });
31
+
32
+ afterEach(() => {
33
+ rmSync(testDir, { recursive: true, force: true });
34
+ });
35
+
36
+ const sampleInput: StoreInput = {
37
+ title: "Lazy DB Test Memory",
38
+ description: "Created to test lazy DB init",
39
+ type: "fact",
40
+ tags: ["test"],
41
+ content: "# Lazy DB\n\nThis memory tests deferred DB creation.",
42
+ };
43
+
44
+ const depsAvailable = areSqliteDepsAvailable();
45
+
46
+ describe("SqliteBackend — lazy DB creation", () => {
47
+ it.skipIf(!depsAvailable)(
48
+ "(a) constructing the backend does NOT create the DB file",
49
+ () => {
50
+ new SqliteBackend(docsDir, { dbPath, skipVec: true });
51
+
52
+ expect(existsSync(dbPath)).toBe(false);
53
+ },
54
+ );
55
+
56
+ it.skipIf(!depsAvailable)(
57
+ "(b) search() on empty backend returns [] without creating the DB",
58
+ async () => {
59
+ const backend = new SqliteBackend(docsDir, { dbPath, skipVec: true });
60
+
61
+ const results = await backend.search({ query: "anything" });
62
+
63
+ expect(results).toEqual([]);
64
+ expect(existsSync(dbPath)).toBe(false);
65
+
66
+ await backend.close();
67
+ },
68
+ );
69
+
70
+ it.skipIf(!depsAvailable)(
71
+ "(b) list() on empty backend returns [] without creating the DB",
72
+ async () => {
73
+ const backend = new SqliteBackend(docsDir, { dbPath, skipVec: true });
74
+
75
+ const metas = await backend.list({});
76
+
77
+ expect(metas).toEqual([]);
78
+ expect(existsSync(dbPath)).toBe(false);
79
+
80
+ await backend.close();
81
+ },
82
+ );
83
+
84
+ it.skipIf(!depsAvailable)(
85
+ "(b) stats() on empty backend returns zeroed stats without creating the DB",
86
+ async () => {
87
+ const backend = new SqliteBackend(docsDir, { dbPath, skipVec: true });
88
+
89
+ const stats = await backend.stats();
90
+
91
+ expect(stats).toEqual({ total: 0, byCategory: {}, byType: {} });
92
+ expect(existsSync(dbPath)).toBe(false);
93
+
94
+ await backend.close();
95
+ },
96
+ );
97
+
98
+ it.skipIf(!depsAvailable)(
99
+ "(c) after store(), DB file exists and search() finds the item",
100
+ async () => {
101
+ const backend = new SqliteBackend(docsDir, { dbPath, skipVec: true });
102
+
103
+ // DB should not exist before write
104
+ expect(existsSync(dbPath)).toBe(false);
105
+
106
+ await backend.store(sampleInput);
107
+
108
+ // DB should now exist
109
+ expect(existsSync(dbPath)).toBe(true);
110
+
111
+ // Item should be findable via empty-query search (routes through list)
112
+ const results = await backend.search({ query: "" });
113
+ expect(results.length).toBe(1);
114
+ expect(results[0].memory.frontmatter.title).toBe("Lazy DB Test Memory");
115
+
116
+ await backend.close();
117
+ },
118
+ );
119
+
120
+ it.skipIf(!depsAvailable)(
121
+ "close() on a never-opened backend does not throw",
122
+ async () => {
123
+ const backend = new SqliteBackend(docsDir, { dbPath, skipVec: true });
124
+
125
+ await expect(backend.close()).resolves.toBeUndefined();
126
+ expect(existsSync(dbPath)).toBe(false);
127
+ },
128
+ );
129
+
130
+ it.skipIf(!depsAvailable)(
131
+ "getDbPath() returns the computed path without touching the filesystem",
132
+ () => {
133
+ const backend = new SqliteBackend(docsDir, { dbPath, skipVec: true });
134
+
135
+ expect(backend.getDbPath()).toBe(dbPath);
136
+ expect(existsSync(dbPath)).toBe(false);
137
+ },
138
+ );
139
+
140
+ it.skipIf(!depsAvailable)(
141
+ "delete(nonexistentId) returns false and does not create the DB",
142
+ async () => {
143
+ const backend = new SqliteBackend(docsDir, { dbPath, skipVec: true });
144
+
145
+ const result = await backend.delete("nonexistent/id");
146
+
147
+ expect(result).toBe(false);
148
+ expect(existsSync(dbPath)).toBe(false);
149
+
150
+ await backend.close();
151
+ },
152
+ );
153
+
154
+ it.skipIf(!depsAvailable)(
155
+ "update(nonexistentId) returns null and does not create the DB",
156
+ async () => {
157
+ const backend = new SqliteBackend(docsDir, { dbPath, skipVec: true });
158
+
159
+ const result = await backend.update({ id: "nonexistent/id", title: "New Title" });
160
+
161
+ expect(result).toBeNull();
162
+ expect(existsSync(dbPath)).toBe(false);
163
+
164
+ await backend.close();
165
+ },
166
+ );
167
+
168
+ it.skipIf(!depsAvailable)(
169
+ "rebuild() on empty docs/memory does not create the DB file",
170
+ async () => {
171
+ // docsDir does not exist (no memories)
172
+ const backend = new SqliteBackend(docsDir, { dbPath, skipVec: true });
173
+
174
+ await backend.rebuild();
175
+
176
+ expect(existsSync(dbPath)).toBe(false);
177
+
178
+ await backend.close();
179
+ },
180
+ );
181
+ });
182
+
183
+ describe("SqliteBackend — lazy DB via createBackendForTier (integration)", () => {
184
+ it.skipIf(!depsAvailable)(
185
+ "createBackendForTier 'full' on empty project does not create DB file before or after read ops",
186
+ async () => {
187
+ // Use explicit dbPath so we do not pollute ~/.coding-friend
188
+ const { backend, tier } = await createBackendForTier(
189
+ docsDir,
190
+ "full",
191
+ undefined,
192
+ { dbPath },
193
+ );
194
+
195
+ expect(tier.name).toBe("full");
196
+
197
+ // (1) No DB file after construction
198
+ expect(existsSync(dbPath)).toBe(false);
199
+
200
+ // (2) Read ops must not create the DB file
201
+ await backend.list({});
202
+ expect(existsSync(dbPath)).toBe(false);
203
+
204
+ await backend.search({ query: "anything" });
205
+ expect(existsSync(dbPath)).toBe(false);
206
+
207
+ await backend.stats();
208
+ expect(existsSync(dbPath)).toBe(false);
209
+
210
+ await backend.close();
211
+ },
212
+ );
213
+ });
@@ -1,11 +1,12 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import { mkdirSync, rmSync } from "fs";
2
+ import { mkdirSync, rmSync, existsSync } from "fs";
3
3
  import { join } from "path";
4
4
  import { tmpdir } from "os";
5
5
  import { detectTier, createBackendForTier, TIERS } from "../lib/tier.js";
6
6
  import { MarkdownBackend } from "../backends/markdown.js";
7
7
  import { DaemonClient } from "../lib/daemon-client.js";
8
8
  import type { DaemonPaths } from "../daemon/process.js";
9
+ import { areSqliteDepsAvailable } from "../lib/lazy-install.js";
9
10
 
10
11
  let testDir: string;
11
12
 
@@ -252,6 +253,26 @@ describe("createBackendForTier()", () => {
252
253
  });
253
254
  });
254
255
 
256
+ describe("createBackendForTier() — lazy DB guarantee", () => {
257
+ it.skipIf(!areSqliteDepsAvailable())(
258
+ "full tier on a non-CF project does not create DB before any write",
259
+ async () => {
260
+ // Use an explicit tmpdir dbPath so ~/.coding-friend is never touched
261
+ const dbPath = join(testDir, "db.sqlite");
262
+ const { backend, tier } = await createBackendForTier(
263
+ testDir,
264
+ "full",
265
+ undefined,
266
+ { dbPath },
267
+ );
268
+ expect(tier.name).toBe("full");
269
+ // No DB file after construction
270
+ expect(existsSync(dbPath)).toBe(false);
271
+ await backend.close();
272
+ },
273
+ );
274
+ });
275
+
255
276
  describe("createBackendForTier() with embeddingConfig", () => {
256
277
  it("accepts an optional embeddingConfig parameter without error", async () => {
257
278
  const embeddingConfig = {