@yeseh/cortex-cli 0.6.8 → 0.6.10
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/dist/program.js +1538 -5
- package/dist/program.js.map +32 -3
- package/dist/run.d.ts +0 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +3 -4
- package/dist/run.js.map +3 -3
- package/package.json +4 -6
- package/dist/chunk-dsfj4baj.js +0 -1543
- package/dist/chunk-dsfj4baj.js.map +0 -38
- package/src/category/commands/create.spec.ts +0 -139
- package/src/category/commands/create.ts +0 -119
- package/src/category/index.ts +0 -24
- package/src/commands/init.spec.ts +0 -203
- package/src/commands/init.ts +0 -301
- package/src/context.spec.ts +0 -60
- package/src/context.ts +0 -170
- package/src/errors.spec.ts +0 -264
- package/src/errors.ts +0 -105
- package/src/memory/commands/add.spec.ts +0 -169
- package/src/memory/commands/add.ts +0 -158
- package/src/memory/commands/definitions.spec.ts +0 -80
- package/src/memory/commands/list.spec.ts +0 -123
- package/src/memory/commands/list.ts +0 -269
- package/src/memory/commands/move.spec.ts +0 -85
- package/src/memory/commands/move.ts +0 -119
- package/src/memory/commands/remove.spec.ts +0 -79
- package/src/memory/commands/remove.ts +0 -108
- package/src/memory/commands/show.spec.ts +0 -71
- package/src/memory/commands/show.ts +0 -165
- package/src/memory/commands/test-helpers.spec.ts +0 -127
- package/src/memory/commands/update.spec.ts +0 -86
- package/src/memory/commands/update.ts +0 -230
- package/src/memory/index.spec.ts +0 -59
- package/src/memory/index.ts +0 -44
- package/src/memory/parsing.spec.ts +0 -105
- package/src/memory/parsing.ts +0 -22
- package/src/observability.spec.ts +0 -126
- package/src/observability.ts +0 -82
- package/src/output.spec.ts +0 -835
- package/src/output.ts +0 -119
- package/src/program.spec.ts +0 -46
- package/src/program.ts +0 -75
- package/src/run.spec.ts +0 -31
- package/src/run.ts +0 -9
- package/src/store/commands/add.spec.ts +0 -131
- package/src/store/commands/add.ts +0 -231
- package/src/store/commands/init.spec.ts +0 -220
- package/src/store/commands/init.ts +0 -272
- package/src/store/commands/list.spec.ts +0 -175
- package/src/store/commands/list.ts +0 -102
- package/src/store/commands/prune.spec.ts +0 -120
- package/src/store/commands/prune.ts +0 -152
- package/src/store/commands/reindexs.spec.ts +0 -94
- package/src/store/commands/reindexs.ts +0 -97
- package/src/store/commands/remove.spec.ts +0 -97
- package/src/store/commands/remove.ts +0 -189
- package/src/store/index.spec.ts +0 -60
- package/src/store/index.ts +0 -49
- package/src/store/utils/resolve-store-name.spec.ts +0 -62
- package/src/store/utils/resolve-store-name.ts +0 -79
- package/src/test-helpers.spec.ts +0 -430
- package/src/tests/cli.integration.spec.ts +0 -1306
- package/src/toon.spec.ts +0 -183
- package/src/toon.ts +0 -462
- package/src/utils/git.spec.ts +0 -95
- package/src/utils/git.ts +0 -51
- package/src/utils/input.spec.ts +0 -326
- package/src/utils/input.ts +0 -150
- package/src/utils/paths.spec.ts +0 -235
- package/src/utils/paths.ts +0 -75
- package/src/utils/prompts.spec.ts +0 -23
- package/src/utils/prompts.ts +0 -88
- package/src/utils/resolve-default-store.spec.ts +0 -135
- package/src/utils/resolve-default-store.ts +0 -74
|
@@ -1,1306 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Comprehensive integration tests for the Cortex CLI with Commander.js.
|
|
3
|
-
*
|
|
4
|
-
* These tests spawn the CLI as a subprocess using Bun shell to test
|
|
5
|
-
* the CLI like a user would interact with it.
|
|
6
|
-
*
|
|
7
|
-
* The tests use local store resolution by creating a project directory
|
|
8
|
-
* with a `.cortex/memory` subdirectory structure.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
|
12
|
-
import * as fs from 'node:fs/promises';
|
|
13
|
-
import { tmpdir } from 'node:os';
|
|
14
|
-
import { join } from 'node:path';
|
|
15
|
-
|
|
16
|
-
const diagnosticsEnabled = process.env.CORTEX_TEST_DIAGNOSTICS === '1';
|
|
17
|
-
|
|
18
|
-
const formatCliFailure = (
|
|
19
|
-
args: string[],
|
|
20
|
-
result: CliResult,
|
|
21
|
-
extra?: Record<string, unknown>,
|
|
22
|
-
): string => {
|
|
23
|
-
const extraLines = extra ? `\nextra: ${JSON.stringify(extra, null, 2)}` : '';
|
|
24
|
-
return (
|
|
25
|
-
[
|
|
26
|
-
'CLI subprocess failed',
|
|
27
|
-
`args: ${JSON.stringify(args)}`,
|
|
28
|
-
`exitCode: ${result.exitCode}`,
|
|
29
|
-
`stdout:\n${result.stdout}`,
|
|
30
|
-
`stderr:\n${result.stderr}`,
|
|
31
|
-
].join('\n') + extraLines
|
|
32
|
-
);
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const expectCliOk = (args: string[], result: CliResult, extra?: Record<string, unknown>): void => {
|
|
36
|
-
if (result.exitCode !== 0) {
|
|
37
|
-
throw new Error(formatCliFailure(args, result, extra));
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
import { FilesystemStorageAdapter, FilesystemConfigAdapter } from '@yeseh/cortex-storage-fs';
|
|
41
|
-
import { Memory, MemoryPath } from '@yeseh/cortex-core/memory';
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* CLI runner that spawns cortex as a subprocess.
|
|
45
|
-
* Uses Bun's shell module for realistic CLI testing.
|
|
46
|
-
*/
|
|
47
|
-
interface CliResult {
|
|
48
|
-
stdout: string;
|
|
49
|
-
stderr: string;
|
|
50
|
-
exitCode: number;
|
|
51
|
-
resolvedConfigPath?: string;
|
|
52
|
-
resolvedStoreRoot?: string;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
interface CliOptions {
|
|
56
|
-
/** Required - must be a directory with .cortex/memory inside */
|
|
57
|
-
cwd: string;
|
|
58
|
-
/** Optional stdin content */
|
|
59
|
-
stdin?: string;
|
|
60
|
-
/** Optional env overrides (merged into minimal env) */
|
|
61
|
-
env?: Record<string, string | undefined>;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Runs the cortex CLI with the given arguments.
|
|
66
|
-
* Uses the runProgram export via bun run for integration testing.
|
|
67
|
-
*
|
|
68
|
-
* The CLI resolves the store based on the current working directory,
|
|
69
|
-
* looking for a `.cortex/memory` subdirectory.
|
|
70
|
-
*/
|
|
71
|
-
const runCortexCli = async (args: string[], options: CliOptions): Promise<CliResult> => {
|
|
72
|
-
const diagnostics = diagnosticsEnabled ? '1' : '0';
|
|
73
|
-
const scriptPath = join(import.meta.dir, '..', 'run.ts');
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
// Minimal environment for isolation. Do NOT spread process.env.
|
|
77
|
-
// Bun still needs PATH for locating executables.
|
|
78
|
-
const env: Record<string, string> = {
|
|
79
|
-
PATH: process.env.PATH ?? '',
|
|
80
|
-
HOME: options.cwd,
|
|
81
|
-
XDG_CONFIG_HOME: join(options.cwd, '.config'),
|
|
82
|
-
CORTEX_CONFIG_DIR: join(options.cwd, '.config', 'cortex'),
|
|
83
|
-
CORTEX_TEST_DIAGNOSTICS: diagnostics,
|
|
84
|
-
// Used by CLI to resolve any relative paths in config
|
|
85
|
-
CORTEX_CONFIG_CWD: options.cwd,
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
// Preserve TERM if available (some help formatting can depend on it)
|
|
89
|
-
if (process.env.TERM) env.TERM = process.env.TERM;
|
|
90
|
-
|
|
91
|
-
for (const [
|
|
92
|
-
key, value,
|
|
93
|
-
] of Object.entries(options.env ?? {})) {
|
|
94
|
-
if (typeof value === 'string') env[key] = value;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Use Bun.spawn for cross-platform subprocess spawning
|
|
98
|
-
const proc = Bun.spawn([
|
|
99
|
-
'bun',
|
|
100
|
-
'run',
|
|
101
|
-
scriptPath,
|
|
102
|
-
...args,
|
|
103
|
-
], {
|
|
104
|
-
cwd: options.cwd,
|
|
105
|
-
env,
|
|
106
|
-
stdin: options.stdin !== undefined ? 'pipe' : 'inherit',
|
|
107
|
-
stdout: 'pipe',
|
|
108
|
-
stderr: 'pipe',
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
if (options.stdin !== undefined) {
|
|
112
|
-
if (!proc.stdin) {
|
|
113
|
-
throw new Error('CLI subprocess stdin was not available.');
|
|
114
|
-
}
|
|
115
|
-
proc.stdin.write(options.stdin);
|
|
116
|
-
proc.stdin.end();
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const [
|
|
120
|
-
stdout, stderr,
|
|
121
|
-
] = await Promise.all([
|
|
122
|
-
new Response(proc.stdout).text(),
|
|
123
|
-
new Response(proc.stderr).text(),
|
|
124
|
-
]);
|
|
125
|
-
const exitCode = await proc.exited;
|
|
126
|
-
|
|
127
|
-
const stdoutTrimmed = stdout.trim();
|
|
128
|
-
const stderrTrimmed = stderr.trim();
|
|
129
|
-
|
|
130
|
-
// Optional diagnostics parsing (no production behavior changes).
|
|
131
|
-
// When CORTEX_TEST_DIAGNOSTICS=1, the CLI will print to stderr:
|
|
132
|
-
// [cortex:diagnostics] configPath=...
|
|
133
|
-
// [cortex:diagnostics] storeRoot=...
|
|
134
|
-
const resolvedConfigPath =
|
|
135
|
-
stderrTrimmed.match(/^\[cortex:diagnostics\] configPath=(.+)$/m)?.[1]?.trim() ??
|
|
136
|
-
undefined;
|
|
137
|
-
const resolvedStoreRoot =
|
|
138
|
-
stderrTrimmed.match(/^\[cortex:diagnostics\] storeRoot=(.+)$/m)?.[1]?.trim() ??
|
|
139
|
-
undefined;
|
|
140
|
-
|
|
141
|
-
return {
|
|
142
|
-
stdout: stdoutTrimmed,
|
|
143
|
-
stderr: stderrTrimmed,
|
|
144
|
-
exitCode,
|
|
145
|
-
resolvedConfigPath,
|
|
146
|
-
resolvedStoreRoot,
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
catch (error) {
|
|
150
|
-
// Handle process errors
|
|
151
|
-
const processError = error as { stdout?: Buffer; stderr?: Buffer; exitCode?: number };
|
|
152
|
-
const stdoutTrimmed = processError.stdout?.toString().trim() ?? '';
|
|
153
|
-
const stderrTrimmed = processError.stderr?.toString().trim() ?? '';
|
|
154
|
-
const resolvedConfigPath =
|
|
155
|
-
stderrTrimmed.match(/^\[cortex:diagnostics\] configPath=(.+)$/m)?.[1]?.trim() ??
|
|
156
|
-
undefined;
|
|
157
|
-
const resolvedStoreRoot =
|
|
158
|
-
stderrTrimmed.match(/^\[cortex:diagnostics\] storeRoot=(.+)$/m)?.[1]?.trim() ??
|
|
159
|
-
undefined;
|
|
160
|
-
|
|
161
|
-
return {
|
|
162
|
-
stdout: stdoutTrimmed,
|
|
163
|
-
stderr: stderrTrimmed,
|
|
164
|
-
exitCode: processError.exitCode ?? 1,
|
|
165
|
-
resolvedConfigPath,
|
|
166
|
-
resolvedStoreRoot,
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Creates a unique test project directory with `.cortex/memory` structure.
|
|
173
|
-
* Returns the project directory path (the cwd for CLI commands).
|
|
174
|
-
*/
|
|
175
|
-
const createTestProject = async (): Promise<string> => {
|
|
176
|
-
const projectDir = await fs.mkdtemp(join(tmpdir(), 'cortex-integration-test-'));
|
|
177
|
-
const storeDir = join(projectDir, '.cortex', 'memory');
|
|
178
|
-
await fs.mkdir(storeDir, { recursive: true });
|
|
179
|
-
await initializeTestStore(storeDir);
|
|
180
|
-
|
|
181
|
-
// Ensure isolated global config dir exists for CLI config resolution.
|
|
182
|
-
// CLI expects config at: $XDG_CONFIG_HOME/cortex/config.yaml
|
|
183
|
-
const globalConfigDir = join(projectDir, '.config', 'cortex');
|
|
184
|
-
await fs.mkdir(globalConfigDir, { recursive: true });
|
|
185
|
-
// Provide a minimal config that points at our local store.
|
|
186
|
-
const configYaml = `stores:\n global:\n kind: filesystem\n properties:\n path: ${storeDir}\n`;
|
|
187
|
-
await fs.writeFile(join(globalConfigDir, 'config.yaml'), configYaml, 'utf8');
|
|
188
|
-
|
|
189
|
-
return projectDir;
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Gets the store directory path from a project directory.
|
|
194
|
-
*/
|
|
195
|
-
const getStoreDir = (projectDir: string): string => join(projectDir, '.cortex', 'memory');
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Initializes a store with the basic structure needed for testing.
|
|
199
|
-
*/
|
|
200
|
-
const initializeTestStore = async (storeRoot: string): Promise<void> => {
|
|
201
|
-
const indexContent = 'memories: []\nsubcategories: []';
|
|
202
|
-
await fs.mkdir(storeRoot, { recursive: true });
|
|
203
|
-
await fs.writeFile(join(storeRoot, 'index.yaml'), indexContent, 'utf8');
|
|
204
|
-
await fs.writeFile(join(storeRoot, 'config.yaml'), '', 'utf8');
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Creates a memory file in the test store with the given content.
|
|
209
|
-
* Uses FilesystemStorageAdapter to ensure indexes are properly created.
|
|
210
|
-
*/
|
|
211
|
-
const createMemoryFile = async (
|
|
212
|
-
storeRoot: string,
|
|
213
|
-
slugPath: string,
|
|
214
|
-
options: {
|
|
215
|
-
content?: string;
|
|
216
|
-
tags?: string[];
|
|
217
|
-
expiresAt?: Date;
|
|
218
|
-
createdAt?: Date;
|
|
219
|
-
updatedAt?: Date;
|
|
220
|
-
} = {},
|
|
221
|
-
): Promise<void> => {
|
|
222
|
-
const content = options.content ?? 'Test memory content.';
|
|
223
|
-
const tags = options.tags ?? ['test'];
|
|
224
|
-
const createdAt = options.createdAt ?? new Date('2024-01-01T00:00:00.000Z');
|
|
225
|
-
const updatedAt = options.updatedAt ?? new Date('2024-01-02T00:00:00.000Z');
|
|
226
|
-
|
|
227
|
-
const memoryPathResult = MemoryPath.fromString(slugPath);
|
|
228
|
-
if (!memoryPathResult.ok()) {
|
|
229
|
-
throw new Error(`Invalid memory path: ${memoryPathResult.error.message}`);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const memoryResult = Memory.init(
|
|
233
|
-
memoryPathResult.value,
|
|
234
|
-
{
|
|
235
|
-
createdAt,
|
|
236
|
-
updatedAt,
|
|
237
|
-
tags,
|
|
238
|
-
source: 'user',
|
|
239
|
-
expiresAt: options.expiresAt,
|
|
240
|
-
citations: [],
|
|
241
|
-
},
|
|
242
|
-
content,
|
|
243
|
-
);
|
|
244
|
-
if (!memoryResult.ok()) {
|
|
245
|
-
throw new Error(`Failed to create memory: ${memoryResult.error.message}`);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
const adapter = new FilesystemStorageAdapter(
|
|
249
|
-
new FilesystemConfigAdapter(join(storeRoot, 'config.yaml')),
|
|
250
|
-
{ rootDirectory: storeRoot },
|
|
251
|
-
);
|
|
252
|
-
const writeResult = await adapter.memories.save(memoryResult.value.path, memoryResult.value);
|
|
253
|
-
if (!writeResult.ok()) {
|
|
254
|
-
throw new Error(`Failed to write memory: ${writeResult.error.message}`);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Update indexes after writing the memory
|
|
258
|
-
const indexResult = await adapter.indexes.updateAfterMemoryWrite(memoryResult.value);
|
|
259
|
-
if (!indexResult.ok()) {
|
|
260
|
-
throw new Error(`Failed to update indexes: ${indexResult.error.message}`);
|
|
261
|
-
}
|
|
262
|
-
};
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* Creates a category using the storage adapter.
|
|
266
|
-
*
|
|
267
|
-
* Note: Category creation has side-effects beyond creating directories. It must
|
|
268
|
-
* update category indexes so the core layer will consider the category "existing"
|
|
269
|
-
* (createMemory checks via storage.categories.exists).
|
|
270
|
-
*
|
|
271
|
-
* The FilesystemStorageAdapter's categories.ensure only creates directories, so
|
|
272
|
-
* we also trigger an index rebuild for consistent test behavior.
|
|
273
|
-
*/
|
|
274
|
-
const createCategory = async (categoryPath: string, storeRoot: string): Promise<void> => {
|
|
275
|
-
// Create category directories directly. `FilesystemStorageAdapter.categories.ensure` expects
|
|
276
|
-
// a valid store root (containing config/index files) and is more strict than we need here.
|
|
277
|
-
await fs.mkdir(join(storeRoot, categoryPath), { recursive: true });
|
|
278
|
-
|
|
279
|
-
// Ensure the root index file exists so list operations work.
|
|
280
|
-
const rootIndexPath = join(storeRoot, 'index.yaml');
|
|
281
|
-
try {
|
|
282
|
-
await fs.access(rootIndexPath);
|
|
283
|
-
}
|
|
284
|
-
catch {
|
|
285
|
-
await fs.writeFile(rootIndexPath, 'memories: []\nsubcategories: []', 'utf8');
|
|
286
|
-
}
|
|
287
|
-
};
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Creates a category index file.
|
|
291
|
-
* Note: Indexes are stored as `<categoryPath>/index.yaml` files inside the store root.
|
|
292
|
-
* The format is strict: list markers on their own line, fields indented with 4 spaces.
|
|
293
|
-
*/
|
|
294
|
-
const createCategoryIndex = async (
|
|
295
|
-
storeRoot: string,
|
|
296
|
-
categoryPath: string,
|
|
297
|
-
memories: { path: string; tokenEstimate: number; summary?: string }[] = [],
|
|
298
|
-
subcategories: { path: string; memoryCount?: number }[] = [],
|
|
299
|
-
): Promise<void> => {
|
|
300
|
-
const lines: string[] = [];
|
|
301
|
-
|
|
302
|
-
// Memory section
|
|
303
|
-
if (memories.length === 0) {
|
|
304
|
-
lines.push('memories: []');
|
|
305
|
-
}
|
|
306
|
-
else {
|
|
307
|
-
lines.push('memories:');
|
|
308
|
-
for (const memory of memories) {
|
|
309
|
-
lines.push(' -');
|
|
310
|
-
lines.push(` path: ${memory.path}`);
|
|
311
|
-
lines.push(` token_estimate: ${memory.tokenEstimate}`);
|
|
312
|
-
if (memory.summary) {
|
|
313
|
-
lines.push(` summary: ${memory.summary}`);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
lines.push('');
|
|
319
|
-
|
|
320
|
-
// Subcategories section
|
|
321
|
-
if (subcategories.length === 0) {
|
|
322
|
-
lines.push('subcategories: []');
|
|
323
|
-
}
|
|
324
|
-
else {
|
|
325
|
-
lines.push('subcategories:');
|
|
326
|
-
for (const sub of subcategories) {
|
|
327
|
-
lines.push(' -');
|
|
328
|
-
lines.push(` path: ${sub.path}`);
|
|
329
|
-
lines.push(` memory_count: ${sub.memoryCount ?? 0}`);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
const content = lines.join('\n');
|
|
334
|
-
const categoryDir = categoryPath ? join(storeRoot, categoryPath) : storeRoot;
|
|
335
|
-
await fs.mkdir(categoryDir, { recursive: true });
|
|
336
|
-
await fs.writeFile(join(categoryDir, 'index.yaml'), content, 'utf8');
|
|
337
|
-
};
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* Checks if a memory file exists.
|
|
341
|
-
*/
|
|
342
|
-
const memoryExists = async (storeRoot: string, slugPath: string): Promise<boolean> => {
|
|
343
|
-
try {
|
|
344
|
-
await fs.access(join(storeRoot, `${slugPath}.md`));
|
|
345
|
-
return true;
|
|
346
|
-
}
|
|
347
|
-
catch {
|
|
348
|
-
return false;
|
|
349
|
-
}
|
|
350
|
-
};
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* Reads a memory file's content.
|
|
354
|
-
*/
|
|
355
|
-
const readMemoryFile = async (storeRoot: string, slugPath: string): Promise<string | null> => {
|
|
356
|
-
try {
|
|
357
|
-
return await fs.readFile(join(storeRoot, `${slugPath}.md`), 'utf8');
|
|
358
|
-
}
|
|
359
|
-
catch {
|
|
360
|
-
return null;
|
|
361
|
-
}
|
|
362
|
-
};
|
|
363
|
-
|
|
364
|
-
describe('Cortex CLI Integration Tests', () => {
|
|
365
|
-
let testProject: string;
|
|
366
|
-
let storeDir: string;
|
|
367
|
-
|
|
368
|
-
beforeEach(async () => {
|
|
369
|
-
testProject = await createTestProject();
|
|
370
|
-
storeDir = getStoreDir(testProject);
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
afterEach(async () => {
|
|
374
|
-
if (testProject) {
|
|
375
|
-
await fs.rm(testProject, { recursive: true, force: true });
|
|
376
|
-
}
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
describe('memory add command', () => {
|
|
380
|
-
it('should add a new memory with inline content', async () => {
|
|
381
|
-
await createCategory('project', storeDir);
|
|
382
|
-
const result = await runCortexCli(
|
|
383
|
-
[
|
|
384
|
-
'memory',
|
|
385
|
-
'add',
|
|
386
|
-
'project/test-memory',
|
|
387
|
-
'--content',
|
|
388
|
-
'This is test content.',
|
|
389
|
-
],
|
|
390
|
-
{ cwd: testProject },
|
|
391
|
-
);
|
|
392
|
-
|
|
393
|
-
expectCliOk(
|
|
394
|
-
[
|
|
395
|
-
'memory',
|
|
396
|
-
'add',
|
|
397
|
-
'project/test-memory',
|
|
398
|
-
'--content',
|
|
399
|
-
'This is test content.',
|
|
400
|
-
],
|
|
401
|
-
result,
|
|
402
|
-
);
|
|
403
|
-
expect(result.exitCode).toBe(0);
|
|
404
|
-
expect(result.stdout).toContain('Added memory');
|
|
405
|
-
expect(result.stdout).toContain('project/test-memory');
|
|
406
|
-
|
|
407
|
-
const exists = await memoryExists(storeDir, 'project/test-memory');
|
|
408
|
-
expect(exists).toBe(true);
|
|
409
|
-
|
|
410
|
-
const content = await readMemoryFile(storeDir, 'project/test-memory');
|
|
411
|
-
expect(content).toContain('This is test content.');
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
it('should add a memory with tags', async () => {
|
|
415
|
-
await createCategory('project', storeDir);
|
|
416
|
-
const result = await runCortexCli(
|
|
417
|
-
[
|
|
418
|
-
'memory',
|
|
419
|
-
'add',
|
|
420
|
-
'project/tagged-memory',
|
|
421
|
-
'--content',
|
|
422
|
-
'Content with tags.',
|
|
423
|
-
'--tags',
|
|
424
|
-
'tag1,tag2,tag3',
|
|
425
|
-
],
|
|
426
|
-
{ cwd: testProject },
|
|
427
|
-
);
|
|
428
|
-
|
|
429
|
-
expect(result.exitCode).toBe(0);
|
|
430
|
-
|
|
431
|
-
const content = await readMemoryFile(storeDir, 'project/tagged-memory');
|
|
432
|
-
expect(content).toContain('tags:');
|
|
433
|
-
expect(content).toContain('tag1');
|
|
434
|
-
expect(content).toContain('tag2');
|
|
435
|
-
expect(content).toContain('tag3');
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
it('should add a memory with multiple -t flags', async () => {
|
|
439
|
-
await createCategory('project', storeDir);
|
|
440
|
-
const result = await runCortexCli(
|
|
441
|
-
[
|
|
442
|
-
'memory',
|
|
443
|
-
'add',
|
|
444
|
-
'project/multi-tag-flags',
|
|
445
|
-
'--content',
|
|
446
|
-
'Content with multiple tag flags.',
|
|
447
|
-
'-t',
|
|
448
|
-
'first',
|
|
449
|
-
'-t',
|
|
450
|
-
'second',
|
|
451
|
-
'-t',
|
|
452
|
-
'third',
|
|
453
|
-
],
|
|
454
|
-
{ cwd: testProject },
|
|
455
|
-
);
|
|
456
|
-
|
|
457
|
-
expect(result.exitCode).toBe(0);
|
|
458
|
-
|
|
459
|
-
const content = await readMemoryFile(storeDir, 'project/multi-tag-flags');
|
|
460
|
-
expect(content).toContain('tags:');
|
|
461
|
-
expect(content).toContain('first');
|
|
462
|
-
expect(content).toContain('second');
|
|
463
|
-
expect(content).toContain('third');
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
it('should add a memory with mixed tag formats (comma-separated and multiple flags)', async () => {
|
|
467
|
-
await createCategory('project', storeDir);
|
|
468
|
-
const result = await runCortexCli(
|
|
469
|
-
[
|
|
470
|
-
'memory',
|
|
471
|
-
'add',
|
|
472
|
-
'project/mixed-tags',
|
|
473
|
-
'--content',
|
|
474
|
-
'Content with mixed tag formats.',
|
|
475
|
-
'-t',
|
|
476
|
-
'alpha,beta',
|
|
477
|
-
'-t',
|
|
478
|
-
'gamma',
|
|
479
|
-
],
|
|
480
|
-
{ cwd: testProject },
|
|
481
|
-
);
|
|
482
|
-
|
|
483
|
-
expect(result.exitCode).toBe(0);
|
|
484
|
-
|
|
485
|
-
const content = await readMemoryFile(storeDir, 'project/mixed-tags');
|
|
486
|
-
expect(content).toContain('tags:');
|
|
487
|
-
expect(content).toContain('alpha');
|
|
488
|
-
expect(content).toContain('beta');
|
|
489
|
-
expect(content).toContain('gamma');
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
it('should add a memory with expiry date', async () => {
|
|
493
|
-
await createCategory('project', storeDir);
|
|
494
|
-
const expiryDate = '2025-12-31T23:59:59.000Z';
|
|
495
|
-
const result = await runCortexCli(
|
|
496
|
-
[
|
|
497
|
-
'memory',
|
|
498
|
-
'add',
|
|
499
|
-
'project/expiring-memory',
|
|
500
|
-
'--content',
|
|
501
|
-
'This will expire.',
|
|
502
|
-
'--expires-at',
|
|
503
|
-
expiryDate,
|
|
504
|
-
],
|
|
505
|
-
{ cwd: testProject },
|
|
506
|
-
);
|
|
507
|
-
|
|
508
|
-
expect(result.exitCode).toBe(0);
|
|
509
|
-
|
|
510
|
-
const content = await readMemoryFile(storeDir, 'project/expiring-memory');
|
|
511
|
-
expect(content).toContain('expires_at:');
|
|
512
|
-
expect(content).toContain('2025-12-31');
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
it('should fail when memory path is missing', async () => {
|
|
516
|
-
const result = await runCortexCli([
|
|
517
|
-
'memory',
|
|
518
|
-
'add',
|
|
519
|
-
'--content',
|
|
520
|
-
'No path provided.',
|
|
521
|
-
], {
|
|
522
|
-
cwd: testProject,
|
|
523
|
-
});
|
|
524
|
-
|
|
525
|
-
expect(result.exitCode).toBe(1);
|
|
526
|
-
// Commander shows "missing required argument" error
|
|
527
|
-
expect(result.stderr.toLowerCase()).toMatch(/missing|required|argument/);
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
it('should fail for invalid memory path format', async () => {
|
|
531
|
-
const result = await runCortexCli(
|
|
532
|
-
[
|
|
533
|
-
'memory',
|
|
534
|
-
'add',
|
|
535
|
-
'invalid-single-segment',
|
|
536
|
-
'--content',
|
|
537
|
-
'Bad path.',
|
|
538
|
-
],
|
|
539
|
-
{ cwd: testProject },
|
|
540
|
-
);
|
|
541
|
-
|
|
542
|
-
expect(result.exitCode).toBe(1);
|
|
543
|
-
});
|
|
544
|
-
|
|
545
|
-
it('should fail for unknown flags', async () => {
|
|
546
|
-
const result = await runCortexCli(
|
|
547
|
-
[
|
|
548
|
-
'memory',
|
|
549
|
-
'add',
|
|
550
|
-
'project/memory',
|
|
551
|
-
'--unknown-flag',
|
|
552
|
-
'value',
|
|
553
|
-
],
|
|
554
|
-
{
|
|
555
|
-
cwd: testProject,
|
|
556
|
-
},
|
|
557
|
-
);
|
|
558
|
-
|
|
559
|
-
expect(result.exitCode).toBe(1);
|
|
560
|
-
// Commander shows "unknown option" error
|
|
561
|
-
expect(result.stderr.toLowerCase()).toMatch(/unknown|option/);
|
|
562
|
-
});
|
|
563
|
-
|
|
564
|
-
it('should add memory from file', async () => {
|
|
565
|
-
await createCategory('project', storeDir);
|
|
566
|
-
// Create a content file
|
|
567
|
-
const contentFile = join(testProject, 'content.txt');
|
|
568
|
-
await fs.writeFile(contentFile, 'Content from file.', 'utf8');
|
|
569
|
-
|
|
570
|
-
const result = await runCortexCli(
|
|
571
|
-
[
|
|
572
|
-
'memory',
|
|
573
|
-
'add',
|
|
574
|
-
'project/file-memory',
|
|
575
|
-
'--file',
|
|
576
|
-
contentFile,
|
|
577
|
-
],
|
|
578
|
-
{
|
|
579
|
-
cwd: testProject,
|
|
580
|
-
},
|
|
581
|
-
);
|
|
582
|
-
|
|
583
|
-
expect(result.exitCode).toBe(0);
|
|
584
|
-
|
|
585
|
-
const content = await readMemoryFile(storeDir, 'project/file-memory');
|
|
586
|
-
expect(content).toContain('Content from file.');
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
it('should add memory in deeply nested category', async () => {
|
|
590
|
-
await createCategory('domain/subdomain/feature', storeDir);
|
|
591
|
-
const result = await runCortexCli(
|
|
592
|
-
[
|
|
593
|
-
'memory',
|
|
594
|
-
'add',
|
|
595
|
-
'domain/subdomain/feature/deep-memory',
|
|
596
|
-
'--content',
|
|
597
|
-
'Deeply nested content.',
|
|
598
|
-
],
|
|
599
|
-
{ cwd: testProject },
|
|
600
|
-
);
|
|
601
|
-
|
|
602
|
-
expect(result.exitCode).toBe(0);
|
|
603
|
-
|
|
604
|
-
const exists = await memoryExists(storeDir, 'domain/subdomain/feature/deep-memory');
|
|
605
|
-
expect(exists).toBe(true);
|
|
606
|
-
});
|
|
607
|
-
|
|
608
|
-
it('should add memory with special characters in content', async () => {
|
|
609
|
-
await createCategory('project', storeDir);
|
|
610
|
-
const specialContent = 'Special chars: $HOME, `backticks`, "quotes", \'single\'';
|
|
611
|
-
const result = await runCortexCli(
|
|
612
|
-
[
|
|
613
|
-
'memory',
|
|
614
|
-
'add',
|
|
615
|
-
'project/special-memory',
|
|
616
|
-
'--content',
|
|
617
|
-
specialContent,
|
|
618
|
-
],
|
|
619
|
-
{ cwd: testProject },
|
|
620
|
-
);
|
|
621
|
-
|
|
622
|
-
expect(result.exitCode).toBe(0);
|
|
623
|
-
|
|
624
|
-
const content = await readMemoryFile(storeDir, 'project/special-memory');
|
|
625
|
-
expect(content).toContain('$HOME');
|
|
626
|
-
expect(content).toContain('backticks');
|
|
627
|
-
});
|
|
628
|
-
});
|
|
629
|
-
|
|
630
|
-
describe('memory list command', () => {
|
|
631
|
-
beforeEach(async () => {
|
|
632
|
-
// Set up some test memories (indexes are created automatically by createMemoryFile)
|
|
633
|
-
await createMemoryFile(storeDir, 'project/memory-one', {
|
|
634
|
-
content: 'First memory content.',
|
|
635
|
-
});
|
|
636
|
-
await createMemoryFile(storeDir, 'project/memory-two', {
|
|
637
|
-
content: 'Second memory content.',
|
|
638
|
-
});
|
|
639
|
-
await createMemoryFile(storeDir, 'domain/other-memory', {
|
|
640
|
-
content: 'Domain memory content.',
|
|
641
|
-
});
|
|
642
|
-
});
|
|
643
|
-
|
|
644
|
-
it('should list all memories across categories', async () => {
|
|
645
|
-
const result = await runCortexCli([
|
|
646
|
-
'memory', 'list',
|
|
647
|
-
], { cwd: testProject });
|
|
648
|
-
|
|
649
|
-
expect(result.exitCode).toBe(0);
|
|
650
|
-
expect(result.stdout).toContain('project/memory-one');
|
|
651
|
-
expect(result.stdout).toContain('project/memory-two');
|
|
652
|
-
expect(result.stdout).toContain('domain/other-memory');
|
|
653
|
-
});
|
|
654
|
-
|
|
655
|
-
it('should list memories in a specific category', async () => {
|
|
656
|
-
const result = await runCortexCli([
|
|
657
|
-
'memory',
|
|
658
|
-
'list',
|
|
659
|
-
'project',
|
|
660
|
-
], {
|
|
661
|
-
cwd: testProject,
|
|
662
|
-
});
|
|
663
|
-
|
|
664
|
-
expect(result.exitCode).toBe(0);
|
|
665
|
-
expect(result.stdout).toContain('project/memory-one');
|
|
666
|
-
expect(result.stdout).toContain('project/memory-two');
|
|
667
|
-
expect(result.stdout).not.toContain('domain/other-memory');
|
|
668
|
-
});
|
|
669
|
-
|
|
670
|
-
it('should output in JSON format', async () => {
|
|
671
|
-
const result = await runCortexCli([
|
|
672
|
-
'memory',
|
|
673
|
-
'list',
|
|
674
|
-
'--format',
|
|
675
|
-
'json',
|
|
676
|
-
], {
|
|
677
|
-
cwd: testProject,
|
|
678
|
-
});
|
|
679
|
-
|
|
680
|
-
expect(result.exitCode).toBe(0);
|
|
681
|
-
|
|
682
|
-
const parsed = JSON.parse(result.stdout);
|
|
683
|
-
expect(parsed.memories).toBeDefined();
|
|
684
|
-
expect(Array.isArray(parsed.memories)).toBe(true);
|
|
685
|
-
});
|
|
686
|
-
|
|
687
|
-
it('should exclude expired memories by default', async () => {
|
|
688
|
-
// Add an expired memory (index is created automatically)
|
|
689
|
-
await createMemoryFile(storeDir, 'project/expired-memory', {
|
|
690
|
-
content: 'This is expired.',
|
|
691
|
-
expiresAt: new Date('2020-01-01T00:00:00.000Z'),
|
|
692
|
-
});
|
|
693
|
-
|
|
694
|
-
const result = await runCortexCli([
|
|
695
|
-
'memory',
|
|
696
|
-
'list',
|
|
697
|
-
'project',
|
|
698
|
-
], {
|
|
699
|
-
cwd: testProject,
|
|
700
|
-
});
|
|
701
|
-
|
|
702
|
-
expect(result.exitCode).toBe(0);
|
|
703
|
-
expect(result.stdout).toContain('project/memory-one');
|
|
704
|
-
expect(result.stdout).not.toContain('project/expired-memory');
|
|
705
|
-
});
|
|
706
|
-
|
|
707
|
-
it('should include expired memories with --include-expired flag', async () => {
|
|
708
|
-
// Add an expired memory (index is created automatically)
|
|
709
|
-
await createMemoryFile(storeDir, 'project/expired-memory', {
|
|
710
|
-
content: 'This is expired.',
|
|
711
|
-
expiresAt: new Date('2020-01-01T00:00:00.000Z'),
|
|
712
|
-
});
|
|
713
|
-
|
|
714
|
-
const result = await runCortexCli([
|
|
715
|
-
'memory',
|
|
716
|
-
'list',
|
|
717
|
-
'project',
|
|
718
|
-
'--include-expired',
|
|
719
|
-
], {
|
|
720
|
-
cwd: testProject,
|
|
721
|
-
});
|
|
722
|
-
|
|
723
|
-
expect(result.exitCode).toBe(0);
|
|
724
|
-
expect(result.stdout).toContain('project/memory-one');
|
|
725
|
-
expect(result.stdout).toContain('project/expired-memory');
|
|
726
|
-
});
|
|
727
|
-
|
|
728
|
-
it('should return empty list for non-existent category', async () => {
|
|
729
|
-
const result = await runCortexCli([
|
|
730
|
-
'memory',
|
|
731
|
-
'list',
|
|
732
|
-
'nonexistent',
|
|
733
|
-
], {
|
|
734
|
-
cwd: testProject,
|
|
735
|
-
});
|
|
736
|
-
|
|
737
|
-
expect(result.exitCode).toBe(0);
|
|
738
|
-
expect(result.stdout).toContain('memories:');
|
|
739
|
-
expect(result.stdout).toContain('[]');
|
|
740
|
-
});
|
|
741
|
-
|
|
742
|
-
it('should use yaml format for invalid format option', async () => {
|
|
743
|
-
// Invalid formats fall back to default YAML formatting
|
|
744
|
-
const result = await runCortexCli([
|
|
745
|
-
'memory',
|
|
746
|
-
'list',
|
|
747
|
-
'--format',
|
|
748
|
-
'invalid',
|
|
749
|
-
], {
|
|
750
|
-
cwd: testProject,
|
|
751
|
-
});
|
|
752
|
-
|
|
753
|
-
expect(result.exitCode).toBe(0);
|
|
754
|
-
// Should still output valid content (in yaml-like format)
|
|
755
|
-
expect(result.stdout).toContain('memories:');
|
|
756
|
-
});
|
|
757
|
-
});
|
|
758
|
-
|
|
759
|
-
describe('memory update command', () => {
|
|
760
|
-
beforeEach(async () => {
|
|
761
|
-
await createMemoryFile(storeDir, 'project/updatable', {
|
|
762
|
-
content: 'Original content.',
|
|
763
|
-
tags: ['original'],
|
|
764
|
-
});
|
|
765
|
-
});
|
|
766
|
-
|
|
767
|
-
it('should update memory content', async () => {
|
|
768
|
-
const result = await runCortexCli(
|
|
769
|
-
[
|
|
770
|
-
'memory',
|
|
771
|
-
'update',
|
|
772
|
-
'project/updatable',
|
|
773
|
-
'--content',
|
|
774
|
-
'Updated content.',
|
|
775
|
-
],
|
|
776
|
-
{ cwd: testProject },
|
|
777
|
-
);
|
|
778
|
-
|
|
779
|
-
expect(result.exitCode).toBe(0);
|
|
780
|
-
expect(result.stdout).toContain('Updated');
|
|
781
|
-
|
|
782
|
-
const content = await readMemoryFile(storeDir, 'project/updatable');
|
|
783
|
-
expect(content).toContain('Updated content.');
|
|
784
|
-
expect(content).not.toContain('Original content.');
|
|
785
|
-
});
|
|
786
|
-
|
|
787
|
-
it('should update memory tags', async () => {
|
|
788
|
-
const result = await runCortexCli(
|
|
789
|
-
[
|
|
790
|
-
'memory',
|
|
791
|
-
'update',
|
|
792
|
-
'project/updatable',
|
|
793
|
-
'--tags',
|
|
794
|
-
'new-tag,updated',
|
|
795
|
-
],
|
|
796
|
-
{ cwd: testProject },
|
|
797
|
-
);
|
|
798
|
-
|
|
799
|
-
expect(result.exitCode).toBe(0);
|
|
800
|
-
|
|
801
|
-
const content = await readMemoryFile(storeDir, 'project/updatable');
|
|
802
|
-
expect(content).toContain('new-tag');
|
|
803
|
-
expect(content).toContain('updated');
|
|
804
|
-
});
|
|
805
|
-
|
|
806
|
-
it('should set expiry date', async () => {
|
|
807
|
-
const result = await runCortexCli(
|
|
808
|
-
[
|
|
809
|
-
'memory',
|
|
810
|
-
'update',
|
|
811
|
-
'project/updatable',
|
|
812
|
-
'--expires-at',
|
|
813
|
-
'2030-01-01T00:00:00.000Z',
|
|
814
|
-
],
|
|
815
|
-
{ cwd: testProject },
|
|
816
|
-
);
|
|
817
|
-
|
|
818
|
-
expect(result.exitCode).toBe(0);
|
|
819
|
-
|
|
820
|
-
const content = await readMemoryFile(storeDir, 'project/updatable');
|
|
821
|
-
expect(content).toContain('expires_at:');
|
|
822
|
-
expect(content).toContain('2030-01-01');
|
|
823
|
-
});
|
|
824
|
-
|
|
825
|
-
it('should clear expiry date with --no-expires-at', async () => {
|
|
826
|
-
// First set an expiry
|
|
827
|
-
await createMemoryFile(storeDir, 'project/with-expiry', {
|
|
828
|
-
content: 'Has expiry.',
|
|
829
|
-
expiresAt: new Date('2025-01-01T00:00:00.000Z'),
|
|
830
|
-
});
|
|
831
|
-
|
|
832
|
-
const result = await runCortexCli(
|
|
833
|
-
[
|
|
834
|
-
'memory',
|
|
835
|
-
'update',
|
|
836
|
-
'project/with-expiry',
|
|
837
|
-
'--no-expires-at',
|
|
838
|
-
],
|
|
839
|
-
{
|
|
840
|
-
cwd: testProject,
|
|
841
|
-
},
|
|
842
|
-
);
|
|
843
|
-
|
|
844
|
-
expect(result.exitCode).toBe(0);
|
|
845
|
-
|
|
846
|
-
const content = await readMemoryFile(storeDir, 'project/with-expiry');
|
|
847
|
-
expect(content).not.toContain('expires_at:');
|
|
848
|
-
});
|
|
849
|
-
|
|
850
|
-
it('should fail when memory does not exist', async () => {
|
|
851
|
-
const result = await runCortexCli(
|
|
852
|
-
[
|
|
853
|
-
'memory',
|
|
854
|
-
'update',
|
|
855
|
-
'project/nonexistent',
|
|
856
|
-
'--content',
|
|
857
|
-
'New content.',
|
|
858
|
-
],
|
|
859
|
-
{ cwd: testProject },
|
|
860
|
-
);
|
|
861
|
-
|
|
862
|
-
expect(result.exitCode).toBe(1);
|
|
863
|
-
expect(result.stderr.toLowerCase()).toContain('not found');
|
|
864
|
-
});
|
|
865
|
-
|
|
866
|
-
it('should fail when no updates provided', async () => {
|
|
867
|
-
const result = await runCortexCli([
|
|
868
|
-
'memory',
|
|
869
|
-
'update',
|
|
870
|
-
'project/updatable',
|
|
871
|
-
], {
|
|
872
|
-
cwd: testProject,
|
|
873
|
-
});
|
|
874
|
-
|
|
875
|
-
expect(result.exitCode).toBe(1);
|
|
876
|
-
expect(result.stderr.toLowerCase()).toContain('no update');
|
|
877
|
-
});
|
|
878
|
-
|
|
879
|
-
it('should update content from file', async () => {
|
|
880
|
-
const contentFile = join(testProject, 'new-content.txt');
|
|
881
|
-
await fs.writeFile(contentFile, 'Content from file update.', 'utf8');
|
|
882
|
-
|
|
883
|
-
const result = await runCortexCli(
|
|
884
|
-
[
|
|
885
|
-
'memory',
|
|
886
|
-
'update',
|
|
887
|
-
'project/updatable',
|
|
888
|
-
'--file',
|
|
889
|
-
contentFile,
|
|
890
|
-
],
|
|
891
|
-
{
|
|
892
|
-
cwd: testProject,
|
|
893
|
-
},
|
|
894
|
-
);
|
|
895
|
-
|
|
896
|
-
expect(result.exitCode).toBe(0);
|
|
897
|
-
|
|
898
|
-
const content = await readMemoryFile(storeDir, 'project/updatable');
|
|
899
|
-
expect(content).toContain('Content from file update.');
|
|
900
|
-
});
|
|
901
|
-
|
|
902
|
-
it('should preserve original content when only updating tags', async () => {
|
|
903
|
-
const result = await runCortexCli(
|
|
904
|
-
[
|
|
905
|
-
'memory',
|
|
906
|
-
'update',
|
|
907
|
-
'project/updatable',
|
|
908
|
-
'--tags',
|
|
909
|
-
'new-tag',
|
|
910
|
-
],
|
|
911
|
-
{
|
|
912
|
-
cwd: testProject,
|
|
913
|
-
},
|
|
914
|
-
);
|
|
915
|
-
|
|
916
|
-
expect(result.exitCode).toBe(0);
|
|
917
|
-
|
|
918
|
-
const content = await readMemoryFile(storeDir, 'project/updatable');
|
|
919
|
-
expect(content).toContain('Original content.');
|
|
920
|
-
expect(content).toContain('new-tag');
|
|
921
|
-
});
|
|
922
|
-
|
|
923
|
-
it('should update memory tags with multiple -t flags', async () => {
|
|
924
|
-
const result = await runCortexCli(
|
|
925
|
-
[
|
|
926
|
-
'memory',
|
|
927
|
-
'update',
|
|
928
|
-
'project/updatable',
|
|
929
|
-
'-t',
|
|
930
|
-
'tag-a',
|
|
931
|
-
'-t',
|
|
932
|
-
'tag-b',
|
|
933
|
-
'-t',
|
|
934
|
-
'tag-c',
|
|
935
|
-
],
|
|
936
|
-
{ cwd: testProject },
|
|
937
|
-
);
|
|
938
|
-
|
|
939
|
-
expect(result.exitCode).toBe(0);
|
|
940
|
-
|
|
941
|
-
const content = await readMemoryFile(storeDir, 'project/updatable');
|
|
942
|
-
expect(content).toContain('tags:');
|
|
943
|
-
expect(content).toContain('tag-a');
|
|
944
|
-
expect(content).toContain('tag-b');
|
|
945
|
-
expect(content).toContain('tag-c');
|
|
946
|
-
});
|
|
947
|
-
|
|
948
|
-
it('should update memory tags with mixed formats (comma-separated and multiple flags)', async () => {
|
|
949
|
-
const result = await runCortexCli(
|
|
950
|
-
[
|
|
951
|
-
'memory',
|
|
952
|
-
'update',
|
|
953
|
-
'project/updatable',
|
|
954
|
-
'-t',
|
|
955
|
-
'x,y',
|
|
956
|
-
'-t',
|
|
957
|
-
'z',
|
|
958
|
-
],
|
|
959
|
-
{ cwd: testProject },
|
|
960
|
-
);
|
|
961
|
-
|
|
962
|
-
expect(result.exitCode).toBe(0);
|
|
963
|
-
|
|
964
|
-
const content = await readMemoryFile(storeDir, 'project/updatable');
|
|
965
|
-
expect(content).toContain('tags:');
|
|
966
|
-
expect(content).toContain('x');
|
|
967
|
-
expect(content).toContain('y');
|
|
968
|
-
expect(content).toContain('z');
|
|
969
|
-
});
|
|
970
|
-
});
|
|
971
|
-
|
|
972
|
-
describe('store prune command', () => {
|
|
973
|
-
beforeEach(async () => {
|
|
974
|
-
// Create some expired and non-expired memories (indexes are created automatically)
|
|
975
|
-
await createMemoryFile(storeDir, 'project/fresh-memory', {
|
|
976
|
-
content: 'Fresh content.',
|
|
977
|
-
expiresAt: new Date('2099-01-01T00:00:00.000Z'),
|
|
978
|
-
});
|
|
979
|
-
await createMemoryFile(storeDir, 'project/expired-one', {
|
|
980
|
-
content: 'Expired content.',
|
|
981
|
-
expiresAt: new Date('2020-01-01T00:00:00.000Z'),
|
|
982
|
-
});
|
|
983
|
-
await createMemoryFile(storeDir, 'project/expired-two', {
|
|
984
|
-
content: 'Also expired.',
|
|
985
|
-
expiresAt: new Date('2019-06-15T00:00:00.000Z'),
|
|
986
|
-
});
|
|
987
|
-
});
|
|
988
|
-
|
|
989
|
-
it('should report expired memories with --dry-run', async () => {
|
|
990
|
-
const result = await runCortexCli([
|
|
991
|
-
'store',
|
|
992
|
-
'prune',
|
|
993
|
-
'--dry-run',
|
|
994
|
-
], {
|
|
995
|
-
cwd: testProject,
|
|
996
|
-
});
|
|
997
|
-
|
|
998
|
-
expect(result.exitCode).toBe(0);
|
|
999
|
-
expect(result.stdout).toContain('Would prune');
|
|
1000
|
-
expect(result.stdout).toContain('project/expired-one');
|
|
1001
|
-
expect(result.stdout).toContain('project/expired-two');
|
|
1002
|
-
|
|
1003
|
-
// Memories should still exist
|
|
1004
|
-
expect(await memoryExists(storeDir, 'project/expired-one')).toBe(true);
|
|
1005
|
-
expect(await memoryExists(storeDir, 'project/expired-two')).toBe(true);
|
|
1006
|
-
});
|
|
1007
|
-
|
|
1008
|
-
it('should delete expired memories without --dry-run', async () => {
|
|
1009
|
-
const result = await runCortexCli([
|
|
1010
|
-
'store', 'prune',
|
|
1011
|
-
], {
|
|
1012
|
-
cwd: testProject,
|
|
1013
|
-
});
|
|
1014
|
-
|
|
1015
|
-
expect(result.exitCode).toBe(0);
|
|
1016
|
-
expect(result.stdout).toContain('Pruned');
|
|
1017
|
-
expect(result.stdout).toContain('2');
|
|
1018
|
-
|
|
1019
|
-
// Expired memories should be deleted
|
|
1020
|
-
expect(await memoryExists(storeDir, 'project/expired-one')).toBe(false);
|
|
1021
|
-
expect(await memoryExists(storeDir, 'project/expired-two')).toBe(false);
|
|
1022
|
-
|
|
1023
|
-
// Fresh memory should remain
|
|
1024
|
-
expect(await memoryExists(storeDir, 'project/fresh-memory')).toBe(true);
|
|
1025
|
-
});
|
|
1026
|
-
|
|
1027
|
-
it('should report when no expired memories found', async () => {
|
|
1028
|
-
// Remove expired memories first
|
|
1029
|
-
await fs.rm(join(storeDir, 'project', 'expired-one.md'));
|
|
1030
|
-
await fs.rm(join(storeDir, 'project', 'expired-two.md'));
|
|
1031
|
-
|
|
1032
|
-
await createCategoryIndex(storeDir, 'project', [{ path: 'project/fresh-memory', tokenEstimate: 10 }]);
|
|
1033
|
-
|
|
1034
|
-
const result = await runCortexCli([
|
|
1035
|
-
'store', 'prune',
|
|
1036
|
-
], {
|
|
1037
|
-
cwd: testProject,
|
|
1038
|
-
});
|
|
1039
|
-
|
|
1040
|
-
expect(result.exitCode).toBe(0);
|
|
1041
|
-
expect(result.stdout).toContain('No expired memories');
|
|
1042
|
-
});
|
|
1043
|
-
});
|
|
1044
|
-
|
|
1045
|
-
describe('store reindex command', () => {
|
|
1046
|
-
beforeEach(async () => {
|
|
1047
|
-
// Create memories without proper indexes
|
|
1048
|
-
await createMemoryFile(storeDir, 'project/memory-a', {
|
|
1049
|
-
content: 'Memory A content.',
|
|
1050
|
-
});
|
|
1051
|
-
await createMemoryFile(storeDir, 'project/memory-b', {
|
|
1052
|
-
content: 'Memory B content.',
|
|
1053
|
-
});
|
|
1054
|
-
});
|
|
1055
|
-
|
|
1056
|
-
it('should rebuild indexes', async () => {
|
|
1057
|
-
const result = await runCortexCli([
|
|
1058
|
-
'store', 'reindex',
|
|
1059
|
-
], {
|
|
1060
|
-
cwd: testProject,
|
|
1061
|
-
});
|
|
1062
|
-
|
|
1063
|
-
expect(result.exitCode).toBe(0);
|
|
1064
|
-
expect(result.stdout).toContain('Reindexed');
|
|
1065
|
-
|
|
1066
|
-
// Verify index was created (indexes are stored as <categoryPath>/index.yaml files)
|
|
1067
|
-
const indexPath = join(storeDir, 'project', 'index.yaml');
|
|
1068
|
-
const indexExists = await fs
|
|
1069
|
-
.access(indexPath)
|
|
1070
|
-
.then(() => true)
|
|
1071
|
-
.catch(() => false);
|
|
1072
|
-
expect(indexExists).toBe(true);
|
|
1073
|
-
});
|
|
1074
|
-
});
|
|
1075
|
-
|
|
1076
|
-
describe('error handling', () => {
|
|
1077
|
-
it('should fail gracefully with unknown command', async () => {
|
|
1078
|
-
const result = await runCortexCli(['unknown-command'], {
|
|
1079
|
-
cwd: testProject,
|
|
1080
|
-
});
|
|
1081
|
-
|
|
1082
|
-
expect(result.exitCode).toBe(1);
|
|
1083
|
-
// Commander shows "unknown command" error
|
|
1084
|
-
expect(result.stderr.toLowerCase()).toMatch(/unknown|command/);
|
|
1085
|
-
});
|
|
1086
|
-
|
|
1087
|
-
it('should show help when no command provided', async () => {
|
|
1088
|
-
const result = await runCortexCli([], {
|
|
1089
|
-
cwd: testProject,
|
|
1090
|
-
});
|
|
1091
|
-
|
|
1092
|
-
// Commander.js by default shows help when no command is provided
|
|
1093
|
-
// Exit code may be 0 (help displayed) or 1 depending on config
|
|
1094
|
-
// The important thing is that it doesn't crash
|
|
1095
|
-
expect(result.exitCode).toBeGreaterThanOrEqual(0);
|
|
1096
|
-
});
|
|
1097
|
-
|
|
1098
|
-
it('should fail when store does not exist and no global fallback', async () => {
|
|
1099
|
-
// Create a temp directory with no .cortex/memory and no global store available
|
|
1100
|
-
const emptyDir = await fs.mkdtemp(join(tmpdir(), 'cortex-empty-'));
|
|
1101
|
-
// Create a fake .cortex directory to prevent global store fallback
|
|
1102
|
-
const fakeCortex = join(emptyDir, '.cortex');
|
|
1103
|
-
await fs.mkdir(fakeCortex, { recursive: true });
|
|
1104
|
-
// Note: .cortex/memory doesn't exist, so resolution should fail
|
|
1105
|
-
try {
|
|
1106
|
-
const result = await runCortexCli([
|
|
1107
|
-
'memory', 'list',
|
|
1108
|
-
], {
|
|
1109
|
-
cwd: emptyDir,
|
|
1110
|
-
});
|
|
1111
|
-
|
|
1112
|
-
// Either fails because no store found, or succeeds with empty list
|
|
1113
|
-
// depending on whether global store exists
|
|
1114
|
-
expect(result.exitCode).toBeGreaterThanOrEqual(0);
|
|
1115
|
-
}
|
|
1116
|
-
finally {
|
|
1117
|
-
await fs.rm(emptyDir, { recursive: true, force: true });
|
|
1118
|
-
}
|
|
1119
|
-
});
|
|
1120
|
-
});
|
|
1121
|
-
|
|
1122
|
-
describe('help and version', () => {
|
|
1123
|
-
it('should show help with --help flag', async () => {
|
|
1124
|
-
const result = await runCortexCli(['--help'], {
|
|
1125
|
-
cwd: testProject,
|
|
1126
|
-
});
|
|
1127
|
-
|
|
1128
|
-
expect(result.exitCode).toBe(0);
|
|
1129
|
-
expect(result.stdout).toContain('Memory system for AI agents');
|
|
1130
|
-
expect(result.stdout).toContain('memory');
|
|
1131
|
-
expect(result.stdout).toContain('store');
|
|
1132
|
-
});
|
|
1133
|
-
|
|
1134
|
-
it('should show version with --version flag', async () => {
|
|
1135
|
-
const result = await runCortexCli(['--version'], {
|
|
1136
|
-
cwd: testProject,
|
|
1137
|
-
});
|
|
1138
|
-
|
|
1139
|
-
expect(result.exitCode).toBe(0);
|
|
1140
|
-
expect(result.stdout).toMatch(/\d+\.\d+\.\d+/);
|
|
1141
|
-
});
|
|
1142
|
-
|
|
1143
|
-
it('should show memory subcommand help', async () => {
|
|
1144
|
-
const result = await runCortexCli([
|
|
1145
|
-
'memory', '--help',
|
|
1146
|
-
], {
|
|
1147
|
-
cwd: testProject,
|
|
1148
|
-
});
|
|
1149
|
-
|
|
1150
|
-
expect(result.exitCode).toBe(0);
|
|
1151
|
-
expect(result.stdout).toContain('Memory operations');
|
|
1152
|
-
expect(result.stdout).toContain('add');
|
|
1153
|
-
expect(result.stdout).toContain('show');
|
|
1154
|
-
expect(result.stdout).toContain('list');
|
|
1155
|
-
});
|
|
1156
|
-
|
|
1157
|
-
it('should show store subcommand help', async () => {
|
|
1158
|
-
const result = await runCortexCli([
|
|
1159
|
-
'store', '--help',
|
|
1160
|
-
], {
|
|
1161
|
-
cwd: testProject,
|
|
1162
|
-
});
|
|
1163
|
-
|
|
1164
|
-
expect(result.exitCode).toBe(0);
|
|
1165
|
-
expect(result.stdout).toContain('Store management');
|
|
1166
|
-
expect(result.stdout).toContain('prune');
|
|
1167
|
-
expect(result.stdout).toContain('reindex');
|
|
1168
|
-
});
|
|
1169
|
-
});
|
|
1170
|
-
|
|
1171
|
-
describe('store init command', () => {
|
|
1172
|
-
it('should initialize a new store when no store is configured', async () => {
|
|
1173
|
-
// Create an isolated directory with no pre-configured stores — the
|
|
1174
|
-
// config.yaml exists but has no store entries, simulating a fresh install.
|
|
1175
|
-
const freshDir = await fs.mkdtemp(join(tmpdir(), 'cortex-store-init-'));
|
|
1176
|
-
const configDir = join(freshDir, '.config', 'cortex');
|
|
1177
|
-
await fs.mkdir(configDir, { recursive: true });
|
|
1178
|
-
await fs.writeFile(join(configDir, 'config.yaml'), 'stores: {}\n', 'utf8');
|
|
1179
|
-
|
|
1180
|
-
const storePath = join(freshDir, '.cortex', 'memory');
|
|
1181
|
-
|
|
1182
|
-
try {
|
|
1183
|
-
const result = await runCortexCli(
|
|
1184
|
-
[
|
|
1185
|
-
'store',
|
|
1186
|
-
'init',
|
|
1187
|
-
storePath,
|
|
1188
|
-
'--name',
|
|
1189
|
-
'my-project',
|
|
1190
|
-
'--format',
|
|
1191
|
-
'json',
|
|
1192
|
-
],
|
|
1193
|
-
{
|
|
1194
|
-
cwd: freshDir,
|
|
1195
|
-
env: { CORTEX_CONFIG_DIR: configDir },
|
|
1196
|
-
},
|
|
1197
|
-
);
|
|
1198
|
-
|
|
1199
|
-
expectCliOk([
|
|
1200
|
-
'store',
|
|
1201
|
-
'init',
|
|
1202
|
-
storePath,
|
|
1203
|
-
'--name',
|
|
1204
|
-
'my-project',
|
|
1205
|
-
], result);
|
|
1206
|
-
expect(result.exitCode).toBe(0);
|
|
1207
|
-
|
|
1208
|
-
const parsed = JSON.parse(result.stdout) as {
|
|
1209
|
-
value: { name: string; path: string };
|
|
1210
|
-
};
|
|
1211
|
-
expect(parsed.value.name).toBe('my-project');
|
|
1212
|
-
expect(parsed.value.path).toBe(storePath);
|
|
1213
|
-
|
|
1214
|
-
// Verify the store was registered in config.yaml
|
|
1215
|
-
const configContent = await fs.readFile(join(configDir, 'config.yaml'), 'utf8');
|
|
1216
|
-
expect(configContent).toContain('my-project');
|
|
1217
|
-
expect(configContent).toContain(storePath);
|
|
1218
|
-
}
|
|
1219
|
-
finally {
|
|
1220
|
-
await fs.rm(freshDir, { recursive: true, force: true });
|
|
1221
|
-
}
|
|
1222
|
-
});
|
|
1223
|
-
|
|
1224
|
-
it('should create the store directory when it does not yet exist', async () => {
|
|
1225
|
-
const freshDir = await fs.mkdtemp(join(tmpdir(), 'cortex-store-init-newdir-'));
|
|
1226
|
-
const configDir = join(freshDir, '.config', 'cortex');
|
|
1227
|
-
await fs.mkdir(configDir, { recursive: true });
|
|
1228
|
-
await fs.writeFile(join(configDir, 'config.yaml'), 'stores: {}\n', 'utf8');
|
|
1229
|
-
|
|
1230
|
-
// Point at a path that does NOT yet exist on disk
|
|
1231
|
-
const storePath = join(freshDir, 'brand-new-store');
|
|
1232
|
-
|
|
1233
|
-
try {
|
|
1234
|
-
const result = await runCortexCli(
|
|
1235
|
-
[
|
|
1236
|
-
'store',
|
|
1237
|
-
'init',
|
|
1238
|
-
storePath,
|
|
1239
|
-
'--name',
|
|
1240
|
-
'new-dir-store',
|
|
1241
|
-
'--format',
|
|
1242
|
-
'json',
|
|
1243
|
-
],
|
|
1244
|
-
{
|
|
1245
|
-
cwd: freshDir,
|
|
1246
|
-
env: { CORTEX_CONFIG_DIR: configDir },
|
|
1247
|
-
},
|
|
1248
|
-
);
|
|
1249
|
-
|
|
1250
|
-
expectCliOk([
|
|
1251
|
-
'store',
|
|
1252
|
-
'init',
|
|
1253
|
-
storePath,
|
|
1254
|
-
'--name',
|
|
1255
|
-
'new-dir-store',
|
|
1256
|
-
], result);
|
|
1257
|
-
|
|
1258
|
-
// The directory must have been created by the init command
|
|
1259
|
-
const dirStat = await fs.stat(storePath);
|
|
1260
|
-
expect(dirStat.isDirectory()).toBe(true);
|
|
1261
|
-
}
|
|
1262
|
-
finally {
|
|
1263
|
-
await fs.rm(freshDir, { recursive: true, force: true });
|
|
1264
|
-
}
|
|
1265
|
-
});
|
|
1266
|
-
|
|
1267
|
-
it('should fail when the store name already exists', async () => {
|
|
1268
|
-
// Register a store first using the existing testProject setup
|
|
1269
|
-
const storePath = join(testProject, '.cortex', 'memory2');
|
|
1270
|
-
const firstInit = await runCortexCli(
|
|
1271
|
-
[
|
|
1272
|
-
'store',
|
|
1273
|
-
'init',
|
|
1274
|
-
storePath,
|
|
1275
|
-
'--name',
|
|
1276
|
-
'existing-store',
|
|
1277
|
-
'--format',
|
|
1278
|
-
'json',
|
|
1279
|
-
],
|
|
1280
|
-
{ cwd: testProject },
|
|
1281
|
-
);
|
|
1282
|
-
expectCliOk([
|
|
1283
|
-
'store',
|
|
1284
|
-
'init',
|
|
1285
|
-
storePath,
|
|
1286
|
-
'--name',
|
|
1287
|
-
'existing-store',
|
|
1288
|
-
], firstInit);
|
|
1289
|
-
|
|
1290
|
-
// Attempt to init the same name again — should fail
|
|
1291
|
-
const secondInit = await runCortexCli(
|
|
1292
|
-
[
|
|
1293
|
-
'store',
|
|
1294
|
-
'init',
|
|
1295
|
-
join(testProject, '.cortex', 'memory3'),
|
|
1296
|
-
'--name',
|
|
1297
|
-
'existing-store',
|
|
1298
|
-
],
|
|
1299
|
-
{ cwd: testProject },
|
|
1300
|
-
);
|
|
1301
|
-
|
|
1302
|
-
expect(secondInit.exitCode).toBe(1);
|
|
1303
|
-
expect(secondInit.stderr.toLowerCase()).toMatch(/already exists/);
|
|
1304
|
-
});
|
|
1305
|
-
});
|
|
1306
|
-
});
|