mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-12 12:47:57 +02:00
fix: harden unified memory boundaries
This commit is contained in:
@@ -365,7 +365,10 @@ function runTests() {
|
||||
assert.ok(result.stdout.includes('Mode: manifest'));
|
||||
assert.ok(result.stdout.includes('Profile: core'));
|
||||
assert.ok(result.stdout.includes('Included components: (none)'));
|
||||
assert.ok(result.stdout.includes('Selected modules: rules-core, agents-core, commands-core, hooks-runtime, platform-configs, workflow-quality'));
|
||||
assert.ok(result.stdout.includes(
|
||||
'Selected modules: rules-core, agents-core, commands-core, hooks-runtime, '
|
||||
+ 'platform-configs, skill-unified-memory, workflow-quality'
|
||||
));
|
||||
assert.ok(!fs.existsSync(path.join(homeDir, '.claude', 'ecc', 'install-state.json')));
|
||||
} finally {
|
||||
cleanup(homeDir);
|
||||
@@ -404,7 +407,10 @@ function runTests() {
|
||||
assert.strictEqual(result.code, 0, result.stderr);
|
||||
assert.ok(result.stdout.includes('Mode: manifest'));
|
||||
assert.ok(result.stdout.includes('Profile: minimal'));
|
||||
assert.ok(result.stdout.includes('Selected modules: rules-core, agents-core, commands-core, platform-configs, workflow-quality'));
|
||||
assert.ok(result.stdout.includes(
|
||||
'Selected modules: rules-core, agents-core, commands-core, platform-configs, '
|
||||
+ 'skill-unified-memory, workflow-quality'
|
||||
));
|
||||
assert.ok(!result.stdout.includes('hooks-runtime'));
|
||||
assert.ok(!fs.existsSync(path.join(homeDir, '.claude', 'ecc', 'install-state.json')));
|
||||
} finally {
|
||||
@@ -491,7 +497,14 @@ function runTests() {
|
||||
assert.strictEqual(state.request.legacyMode, false);
|
||||
assert.deepStrictEqual(
|
||||
state.resolution.selectedModules,
|
||||
['rules-core', 'agents-core', 'commands-core', 'platform-configs', 'workflow-quality']
|
||||
[
|
||||
'rules-core',
|
||||
'agents-core',
|
||||
'commands-core',
|
||||
'platform-configs',
|
||||
'skill-unified-memory',
|
||||
'workflow-quality'
|
||||
]
|
||||
);
|
||||
assert.ok(state.resolution.skippedModules.includes('hooks-runtime'));
|
||||
assert.ok(!state.resolution.skippedModules.includes('workflow-quality'));
|
||||
|
||||
@@ -9,6 +9,11 @@ const { PassThrough } = require('stream');
|
||||
const { pathToFileURL } = require('url');
|
||||
|
||||
const SERVER = path.join(__dirname, '..', '..', 'scripts', 'memory-mcp.mjs');
|
||||
const {
|
||||
MAX_RESULTS,
|
||||
resolveVaultRoots,
|
||||
saveMemory,
|
||||
} = require('../../scripts/lib/memory-vault');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
@@ -345,6 +350,51 @@ async function main() {
|
||||
});
|
||||
});
|
||||
|
||||
await test('filters harness-visible backlinks before applying the response cap', async () => {
|
||||
await withClient(async (client, fixture) => {
|
||||
const roots = resolveVaultRoots({
|
||||
cwd: fixture.projectRoot,
|
||||
env: fixture.env,
|
||||
});
|
||||
const saveWithId = (input, id) => saveMemory(input, {
|
||||
roots,
|
||||
now: () => '2026-07-26T20:00:00.000Z',
|
||||
idFactory: () => id,
|
||||
});
|
||||
const targetId = 'mem_backlink_target';
|
||||
saveWithId({
|
||||
title: 'Backlink target',
|
||||
body: 'Visible target body.',
|
||||
targetHarnesses: ['claude'],
|
||||
}, targetId);
|
||||
|
||||
for (let index = 0; index < MAX_RESULTS; index += 1) {
|
||||
saveWithId({
|
||||
title: `Hidden backlink ${index}`,
|
||||
body: 'Only Hermes may see this backlink.',
|
||||
targetHarnesses: ['hermes'],
|
||||
links: [targetId],
|
||||
}, `mem_backlink_hidden_${String(index).padStart(3, '0')}`);
|
||||
}
|
||||
saveWithId({
|
||||
title: 'Visible backlink',
|
||||
body: 'Claude must still receive this backlink.',
|
||||
targetHarnesses: ['claude'],
|
||||
links: [targetId],
|
||||
}, 'mem_backlink_visible_zzz');
|
||||
|
||||
const read = parseTextResult(await client.callTool({
|
||||
name: 'memory_read',
|
||||
arguments: { id: targetId },
|
||||
}));
|
||||
assert.deepStrictEqual(
|
||||
read.backlinks.map(memory => memory.id),
|
||||
['mem_backlink_visible_zzz']
|
||||
);
|
||||
assert.strictEqual(read.backlinksTruncated, false);
|
||||
});
|
||||
});
|
||||
|
||||
await test('denies user scope unless the server explicitly grants it', async () => {
|
||||
await withClient(async client => {
|
||||
await assert.rejects(
|
||||
@@ -429,7 +479,8 @@ async function main() {
|
||||
env: fixture.env,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
assert.notStrictEqual(started.status, 0);
|
||||
assert.strictEqual(started.error, undefined);
|
||||
assert.strictEqual(started.status, 1);
|
||||
assert.match(started.stderr, /ECC_MEMORY_HARNESS/);
|
||||
assert.ok(!started.stderr.includes('\n at '));
|
||||
} finally {
|
||||
|
||||
@@ -8,7 +8,7 @@ const { spawnSync } = require('child_process');
|
||||
|
||||
const MEMORY_SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'memory.js');
|
||||
const ECC_SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'ecc.js');
|
||||
const { sanitizeTerminalText } = require(MEMORY_SCRIPT);
|
||||
const { readBoundedStdin, sanitizeTerminalText } = require(MEMORY_SCRIPT);
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
@@ -103,6 +103,68 @@ test('routes stdin through ecc memory without dropping the body', () => {
|
||||
}
|
||||
});
|
||||
|
||||
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 {
|
||||
@@ -310,6 +372,20 @@ test('rejects ambiguous body sources and does not expose a trust promotion flag'
|
||||
], 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 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user