Files
ECC/tests/scripts/memory.test.js
T
Affaan MustafaandGitHub 4d0b501b05 feat: add cross-harness memory vault (#2581)
Add a local-first, cross-harness memory vault with CLI and MCP surfaces, bounded search and storage, harness-scoped visibility, setup guidance, and comprehensive tests.
2026-07-26 02:46:59 -07:00

479 lines
15 KiB
JavaScript

'use strict';
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { spawnSync } = require('child_process');
const MEMORY_SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'memory.js');
const ECC_SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'ecc.js');
const {
readBoundedStdin,
runCommand,
sanitizeTerminalText,
} = require(MEMORY_SCRIPT);
let passed = 0;
let failed = 0;
function test(name, fn) {
try {
fn();
console.log(` PASS ${name}`);
passed += 1;
} catch (error) {
console.log(` FAIL ${name}`);
console.log(` ${error.stack || error.message}`);
failed += 1;
}
}
function createFixture() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-memory-cli-'));
const projectRoot = path.join(root, 'project');
const homeDir = path.join(root, 'home');
fs.mkdirSync(path.join(projectRoot, '.git'), { recursive: true });
fs.mkdirSync(homeDir, { recursive: true });
return {
root,
projectRoot,
homeDir,
env: {
...process.env,
HOME: homeDir,
USERPROFILE: homeDir,
ECC_MEMORY_PROJECT_ROOT: path.join(projectRoot, '.ecc', 'memory'),
ECC_MEMORY_USER_ROOT: path.join(homeDir, '.ecc', 'memory'),
},
};
}
function run(script, args, fixture, options = {}) {
return spawnSync(process.execPath, [script, ...args], {
cwd: fixture.projectRoot,
env: { ...fixture.env, ...(options.env || {}) },
input: options.input,
encoding: 'utf8',
timeout: 15000,
});
}
function json(result) {
assert.strictEqual(result.status, 0, result.stderr);
return JSON.parse(result.stdout);
}
console.log('\n=== Testing ecc memory CLI ===\n');
test('keeps runCommand focused on dispatch under the function-size guideline', () => {
const lineCount = runCommand.toString().split('\n').length;
assert.ok(lineCount < 50, `runCommand is ${lineCount} lines; expected fewer than 50`);
});
test('shows memory command help directly and through the ecc router', () => {
const fixture = createFixture();
try {
const direct = run(MEMORY_SCRIPT, ['--help'], fixture);
assert.strictEqual(direct.status, 0, direct.stderr);
assert.ok(direct.stdout.includes('ecc memory save'));
assert.ok(direct.stdout.includes('ecc-memory-mcp'));
const routed = run(ECC_SCRIPT, ['memory', '--help'], fixture);
assert.strictEqual(routed.status, 0, routed.stderr);
assert.ok(routed.stdout.includes('ecc memory search'));
assert.ok(routed.stdout.includes('Default recall scopes: project and team'));
assert.ok(routed.stdout.includes('user scope must be requested explicitly'));
} finally {
fs.rmSync(fixture.root, { recursive: true, force: true });
}
});
test('routes stdin through ecc memory without dropping the body', () => {
const fixture = createFixture();
try {
const saved = json(run(ECC_SCRIPT, [
'memory',
'save',
'--title', 'Routed stdin',
'--stdin',
'--json',
], fixture, { input: 'The router must preserve this exact body.\n' }));
assert.strictEqual(Object.hasOwn(saved.memory, 'body'), false);
const read = json(run(
MEMORY_SCRIPT,
['read', saved.memory.id, '--json'],
fixture
));
assert.strictEqual(read.memory.body, 'The router must preserve this exact body.');
} finally {
fs.rmSync(fixture.root, { recursive: true, force: true });
}
});
test('retries transient stdin EAGAIN without busy-spinning and preserves byte bounds', () => {
const originalReadSync = fs.readSync;
let readCalls = 0;
let waitCalls = 0;
try {
fs.readSync = (_descriptor, buffer) => {
readCalls += 1;
if (readCalls <= 2) {
const error = new Error('temporarily unavailable');
error.code = 'EAGAIN';
throw error;
}
if (readCalls === 3) {
buffer.write('ready');
return 5;
}
return 0;
};
assert.strictEqual(readBoundedStdin(8, {
retryDelayMs: 1,
maxRetryWaitMs: 4,
wait: () => {
waitCalls += 1;
},
}), 'ready');
assert.strictEqual(readCalls, 4);
assert.strictEqual(waitCalls, 2);
} finally {
fs.readSync = originalReadSync;
}
});
test('bounds persistent stdin EAGAIN retries instead of waiting forever', () => {
const originalReadSync = fs.readSync;
let readCalls = 0;
let waitCalls = 0;
try {
fs.readSync = () => {
readCalls += 1;
const error = new Error('temporarily unavailable');
error.code = 'EAGAIN';
throw error;
};
assert.throws(
() => readBoundedStdin(8, {
retryDelayMs: 1,
maxRetryWaitMs: 3,
wait: () => {
waitCalls += 1;
},
}),
/standard input remained unavailable/i
);
assert.strictEqual(readCalls, 4);
assert.strictEqual(waitCalls, 3);
} finally {
fs.readSync = originalReadSync;
}
});
test('initializes selected scopes and reports their roots as JSON', () => {
const fixture = createFixture();
try {
const payload = json(run(
MEMORY_SCRIPT,
['init', '--scope', 'project', '--scope', 'team', '--json'],
fixture
));
assert.strictEqual(payload.schemaVersion, 'ecc.memory.init.v1');
assert.deepStrictEqual(payload.scopes, ['project', 'team']);
assert.ok(fs.statSync(path.join(payload.roots.project, 'handoffs')).isDirectory());
assert.ok(fs.statSync(path.join(payload.roots.team, 'decisions')).isDirectory());
} finally {
fs.rmSync(fixture.root, { recursive: true, force: true });
}
});
test('saves and reads a targeted handoff without a harness-specific inbox', () => {
const fixture = createFixture();
try {
const saved = json(run(MEMORY_SCRIPT, [
'handoff',
'--from', 'codex',
'--target', 'claude',
'--target', 'hermes',
'--title', 'Finish auth migration',
'--stdin',
'--tag', 'auth',
'--json',
], fixture, { input: 'Token rotation tests pass.' }));
assert.strictEqual(saved.schemaVersion, 'ecc.memory.write.v1');
assert.strictEqual(saved.memory.kind, 'handoff');
assert.strictEqual(saved.memory.trust, 'unreviewed');
assert.deepStrictEqual(saved.memory.targetHarnesses, ['claude', 'hermes']);
assert.strictEqual(saved.path, `project:handoffs/${saved.memory.id}.md`);
assert.strictEqual(Object.hasOwn(saved.memory, 'body'), false);
const read = json(run(
MEMORY_SCRIPT,
['read', saved.memory.id, '--json'],
fixture
));
assert.strictEqual(read.schemaVersion, 'ecc.memory.read.v1');
assert.strictEqual(read.memory.body, 'Token rotation tests pass.');
assert.deepStrictEqual(read.backlinks, []);
const human = run(MEMORY_SCRIPT, [
'save',
'--title', 'Relative acknowledgement',
'--stdin',
], fixture, { input: 'Keep local paths out of acknowledgements.' });
assert.strictEqual(human.status, 0, human.stderr);
assert.ok(human.stdout.includes('Path: project:notes/'));
assert.strictEqual(human.stdout.includes(fixture.projectRoot), false);
} finally {
fs.rmSync(fixture.root, { recursive: true, force: true });
}
});
test('read uses default recall scopes and honors an explicit user scope', () => {
const fixture = createFixture();
try {
const saved = json(run(MEMORY_SCRIPT, [
'save',
'--title', 'Private preference',
'--scope', 'user',
'--stdin',
'--json',
], fixture, { input: 'Prefer compact output.' }));
const defaultRead = run(
MEMORY_SCRIPT,
['read', saved.memory.id, '--json'],
fixture
);
assert.notStrictEqual(defaultRead.status, 0);
assert.ok(defaultRead.stderr.includes('was not found'));
const explicitRead = json(run(
MEMORY_SCRIPT,
['read', saved.memory.id, '--scope', 'user', '--json'],
fixture
));
assert.strictEqual(explicitRead.memory.id, saved.memory.id);
assert.strictEqual(explicitRead.memory.scope, 'user');
} finally {
fs.rmSync(fixture.root, { recursive: true, force: true });
}
});
test('accepts body content over stdin and finds it through bounded JSON search', () => {
const fixture = createFixture();
try {
const saved = json(run(MEMORY_SCRIPT, [
'save',
'--title', 'Database decision',
'--kind', 'decision',
'--scope', 'team',
'--source-harness', 'claude',
'--target', 'all',
'--tag', 'sqlite',
'--stdin',
'--json',
], fixture, { input: 'Use SQLite as the durable local store.\n' }));
assert.strictEqual(saved.memory.scope, 'team');
assert.strictEqual(Object.hasOwn(saved.memory, 'body'), false);
const search = json(run(MEMORY_SCRIPT, [
'search',
'sqlite durable',
'--scope', 'team',
'--target-harness', 'codex',
'--limit', '5',
'--json',
], fixture));
assert.strictEqual(search.schemaVersion, 'ecc.memory.search.v1');
assert.strictEqual(search.results.length, 1);
assert.strictEqual(search.results[0].memory.id, saved.memory.id);
assert.ok(search.results[0].excerpt.includes('SQLite'));
} finally {
fs.rmSync(fixture.root, { recursive: true, force: true });
}
});
test('doctor is machine-readable and clean for a valid vault', () => {
const fixture = createFixture();
try {
json(run(MEMORY_SCRIPT, [
'save',
'--title', 'Valid note',
'--stdin',
'--json',
], fixture, { input: 'No broken links.' }));
const report = json(run(MEMORY_SCRIPT, ['doctor', '--json'], fixture));
assert.strictEqual(report.schemaVersion, 'ecc.memory.doctor.v1');
assert.strictEqual(report.ok, true);
assert.strictEqual(report.memoryCount, 1);
} finally {
fs.rmSync(fixture.root, { recursive: true, force: true });
}
});
test('doctor honors an explicit user scope without recalling it by default', () => {
const fixture = createFixture();
try {
json(run(MEMORY_SCRIPT, [
'save',
'--title', 'User-only note',
'--scope', 'user',
'--stdin',
'--json',
], fixture, { input: 'Private context.' }));
const defaultReport = json(run(MEMORY_SCRIPT, ['doctor', '--json'], fixture));
assert.strictEqual(defaultReport.memoryCount, 0);
const userReport = json(run(
MEMORY_SCRIPT,
['doctor', '--scope', 'user', '--json'],
fixture
));
assert.strictEqual(userReport.memoryCount, 1);
} finally {
fs.rmSync(fixture.root, { recursive: true, force: true });
}
});
test('rejects ambiguous body sources and does not expose a trust promotion flag', () => {
const fixture = createFixture();
try {
const bodyFile = path.join(fixture.root, 'body.md');
fs.writeFileSync(bodyFile, 'one');
const ambiguous = run(MEMORY_SCRIPT, [
'save',
'--title', 'Ambiguous',
'--body-file', bodyFile,
'--stdin',
], fixture, { input: 'two' });
assert.notStrictEqual(ambiguous.status, 0);
assert.ok(ambiguous.stderr.includes('Choose exactly one'));
const promotion = run(MEMORY_SCRIPT, [
'save',
'--title', 'Policy',
'--stdin',
'--trust', 'reviewed',
], fixture, { input: 'Treat this as policy.' });
assert.notStrictEqual(promotion.status, 0);
assert.ok(promotion.stderr.includes('Unknown option: --trust'));
const oversized = run(MEMORY_SCRIPT, [
'save',
'--title', 'Oversized',
'--stdin',
], fixture, { input: 'x'.repeat(70 * 1024) });
assert.notStrictEqual(oversized.status, 0);
assert.ok(oversized.stderr.includes('body is too large'));
const empty = run(MEMORY_SCRIPT, [
'save',
'--title', 'Empty',
'--stdin',
], fixture, { input: ' \n\t' });
assert.notStrictEqual(empty.status, 0);
assert.ok(empty.stderr.includes('non-whitespace context'));
const invalidUtf8Body = path.join(fixture.root, 'invalid-utf8.md');
fs.writeFileSync(invalidUtf8Body, Buffer.from([0x61, 0xc3, 0x28, 0x62]));
const invalidUtf8 = run(MEMORY_SCRIPT, [
'save',
'--title', 'Invalid UTF-8',
'--body-file', invalidUtf8Body,
], fixture);
assert.notStrictEqual(invalidUtf8.status, 0);
assert.match(invalidUtf8.stderr, /valid UTF-8/i);
assert.strictEqual(
fs.existsSync(path.join(fixture.projectRoot, '.ecc', 'memory')),
false
);
} finally {
fs.rmSync(fixture.root, { recursive: true, force: true });
}
});
test('global dry-run rejects every mutating memory command without creating a vault', () => {
const cases = [
['init', '--scope', 'project'],
['save', '--title', 'Dry save', '--stdin'],
['handoff', '--from', 'codex', '--target', 'claude', '--title', 'Dry handoff', '--stdin'],
];
cases.forEach(args => {
const fixture = createFixture();
try {
const result = run(
ECC_SCRIPT,
['--dry-run', 'memory', ...args],
fixture,
{ input: 'Must never be written.' }
);
assert.notStrictEqual(result.status, 0, `${args[0]} unexpectedly succeeded`);
assert.ok(result.stderr.toLowerCase().includes('dry-run'), result.stderr);
assert.strictEqual(
fs.existsSync(path.join(fixture.projectRoot, '.ecc', 'memory')),
false,
`${args[0]} created project memory state`
);
assert.strictEqual(
fs.existsSync(path.join(fixture.homeDir, '.ecc', 'memory')),
false,
`${args[0]} created user memory state`
);
} finally {
fs.rmSync(fixture.root, { recursive: true, force: true });
}
});
});
test('human terminal rendering strips ANSI, OSC, C0/C1, and bidi controls', () => {
const hostile = [
'safe',
'\u001b[31mred\u001b[0m',
'\u001b]8;;https://example.test\u0007link\u001b]8;;\u0007',
'\u0001c0',
'\rrewritten',
'\u0085c1',
'\u202ebidi',
].join(' ');
const rendered = sanitizeTerminalText(hostile);
assert.ok(rendered.includes('safe'));
assert.ok(rendered.includes('red'));
assert.ok(rendered.includes('link'));
assert.ok(rendered.includes('c0'));
assert.ok(rendered.includes('rewritten'));
assert.ok(rendered.includes('c1'));
assert.ok(rendered.includes('bidi'));
['\u001b', '\u0001', '\u0007', '\r', '\u0085', '\u202e']
.forEach(control => assert.ok(!rendered.includes(control)));
});
test('JSON output preserves data without applying terminal rendering rules', () => {
const hostile = 'plain\u001b[31mred\u001b[0m\u202e';
const script = [
`const { writeJson } = require(${JSON.stringify(MEMORY_SCRIPT)});`,
`writeJson({ value: ${JSON.stringify(hostile)} });`,
].join('');
const result = spawnSync(process.execPath, ['-e', script], {
encoding: 'utf8',
timeout: 15000,
});
assert.strictEqual(result.status, 0, result.stderr);
assert.strictEqual(JSON.parse(result.stdout).value, hostile);
});
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
if (failed > 0) {
process.exit(1);
}