coding-friend-cli 1.36.4 → 1.38.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.
- package/README.md +8 -3
- 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-XHP3EBR3.js → chunk-776ZY62N.js} +112 -4
- package/dist/{chunk-N4WZWRCI.js → chunk-BXSCYGWE.js} +85 -37
- package/dist/{chunk-AK6RMCAC.js → chunk-CGEMETKQ.js} +44 -30
- package/dist/{chunk-XEKQW4RN.js → chunk-ETTZRJ55.js} +1 -1
- package/dist/{chunk-IKLMHJH6.js → chunk-GIH6E7GJ.js} +62 -1
- package/dist/{chunk-56YJWGAN.js → chunk-ONG4KRYP.js} +60 -47
- package/dist/{chunk-X2H7JJMR.js → chunk-PLSJW7QJ.js} +29 -1
- package/dist/{chunk-O5ZKLFSL.js → chunk-RCE4OWNX.js} +1 -1
- package/dist/{chunk-K4WSC62G.js → chunk-RFNKPAJW.js} +42 -2
- package/dist/{clean-Q4VU662V.js → clean-YWEMYDE5.js} +3 -2
- package/dist/{config-CTTHWJH5.js → config-NG7SICM4.js} +15 -27
- 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-NCJMUSME.js → host-EV6QIVTI.js} +3 -3
- package/dist/index.js +62 -48
- package/dist/{init-UABKVDVT.js → init-KYE5CJMW.js} +135 -17
- package/dist/{install-R2HDM3N4.js → install-XAFQY5YF.js} +80 -5
- package/dist/{learn-K64AUUFB.js → learn-MC4VDHZH.js} +2 -2
- package/dist/{mcp-OMQFSJP4.js → mcp-FV6YX3ZT.js} +24 -24
- package/dist/{mcp-serve-3FEPFVVQ.js → mcp-serve-PXREYQRZ.js} +10 -1
- package/dist/{mcp-serve-learn-OF3ZJUJC.js → mcp-serve-learn-UVXIDOEZ.js} +1 -1
- package/dist/{memory-Y6GNALG3.js → memory-DF7OUOTV.js} +7 -7
- package/dist/{permission-I3OFJUYA.js → permission-VISPGMUK.js} +38 -4
- package/dist/{permissions-J46VP6US.js → permissions-2SBACQHV.js} +2 -2
- package/dist/{session-Q2DLQPYP.js → session-QK6OZO3X.js} +3 -2
- package/dist/{status-ML4XEDYL.js → status-FVFTJ6GS.js} +18 -14
- package/dist/{statusline-KKFJPTI7.js → statusline-AOZHEQSV.js} +2 -2
- package/dist/{uninstall-ASLHT3MH.js → uninstall-MF6FRUBO.js} +64 -3
- package/dist/{update-DIOH45TE.js → update-QNUCSDSA.js} +7 -4
- package/dist/{update-check-25SCLHZO.js → update-check-7LKJLJPM.js} +1 -1
- package/lib/cf-memory/src/backends/sqlite/index.ts +142 -85
- 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__/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 -367
- 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,223 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { mkdirSync, rmSync, existsSync, readFileSync } from "fs";
|
|
3
|
-
import { join } from "path";
|
|
4
|
-
import { tmpdir } from "os";
|
|
5
|
-
import http from "node:http";
|
|
6
|
-
import matter from "gray-matter";
|
|
7
|
-
import { MiniSearchBackend } from "../backends/minisearch.js";
|
|
8
|
-
import { startDaemonServer, type DaemonPaths } from "../daemon/process.js";
|
|
9
|
-
import { DaemonClient } from "../lib/daemon-client.js";
|
|
10
|
-
|
|
11
|
-
let testDir: string;
|
|
12
|
-
let docsDir: string;
|
|
13
|
-
let testPaths: DaemonPaths;
|
|
14
|
-
let handle: ReturnType<typeof startDaemonServer> | null = null;
|
|
15
|
-
let client: DaemonClient;
|
|
16
|
-
let counter = 0;
|
|
17
|
-
|
|
18
|
-
beforeEach(async () => {
|
|
19
|
-
testDir = join(tmpdir(), `cf-memory-e2e-${Date.now()}-${++counter}`);
|
|
20
|
-
docsDir = join(testDir, "docs");
|
|
21
|
-
mkdirSync(docsDir, { recursive: true });
|
|
22
|
-
|
|
23
|
-
const daemonDir = join(testDir, "daemon");
|
|
24
|
-
mkdirSync(daemonDir, { recursive: true });
|
|
25
|
-
|
|
26
|
-
testPaths = {
|
|
27
|
-
socketPath: join(daemonDir, "daemon.sock"),
|
|
28
|
-
pidFile: join(daemonDir, "daemon.pid"),
|
|
29
|
-
logFile: join(daemonDir, "daemon.log"),
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const backend = new MiniSearchBackend(docsDir);
|
|
33
|
-
handle = startDaemonServer(backend, {
|
|
34
|
-
paths: testPaths,
|
|
35
|
-
idleTimeoutMs: 0,
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
client = new DaemonClient(testPaths.socketPath);
|
|
39
|
-
|
|
40
|
-
// Wait for server to be listening
|
|
41
|
-
await new Promise<void>((resolve) => {
|
|
42
|
-
handle!.server.once("listening", resolve);
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
afterEach(async () => {
|
|
47
|
-
if (handle) {
|
|
48
|
-
await new Promise<void>((resolve) => {
|
|
49
|
-
handle!.server.close(() => resolve());
|
|
50
|
-
});
|
|
51
|
-
try {
|
|
52
|
-
rmSync(testPaths.socketPath, { force: true });
|
|
53
|
-
rmSync(testPaths.pidFile, { force: true });
|
|
54
|
-
} catch {
|
|
55
|
-
// ignore
|
|
56
|
-
}
|
|
57
|
-
handle = null;
|
|
58
|
-
}
|
|
59
|
-
rmSync(testDir, { recursive: true, force: true });
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
describe("Daemon E2E: DaemonClient → Daemon → MiniSearch → Markdown", () => {
|
|
63
|
-
it("full pipeline: store → search → retrieve → update → delete", async () => {
|
|
64
|
-
// Store
|
|
65
|
-
const memory = await client.store({
|
|
66
|
-
title: "E2E Auth Pattern",
|
|
67
|
-
description: "JWT auth for API endpoints",
|
|
68
|
-
type: "fact",
|
|
69
|
-
tags: ["auth", "jwt"],
|
|
70
|
-
content: "# Auth\n\nUses RS256 signed JWT tokens.",
|
|
71
|
-
});
|
|
72
|
-
expect(memory.id).toBe("features/e2e-auth-pattern");
|
|
73
|
-
|
|
74
|
-
// Verify file on disk
|
|
75
|
-
const filePath = join(docsDir, "features", "e2e-auth-pattern.md");
|
|
76
|
-
expect(existsSync(filePath)).toBe(true);
|
|
77
|
-
const raw = matter(readFileSync(filePath, "utf-8"));
|
|
78
|
-
expect(raw.data.title).toBe("E2E Auth Pattern");
|
|
79
|
-
expect(raw.data.type).toBe("fact");
|
|
80
|
-
|
|
81
|
-
// Search via daemon
|
|
82
|
-
const searchResults = await client.search({ query: "auth" });
|
|
83
|
-
expect(searchResults.length).toBeGreaterThan(0);
|
|
84
|
-
|
|
85
|
-
// Retrieve
|
|
86
|
-
const retrieved = await client.retrieve("features/e2e-auth-pattern");
|
|
87
|
-
expect(retrieved).not.toBeNull();
|
|
88
|
-
expect(retrieved!.frontmatter.title).toBe("E2E Auth Pattern");
|
|
89
|
-
expect(retrieved!.content).toContain("RS256");
|
|
90
|
-
|
|
91
|
-
// Update
|
|
92
|
-
const updated = await client.update({
|
|
93
|
-
id: "features/e2e-auth-pattern",
|
|
94
|
-
title: "Updated E2E Auth Pattern",
|
|
95
|
-
tags: ["security"],
|
|
96
|
-
});
|
|
97
|
-
expect(updated).not.toBeNull();
|
|
98
|
-
expect(updated!.frontmatter.title).toBe("Updated E2E Auth Pattern");
|
|
99
|
-
expect(updated!.frontmatter.tags).toContain("security");
|
|
100
|
-
expect(updated!.frontmatter.tags).toContain("auth");
|
|
101
|
-
|
|
102
|
-
// Verify update on disk
|
|
103
|
-
const updatedRaw = matter(readFileSync(filePath, "utf-8"));
|
|
104
|
-
expect(updatedRaw.data.title).toBe("Updated E2E Auth Pattern");
|
|
105
|
-
|
|
106
|
-
// Delete
|
|
107
|
-
const deleted = await client.delete("features/e2e-auth-pattern");
|
|
108
|
-
expect(deleted).toBe(true);
|
|
109
|
-
|
|
110
|
-
// Verify deleted from disk
|
|
111
|
-
expect(existsSync(filePath)).toBe(false);
|
|
112
|
-
|
|
113
|
-
// Verify search no longer finds it
|
|
114
|
-
const afterDelete = await client.search({ query: "e2e auth" });
|
|
115
|
-
expect(afterDelete.length).toBe(0);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("list returns all stored memories", async () => {
|
|
119
|
-
await client.store({
|
|
120
|
-
title: "Memory One",
|
|
121
|
-
description: "First memory",
|
|
122
|
-
type: "fact",
|
|
123
|
-
tags: ["one"],
|
|
124
|
-
content: "Content one.",
|
|
125
|
-
});
|
|
126
|
-
await client.store({
|
|
127
|
-
title: "Memory Two",
|
|
128
|
-
description: "Second memory",
|
|
129
|
-
type: "episode",
|
|
130
|
-
tags: ["two"],
|
|
131
|
-
content: "Content two.",
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
const list = await client.list({});
|
|
135
|
-
expect(list.length).toBe(2);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it("stats reflects stored memories", async () => {
|
|
139
|
-
await client.store({
|
|
140
|
-
title: "A Fact",
|
|
141
|
-
description: "Fact desc",
|
|
142
|
-
type: "fact",
|
|
143
|
-
tags: [],
|
|
144
|
-
content: "Fact content.",
|
|
145
|
-
});
|
|
146
|
-
await client.store({
|
|
147
|
-
title: "A Bug",
|
|
148
|
-
description: "Bug desc",
|
|
149
|
-
type: "episode",
|
|
150
|
-
tags: [],
|
|
151
|
-
content: "Bug content.",
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
const stats = await client.stats();
|
|
155
|
-
expect(stats.total).toBe(2);
|
|
156
|
-
expect(stats.byType.fact).toBe(1);
|
|
157
|
-
expect(stats.byType.episode).toBe(1);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it("rebuild rebuilds the search index", async () => {
|
|
161
|
-
await client.store({
|
|
162
|
-
title: "Before Rebuild",
|
|
163
|
-
description: "Test rebuild",
|
|
164
|
-
type: "fact",
|
|
165
|
-
tags: ["rebuild"],
|
|
166
|
-
content: "Before rebuild content.",
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
await client.rebuild();
|
|
170
|
-
|
|
171
|
-
// Search still works after rebuild
|
|
172
|
-
const results = await client.search({ query: "rebuild" });
|
|
173
|
-
expect(results.length).toBeGreaterThan(0);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it("ping returns true when daemon is running", async () => {
|
|
177
|
-
const alive = await client.ping();
|
|
178
|
-
expect(alive).toBe(true);
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it("retrieve returns null for non-existent memory", async () => {
|
|
182
|
-
const result = await client.retrieve("features/nonexistent");
|
|
183
|
-
expect(result).toBeNull();
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it("delete returns false for non-existent memory", async () => {
|
|
187
|
-
const result = await client.delete("features/nonexistent");
|
|
188
|
-
expect(result).toBe(false);
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it("update returns null for non-existent memory", async () => {
|
|
192
|
-
const result = await client.update({
|
|
193
|
-
id: "features/nonexistent",
|
|
194
|
-
title: "Test",
|
|
195
|
-
});
|
|
196
|
-
expect(result).toBeNull();
|
|
197
|
-
});
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
describe("DaemonClient fallback", () => {
|
|
201
|
-
it("ping returns false when daemon not reachable", async () => {
|
|
202
|
-
const deadClient = new DaemonClient(
|
|
203
|
-
"/tmp/nonexistent-socket-" + Date.now(),
|
|
204
|
-
);
|
|
205
|
-
const alive = await deadClient.ping();
|
|
206
|
-
expect(alive).toBe(false);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
it("store throws when daemon not reachable", async () => {
|
|
210
|
-
const deadClient = new DaemonClient(
|
|
211
|
-
"/tmp/nonexistent-socket-" + Date.now(),
|
|
212
|
-
);
|
|
213
|
-
await expect(
|
|
214
|
-
deadClient.store({
|
|
215
|
-
title: "Test",
|
|
216
|
-
description: "Test",
|
|
217
|
-
type: "fact",
|
|
218
|
-
tags: [],
|
|
219
|
-
content: "Test",
|
|
220
|
-
}),
|
|
221
|
-
).rejects.toThrow();
|
|
222
|
-
});
|
|
223
|
-
});
|
|
@@ -1,407 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { mkdirSync, rmSync, existsSync, writeFileSync } from "fs";
|
|
3
|
-
import { join } from "path";
|
|
4
|
-
import { tmpdir } from "os";
|
|
5
|
-
import http from "node:http";
|
|
6
|
-
import {
|
|
7
|
-
getDaemonPaths,
|
|
8
|
-
isDaemonRunning,
|
|
9
|
-
getDaemonInfo,
|
|
10
|
-
startDaemonServer,
|
|
11
|
-
type DaemonPaths,
|
|
12
|
-
} from "../daemon/process.js";
|
|
13
|
-
import { MiniSearchBackend } from "../backends/minisearch.js";
|
|
14
|
-
import type { StoreInput } from "../lib/types.js";
|
|
15
|
-
|
|
16
|
-
let testDir: string;
|
|
17
|
-
let testPaths: DaemonPaths;
|
|
18
|
-
let counter = 0;
|
|
19
|
-
|
|
20
|
-
function requestJson<T>(
|
|
21
|
-
socketPath: string,
|
|
22
|
-
method: string,
|
|
23
|
-
path: string,
|
|
24
|
-
body?: unknown,
|
|
25
|
-
): Promise<{ status: number; data: T }> {
|
|
26
|
-
return new Promise((resolve, reject) => {
|
|
27
|
-
const options: http.RequestOptions = {
|
|
28
|
-
socketPath,
|
|
29
|
-
path,
|
|
30
|
-
method,
|
|
31
|
-
headers: { "Content-Type": "application/json" },
|
|
32
|
-
};
|
|
33
|
-
const req = http.request(options, (res) => {
|
|
34
|
-
let data = "";
|
|
35
|
-
res.on("data", (chunk: Buffer) => {
|
|
36
|
-
data += chunk.toString();
|
|
37
|
-
});
|
|
38
|
-
res.on("end", () => {
|
|
39
|
-
try {
|
|
40
|
-
resolve({ status: res.statusCode ?? 0, data: JSON.parse(data) as T });
|
|
41
|
-
} catch {
|
|
42
|
-
reject(new Error(`Invalid JSON: ${data}`));
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
req.on("error", reject);
|
|
47
|
-
if (body) req.write(JSON.stringify(body));
|
|
48
|
-
req.end();
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
beforeEach(() => {
|
|
53
|
-
testDir = join(tmpdir(), `cf-memory-daemon-test-${Date.now()}-${++counter}`);
|
|
54
|
-
mkdirSync(testDir, { recursive: true });
|
|
55
|
-
|
|
56
|
-
const daemonDir = join(testDir, "daemon");
|
|
57
|
-
mkdirSync(daemonDir, { recursive: true });
|
|
58
|
-
|
|
59
|
-
testPaths = {
|
|
60
|
-
socketPath: join(daemonDir, "daemon.sock"),
|
|
61
|
-
pidFile: join(daemonDir, "daemon.pid"),
|
|
62
|
-
logFile: join(daemonDir, "daemon.log"),
|
|
63
|
-
};
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
afterEach(() => {
|
|
67
|
-
rmSync(testDir, { recursive: true, force: true });
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
describe("Daemon process helpers", () => {
|
|
71
|
-
it("isDaemonRunning returns false when no PID file", async () => {
|
|
72
|
-
const running = await isDaemonRunning(testPaths);
|
|
73
|
-
expect(running).toBe(false);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("isDaemonRunning returns false for stale PID file", async () => {
|
|
77
|
-
writeFileSync(testPaths.pidFile, "99999999\n1234567890", "utf-8");
|
|
78
|
-
const running = await isDaemonRunning(testPaths);
|
|
79
|
-
expect(running).toBe(false);
|
|
80
|
-
// Should clean up stale PID file
|
|
81
|
-
expect(existsSync(testPaths.pidFile)).toBe(false);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("getDaemonInfo returns null when no PID file", () => {
|
|
85
|
-
const info = getDaemonInfo(testPaths);
|
|
86
|
-
expect(info).toBeNull();
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it("getDaemonInfo reads PID and startedAt", () => {
|
|
90
|
-
const now = Date.now();
|
|
91
|
-
writeFileSync(testPaths.pidFile, `12345\n${now}`, "utf-8");
|
|
92
|
-
const info = getDaemonInfo(testPaths);
|
|
93
|
-
expect(info).not.toBeNull();
|
|
94
|
-
expect(info!.pid).toBe(12345);
|
|
95
|
-
expect(info!.startedAt).toBe(now);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("getDaemonPaths returns expected structure", () => {
|
|
99
|
-
const paths = getDaemonPaths();
|
|
100
|
-
expect(paths.socketPath).toContain("daemon.sock");
|
|
101
|
-
expect(paths.pidFile).toContain("daemon.pid");
|
|
102
|
-
expect(paths.logFile).toContain("daemon.log");
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
describe("Daemon server", () => {
|
|
107
|
-
let docsDir: string;
|
|
108
|
-
let handle: ReturnType<typeof startDaemonServer> | null = null;
|
|
109
|
-
|
|
110
|
-
beforeEach(() => {
|
|
111
|
-
docsDir = join(testDir, "docs");
|
|
112
|
-
mkdirSync(docsDir, { recursive: true });
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
afterEach(async () => {
|
|
116
|
-
if (handle) {
|
|
117
|
-
// Clean up server without exiting process
|
|
118
|
-
await new Promise<void>((resolve) => {
|
|
119
|
-
handle!.server.close(() => resolve());
|
|
120
|
-
});
|
|
121
|
-
try {
|
|
122
|
-
rmSync(testPaths.socketPath, { force: true });
|
|
123
|
-
rmSync(testPaths.pidFile, { force: true });
|
|
124
|
-
} catch {
|
|
125
|
-
// ignore
|
|
126
|
-
}
|
|
127
|
-
handle = null;
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
function startTestDaemon() {
|
|
132
|
-
const backend = new MiniSearchBackend(docsDir);
|
|
133
|
-
handle = startDaemonServer(backend, {
|
|
134
|
-
paths: testPaths,
|
|
135
|
-
idleTimeoutMs: 0, // Disable idle timeout for tests
|
|
136
|
-
});
|
|
137
|
-
return new Promise<void>((resolve) => {
|
|
138
|
-
// Wait for server to be listening
|
|
139
|
-
handle!.server.once("listening", resolve);
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
it("health endpoint responds with status ok", async () => {
|
|
144
|
-
await startTestDaemon();
|
|
145
|
-
const { status, data } = await requestJson<{ status: string }>(
|
|
146
|
-
testPaths.socketPath,
|
|
147
|
-
"GET",
|
|
148
|
-
"/health",
|
|
149
|
-
);
|
|
150
|
-
expect(status).toBe(200);
|
|
151
|
-
expect(data.status).toBe("ok");
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it("stats endpoint returns memory stats", async () => {
|
|
155
|
-
await startTestDaemon();
|
|
156
|
-
const { status, data } = await requestJson<{ total: number }>(
|
|
157
|
-
testPaths.socketPath,
|
|
158
|
-
"GET",
|
|
159
|
-
"/stats",
|
|
160
|
-
);
|
|
161
|
-
expect(status).toBe(200);
|
|
162
|
-
expect(data.total).toBe(0);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it("store and retrieve a memory via HTTP", async () => {
|
|
166
|
-
await startTestDaemon();
|
|
167
|
-
|
|
168
|
-
const input: StoreInput = {
|
|
169
|
-
title: "Test Memory",
|
|
170
|
-
description: "A test memory",
|
|
171
|
-
type: "fact",
|
|
172
|
-
tags: ["test"],
|
|
173
|
-
content: "Test content.",
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
// Store
|
|
177
|
-
const storeResult = await requestJson<{
|
|
178
|
-
id: string;
|
|
179
|
-
stored: boolean;
|
|
180
|
-
}>(testPaths.socketPath, "POST", "/memory", input);
|
|
181
|
-
expect(storeResult.status).toBe(201);
|
|
182
|
-
expect(storeResult.data.stored).toBe(true);
|
|
183
|
-
expect(storeResult.data.id).toBe("features/test-memory");
|
|
184
|
-
|
|
185
|
-
// Retrieve
|
|
186
|
-
const { status, data } = await requestJson<{
|
|
187
|
-
id: string;
|
|
188
|
-
frontmatter: { title: string };
|
|
189
|
-
}>(testPaths.socketPath, "GET", "/memory/features/test-memory");
|
|
190
|
-
expect(status).toBe(200);
|
|
191
|
-
expect(data.frontmatter.title).toBe("Test Memory");
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it("search returns results", async () => {
|
|
195
|
-
await startTestDaemon();
|
|
196
|
-
|
|
197
|
-
await requestJson(testPaths.socketPath, "POST", "/memory", {
|
|
198
|
-
title: "JWT Authentication",
|
|
199
|
-
description: "Auth pattern",
|
|
200
|
-
type: "fact",
|
|
201
|
-
tags: ["auth"],
|
|
202
|
-
content: "JWT tokens.",
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
const { status, data } = await requestJson<
|
|
206
|
-
Array<{ memory: { frontmatter: { title: string } } }>
|
|
207
|
-
>(testPaths.socketPath, "GET", "/memory/search?query=JWT");
|
|
208
|
-
expect(status).toBe(200);
|
|
209
|
-
expect(data.length).toBeGreaterThan(0);
|
|
210
|
-
expect(data[0].memory.frontmatter.title).toContain("JWT");
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it("list returns all memories", async () => {
|
|
214
|
-
await startTestDaemon();
|
|
215
|
-
|
|
216
|
-
await requestJson(testPaths.socketPath, "POST", "/memory", {
|
|
217
|
-
title: "Memory One",
|
|
218
|
-
description: "First",
|
|
219
|
-
type: "fact",
|
|
220
|
-
tags: [],
|
|
221
|
-
content: "One.",
|
|
222
|
-
});
|
|
223
|
-
await requestJson(testPaths.socketPath, "POST", "/memory", {
|
|
224
|
-
title: "Memory Two",
|
|
225
|
-
description: "Second",
|
|
226
|
-
type: "episode",
|
|
227
|
-
tags: [],
|
|
228
|
-
content: "Two.",
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
const { status, data } = await requestJson<Array<{ id: string }>>(
|
|
232
|
-
testPaths.socketPath,
|
|
233
|
-
"GET",
|
|
234
|
-
"/memory",
|
|
235
|
-
);
|
|
236
|
-
expect(status).toBe(200);
|
|
237
|
-
expect(data.length).toBe(2);
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it("update modifies a memory", async () => {
|
|
241
|
-
await startTestDaemon();
|
|
242
|
-
|
|
243
|
-
await requestJson(testPaths.socketPath, "POST", "/memory", {
|
|
244
|
-
title: "Original Title",
|
|
245
|
-
description: "Description",
|
|
246
|
-
type: "fact",
|
|
247
|
-
tags: [],
|
|
248
|
-
content: "Content.",
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
const { status, data } = await requestJson<{
|
|
252
|
-
updated: boolean;
|
|
253
|
-
title: string;
|
|
254
|
-
}>(testPaths.socketPath, "PATCH", "/memory/features/original-title", {
|
|
255
|
-
title: "New Title",
|
|
256
|
-
});
|
|
257
|
-
expect(status).toBe(200);
|
|
258
|
-
expect(data.updated).toBe(true);
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
it("delete removes a memory", async () => {
|
|
262
|
-
await startTestDaemon();
|
|
263
|
-
|
|
264
|
-
await requestJson(testPaths.socketPath, "POST", "/memory", {
|
|
265
|
-
title: "Delete Me",
|
|
266
|
-
description: "To be deleted",
|
|
267
|
-
type: "fact",
|
|
268
|
-
tags: [],
|
|
269
|
-
content: "Content.",
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
const { status, data } = await requestJson<{ deleted: boolean }>(
|
|
273
|
-
testPaths.socketPath,
|
|
274
|
-
"DELETE",
|
|
275
|
-
"/memory/features/delete-me",
|
|
276
|
-
);
|
|
277
|
-
expect(status).toBe(200);
|
|
278
|
-
expect(data.deleted).toBe(true);
|
|
279
|
-
|
|
280
|
-
// Verify gone
|
|
281
|
-
const { status: getStatus } = await requestJson(
|
|
282
|
-
testPaths.socketPath,
|
|
283
|
-
"GET",
|
|
284
|
-
"/memory/features/delete-me",
|
|
285
|
-
);
|
|
286
|
-
expect(getStatus).toBe(404);
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
it("rebuild endpoint works", async () => {
|
|
290
|
-
await startTestDaemon();
|
|
291
|
-
|
|
292
|
-
const { status, data } = await requestJson<{ rebuilt: boolean }>(
|
|
293
|
-
testPaths.socketPath,
|
|
294
|
-
"POST",
|
|
295
|
-
"/rebuild",
|
|
296
|
-
);
|
|
297
|
-
expect(status).toBe(200);
|
|
298
|
-
expect(data.rebuilt).toBe(true);
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
it("returns 404 for non-existent memory", async () => {
|
|
302
|
-
await startTestDaemon();
|
|
303
|
-
|
|
304
|
-
const { status } = await requestJson(
|
|
305
|
-
testPaths.socketPath,
|
|
306
|
-
"GET",
|
|
307
|
-
"/memory/features/nonexistent",
|
|
308
|
-
);
|
|
309
|
-
expect(status).toBe(404);
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
it("writes PID file on start", async () => {
|
|
313
|
-
await startTestDaemon();
|
|
314
|
-
expect(existsSync(testPaths.pidFile)).toBe(true);
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
it("rejects store with invalid body (missing required fields)", async () => {
|
|
318
|
-
await startTestDaemon();
|
|
319
|
-
|
|
320
|
-
const { status, data } = await requestJson<{ error: string }>(
|
|
321
|
-
testPaths.socketPath,
|
|
322
|
-
"POST",
|
|
323
|
-
"/memory",
|
|
324
|
-
{ title: "Test" }, // missing type, description, tags, content
|
|
325
|
-
);
|
|
326
|
-
expect(status).toBe(400);
|
|
327
|
-
expect(data.error).toContain("Validation failed");
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
it("rejects store with invalid type", async () => {
|
|
331
|
-
await startTestDaemon();
|
|
332
|
-
|
|
333
|
-
const { status, data } = await requestJson<{ error: string }>(
|
|
334
|
-
testPaths.socketPath,
|
|
335
|
-
"POST",
|
|
336
|
-
"/memory",
|
|
337
|
-
{
|
|
338
|
-
title: "Test",
|
|
339
|
-
description: "desc",
|
|
340
|
-
type: "invalid_type",
|
|
341
|
-
tags: [],
|
|
342
|
-
content: "content",
|
|
343
|
-
},
|
|
344
|
-
);
|
|
345
|
-
expect(status).toBe(400);
|
|
346
|
-
expect(data.error).toContain("Validation failed");
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
it("rejects update with invalid importance", async () => {
|
|
350
|
-
await startTestDaemon();
|
|
351
|
-
|
|
352
|
-
// First store a valid memory
|
|
353
|
-
await requestJson(testPaths.socketPath, "POST", "/memory", {
|
|
354
|
-
title: "Test Memory",
|
|
355
|
-
description: "desc",
|
|
356
|
-
type: "fact",
|
|
357
|
-
tags: [],
|
|
358
|
-
content: "content",
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
const { status, data } = await requestJson<{ error: string }>(
|
|
362
|
-
testPaths.socketPath,
|
|
363
|
-
"PATCH",
|
|
364
|
-
"/memory/features/test-memory",
|
|
365
|
-
{ importance: 99 }, // out of range
|
|
366
|
-
);
|
|
367
|
-
expect(status).toBe(400);
|
|
368
|
-
expect(data.error).toContain("Validation failed");
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
it("ignores invalid type query param on search (returns results)", async () => {
|
|
372
|
-
await startTestDaemon();
|
|
373
|
-
|
|
374
|
-
await requestJson(testPaths.socketPath, "POST", "/memory", {
|
|
375
|
-
title: "Searchable",
|
|
376
|
-
description: "A searchable memory",
|
|
377
|
-
type: "fact",
|
|
378
|
-
tags: ["test"],
|
|
379
|
-
content: "content",
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
// Invalid type is ignored (parsed as undefined)
|
|
383
|
-
const { status, data } = await requestJson<Array<unknown>>(
|
|
384
|
-
testPaths.socketPath,
|
|
385
|
-
"GET",
|
|
386
|
-
"/memory/search?query=searchable&type=bogus",
|
|
387
|
-
);
|
|
388
|
-
expect(status).toBe(200);
|
|
389
|
-
// No type filter applied → results returned
|
|
390
|
-
expect(data.length).toBeGreaterThan(0);
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
it("global error handler returns JSON 500 on unhandled errors", async () => {
|
|
394
|
-
await startTestDaemon();
|
|
395
|
-
|
|
396
|
-
// Send malformed JSON to trigger parse error
|
|
397
|
-
const { status, data } = await requestJson<{ error: string }>(
|
|
398
|
-
testPaths.socketPath,
|
|
399
|
-
"POST",
|
|
400
|
-
"/memory",
|
|
401
|
-
"not valid json body -- this gets stringified so let's test empty object parse",
|
|
402
|
-
);
|
|
403
|
-
// The body is valid JSON (string gets stringified), but Zod rejects it
|
|
404
|
-
expect(status).toBe(400);
|
|
405
|
-
expect(data.error).toBeDefined();
|
|
406
|
-
});
|
|
407
|
-
});
|