mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-28 20:45:11 +02:00
Merge reviewed observer status counting repair and retain combined registry binding
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -156,8 +156,14 @@ case "$ACTION" in
|
||||
echo "Observer is running (PID: $pid)"
|
||||
echo "Log: $LOG_FILE"
|
||||
echo "Observations: $(wc -l < "$OBSERVATIONS_FILE" 2>/dev/null || echo 0) lines"
|
||||
# Also show instinct count
|
||||
instinct_count=$(find "$INSTINCTS_DIR" -name "*.yaml" 2>/dev/null | wc -l)
|
||||
# Count eligible files, not parsed records: the loader accepts these
|
||||
# suffixes case-insensitively and follows links to regular files.
|
||||
# Stay at the top level, excluding dot-only names with no Path.suffix.
|
||||
# Count NUL records so newlines in filenames cannot inflate the result.
|
||||
instinct_find_expr=( \( -iname "*.yaml" -o -iname "*.yml" -o -iname "*.md" \) )
|
||||
instinct_count=$(find -L "$INSTINCTS_DIR" -mindepth 1 -maxdepth 1 -type f \
|
||||
"${instinct_find_expr[@]}" ! -iname ".yaml" ! -iname ".yml" ! -iname ".md" \
|
||||
-print0 2>/dev/null | tr -cd '\000' | wc -c | tr -d '[:space:]')
|
||||
echo "Instincts: $instinct_count"
|
||||
exit 0
|
||||
else
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
/**
|
||||
* Regression tests for #2859: status counts eligible top-level instinct files.
|
||||
* The loader accepts .yaml/.yml/.md; files can contain zero or many instincts,
|
||||
* so this count does not establish parsed-record counts or observer health.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const { spawnSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..', '..');
|
||||
const skillRoot = path.join(repoRoot, 'skills', 'continuous-learning-v2');
|
||||
const observerScript = path.join(skillRoot, 'agents', 'start-observer.sh');
|
||||
const instinctCli = path.join(skillRoot, 'scripts', 'instinct-cli.py');
|
||||
const bashBinary = process.env.ECC_TEST_BASH || (process.platform === 'win32' ? null : '/bin/bash');
|
||||
const childTimeoutMs = 3000;
|
||||
const childMaxBuffer = 64 * 1024;
|
||||
|
||||
class SkipTest extends Error {}
|
||||
|
||||
function toShellPath(filePath) {
|
||||
const normalized = filePath.split(path.sep).join('/');
|
||||
return normalized.replace(/^([A-Za-z]):\//, (_, drive) => `/${drive.toLowerCase()}/`);
|
||||
}
|
||||
|
||||
function readAllowedExtensions() {
|
||||
const cliSource = fs.readFileSync(instinctCli, 'utf8');
|
||||
const match = cliSource.match(/ALLOWED_INSTINCT_EXTENSIONS\s*=\s*\(([^)]*)\)/);
|
||||
assert.ok(match, 'ALLOWED_INSTINCT_EXTENSIONS not found in instinct-cli.py');
|
||||
return match[1].split(',')
|
||||
.map(part => part.trim().replace(/^["']|["']$/g, ''))
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function fixtureAt(root) {
|
||||
const home = path.join(root, 'home');
|
||||
const temp = path.join(root, 'tmp');
|
||||
const bin = path.join(root, 'bin');
|
||||
const homunculus = path.join(root, 'homunculus');
|
||||
const instincts = path.join(homunculus, 'instincts', 'personal');
|
||||
const unexpected = path.join(root, 'unexpected-command');
|
||||
for (const directory of [home, temp, bin, instincts]) fs.mkdirSync(directory, { recursive: true });
|
||||
for (const command of ['python', 'python3', 'git', 'claude']) {
|
||||
fs.writeFileSync(path.join(bin, command),
|
||||
'#!/bin/sh\nprintf unexpected > "$FIXTURE_UNEXPECTED_COMMAND"\nexit 97\n', { mode: 0o700 });
|
||||
}
|
||||
fs.writeFileSync(path.join(homunculus, 'observations.jsonl'), '');
|
||||
return {
|
||||
root, instincts, unexpected,
|
||||
env: {
|
||||
PATH: [toShellPath(bin), '/usr/bin', '/bin'].join(':'),
|
||||
HOME: toShellPath(home), USERPROFILE: home, TMPDIR: toShellPath(temp),
|
||||
TMP: temp, TEMP: temp, LC_ALL: 'C',
|
||||
...(process.platform === 'win32' ? { SystemRoot: process.env.SystemRoot } : {}),
|
||||
CLV2_NO_PROJECT: '1', CLV2_HOMUNCULUS_DIR: toShellPath(homunculus),
|
||||
CLV2_CONFIG: toShellPath(path.join(root, 'absent-config.json')),
|
||||
CLV2_PYTHON_CMD: toShellPath(path.join(bin, 'python3')),
|
||||
FIXTURE_UNEXPECTED_COMMAND: toShellPath(unexpected),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function withFixture(callback, remove = fs.rmSync) {
|
||||
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-observer-')));
|
||||
let failed = false; let primary; let result;
|
||||
try {
|
||||
result = callback(fixtureAt(root));
|
||||
} catch (error) {
|
||||
failed = true;
|
||||
primary = error;
|
||||
}
|
||||
try { remove(root, { recursive: true, force: true }); }
|
||||
catch (error) { if (!failed) throw error; }
|
||||
if (failed) throw primary;
|
||||
return result;
|
||||
}
|
||||
|
||||
function checkChild(result) {
|
||||
if (result.error) throw result.error;
|
||||
assert.strictEqual(result.status, 0, result.stderr || result.stdout || `child signal: ${result.signal}`);
|
||||
}
|
||||
|
||||
function runStatus(files, { run = spawnSync, setup = () => {}, remove = fs.rmSync } = {}) {
|
||||
return withFixture(fixture => {
|
||||
for (const name of files) {
|
||||
const target = path.join(fixture.instincts, name);
|
||||
fs.mkdirSync(path.dirname(target), { recursive: true });
|
||||
fs.writeFileSync(target, 'id: fixture\n');
|
||||
}
|
||||
setup(fixture);
|
||||
// Only this foreground shell's own PID is advertised. No observer, sleep,
|
||||
// provider or other background child is started or signalled.
|
||||
const program = 'printf "%s\\n" "$$" > "$CLV2_HOMUNCULUS_DIR/.observer.pid"\nexec "$BASH" "$1" status';
|
||||
const result = run(bashBinary, ['--noprofile', '--norc', '-c', program,
|
||||
'observer-status-test', toShellPath(observerScript)], {
|
||||
cwd: fixture.root, encoding: 'utf8', env: fixture.env,
|
||||
timeout: childTimeoutMs, maxBuffer: childMaxBuffer, killSignal: 'SIGKILL',
|
||||
});
|
||||
checkChild(result);
|
||||
assert.ok(!fs.existsSync(fixture.unexpected), 'status must not invoke Git, Python or a provider');
|
||||
const lines = (result.stdout || '').split('\n').filter(line => line.startsWith('Instincts:'));
|
||||
assert.strictEqual(lines.length, 1, `expected one count in status output:\n${result.stdout}`);
|
||||
assert.match(lines[0], /^Instincts:\s+\d+\s*$/);
|
||||
return Number(lines[0].split(':')[1].trim());
|
||||
}, remove);
|
||||
}
|
||||
|
||||
function fileLink(target, link, type = 'file') {
|
||||
try { fs.symlinkSync(target, link, type); }
|
||||
catch (error) {
|
||||
const unsupported = ['ENOSYS', 'ENOTSUP'].includes(error.code)
|
||||
|| (process.platform === 'win32' && ['EPERM', 'EACCES'].includes(error.code));
|
||||
if (unsupported) throw new SkipTest(`symlink capability unavailable: ${error.code}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function shellTest(fn) {
|
||||
return () => {
|
||||
if (!bashBinary) throw new SkipTest('requires bash; set ECC_TEST_BASH on Windows');
|
||||
return fn();
|
||||
};
|
||||
}
|
||||
|
||||
function cleanupTests() {
|
||||
return [
|
||||
['status child has a private environment and bounded execution', () => {
|
||||
let root;
|
||||
assert.strictEqual(runStatus([], { run: (_command, args, options) => {
|
||||
root = options.cwd;
|
||||
assert.strictEqual(options.timeout, childTimeoutMs);
|
||||
assert.strictEqual(options.maxBuffer, childMaxBuffer);
|
||||
assert.strictEqual(options.killSignal, 'SIGKILL');
|
||||
assert.strictEqual(options.env.CLV2_NO_PROJECT, '1');
|
||||
for (const key of ['BASH_ENV', 'ENV', 'NODE_OPTIONS', 'ANTHROPIC_API_KEY', 'CLAUDE_PROJECT_DIR']) {
|
||||
assert.ok(!Object.hasOwn(options.env, key), `unexpected inherited ${key}`);
|
||||
}
|
||||
assert.ok(options.env.CLV2_CONFIG.endsWith('/absent-config.json'));
|
||||
assert.ok(!fs.existsSync(options.env.CLV2_CONFIG));
|
||||
assert.strictEqual(args.at(-1), toShellPath(observerScript));
|
||||
assert.match(args[3], /\nexec "\$BASH" "\$1" status$/);
|
||||
assert.doesNotMatch(args[3], /sleep|kill|&/);
|
||||
return { status: 0, stdout: 'Instincts: 0\n', stderr: '' };
|
||||
} }), 0);
|
||||
assert.ok(!fs.existsSync(root));
|
||||
}],
|
||||
...['timeout', 'nonzero', 'spawn error'].map(kind => [`${kind} cleans its private fixture`, () => {
|
||||
let root;
|
||||
const failure = Object.assign(new Error(kind), { code: kind === 'timeout' ? 'ETIMEDOUT' : 'EIO' });
|
||||
assert.throws(() => runStatus([], { run: (_command, _args, options) => {
|
||||
root = options.cwd;
|
||||
return kind === 'nonzero' ? { status: 7, stdout: '', stderr: 'fixture rejected' } : { error: failure };
|
||||
} }), error => kind === 'nonzero' ? /fixture rejected/.test(error.message) : error === failure);
|
||||
assert.ok(!fs.existsSync(root));
|
||||
}]),
|
||||
['cleanup preserves frozen and falsy primary failures', () => {
|
||||
for (const primary of [Object.freeze(new Error('primary')), null, false, 0, undefined]) {
|
||||
let root; let caught = false;
|
||||
try {
|
||||
withFixture(value => { root = value.root; throw primary; }, (value, options) => {
|
||||
fs.rmSync(value, options); throw new Error('cleanup');
|
||||
});
|
||||
} catch (error) { caught = true; assert.strictEqual(error, primary); }
|
||||
assert.ok(caught);
|
||||
assert.ok(!fs.existsSync(root));
|
||||
}
|
||||
}],
|
||||
['cleanup failure is reported when the fixture otherwise succeeds', () => {
|
||||
let root;
|
||||
const failure = new Error('cleanup');
|
||||
assert.throws(() => withFixture(value => { root = value.root; }, (value, options) => {
|
||||
fs.rmSync(value, options); throw failure;
|
||||
}), error => error === failure);
|
||||
assert.ok(!fs.existsSync(root));
|
||||
}],
|
||||
];
|
||||
}
|
||||
|
||||
function buildTests() {
|
||||
const allowed = readAllowedExtensions();
|
||||
return [
|
||||
['the loader still declares several instinct extensions', () => {
|
||||
assert.ok(allowed.length >= 3, `expected several extensions, got ${allowed}`);
|
||||
}],
|
||||
...allowed.map(ext => [`status counts ${ext} - the loader accepts it`, shellTest(() => {
|
||||
assert.strictEqual(runStatus([`one${ext}`]), 1);
|
||||
})]),
|
||||
['status does not recurse - the loader does not', shellTest(() => {
|
||||
assert.strictEqual(runStatus(['a.md', 'nested/deep.yaml']), 1);
|
||||
})],
|
||||
['status skips directories', shellTest(() => {
|
||||
assert.strictEqual(runStatus([], { setup: value => fs.mkdirSync(path.join(value.instincts, 'directory.md')) }), 0);
|
||||
})],
|
||||
['status matches case-insensitively', shellTest(() => {
|
||||
assert.strictEqual(runStatus(['a.YAML', 'b.YmL', 'c.MD']), 3);
|
||||
})],
|
||||
['start-observer.sh parses', shellTest(() => withFixture(fixture => {
|
||||
checkChild(spawnSync(bashBinary, ['--noprofile', '--norc', '-n', toShellPath(observerScript)], {
|
||||
cwd: fixture.root, env: fixture.env, encoding: 'utf8',
|
||||
timeout: childTimeoutMs, maxBuffer: childMaxBuffer, killSignal: 'SIGKILL',
|
||||
}));
|
||||
}))],
|
||||
['markdown instincts are counted', shellTest(() => {
|
||||
assert.strictEqual(runStatus(['a.md', 'b.md', 'c.md']), 3);
|
||||
})],
|
||||
['the count matches the loader eligible-file rules', shellTest(() => {
|
||||
assert.strictEqual(runStatus(['a.md', 'b.yaml', 'c.yml', 'd.YAML', 'notes.txt', 'nested/deep.md']), 4);
|
||||
})],
|
||||
['an empty instincts directory reports 0', shellTest(() => {
|
||||
assert.strictEqual(runStatus([]), 0);
|
||||
})],
|
||||
['hidden stems count but dot-only extension names have no suffix', shellTest(() => {
|
||||
assert.strictEqual(runStatus(['.note.MD', '.yaml', '.YML', '.md']), 1);
|
||||
})],
|
||||
['newlines and spaces in one eligible filename count once', shellTest(() => {
|
||||
assert.strictEqual(runStatus(['two\nlines with spaces.MD']), 1);
|
||||
})],
|
||||
['shell metacharacters in a filename remain inert data', shellTest(() => {
|
||||
assert.strictEqual(runStatus(['$(touch unwanted).md'], { setup: value => {
|
||||
assert.ok(!fs.existsSync(path.join(value.root, 'unwanted')));
|
||||
}, run: (command, args, options) => {
|
||||
const result = spawnSync(command, args, options);
|
||||
assert.ok(!fs.existsSync(path.join(options.cwd, 'unwanted')));
|
||||
return result;
|
||||
} }), 1);
|
||||
})],
|
||||
['linked regular files count without traversing directory links', shellTest(() => {
|
||||
assert.strictEqual(runStatus(['a.md', 'b.yaml', 'c.yml', 'd.YAML', '.note.MD',
|
||||
'.md', '.yaml', '.YML', 'notes.txt', 'nested/deep.md'], { setup: value => {
|
||||
const outside = path.join(value.root, 'outside');
|
||||
fs.mkdirSync(outside);
|
||||
const target = path.join(outside, 'regular.txt');
|
||||
fs.writeFileSync(target, 'private fixture\n');
|
||||
fs.writeFileSync(path.join(outside, 'deep.md'), 'not top-level\n');
|
||||
fs.mkdirSync(path.join(value.instincts, 'directory.md'));
|
||||
fileLink(target, path.join(value.instincts, 'linked.MD'));
|
||||
fileLink(outside, path.join(value.instincts, 'directory-link.yaml'), 'dir');
|
||||
fileLink(path.join(outside, 'missing'), path.join(value.instincts, 'dangling.yml'));
|
||||
} }), 6);
|
||||
})],
|
||||
...cleanupTests(),
|
||||
];
|
||||
}
|
||||
|
||||
function main() {
|
||||
console.log('\n=== Testing observer status instinct count (#2859) ===\n');
|
||||
let passed = 0; let failed = 0; let skipped = 0; let tests;
|
||||
try { tests = buildTests(); }
|
||||
catch (error) {
|
||||
console.error(` FAIL could not build the test list: ${error.message}`);
|
||||
tests = []; failed += 1;
|
||||
}
|
||||
for (const [name, fn] of tests) {
|
||||
try { fn(); console.log(` PASS ${name}`); passed += 1; }
|
||||
catch (error) {
|
||||
if (error instanceof SkipTest) {
|
||||
console.log(` SKIP ${name}: ${error.message}`); skipped += 1;
|
||||
} else {
|
||||
console.error(` FAIL ${name}: ${error.message}`); failed += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(`\n Passed: ${passed}\n Failed: ${failed}\n Skipped: ${skipped}`);
|
||||
// Let piped summary output drain before the aggregate runner reads it.
|
||||
if (failed > 0) process.exitCode = 1;
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user