coding-friend-cli 1.37.0 → 1.38.1
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/README.md +7 -2
- package/dist/{chunk-GZT3YPAP.js → chunk-32UAW3NX.js} +1 -1
- package/dist/chunk-5QCGEPQO.js +238 -0
- package/dist/{chunk-YIXAQ4R2.js → chunk-6NXZKAOV.js} +1 -1
- package/dist/{chunk-DP6DFQ4G.js → chunk-BXSCYGWE.js} +2 -2
- package/dist/{chunk-W6D3J5DX.js → chunk-CGEMETKQ.js} +8 -6
- package/dist/{chunk-XEKQW4RN.js → chunk-ETTZRJ55.js} +1 -1
- package/dist/{chunk-IKLMHJH6.js → chunk-GIH6E7GJ.js} +62 -1
- package/dist/{chunk-6S7352JG.js → chunk-ONG4KRYP.js} +1 -1
- package/dist/{chunk-X2H7JJMR.js → chunk-PLSJW7QJ.js} +29 -1
- package/dist/{chunk-O5ZKLFSL.js → chunk-RCE4OWNX.js} +1 -1
- package/dist/{chunk-PI4P34KF.js → chunk-RFNKPAJW.js} +3 -1
- package/dist/{chunk-676YCW6T.js → chunk-XAYWGZQT.js} +101 -8
- package/dist/{clean-WEYQZB34.js → clean-YWEMYDE5.js} +2 -2
- package/dist/{config-ZMR2AOBB.js → config-NG7SICM4.js} +12 -24
- package/dist/{dev-IGHMMEP4.js → dev-PTBNMKZT.js} +3 -3
- package/dist/{disable-HL5OEURT.js → disable-MO7AXYYF.js} +20 -3
- package/dist/{enable-CZIOCKKJ.js → enable-4GXLQEHD.js} +20 -3
- package/dist/{guide-CO2V4ZLZ.js → guide-HCR43LJN.js} +1 -1
- package/dist/{host-SSRI4JZV.js → host-EV6QIVTI.js} +2 -2
- package/dist/index.js +59 -47
- package/dist/{init-6MFQWPOP.js → init-KYE5CJMW.js} +115 -12
- package/dist/{install-37NN4CQH.js → install-3YEPLDMD.js} +79 -6
- package/dist/{learn-OPH3CI67.js → learn-MC4VDHZH.js} +2 -2
- package/dist/{mcp-DWKVMNC7.js → mcp-FV6YX3ZT.js} +7 -7
- package/dist/{mcp-serve-5GODJ4DJ.js → mcp-serve-PXREYQRZ.js} +2 -2
- package/dist/{mcp-serve-learn-OF3ZJUJC.js → mcp-serve-learn-UVXIDOEZ.js} +1 -1
- package/dist/{memory-TRDLADUM.js → memory-DF7OUOTV.js} +6 -6
- package/dist/{permission-I3OFJUYA.js → permission-VISPGMUK.js} +38 -4
- package/dist/{permissions-J46VP6US.js → permissions-2SBACQHV.js} +2 -2
- package/dist/{session-S4WKA4P5.js → session-QK6OZO3X.js} +2 -2
- package/dist/{status-YZEOUX46.js → status-6HQRVZKR.js} +11 -10
- package/dist/{statusline-KKFJPTI7.js → statusline-AOZHEQSV.js} +2 -2
- package/dist/{uninstall-AVA3J554.js → uninstall-MF6FRUBO.js} +51 -4
- package/dist/{update-Q3ACCK4Z.js → update-SMWXKJ3R.js} +6 -5
- package/dist/{update-check-25SCLHZO.js → update-check-7LKJLJPM.js} +1 -1
- package/lib/cf-memory/src/lib/claude-md.ts +82 -70
- package/lib/cf-memory/src/lib/status-frame.ts +2 -2
- package/lib/cf-memory/src/tools/delete.ts +1 -1
- package/lib/cf-memory/src/tools/store.ts +6 -6
- package/lib/cf-memory/src/tools/update.ts +5 -11
- package/package.json +32 -4
- package/lib/cf-memory/README.md +0 -317
- package/lib/cf-memory/src/__tests__/claude-md.test.ts +0 -343
- package/lib/cf-memory/src/__tests__/daemon-client.test.ts +0 -146
- package/lib/cf-memory/src/__tests__/daemon-e2e.test.ts +0 -223
- package/lib/cf-memory/src/__tests__/daemon.test.ts +0 -407
- package/lib/cf-memory/src/__tests__/dedup.test.ts +0 -103
- package/lib/cf-memory/src/__tests__/embeddings.test.ts +0 -292
- package/lib/cf-memory/src/__tests__/lazy-install.test.ts +0 -210
- package/lib/cf-memory/src/__tests__/markdown-backend.test.ts +0 -450
- package/lib/cf-memory/src/__tests__/migration.test.ts +0 -255
- package/lib/cf-memory/src/__tests__/migrations.test.ts +0 -288
- package/lib/cf-memory/src/__tests__/minisearch-backend.test.ts +0 -262
- package/lib/cf-memory/src/__tests__/ollama.test.ts +0 -48
- package/lib/cf-memory/src/__tests__/project-id.test.ts +0 -43
- package/lib/cf-memory/src/__tests__/schema.test.ts +0 -128
- package/lib/cf-memory/src/__tests__/search.test.ts +0 -115
- package/lib/cf-memory/src/__tests__/sqlite-lazy-db.test.ts +0 -213
- package/lib/cf-memory/src/__tests__/status-frame.test.ts +0 -136
- package/lib/cf-memory/src/__tests__/temporal-decay.test.ts +0 -54
- package/lib/cf-memory/src/__tests__/tier.test.ts +0 -388
- package/lib/cf-memory/src/__tests__/tools.test.ts +0 -83
- package/lib/cf-memory/vitest.config.ts +0 -7
- package/lib/learn-host/.prettierignore +0 -4
- package/lib/learn-host/.prettierrc +0 -8
- package/lib/learn-host/README.md +0 -113
- package/lib/learn-host/eslint.config.mjs +0 -6
- package/lib/learn-mcp/README.md +0 -187
- package/lib/learn-mcp/src/__tests__/docs.test.ts +0 -117
- package/lib/learn-mcp/vitest.config.ts +0 -8
|
@@ -1,343 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { mkdirSync, rmSync, writeFileSync, readFileSync, existsSync } from "fs";
|
|
3
|
-
import { join } from "path";
|
|
4
|
-
import { tmpdir } from "os";
|
|
5
|
-
import {
|
|
6
|
-
syncToClaudeMd,
|
|
7
|
-
removeFromClaudeMd,
|
|
8
|
-
updateInClaudeMd,
|
|
9
|
-
claudeMdPath,
|
|
10
|
-
SECTION_HEADER,
|
|
11
|
-
} from "../lib/claude-md.js";
|
|
12
|
-
|
|
13
|
-
let testDir: string;
|
|
14
|
-
let claudeMdFile: string;
|
|
15
|
-
let counter = 0;
|
|
16
|
-
|
|
17
|
-
beforeEach(() => {
|
|
18
|
-
testDir = join(tmpdir(), `cf-claude-md-test-${Date.now()}-${++counter}`);
|
|
19
|
-
mkdirSync(testDir, { recursive: true });
|
|
20
|
-
claudeMdFile = join(testDir, "CLAUDE.md");
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
afterEach(() => {
|
|
24
|
-
rmSync(testDir, { recursive: true, force: true });
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
describe("syncToClaudeMd", () => {
|
|
28
|
-
it("creates CLAUDE.md with section if it does not exist", () => {
|
|
29
|
-
syncToClaudeMd(
|
|
30
|
-
testDir,
|
|
31
|
-
"conventions/code-style",
|
|
32
|
-
"Use 2-space indentation",
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
expect(existsSync(claudeMdFile)).toBe(true);
|
|
36
|
-
const content = readFileSync(claudeMdFile, "utf-8");
|
|
37
|
-
expect(content).toContain(SECTION_HEADER);
|
|
38
|
-
expect(content).toContain("Use 2-space indentation");
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("appends section to existing CLAUDE.md that has no memory section", () => {
|
|
42
|
-
writeFileSync(claudeMdFile, "# My Project\n\nSome existing content.\n");
|
|
43
|
-
|
|
44
|
-
syncToClaudeMd(
|
|
45
|
-
testDir,
|
|
46
|
-
"conventions/code-style",
|
|
47
|
-
"Use 2-space indentation",
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
const content = readFileSync(claudeMdFile, "utf-8");
|
|
51
|
-
expect(content).toContain("# My Project");
|
|
52
|
-
expect(content).toContain("Some existing content.");
|
|
53
|
-
expect(content).toContain(SECTION_HEADER);
|
|
54
|
-
expect(content).toContain("Use 2-space indentation");
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("adds entry to existing memory section", () => {
|
|
58
|
-
writeFileSync(
|
|
59
|
-
claudeMdFile,
|
|
60
|
-
`# My Project\n\n${SECTION_HEADER}\n\n- Use 2-space indentation\n`,
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
syncToClaudeMd(
|
|
64
|
-
testDir,
|
|
65
|
-
"conventions/naming",
|
|
66
|
-
"Use camelCase for variables",
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
const content = readFileSync(claudeMdFile, "utf-8");
|
|
70
|
-
expect(content).toContain("Use 2-space indentation");
|
|
71
|
-
expect(content).toContain("Use camelCase for variables");
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("does not duplicate entries with same ID", () => {
|
|
75
|
-
syncToClaudeMd(
|
|
76
|
-
testDir,
|
|
77
|
-
"conventions/code-style",
|
|
78
|
-
"Use 2-space indentation",
|
|
79
|
-
);
|
|
80
|
-
syncToClaudeMd(
|
|
81
|
-
testDir,
|
|
82
|
-
"conventions/code-style",
|
|
83
|
-
"Use 4-space indentation",
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
const content = readFileSync(claudeMdFile, "utf-8");
|
|
87
|
-
// Should have the updated version, not both
|
|
88
|
-
expect(content).toContain("Use 4-space indentation");
|
|
89
|
-
expect(content).not.toContain("Use 2-space indentation");
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("stores entry with ID as HTML comment for tracking", () => {
|
|
93
|
-
syncToClaudeMd(
|
|
94
|
-
testDir,
|
|
95
|
-
"conventions/code-style",
|
|
96
|
-
"Use 2-space indentation",
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
const content = readFileSync(claudeMdFile, "utf-8");
|
|
100
|
-
expect(content).toContain("<!-- cf:conventions/code-style -->");
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("handles multi-line description by taking first line only", () => {
|
|
104
|
-
syncToClaudeMd(
|
|
105
|
-
testDir,
|
|
106
|
-
"conventions/style",
|
|
107
|
-
"Line one\nLine two\nLine three",
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
const content = readFileSync(claudeMdFile, "utf-8");
|
|
111
|
-
expect(content).toContain("Line one");
|
|
112
|
-
// Multi-line content should be joined into a single bullet
|
|
113
|
-
expect(content.split(SECTION_HEADER)[1]).not.toContain("\nLine two");
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it("preserves content after the memory section", () => {
|
|
117
|
-
writeFileSync(
|
|
118
|
-
claudeMdFile,
|
|
119
|
-
`# My Project\n\n${SECTION_HEADER}\n\n- Old entry <!-- cf:conventions/old -->\n\n## Other Section\n\nMore content.\n`,
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
syncToClaudeMd(testDir, "conventions/new-rule", "New convention");
|
|
123
|
-
|
|
124
|
-
const content = readFileSync(claudeMdFile, "utf-8");
|
|
125
|
-
expect(content).toContain("## Other Section");
|
|
126
|
-
expect(content).toContain("More content.");
|
|
127
|
-
expect(content).toContain("New convention");
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
describe("removeFromClaudeMd", () => {
|
|
132
|
-
it("removes an entry by ID", () => {
|
|
133
|
-
syncToClaudeMd(
|
|
134
|
-
testDir,
|
|
135
|
-
"conventions/code-style",
|
|
136
|
-
"Use 2-space indentation",
|
|
137
|
-
);
|
|
138
|
-
syncToClaudeMd(testDir, "conventions/naming", "Use camelCase");
|
|
139
|
-
|
|
140
|
-
removeFromClaudeMd(testDir, "conventions/code-style");
|
|
141
|
-
|
|
142
|
-
const content = readFileSync(claudeMdFile, "utf-8");
|
|
143
|
-
expect(content).not.toContain("Use 2-space indentation");
|
|
144
|
-
expect(content).not.toContain("conventions/code-style");
|
|
145
|
-
expect(content).toContain("Use camelCase");
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it("does nothing if CLAUDE.md does not exist", () => {
|
|
149
|
-
// Should not throw
|
|
150
|
-
removeFromClaudeMd(testDir, "conventions/code-style");
|
|
151
|
-
expect(existsSync(claudeMdFile)).toBe(false);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it("does nothing if ID is not found", () => {
|
|
155
|
-
writeFileSync(
|
|
156
|
-
claudeMdFile,
|
|
157
|
-
`# Project\n\n${SECTION_HEADER}\n\n- Some entry <!-- cf:conventions/other -->\n`,
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
removeFromClaudeMd(testDir, "conventions/nonexistent");
|
|
161
|
-
|
|
162
|
-
const content = readFileSync(claudeMdFile, "utf-8");
|
|
163
|
-
expect(content).toContain("Some entry");
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it("removes section header when last entry is removed", () => {
|
|
167
|
-
syncToClaudeMd(testDir, "conventions/only-one", "The only convention");
|
|
168
|
-
|
|
169
|
-
removeFromClaudeMd(testDir, "conventions/only-one");
|
|
170
|
-
|
|
171
|
-
const content = readFileSync(claudeMdFile, "utf-8");
|
|
172
|
-
expect(content).not.toContain(SECTION_HEADER);
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
describe("updateInClaudeMd", () => {
|
|
177
|
-
it("updates the description of an existing entry", () => {
|
|
178
|
-
syncToClaudeMd(testDir, "conventions/style", "Old style guide");
|
|
179
|
-
|
|
180
|
-
updateInClaudeMd(testDir, "conventions/style", "New style guide");
|
|
181
|
-
|
|
182
|
-
const content = readFileSync(claudeMdFile, "utf-8");
|
|
183
|
-
expect(content).toContain("New style guide");
|
|
184
|
-
expect(content).not.toContain("Old style guide");
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it("does nothing if ID is not found (no upsert)", () => {
|
|
188
|
-
writeFileSync(claudeMdFile, "# Project\n");
|
|
189
|
-
|
|
190
|
-
updateInClaudeMd(testDir, "conventions/nonexistent", "Something");
|
|
191
|
-
|
|
192
|
-
const content = readFileSync(claudeMdFile, "utf-8");
|
|
193
|
-
expect(content).not.toContain("Something");
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it("does nothing if CLAUDE.md does not exist", () => {
|
|
197
|
-
updateInClaudeMd(testDir, "conventions/style", "Something");
|
|
198
|
-
expect(existsSync(claudeMdFile)).toBe(false);
|
|
199
|
-
});
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
describe("claudeMdPath", () => {
|
|
203
|
-
it("strips /docs/memory suffix to derive project root", () => {
|
|
204
|
-
const docsDir = join(testDir, "docs", "memory");
|
|
205
|
-
mkdirSync(docsDir, { recursive: true });
|
|
206
|
-
|
|
207
|
-
const result = claudeMdPath(docsDir);
|
|
208
|
-
expect(result).toBe(join(testDir, "CLAUDE.md"));
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it("strips /memory suffix as fallback", () => {
|
|
212
|
-
const docsDir = join(testDir, "memory");
|
|
213
|
-
mkdirSync(docsDir, { recursive: true });
|
|
214
|
-
|
|
215
|
-
const result = claudeMdPath(docsDir);
|
|
216
|
-
expect(result).toBe(join(testDir, "CLAUDE.md"));
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it("uses docsDir as-is when no known suffix matches", () => {
|
|
220
|
-
const result = claudeMdPath(testDir);
|
|
221
|
-
expect(result).toBe(join(testDir, "CLAUDE.md"));
|
|
222
|
-
});
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
describe("syncToClaudeMd with realistic docsDir", () => {
|
|
226
|
-
it("writes CLAUDE.md to project root when docsDir is docs/memory", () => {
|
|
227
|
-
const docsDir = join(testDir, "docs", "memory");
|
|
228
|
-
mkdirSync(docsDir, { recursive: true });
|
|
229
|
-
|
|
230
|
-
syncToClaudeMd(docsDir, "conventions/style", "Use tabs");
|
|
231
|
-
|
|
232
|
-
const expectedPath = join(testDir, "CLAUDE.md");
|
|
233
|
-
expect(existsSync(expectedPath)).toBe(true);
|
|
234
|
-
const content = readFileSync(expectedPath, "utf-8");
|
|
235
|
-
expect(content).toContain("Use tabs");
|
|
236
|
-
expect(content).toContain(SECTION_HEADER);
|
|
237
|
-
});
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
describe("migration from old header", () => {
|
|
241
|
-
const OLD_HEADER = "## CF Memory: Conventions";
|
|
242
|
-
|
|
243
|
-
it("reads entries from old header and rewrites with new header on sync", () => {
|
|
244
|
-
writeFileSync(
|
|
245
|
-
claudeMdFile,
|
|
246
|
-
`# My Project\n\n${OLD_HEADER}\n\n- Old rule <!-- cf:conventions/old-rule -->\n`,
|
|
247
|
-
);
|
|
248
|
-
|
|
249
|
-
syncToClaudeMd(testDir, "conventions/new-rule", "New rule");
|
|
250
|
-
|
|
251
|
-
const content = readFileSync(claudeMdFile, "utf-8");
|
|
252
|
-
// Should have new header
|
|
253
|
-
expect(content).toContain(SECTION_HEADER);
|
|
254
|
-
// Should NOT have old header (unless they're the same)
|
|
255
|
-
if (SECTION_HEADER !== OLD_HEADER) {
|
|
256
|
-
expect(content).not.toContain(OLD_HEADER);
|
|
257
|
-
}
|
|
258
|
-
// Both entries should be preserved
|
|
259
|
-
expect(content).toContain("Old rule");
|
|
260
|
-
expect(content).toContain("New rule");
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
it("removes entries from old header format", () => {
|
|
264
|
-
writeFileSync(
|
|
265
|
-
claudeMdFile,
|
|
266
|
-
`# My Project\n\n${OLD_HEADER}\n\n- Old rule <!-- cf:conventions/old-rule -->\n- Keep this <!-- cf:conventions/keep -->\n`,
|
|
267
|
-
);
|
|
268
|
-
|
|
269
|
-
removeFromClaudeMd(testDir, "conventions/old-rule");
|
|
270
|
-
|
|
271
|
-
const content = readFileSync(claudeMdFile, "utf-8");
|
|
272
|
-
expect(content).not.toContain("Old rule");
|
|
273
|
-
expect(content).toContain("Keep this");
|
|
274
|
-
});
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
describe("syncToClaudeMd with non-convention categories", () => {
|
|
278
|
-
it("syncs decisions entries to CLAUDE.md", () => {
|
|
279
|
-
syncToClaudeMd(
|
|
280
|
-
testDir,
|
|
281
|
-
"decisions/api-versioning",
|
|
282
|
-
"Always use URL-based API versioning (v1, v2)",
|
|
283
|
-
);
|
|
284
|
-
|
|
285
|
-
const content = readFileSync(claudeMdFile, "utf-8");
|
|
286
|
-
expect(content).toContain(SECTION_HEADER);
|
|
287
|
-
expect(content).toContain("Always use URL-based API versioning");
|
|
288
|
-
expect(content).toContain("<!-- cf:decisions/api-versioning -->");
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
it("syncs infrastructure entries to CLAUDE.md", () => {
|
|
292
|
-
syncToClaudeMd(
|
|
293
|
-
testDir,
|
|
294
|
-
"infrastructure/deploy-checklist",
|
|
295
|
-
"Always run migrations before deploying",
|
|
296
|
-
);
|
|
297
|
-
|
|
298
|
-
const content = readFileSync(claudeMdFile, "utf-8");
|
|
299
|
-
expect(content).toContain("Always run migrations before deploying");
|
|
300
|
-
expect(content).toContain("<!-- cf:infrastructure/deploy-checklist -->");
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
it("mixes convention and non-convention entries in same section", () => {
|
|
304
|
-
syncToClaudeMd(testDir, "conventions/naming", "Use camelCase");
|
|
305
|
-
syncToClaudeMd(
|
|
306
|
-
testDir,
|
|
307
|
-
"decisions/db-choice",
|
|
308
|
-
"Use PostgreSQL for all new services",
|
|
309
|
-
);
|
|
310
|
-
|
|
311
|
-
const content = readFileSync(claudeMdFile, "utf-8");
|
|
312
|
-
expect(content).toContain("Use camelCase");
|
|
313
|
-
expect(content).toContain("Use PostgreSQL for all new services");
|
|
314
|
-
// Only one section header
|
|
315
|
-
const headerCount = content.split(SECTION_HEADER).length - 1;
|
|
316
|
-
expect(headerCount).toBe(1);
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
it("removes non-convention entries by ID", () => {
|
|
320
|
-
syncToClaudeMd(testDir, "decisions/api-versioning", "URL-based versioning");
|
|
321
|
-
syncToClaudeMd(testDir, "conventions/naming", "Use camelCase");
|
|
322
|
-
|
|
323
|
-
removeFromClaudeMd(testDir, "decisions/api-versioning");
|
|
324
|
-
|
|
325
|
-
const content = readFileSync(claudeMdFile, "utf-8");
|
|
326
|
-
expect(content).not.toContain("URL-based versioning");
|
|
327
|
-
expect(content).toContain("Use camelCase");
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
it("updates non-convention entries", () => {
|
|
331
|
-
syncToClaudeMd(testDir, "infrastructure/ci-rule", "Run lint before tests");
|
|
332
|
-
|
|
333
|
-
updateInClaudeMd(
|
|
334
|
-
testDir,
|
|
335
|
-
"infrastructure/ci-rule",
|
|
336
|
-
"Run lint and type-check before tests",
|
|
337
|
-
);
|
|
338
|
-
|
|
339
|
-
const content = readFileSync(claudeMdFile, "utf-8");
|
|
340
|
-
expect(content).toContain("Run lint and type-check before tests");
|
|
341
|
-
expect(content).not.toContain("Run lint before tests\n");
|
|
342
|
-
});
|
|
343
|
-
});
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { mkdirSync, rmSync } from "fs";
|
|
3
|
-
import { join } from "path";
|
|
4
|
-
import { tmpdir } from "os";
|
|
5
|
-
import { DaemonClient } from "../lib/daemon-client.js";
|
|
6
|
-
import { startDaemonServer, type DaemonPaths } from "../daemon/process.js";
|
|
7
|
-
import { MiniSearchBackend } from "../backends/minisearch.js";
|
|
8
|
-
|
|
9
|
-
let testDir: string;
|
|
10
|
-
let testPaths: DaemonPaths;
|
|
11
|
-
let counter = 0;
|
|
12
|
-
|
|
13
|
-
beforeEach(() => {
|
|
14
|
-
testDir = join(tmpdir(), `cf-daemon-client-test-${Date.now()}-${++counter}`);
|
|
15
|
-
mkdirSync(testDir, { recursive: true });
|
|
16
|
-
|
|
17
|
-
const daemonDir = join(testDir, "daemon");
|
|
18
|
-
mkdirSync(daemonDir, { recursive: true });
|
|
19
|
-
|
|
20
|
-
testPaths = {
|
|
21
|
-
socketPath: join(daemonDir, "daemon.sock"),
|
|
22
|
-
pidFile: join(daemonDir, "daemon.pid"),
|
|
23
|
-
logFile: join(daemonDir, "daemon.log"),
|
|
24
|
-
};
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
afterEach(() => {
|
|
28
|
-
rmSync(testDir, { recursive: true, force: true });
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
describe("DaemonClient auto-reconnect", () => {
|
|
32
|
-
it("calls respawn and retries on connection error", async () => {
|
|
33
|
-
const docsDir = join(testDir, "docs");
|
|
34
|
-
mkdirSync(docsDir, { recursive: true });
|
|
35
|
-
|
|
36
|
-
// Start daemon
|
|
37
|
-
const backend = new MiniSearchBackend(docsDir);
|
|
38
|
-
let handle = startDaemonServer(backend, {
|
|
39
|
-
paths: testPaths,
|
|
40
|
-
idleTimeoutMs: 0,
|
|
41
|
-
});
|
|
42
|
-
await new Promise<void>((resolve) => {
|
|
43
|
-
handle.server.once("listening", resolve);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
const respawn = vi.fn(async () => {
|
|
47
|
-
// Re-create a fresh backend and server on same paths
|
|
48
|
-
const newBackend = new MiniSearchBackend(docsDir);
|
|
49
|
-
handle = startDaemonServer(newBackend, {
|
|
50
|
-
paths: testPaths,
|
|
51
|
-
idleTimeoutMs: 0,
|
|
52
|
-
});
|
|
53
|
-
await new Promise<void>((resolve) => {
|
|
54
|
-
handle.server.once("listening", resolve);
|
|
55
|
-
});
|
|
56
|
-
return true;
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
const client = new DaemonClient(testPaths.socketPath, { respawn });
|
|
60
|
-
|
|
61
|
-
// Verify initial connection works
|
|
62
|
-
const alive = await client.ping();
|
|
63
|
-
expect(alive).toBe(true);
|
|
64
|
-
|
|
65
|
-
// Kill daemon and wait for socket cleanup
|
|
66
|
-
await new Promise<void>((resolve) => {
|
|
67
|
-
handle.server.close(() => resolve());
|
|
68
|
-
});
|
|
69
|
-
rmSync(testPaths.socketPath, { force: true });
|
|
70
|
-
rmSync(testPaths.pidFile, { force: true });
|
|
71
|
-
// Small delay to ensure OS fully releases the socket
|
|
72
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
73
|
-
|
|
74
|
-
// Next request should trigger respawn and succeed
|
|
75
|
-
const stats = await client.stats();
|
|
76
|
-
expect(stats.total).toBe(0);
|
|
77
|
-
expect(respawn).toHaveBeenCalledOnce();
|
|
78
|
-
|
|
79
|
-
// Cleanup
|
|
80
|
-
await new Promise<void>((resolve) => {
|
|
81
|
-
handle.server.close(() => resolve());
|
|
82
|
-
});
|
|
83
|
-
rmSync(testPaths.socketPath, { force: true });
|
|
84
|
-
rmSync(testPaths.pidFile, { force: true });
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("throws original error when respawn fails", async () => {
|
|
88
|
-
const respawn = vi.fn(async () => false);
|
|
89
|
-
const client = new DaemonClient(
|
|
90
|
-
"/tmp/nonexistent-cf-test-sock-" + Date.now(),
|
|
91
|
-
{ respawn },
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
await expect(client.stats()).rejects.toThrow();
|
|
95
|
-
expect(respawn).toHaveBeenCalledOnce();
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("throws directly without respawn callback", async () => {
|
|
99
|
-
const client = new DaemonClient(
|
|
100
|
-
"/tmp/nonexistent-cf-test-sock-" + Date.now(),
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
await expect(client.stats()).rejects.toThrow();
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it("ping does not trigger respawn", async () => {
|
|
107
|
-
const respawn = vi.fn(async () => true);
|
|
108
|
-
const client = new DaemonClient(
|
|
109
|
-
"/tmp/nonexistent-cf-test-sock-" + Date.now(),
|
|
110
|
-
{ respawn },
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
// ping() should return false without attempting respawn
|
|
114
|
-
const alive = await client.ping();
|
|
115
|
-
expect(alive).toBe(false);
|
|
116
|
-
expect(respawn).not.toHaveBeenCalled();
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("does not call respawn for HTTP 4xx errors", async () => {
|
|
120
|
-
const docsDir = join(testDir, "docs");
|
|
121
|
-
mkdirSync(docsDir, { recursive: true });
|
|
122
|
-
|
|
123
|
-
const backend = new MiniSearchBackend(docsDir);
|
|
124
|
-
const handle = startDaemonServer(backend, {
|
|
125
|
-
paths: testPaths,
|
|
126
|
-
idleTimeoutMs: 0,
|
|
127
|
-
});
|
|
128
|
-
await new Promise<void>((resolve) => {
|
|
129
|
-
handle.server.once("listening", resolve);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
const respawn = vi.fn(async () => true);
|
|
133
|
-
const client = new DaemonClient(testPaths.socketPath, { respawn });
|
|
134
|
-
|
|
135
|
-
// 404 is not a connection error — should not trigger respawn
|
|
136
|
-
const result = await client.retrieve("nonexistent/id");
|
|
137
|
-
expect(result).toBeNull();
|
|
138
|
-
expect(respawn).not.toHaveBeenCalled();
|
|
139
|
-
|
|
140
|
-
await new Promise<void>((resolve) => {
|
|
141
|
-
handle.server.close(() => resolve());
|
|
142
|
-
});
|
|
143
|
-
rmSync(testPaths.socketPath, { force: true });
|
|
144
|
-
rmSync(testPaths.pidFile, { force: true });
|
|
145
|
-
});
|
|
146
|
-
});
|