@exaudeus/memory-mcp 0.1.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/LICENSE +21 -0
- package/README.md +264 -0
- package/dist/__tests__/clock-and-validators.test.d.ts +1 -0
- package/dist/__tests__/clock-and-validators.test.js +237 -0
- package/dist/__tests__/config-manager.test.d.ts +1 -0
- package/dist/__tests__/config-manager.test.js +142 -0
- package/dist/__tests__/config.test.d.ts +1 -0
- package/dist/__tests__/config.test.js +236 -0
- package/dist/__tests__/crash-journal.test.d.ts +1 -0
- package/dist/__tests__/crash-journal.test.js +203 -0
- package/dist/__tests__/e2e.test.d.ts +1 -0
- package/dist/__tests__/e2e.test.js +788 -0
- package/dist/__tests__/ephemeral-benchmark.test.d.ts +1 -0
- package/dist/__tests__/ephemeral-benchmark.test.js +651 -0
- package/dist/__tests__/ephemeral.test.d.ts +1 -0
- package/dist/__tests__/ephemeral.test.js +435 -0
- package/dist/__tests__/git-service.test.d.ts +1 -0
- package/dist/__tests__/git-service.test.js +43 -0
- package/dist/__tests__/normalize.test.d.ts +1 -0
- package/dist/__tests__/normalize.test.js +161 -0
- package/dist/__tests__/store.test.d.ts +1 -0
- package/dist/__tests__/store.test.js +1153 -0
- package/dist/config-manager.d.ts +49 -0
- package/dist/config-manager.js +126 -0
- package/dist/config.d.ts +32 -0
- package/dist/config.js +162 -0
- package/dist/crash-journal.d.ts +38 -0
- package/dist/crash-journal.js +198 -0
- package/dist/ephemeral-weights.json +1847 -0
- package/dist/ephemeral.d.ts +20 -0
- package/dist/ephemeral.js +516 -0
- package/dist/formatters.d.ts +10 -0
- package/dist/formatters.js +92 -0
- package/dist/git-service.d.ts +5 -0
- package/dist/git-service.js +39 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1197 -0
- package/dist/normalize.d.ts +2 -0
- package/dist/normalize.js +69 -0
- package/dist/store.d.ts +84 -0
- package/dist/store.js +813 -0
- package/dist/text-analyzer.d.ts +32 -0
- package/dist/text-analyzer.js +190 -0
- package/dist/thresholds.d.ts +39 -0
- package/dist/thresholds.js +75 -0
- package/dist/types.d.ts +186 -0
- package/dist/types.js +33 -0
- package/package.json +57 -0
|
@@ -0,0 +1,788 @@
|
|
|
1
|
+
// E2E test: spawns the actual MCP server and communicates via JSON-RPC over stdio.
|
|
2
|
+
// Tests the full lifecycle with zero mocks — the real binary, real disk I/O, real config.
|
|
3
|
+
//
|
|
4
|
+
// Framing: MCP uses LSP-style Content-Length headers over stdio.
|
|
5
|
+
import { describe, it, before, after } from 'node:test';
|
|
6
|
+
import assert from 'node:assert';
|
|
7
|
+
import { spawn } from 'child_process';
|
|
8
|
+
import { promises as fs } from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import os from 'os';
|
|
11
|
+
// --- JSON-RPC / MCP helpers ---
|
|
12
|
+
// MCP stdio transport uses newline-delimited JSON (one JSON object per line).
|
|
13
|
+
/** Encode a JSON-RPC message as a newline-delimited JSON string */
|
|
14
|
+
function encode(msg) {
|
|
15
|
+
return JSON.stringify(msg) + '\n';
|
|
16
|
+
}
|
|
17
|
+
/** Parse newline-delimited JSON messages from a buffer */
|
|
18
|
+
function parseLines(buffer) {
|
|
19
|
+
const messages = [];
|
|
20
|
+
let remaining = buffer;
|
|
21
|
+
while (true) {
|
|
22
|
+
const newlineIdx = remaining.indexOf('\n');
|
|
23
|
+
if (newlineIdx === -1)
|
|
24
|
+
break;
|
|
25
|
+
const line = remaining.substring(0, newlineIdx).replace(/\r$/, '');
|
|
26
|
+
remaining = remaining.substring(newlineIdx + 1);
|
|
27
|
+
if (line.trim().length === 0)
|
|
28
|
+
continue;
|
|
29
|
+
try {
|
|
30
|
+
messages.push(JSON.parse(line));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Incomplete or malformed line — skip
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { messages, remaining };
|
|
37
|
+
}
|
|
38
|
+
/** Manages a spawned MCP server subprocess and provides a high-level call interface */
|
|
39
|
+
class McpTestClient {
|
|
40
|
+
constructor(repoRoot, memoryDir) {
|
|
41
|
+
this.repoRoot = repoRoot;
|
|
42
|
+
this.memoryDir = memoryDir;
|
|
43
|
+
this.proc = null;
|
|
44
|
+
this.buffer = '';
|
|
45
|
+
this.pendingResolves = new Map();
|
|
46
|
+
this.nextId = 1;
|
|
47
|
+
this.notifications = [];
|
|
48
|
+
}
|
|
49
|
+
/** Spawn the MCP server with env-based config pointing to our temp dir.
|
|
50
|
+
* Temporarily renames memory-config.json so the server falls through
|
|
51
|
+
* to env/default config pointing at our isolated temp dir. */
|
|
52
|
+
async start() {
|
|
53
|
+
// Resolve paths relative to this test file
|
|
54
|
+
const thisDir = path.dirname(new URL(import.meta.url).pathname);
|
|
55
|
+
const srcDir = path.resolve(thisDir, '..');
|
|
56
|
+
const projectRoot = path.resolve(srcDir, '..');
|
|
57
|
+
const serverPath = path.join(srcDir, 'index.ts');
|
|
58
|
+
// Temporarily rename memory-config.json so the server uses our env config
|
|
59
|
+
const configFile = path.join(projectRoot, 'memory-config.json');
|
|
60
|
+
const configBackup = configFile + '.e2e-backup';
|
|
61
|
+
try {
|
|
62
|
+
await fs.rename(configFile, configBackup);
|
|
63
|
+
this._configBackupPath = configBackup;
|
|
64
|
+
this._configOrigPath = configFile;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// No config file to rename — env-based config will work directly
|
|
68
|
+
}
|
|
69
|
+
this.proc = spawn('node', ['--import', 'tsx', serverPath], {
|
|
70
|
+
env: {
|
|
71
|
+
...process.env,
|
|
72
|
+
// Override config to use our temp dir as a single-lobe default
|
|
73
|
+
MEMORY_MCP_REPO_ROOT: this.repoRoot,
|
|
74
|
+
MEMORY_MCP_DIR: this.memoryDir,
|
|
75
|
+
// Clear any existing config env vars so they don't interfere
|
|
76
|
+
MEMORY_MCP_WORKSPACES: '',
|
|
77
|
+
},
|
|
78
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
79
|
+
});
|
|
80
|
+
this.proc.stdout.setEncoding('utf-8');
|
|
81
|
+
this.proc.stdout.on('data', (chunk) => {
|
|
82
|
+
this.buffer += chunk;
|
|
83
|
+
const { messages, remaining } = parseLines(this.buffer);
|
|
84
|
+
this.buffer = remaining;
|
|
85
|
+
for (const msg of messages) {
|
|
86
|
+
const rpc = msg;
|
|
87
|
+
if (rpc.id !== undefined && this.pendingResolves.has(rpc.id)) {
|
|
88
|
+
this.pendingResolves.get(rpc.id)(rpc);
|
|
89
|
+
this.pendingResolves.delete(rpc.id);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
this.notifications.push(msg);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
// Wait for process to be ready
|
|
97
|
+
await new Promise((resolve, reject) => {
|
|
98
|
+
const timeout = setTimeout(() => reject(new Error('Server start timeout')), 10000);
|
|
99
|
+
this.proc.stderr.setEncoding('utf-8');
|
|
100
|
+
this.proc.stderr.on('data', (chunk) => {
|
|
101
|
+
if (chunk.includes('Server started')) {
|
|
102
|
+
clearTimeout(timeout);
|
|
103
|
+
resolve();
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
this.proc.on('error', (err) => {
|
|
107
|
+
clearTimeout(timeout);
|
|
108
|
+
reject(err);
|
|
109
|
+
});
|
|
110
|
+
this.proc.on('exit', (code) => {
|
|
111
|
+
if (code !== 0) {
|
|
112
|
+
clearTimeout(timeout);
|
|
113
|
+
reject(new Error(`Server exited with code ${code}`));
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
// MCP handshake: initialize
|
|
118
|
+
const initResult = await this.send('initialize', {
|
|
119
|
+
protocolVersion: '2024-11-05',
|
|
120
|
+
capabilities: {},
|
|
121
|
+
clientInfo: { name: 'e2e-test', version: '1.0.0' },
|
|
122
|
+
});
|
|
123
|
+
assert.ok(initResult.result, 'Initialize should return a result');
|
|
124
|
+
// Send initialized notification (no id = notification)
|
|
125
|
+
this.proc.stdin.write(encode({
|
|
126
|
+
jsonrpc: '2.0',
|
|
127
|
+
method: 'notifications/initialized',
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
130
|
+
/** Send a JSON-RPC request and wait for the response */
|
|
131
|
+
async send(method, params) {
|
|
132
|
+
const id = this.nextId++;
|
|
133
|
+
return new Promise((resolve, reject) => {
|
|
134
|
+
const timeout = setTimeout(() => {
|
|
135
|
+
this.pendingResolves.delete(id);
|
|
136
|
+
reject(new Error(`Timeout waiting for response to ${method} (id=${id})`));
|
|
137
|
+
}, 15000);
|
|
138
|
+
this.pendingResolves.set(id, (msg) => {
|
|
139
|
+
clearTimeout(timeout);
|
|
140
|
+
resolve(msg);
|
|
141
|
+
});
|
|
142
|
+
this.proc.stdin.write(encode({ jsonrpc: '2.0', id, method, params }));
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
/** Call an MCP tool and return the response */
|
|
146
|
+
async callTool(name, args = {}) {
|
|
147
|
+
return this.send('tools/call', { name, arguments: args });
|
|
148
|
+
}
|
|
149
|
+
/** Get the text content from a tool response */
|
|
150
|
+
getText(response) {
|
|
151
|
+
return response.result?.content?.[0]?.text ?? '';
|
|
152
|
+
}
|
|
153
|
+
/** Check if response is an error */
|
|
154
|
+
isError(response) {
|
|
155
|
+
return response.result?.isError === true || !!response.error;
|
|
156
|
+
}
|
|
157
|
+
/** Gracefully shut down the server and restore any modified config files */
|
|
158
|
+
async stop() {
|
|
159
|
+
if (this.proc) {
|
|
160
|
+
this.proc.stdin.end();
|
|
161
|
+
await new Promise((resolve) => {
|
|
162
|
+
const timeout = setTimeout(() => {
|
|
163
|
+
this.proc?.kill('SIGKILL');
|
|
164
|
+
resolve();
|
|
165
|
+
}, 3000);
|
|
166
|
+
this.proc.on('exit', () => {
|
|
167
|
+
clearTimeout(timeout);
|
|
168
|
+
resolve();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
this.proc = null;
|
|
172
|
+
}
|
|
173
|
+
// Restore config file if we renamed it
|
|
174
|
+
if (this._configBackupPath && this._configOrigPath) {
|
|
175
|
+
await fs.rename(this._configBackupPath, this._configOrigPath).catch(() => { });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// --- E2E Tests ---
|
|
180
|
+
describe('E2E: MCP Server', () => {
|
|
181
|
+
let tempDir;
|
|
182
|
+
let memoryDir;
|
|
183
|
+
let client;
|
|
184
|
+
before(async () => {
|
|
185
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'memory-mcp-e2e-'));
|
|
186
|
+
memoryDir = path.join(tempDir, '.memory');
|
|
187
|
+
// Create a minimal repo structure for bootstrap
|
|
188
|
+
await fs.mkdir(path.join(tempDir, 'src'), { recursive: true });
|
|
189
|
+
await fs.writeFile(path.join(tempDir, 'package.json'), '{"name": "test-repo"}');
|
|
190
|
+
await fs.writeFile(path.join(tempDir, 'README.md'), '# Test Repo\n\nA repo for e2e testing the memory MCP server.');
|
|
191
|
+
client = new McpTestClient(tempDir, memoryDir);
|
|
192
|
+
await client.start();
|
|
193
|
+
});
|
|
194
|
+
after(async () => {
|
|
195
|
+
await client.stop();
|
|
196
|
+
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => { });
|
|
197
|
+
});
|
|
198
|
+
describe('tool listing', () => {
|
|
199
|
+
it('lists all available tools', async () => {
|
|
200
|
+
const response = await client.send('tools/list', {});
|
|
201
|
+
const tools = response.result?.tools;
|
|
202
|
+
assert.ok(Array.isArray(tools), 'Should return an array of tools');
|
|
203
|
+
const toolNames = tools.map(t => t.name);
|
|
204
|
+
// 5 visible tools
|
|
205
|
+
assert.ok(toolNames.includes('memory_store'), 'Should have memory_store');
|
|
206
|
+
assert.ok(toolNames.includes('memory_query'), 'Should have memory_query');
|
|
207
|
+
assert.ok(toolNames.includes('memory_correct'), 'Should have memory_correct');
|
|
208
|
+
assert.ok(toolNames.includes('memory_context'), 'Should have memory_context');
|
|
209
|
+
assert.ok(toolNames.includes('memory_bootstrap'), 'Should have memory_bootstrap');
|
|
210
|
+
// Hidden tools — still callable but not in the catalog
|
|
211
|
+
assert.ok(!toolNames.includes('memory_briefing'), 'Should NOT have memory_briefing (replaced by memory_context)');
|
|
212
|
+
assert.ok(!toolNames.includes('memory_diagnose'), 'Should NOT list memory_diagnose (hidden)');
|
|
213
|
+
assert.ok(!toolNames.includes('memory_stats'), 'Should NOT list memory_stats (hidden)');
|
|
214
|
+
assert.ok(!toolNames.includes('memory_list_lobes'), 'Should NOT list memory_list_lobes (hidden)');
|
|
215
|
+
assert.strictEqual(toolNames.length, 5, 'Should have exactly 5 visible tools');
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
describe('memory_list_lobes', () => {
|
|
219
|
+
it('returns server info with default lobe', async () => {
|
|
220
|
+
const response = await client.callTool('memory_list_lobes');
|
|
221
|
+
assert.ok(!client.isError(response), 'Should not be an error');
|
|
222
|
+
const text = client.getText(response);
|
|
223
|
+
const data = JSON.parse(text);
|
|
224
|
+
assert.strictEqual(data.serverMode, 'running');
|
|
225
|
+
assert.ok(data.lobes.length >= 1, 'Should have at least one lobe');
|
|
226
|
+
assert.strictEqual(data.lobes[0].health, 'healthy');
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
describe('store -> query -> correct lifecycle', () => {
|
|
230
|
+
let storedId;
|
|
231
|
+
it('stores a knowledge entry', async () => {
|
|
232
|
+
const response = await client.callTool('memory_store', {
|
|
233
|
+
topic: 'architecture',
|
|
234
|
+
title: 'E2E Test Pattern',
|
|
235
|
+
content: 'This repo uses MVI architecture with standalone reducers',
|
|
236
|
+
trust: 'user',
|
|
237
|
+
});
|
|
238
|
+
assert.ok(!client.isError(response), `Store should succeed: ${client.getText(response)}`);
|
|
239
|
+
const text = client.getText(response);
|
|
240
|
+
assert.ok(text.includes('Stored entry'), 'Should confirm storage');
|
|
241
|
+
assert.ok(text.includes('architecture'), 'Should mention topic');
|
|
242
|
+
// Extract ID from response
|
|
243
|
+
const idMatch = text.match(/Stored entry (arch-[0-9a-f]+)/);
|
|
244
|
+
assert.ok(idMatch, 'Should contain entry ID');
|
|
245
|
+
storedId = idMatch[1];
|
|
246
|
+
});
|
|
247
|
+
it('queries the stored entry back', async () => {
|
|
248
|
+
const response = await client.callTool('memory_query', {
|
|
249
|
+
scope: 'architecture',
|
|
250
|
+
detail: 'full',
|
|
251
|
+
});
|
|
252
|
+
assert.ok(!client.isError(response));
|
|
253
|
+
const text = client.getText(response);
|
|
254
|
+
assert.ok(text.includes('E2E Test Pattern'), 'Should find stored entry by title');
|
|
255
|
+
assert.ok(text.includes('MVI architecture'), 'Should contain the stored content');
|
|
256
|
+
assert.ok(text.includes(storedId), 'Should contain the entry ID');
|
|
257
|
+
});
|
|
258
|
+
it('queries with wildcard scope', async () => {
|
|
259
|
+
const response = await client.callTool('memory_query', {
|
|
260
|
+
scope: '*',
|
|
261
|
+
detail: 'brief',
|
|
262
|
+
});
|
|
263
|
+
assert.ok(!client.isError(response));
|
|
264
|
+
const text = client.getText(response);
|
|
265
|
+
assert.ok(text.includes('E2E Test Pattern'));
|
|
266
|
+
});
|
|
267
|
+
it('queries with filter', async () => {
|
|
268
|
+
const response = await client.callTool('memory_query', {
|
|
269
|
+
scope: '*',
|
|
270
|
+
detail: 'brief',
|
|
271
|
+
filter: 'MVI reducer',
|
|
272
|
+
});
|
|
273
|
+
assert.ok(!client.isError(response));
|
|
274
|
+
const text = client.getText(response);
|
|
275
|
+
assert.ok(text.includes('E2E Test Pattern'), 'Filter should match entry');
|
|
276
|
+
});
|
|
277
|
+
it('corrects an entry with append', async () => {
|
|
278
|
+
const response = await client.callTool('memory_correct', {
|
|
279
|
+
id: storedId,
|
|
280
|
+
action: 'append',
|
|
281
|
+
correction: 'Also uses sealed interfaces for events',
|
|
282
|
+
});
|
|
283
|
+
assert.ok(!client.isError(response), `Correct should succeed: ${client.getText(response)}`);
|
|
284
|
+
const text = client.getText(response);
|
|
285
|
+
assert.ok(text.includes('Corrected entry'), 'Should confirm correction');
|
|
286
|
+
// Verify the append persisted
|
|
287
|
+
const queryResp = await client.callTool('memory_query', {
|
|
288
|
+
scope: 'architecture',
|
|
289
|
+
detail: 'full',
|
|
290
|
+
});
|
|
291
|
+
const queryText = client.getText(queryResp);
|
|
292
|
+
assert.ok(queryText.includes('sealed interfaces'), 'Appended content should be visible');
|
|
293
|
+
assert.ok(queryText.includes('MVI architecture'), 'Original content should remain');
|
|
294
|
+
});
|
|
295
|
+
it('corrects an entry with replace', async () => {
|
|
296
|
+
const response = await client.callTool('memory_correct', {
|
|
297
|
+
id: storedId,
|
|
298
|
+
action: 'replace',
|
|
299
|
+
correction: 'Clean Architecture with MVVM pattern',
|
|
300
|
+
});
|
|
301
|
+
assert.ok(!client.isError(response));
|
|
302
|
+
const queryResp = await client.callTool('memory_query', {
|
|
303
|
+
scope: 'architecture',
|
|
304
|
+
detail: 'full',
|
|
305
|
+
});
|
|
306
|
+
const queryText = client.getText(queryResp);
|
|
307
|
+
assert.ok(queryText.includes('Clean Architecture'), 'Replaced content should be visible');
|
|
308
|
+
assert.ok(!queryText.includes('MVI architecture'), 'Original content should be gone');
|
|
309
|
+
});
|
|
310
|
+
it('deletes an entry', async () => {
|
|
311
|
+
const response = await client.callTool('memory_correct', {
|
|
312
|
+
id: storedId,
|
|
313
|
+
action: 'delete',
|
|
314
|
+
});
|
|
315
|
+
assert.ok(!client.isError(response));
|
|
316
|
+
const text = client.getText(response);
|
|
317
|
+
assert.ok(text.includes('Deleted entry'), 'Should confirm deletion');
|
|
318
|
+
// Verify it's gone
|
|
319
|
+
const queryResp = await client.callTool('memory_query', {
|
|
320
|
+
scope: 'architecture',
|
|
321
|
+
detail: 'brief',
|
|
322
|
+
});
|
|
323
|
+
const queryText = client.getText(queryResp);
|
|
324
|
+
assert.ok(!queryText.includes(storedId), 'Deleted entry should not appear');
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
describe('context search', () => {
|
|
328
|
+
before(async () => {
|
|
329
|
+
// Seed entries for context search
|
|
330
|
+
await client.callTool('memory_store', {
|
|
331
|
+
topic: 'architecture',
|
|
332
|
+
title: 'Kotlin Reducer Pattern',
|
|
333
|
+
content: 'Standalone reducer classes with inject constructor in Kotlin',
|
|
334
|
+
trust: 'user',
|
|
335
|
+
});
|
|
336
|
+
await client.callTool('memory_store', {
|
|
337
|
+
topic: 'conventions',
|
|
338
|
+
title: 'Kotlin Naming',
|
|
339
|
+
content: 'Use Real prefix instead of Impl postfix for Kotlin implementation classes',
|
|
340
|
+
trust: 'user',
|
|
341
|
+
});
|
|
342
|
+
await client.callTool('memory_store', {
|
|
343
|
+
topic: 'gotchas',
|
|
344
|
+
title: 'Kotlin Build Gotcha',
|
|
345
|
+
content: 'Must clean build after Kotlin module dependency changes',
|
|
346
|
+
trust: 'user',
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
it('returns relevant entries for a context description', async () => {
|
|
350
|
+
const response = await client.callTool('memory_context', {
|
|
351
|
+
context: 'writing a Kotlin reducer for state management',
|
|
352
|
+
});
|
|
353
|
+
assert.ok(!client.isError(response));
|
|
354
|
+
const text = client.getText(response);
|
|
355
|
+
assert.ok(text.includes('Kotlin Reducer Pattern') || text.includes('Kotlin Naming') || text.includes('Kotlin Build Gotcha'), 'Should return at least one Kotlin-related entry');
|
|
356
|
+
});
|
|
357
|
+
it('returns no results for unrelated context', async () => {
|
|
358
|
+
const response = await client.callTool('memory_context', {
|
|
359
|
+
context: 'quantum computing neural networks',
|
|
360
|
+
});
|
|
361
|
+
const text = client.getText(response);
|
|
362
|
+
assert.ok(text.includes('No relevant knowledge found') || text.includes('Context:'), 'Should either find nothing or return minimal matches');
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
describe('briefing mode (memory_context with no args)', () => {
|
|
366
|
+
it('generates a session briefing when context is omitted', async () => {
|
|
367
|
+
const response = await client.callTool('memory_context', {});
|
|
368
|
+
assert.ok(!client.isError(response));
|
|
369
|
+
const text = client.getText(response);
|
|
370
|
+
// Should have meaningful content (user + preferences + stale nudges)
|
|
371
|
+
assert.ok(text.length > 50, 'Briefing should have meaningful content');
|
|
372
|
+
assert.ok(text.includes('memory_context'), 'Briefing should mention memory_context for task-specific lookup');
|
|
373
|
+
});
|
|
374
|
+
it('memory_diagnose still works when called directly (hidden tool)', async () => {
|
|
375
|
+
const response = await client.callTool('memory_diagnose', {});
|
|
376
|
+
assert.ok(!client.isError(response));
|
|
377
|
+
const text = client.getText(response);
|
|
378
|
+
assert.ok(text.includes('Diagnostics') || text.includes('Server mode'), 'Should return diagnostics');
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
describe('stats', () => {
|
|
382
|
+
it('returns memory stats', async () => {
|
|
383
|
+
const response = await client.callTool('memory_stats', {});
|
|
384
|
+
assert.ok(!client.isError(response));
|
|
385
|
+
const text = client.getText(response);
|
|
386
|
+
assert.ok(text.includes('Memory Stats'), 'Should have stats header');
|
|
387
|
+
assert.ok(text.includes('Total entries'), 'Should show total entries');
|
|
388
|
+
assert.ok(text.includes('By Topic'), 'Should show topic breakdown');
|
|
389
|
+
assert.ok(text.includes('By Trust'), 'Should show trust breakdown');
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
describe('bootstrap', () => {
|
|
393
|
+
it('seeds entries from repo structure', async () => {
|
|
394
|
+
const response = await client.callTool('memory_bootstrap', {});
|
|
395
|
+
assert.ok(!client.isError(response), `Bootstrap should succeed: ${client.getText(response)}`);
|
|
396
|
+
const text = client.getText(response);
|
|
397
|
+
assert.ok(text.includes('Bootstrap Complete'), 'Should confirm bootstrap');
|
|
398
|
+
assert.ok(text.includes('Stored'), 'Should report stored entries');
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
describe('diagnose', () => {
|
|
402
|
+
it('returns diagnostics', async () => {
|
|
403
|
+
const response = await client.callTool('memory_diagnose', {});
|
|
404
|
+
assert.ok(!client.isError(response));
|
|
405
|
+
const text = client.getText(response);
|
|
406
|
+
assert.ok(text.includes('Diagnostics'), 'Should have diagnostics header');
|
|
407
|
+
assert.ok(text.includes('Server mode'), 'Should show server mode');
|
|
408
|
+
assert.ok(text.includes('running'), 'Server should be in running mode');
|
|
409
|
+
assert.ok(text.includes('Lobe Health'), 'Should show lobe health');
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
describe('error handling', () => {
|
|
413
|
+
it('rejects invalid topic', async () => {
|
|
414
|
+
const response = await client.callTool('memory_store', {
|
|
415
|
+
topic: 'banana',
|
|
416
|
+
title: 'Test',
|
|
417
|
+
content: 'Test',
|
|
418
|
+
});
|
|
419
|
+
assert.ok(client.isError(response), 'Should be an error');
|
|
420
|
+
const text = client.getText(response);
|
|
421
|
+
assert.ok(text.includes('Invalid topic'), 'Should mention invalid topic');
|
|
422
|
+
});
|
|
423
|
+
it('rejects missing required fields', async () => {
|
|
424
|
+
const response = await client.callTool('memory_store', {
|
|
425
|
+
topic: 'architecture',
|
|
426
|
+
// title and content missing
|
|
427
|
+
});
|
|
428
|
+
assert.ok(client.isError(response), 'Should be an error');
|
|
429
|
+
});
|
|
430
|
+
it('rejects correct on non-existent entry', async () => {
|
|
431
|
+
const response = await client.callTool('memory_correct', {
|
|
432
|
+
id: 'nonexistent-999',
|
|
433
|
+
action: 'replace',
|
|
434
|
+
correction: 'fix',
|
|
435
|
+
});
|
|
436
|
+
assert.ok(client.isError(response), 'Should be an error');
|
|
437
|
+
const text = client.getText(response);
|
|
438
|
+
assert.ok(text.includes('not found') || text.includes('Failed to correct'), 'Should mention not found');
|
|
439
|
+
});
|
|
440
|
+
it('returns error for unknown tool', async () => {
|
|
441
|
+
const response = await client.callTool('memory_nonexistent', {});
|
|
442
|
+
const text = client.getText(response);
|
|
443
|
+
assert.ok(text.includes('Unknown tool') || client.isError(response));
|
|
444
|
+
});
|
|
445
|
+
it('handles correction without text for append', async () => {
|
|
446
|
+
const storeResp = await client.callTool('memory_store', {
|
|
447
|
+
topic: 'conventions',
|
|
448
|
+
title: 'Temp Entry for Error Test',
|
|
449
|
+
content: 'temp',
|
|
450
|
+
});
|
|
451
|
+
const idMatch = client.getText(storeResp).match(/(conv-[0-9a-f]+)/);
|
|
452
|
+
if (idMatch) {
|
|
453
|
+
const response = await client.callTool('memory_correct', {
|
|
454
|
+
id: idMatch[1],
|
|
455
|
+
action: 'append',
|
|
456
|
+
// correction missing
|
|
457
|
+
});
|
|
458
|
+
assert.ok(client.isError(response), 'Should reject append without correction text');
|
|
459
|
+
// Cleanup
|
|
460
|
+
await client.callTool('memory_correct', { id: idMatch[1], action: 'delete' });
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
describe('param alias normalization (e2e)', () => {
|
|
465
|
+
it('resolves key/value aliases to title/content', async () => {
|
|
466
|
+
const response = await client.callTool('memory_store', {
|
|
467
|
+
topic: 'conventions',
|
|
468
|
+
key: 'Alias Test',
|
|
469
|
+
value: 'Testing param alias normalization works e2e',
|
|
470
|
+
trust: 'agent-inferred',
|
|
471
|
+
});
|
|
472
|
+
assert.ok(!client.isError(response), `Alias store should succeed: ${client.getText(response)}`);
|
|
473
|
+
const text = client.getText(response);
|
|
474
|
+
assert.ok(text.includes('Stored entry'), 'Should confirm storage');
|
|
475
|
+
// Verify the entry was actually stored with the alias-resolved title
|
|
476
|
+
const queryResp = await client.callTool('memory_query', {
|
|
477
|
+
scope: 'conventions',
|
|
478
|
+
detail: 'full',
|
|
479
|
+
filter: 'Alias Test',
|
|
480
|
+
});
|
|
481
|
+
assert.ok(client.getText(queryResp).includes('Alias Test'), 'Should store with aliased title');
|
|
482
|
+
// Cleanup
|
|
483
|
+
const idMatch = text.match(/(conv-[0-9a-f]+)/);
|
|
484
|
+
if (idMatch) {
|
|
485
|
+
await client.callTool('memory_correct', { id: idMatch[1], action: 'delete' });
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
describe('dedup detection (e2e)', () => {
|
|
490
|
+
it('surfaces related entries when storing similar content', async () => {
|
|
491
|
+
// Store first entry
|
|
492
|
+
await client.callTool('memory_store', {
|
|
493
|
+
topic: 'conventions',
|
|
494
|
+
title: 'Sealed Interface Convention',
|
|
495
|
+
content: 'Always use sealed interfaces for state management events in Kotlin',
|
|
496
|
+
trust: 'user',
|
|
497
|
+
});
|
|
498
|
+
// Store very similar entry
|
|
499
|
+
const response = await client.callTool('memory_store', {
|
|
500
|
+
topic: 'conventions',
|
|
501
|
+
title: 'State Management Conventions',
|
|
502
|
+
content: 'Use sealed interfaces for state management events and actions in Kotlin modules',
|
|
503
|
+
trust: 'user',
|
|
504
|
+
});
|
|
505
|
+
assert.ok(!client.isError(response));
|
|
506
|
+
const text = client.getText(response);
|
|
507
|
+
// The dedup detection may or may not fire depending on similarity threshold
|
|
508
|
+
// Just verify the store succeeded
|
|
509
|
+
assert.ok(text.includes('Stored entry'), 'Similar entry should still be stored');
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
describe('multiple topics lifecycle', () => {
|
|
513
|
+
it('stores and queries across multiple topics', async () => {
|
|
514
|
+
// Store in multiple topics
|
|
515
|
+
const arch = await client.callTool('memory_store', {
|
|
516
|
+
topic: 'architecture',
|
|
517
|
+
title: 'Multi-Topic Arch',
|
|
518
|
+
content: 'Multi-topic architecture test',
|
|
519
|
+
});
|
|
520
|
+
const conv = await client.callTool('memory_store', {
|
|
521
|
+
topic: 'conventions',
|
|
522
|
+
title: 'Multi-Topic Conv',
|
|
523
|
+
content: 'Multi-topic conventions test',
|
|
524
|
+
});
|
|
525
|
+
const gotcha = await client.callTool('memory_store', {
|
|
526
|
+
topic: 'gotchas',
|
|
527
|
+
title: 'Multi-Topic Gotcha',
|
|
528
|
+
content: 'Multi-topic gotcha test',
|
|
529
|
+
});
|
|
530
|
+
assert.ok(!client.isError(arch));
|
|
531
|
+
assert.ok(!client.isError(conv));
|
|
532
|
+
assert.ok(!client.isError(gotcha));
|
|
533
|
+
// Query all — should find entries from all topics
|
|
534
|
+
const allResp = await client.callTool('memory_query', {
|
|
535
|
+
scope: '*',
|
|
536
|
+
detail: 'brief',
|
|
537
|
+
filter: 'Multi-Topic',
|
|
538
|
+
});
|
|
539
|
+
const allText = client.getText(allResp);
|
|
540
|
+
assert.ok(allText.includes('Multi-Topic Arch'), 'Should find architecture entry');
|
|
541
|
+
assert.ok(allText.includes('Multi-Topic Conv'), 'Should find conventions entry');
|
|
542
|
+
assert.ok(allText.includes('Multi-Topic Gotcha'), 'Should find gotcha entry');
|
|
543
|
+
// Query single topic
|
|
544
|
+
const gotchaResp = await client.callTool('memory_query', {
|
|
545
|
+
scope: 'gotchas',
|
|
546
|
+
detail: 'brief',
|
|
547
|
+
filter: 'Multi-Topic',
|
|
548
|
+
});
|
|
549
|
+
const gotchaText = client.getText(gotchaResp);
|
|
550
|
+
assert.ok(gotchaText.includes('Multi-Topic Gotcha'), 'Should find gotcha');
|
|
551
|
+
assert.ok(!gotchaText.includes('Multi-Topic Arch'), 'Should not find architecture in gotchas scope');
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
describe('module-scoped topics', () => {
|
|
555
|
+
it('stores and queries module-scoped entries', async () => {
|
|
556
|
+
const response = await client.callTool('memory_store', {
|
|
557
|
+
topic: 'modules/messaging',
|
|
558
|
+
title: 'Messaging Module Overview',
|
|
559
|
+
content: 'The messaging module handles real-time chat with StreamCoordinator',
|
|
560
|
+
});
|
|
561
|
+
assert.ok(!client.isError(response), `Module store should succeed: ${client.getText(response)}`);
|
|
562
|
+
const text = client.getText(response);
|
|
563
|
+
assert.ok(text.includes('modules/messaging'), 'Should mention the module topic');
|
|
564
|
+
// Query it back
|
|
565
|
+
const queryResp = await client.callTool('memory_query', {
|
|
566
|
+
scope: 'modules/messaging',
|
|
567
|
+
detail: 'full',
|
|
568
|
+
});
|
|
569
|
+
assert.ok(!client.isError(queryResp));
|
|
570
|
+
const queryText = client.getText(queryResp);
|
|
571
|
+
assert.ok(queryText.includes('StreamCoordinator'), 'Should find module entry content');
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
575
|
+
// Feature 1: references field (e2e)
|
|
576
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
577
|
+
describe('references field (e2e)', () => {
|
|
578
|
+
it('stores and queries references via tool call', async () => {
|
|
579
|
+
const storeResp = await client.callTool('memory_store', {
|
|
580
|
+
topic: 'architecture',
|
|
581
|
+
title: 'Messaging Reducer Architecture',
|
|
582
|
+
content: 'The messaging feature uses a standalone reducer pattern with sealed interface events',
|
|
583
|
+
references: ['features/messaging/impl/MessagingReducer.kt', 'features/messaging/api/MessagingEvent.kt'],
|
|
584
|
+
trust: 'agent-confirmed',
|
|
585
|
+
});
|
|
586
|
+
assert.ok(!client.isError(storeResp), `Store with references should succeed: ${client.getText(storeResp)}`);
|
|
587
|
+
const storeText = client.getText(storeResp);
|
|
588
|
+
assert.ok(storeText.includes('Stored entry'), 'Should confirm storage');
|
|
589
|
+
// Query back with full detail — references should appear
|
|
590
|
+
const queryResp = await client.callTool('memory_query', {
|
|
591
|
+
scope: 'architecture',
|
|
592
|
+
detail: 'full',
|
|
593
|
+
filter: 'Messaging Reducer Architecture',
|
|
594
|
+
});
|
|
595
|
+
assert.ok(!client.isError(queryResp));
|
|
596
|
+
const queryText = client.getText(queryResp);
|
|
597
|
+
assert.ok(queryText.includes('MessagingReducer.kt'), 'References should appear in full detail response');
|
|
598
|
+
assert.ok(queryText.includes('MessagingEvent.kt'), 'All references should appear');
|
|
599
|
+
// Extract ID and clean up
|
|
600
|
+
const idMatch = storeText.match(/(arch-[0-9a-f]+)/);
|
|
601
|
+
if (idMatch) {
|
|
602
|
+
await client.callTool('memory_correct', { id: idMatch[1], action: 'delete' });
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
it('accepts refs alias for references', async () => {
|
|
606
|
+
const storeResp = await client.callTool('memory_store', {
|
|
607
|
+
topic: 'conventions',
|
|
608
|
+
title: 'Refs Alias Test',
|
|
609
|
+
content: 'Testing that refs is accepted as an alias for references field in tool calls',
|
|
610
|
+
refs: ['src/SomeClass.kt'],
|
|
611
|
+
});
|
|
612
|
+
assert.ok(!client.isError(storeResp), `Store with refs alias should succeed: ${client.getText(storeResp)}`);
|
|
613
|
+
// Query back with full detail
|
|
614
|
+
const queryResp = await client.callTool('memory_query', {
|
|
615
|
+
scope: 'conventions',
|
|
616
|
+
detail: 'full',
|
|
617
|
+
filter: 'Refs Alias Test',
|
|
618
|
+
});
|
|
619
|
+
const queryText = client.getText(queryResp);
|
|
620
|
+
assert.ok(queryText.includes('SomeClass.kt'), 'refs alias should be normalized to references');
|
|
621
|
+
// Clean up
|
|
622
|
+
const storeText = client.getText(storeResp);
|
|
623
|
+
const idMatch = storeText.match(/(conv-[0-9a-f]+)/);
|
|
624
|
+
if (idMatch) {
|
|
625
|
+
await client.callTool('memory_correct', { id: idMatch[1], action: 'delete' });
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
});
|
|
629
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
630
|
+
// Feature 2: Stale entry nudges in briefing (e2e)
|
|
631
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
632
|
+
describe('stale entry touch via memory_correct (e2e)', () => {
|
|
633
|
+
it('refreshes lastAccessed when correcting with empty append', async () => {
|
|
634
|
+
// Store an entry
|
|
635
|
+
const storeResp = await client.callTool('memory_store', {
|
|
636
|
+
topic: 'architecture',
|
|
637
|
+
title: 'Touchable Entry',
|
|
638
|
+
content: 'Architecture pattern to verify timestamp refresh via empty append',
|
|
639
|
+
trust: 'agent-inferred',
|
|
640
|
+
});
|
|
641
|
+
assert.ok(!client.isError(storeResp));
|
|
642
|
+
const storeText = client.getText(storeResp);
|
|
643
|
+
const idMatch = storeText.match(/(arch-[0-9a-f]+)/);
|
|
644
|
+
assert.ok(idMatch, 'Should get an entry ID');
|
|
645
|
+
const entryId = idMatch[1];
|
|
646
|
+
// Touch the entry with an empty append — this refreshes lastAccessed
|
|
647
|
+
const touchResp = await client.callTool('memory_correct', {
|
|
648
|
+
id: entryId,
|
|
649
|
+
action: 'append',
|
|
650
|
+
correction: '',
|
|
651
|
+
});
|
|
652
|
+
assert.ok(!client.isError(touchResp), `Empty append should succeed: ${client.getText(touchResp)}`);
|
|
653
|
+
assert.ok(client.getText(touchResp).includes('Corrected entry'), 'Should confirm the correction');
|
|
654
|
+
// Query back — entry should still exist with original content structure
|
|
655
|
+
const queryResp = await client.callTool('memory_query', {
|
|
656
|
+
scope: 'architecture',
|
|
657
|
+
detail: 'full',
|
|
658
|
+
filter: 'Touchable Entry',
|
|
659
|
+
});
|
|
660
|
+
assert.ok(!client.isError(queryResp));
|
|
661
|
+
assert.ok(client.getText(queryResp).includes('Architecture pattern'), 'Content should persist after touch');
|
|
662
|
+
// Clean up
|
|
663
|
+
await client.callTool('memory_correct', { id: entryId, action: 'delete' });
|
|
664
|
+
});
|
|
665
|
+
});
|
|
666
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
667
|
+
// Feature 3: Conflict detection (e2e)
|
|
668
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
669
|
+
describe('conflict detection (e2e)', () => {
|
|
670
|
+
it('surfaces conflict warning when querying highly similar entries', async () => {
|
|
671
|
+
const longShared = 'This codebase uses MVI architecture pattern with standalone reducer classes sealed interface events ViewModels as orchestrators and coroutines for async operations';
|
|
672
|
+
const r1 = await client.callTool('memory_store', {
|
|
673
|
+
topic: 'architecture',
|
|
674
|
+
title: 'MVI Architecture Overview',
|
|
675
|
+
content: longShared,
|
|
676
|
+
trust: 'user',
|
|
677
|
+
});
|
|
678
|
+
const r2 = await client.callTool('memory_store', {
|
|
679
|
+
topic: 'conventions',
|
|
680
|
+
title: 'Architecture Conventions',
|
|
681
|
+
content: longShared + ' following clean architecture principles',
|
|
682
|
+
trust: 'user',
|
|
683
|
+
});
|
|
684
|
+
assert.ok(!client.isError(r1) && !client.isError(r2), 'Both stores should succeed');
|
|
685
|
+
// Query with wildcard — should detect the conflict cross-topic
|
|
686
|
+
const queryResp = await client.callTool('memory_query', {
|
|
687
|
+
scope: '*',
|
|
688
|
+
detail: 'full',
|
|
689
|
+
filter: 'MVI architecture reducer',
|
|
690
|
+
});
|
|
691
|
+
const queryText = client.getText(queryResp);
|
|
692
|
+
assert.ok(queryText.includes('⚠') || queryText.includes('Potential conflicts'), 'Should surface conflict warning for highly similar entries');
|
|
693
|
+
// Clean up
|
|
694
|
+
const t1 = client.getText(r1).match(/(arch-[0-9a-f]+)/);
|
|
695
|
+
const t2 = client.getText(r2).match(/(conv-[0-9a-f]+)/);
|
|
696
|
+
if (t1)
|
|
697
|
+
await client.callTool('memory_correct', { id: t1[1], action: 'delete' });
|
|
698
|
+
if (t2)
|
|
699
|
+
await client.callTool('memory_correct', { id: t2[1], action: 'delete' });
|
|
700
|
+
});
|
|
701
|
+
});
|
|
702
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
703
|
+
// Operational tools and diagnostics (e2e)
|
|
704
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
705
|
+
describe('operational tools (e2e)', () => {
|
|
706
|
+
it('memory_list_lobes returns lobe info', async () => {
|
|
707
|
+
const resp = await client.callTool('memory_list_lobes');
|
|
708
|
+
assert.ok(!client.isError(resp), 'memory_list_lobes should work');
|
|
709
|
+
const data = JSON.parse(client.getText(resp));
|
|
710
|
+
assert.ok(data.lobes, 'Should have lobes array');
|
|
711
|
+
assert.ok(data.serverMode, 'Should have serverMode');
|
|
712
|
+
});
|
|
713
|
+
it('memory_stats returns stats', async () => {
|
|
714
|
+
const resp = await client.callTool('memory_stats');
|
|
715
|
+
assert.ok(!client.isError(resp), 'memory_stats should work');
|
|
716
|
+
assert.ok(client.getText(resp).includes('Memory Stats'), 'Should contain stats header');
|
|
717
|
+
});
|
|
718
|
+
it('memory_diagnose returns diagnostics', async () => {
|
|
719
|
+
const resp = await client.callTool('memory_diagnose');
|
|
720
|
+
assert.ok(!client.isError(resp), 'memory_diagnose should work');
|
|
721
|
+
assert.ok(client.getText(resp).includes('Diagnostics'), 'Should contain diagnostics header');
|
|
722
|
+
});
|
|
723
|
+
it('memory_diagnose surfaces active behavior config section', async () => {
|
|
724
|
+
const resp = await client.callTool('memory_diagnose');
|
|
725
|
+
assert.ok(!client.isError(resp), `memory_diagnose should succeed: ${client.getText(resp)}`);
|
|
726
|
+
const text = client.getText(resp);
|
|
727
|
+
// Behavior config section should always be present in diagnostics
|
|
728
|
+
assert.ok(text.includes('Active Behavior Config'), 'Should include behavior config section header');
|
|
729
|
+
assert.ok(text.includes('staleDaysStandard'), 'Should show staleDaysStandard');
|
|
730
|
+
assert.ok(text.includes('staleDaysPreferences'), 'Should show staleDaysPreferences');
|
|
731
|
+
assert.ok(text.includes('maxStaleInBriefing'), 'Should show maxStaleInBriefing');
|
|
732
|
+
// In tests there is no memory-config.json, so all values should be defaults
|
|
733
|
+
assert.ok(text.includes('(default)'), 'Should mark values as default when no behavior override is set');
|
|
734
|
+
assert.ok(text.includes('memory-config.json'), 'Should hint how to customize');
|
|
735
|
+
});
|
|
736
|
+
});
|
|
737
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
738
|
+
// Ephemeral content detection (e2e)
|
|
739
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
740
|
+
describe('ephemeral content detection (e2e)', () => {
|
|
741
|
+
it('surfaces ephemeral warning for temporal content via tool call', async () => {
|
|
742
|
+
const resp = await client.callTool('memory_store', {
|
|
743
|
+
topic: 'gotchas',
|
|
744
|
+
title: 'Ephemeral Test Entry',
|
|
745
|
+
content: 'The build is currently broken and we are investigating the root cause right now',
|
|
746
|
+
trust: 'agent-inferred',
|
|
747
|
+
});
|
|
748
|
+
assert.ok(!client.isError(resp), `Store should succeed: ${client.getText(resp)}`);
|
|
749
|
+
const text = client.getText(resp);
|
|
750
|
+
assert.ok(text.includes('Stored entry'), 'Entry should be stored (soft warning, not blocked)');
|
|
751
|
+
assert.ok(text.includes('⏳'), 'Should include ephemeral warning marker');
|
|
752
|
+
assert.ok(text.includes('ephemeral'), 'Should mention ephemeral content');
|
|
753
|
+
// Clean up
|
|
754
|
+
const idMatch = text.match(/(gotcha-[0-9a-f]+)/);
|
|
755
|
+
if (idMatch)
|
|
756
|
+
await client.callTool('memory_correct', { id: idMatch[1], action: 'delete' });
|
|
757
|
+
});
|
|
758
|
+
it('surfaces ephemeral warning for fixed-bug content', async () => {
|
|
759
|
+
const resp = await client.callTool('memory_store', {
|
|
760
|
+
topic: 'gotchas',
|
|
761
|
+
title: 'Fixed Bug Entry',
|
|
762
|
+
content: 'The crash bug in messaging has been resolved by updating the dependency injection scope',
|
|
763
|
+
trust: 'agent-confirmed',
|
|
764
|
+
});
|
|
765
|
+
assert.ok(!client.isError(resp));
|
|
766
|
+
const text = client.getText(resp);
|
|
767
|
+
assert.ok(text.includes('⏳'), 'Should include ephemeral warning for fixed bugs');
|
|
768
|
+
assert.ok(text.includes('Resolved issue') || text.includes('resolved'), 'Should flag resolved issues');
|
|
769
|
+
const idMatch = text.match(/(gotcha-[0-9a-f]+)/);
|
|
770
|
+
if (idMatch)
|
|
771
|
+
await client.callTool('memory_correct', { id: idMatch[1], action: 'delete' });
|
|
772
|
+
});
|
|
773
|
+
it('does not surface ephemeral warning for durable content', async () => {
|
|
774
|
+
const resp = await client.callTool('memory_store', {
|
|
775
|
+
topic: 'architecture',
|
|
776
|
+
title: 'Durable Architecture Entry',
|
|
777
|
+
content: 'The messaging feature uses MVI with standalone reducer classes and sealed interface events for exhaustive handling',
|
|
778
|
+
trust: 'user',
|
|
779
|
+
});
|
|
780
|
+
assert.ok(!client.isError(resp));
|
|
781
|
+
const text = client.getText(resp);
|
|
782
|
+
assert.ok(!text.includes('⏳'), 'Durable content should not trigger ephemeral warning');
|
|
783
|
+
const idMatch = text.match(/(arch-[0-9a-f]+)/);
|
|
784
|
+
if (idMatch)
|
|
785
|
+
await client.callTool('memory_correct', { id: idMatch[1], action: 'delete' });
|
|
786
|
+
});
|
|
787
|
+
});
|
|
788
|
+
});
|