mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-06 01:03:32 +08:00
fix: clean up observer sessions on lifecycle end
This commit is contained in:
@@ -76,6 +76,12 @@ test('observe.sh default throttle is 20 observations per signal', () => {
|
||||
assert.ok(content.includes('ECC_OBSERVER_SIGNAL_EVERY_N:-20'), 'Default signal frequency should be every 20 observations');
|
||||
});
|
||||
|
||||
test('observe.sh touches observer activity marker on each observation', () => {
|
||||
const content = fs.readFileSync(observeShPath, 'utf8');
|
||||
assert.ok(content.includes('ACTIVITY_FILE="${PROJECT_DIR}/.observer-last-activity"'), 'observe.sh should define a project-scoped activity marker');
|
||||
assert.ok(content.includes('touch "$ACTIVITY_FILE"'), 'observe.sh should update activity marker during observation capture');
|
||||
});
|
||||
|
||||
// ──────────────────────────────────────────────────────
|
||||
// Test group 2: observer-loop.sh re-entrancy guard
|
||||
// ──────────────────────────────────────────────────────
|
||||
@@ -126,6 +132,19 @@ test('default cooldown is 60 seconds', () => {
|
||||
assert.ok(content.includes('ECC_OBSERVER_ANALYSIS_COOLDOWN:-60'), 'Default cooldown should be 60 seconds');
|
||||
});
|
||||
|
||||
test('observer-loop.sh defines idle timeout fallback', () => {
|
||||
const content = fs.readFileSync(observerLoopPath, 'utf8');
|
||||
assert.ok(content.includes('IDLE_TIMEOUT_SECONDS'), 'observer-loop.sh should define an idle timeout');
|
||||
assert.ok(content.includes('ECC_OBSERVER_IDLE_TIMEOUT_SECONDS:-1800'), 'Default idle timeout should be 30 minutes');
|
||||
});
|
||||
|
||||
test('observer-loop.sh checks session lease directory before self-termination', () => {
|
||||
const content = fs.readFileSync(observerLoopPath, 'utf8');
|
||||
assert.ok(content.includes('SESSION_LEASE_DIR="${PROJECT_DIR}/.observer-sessions"'), 'observer-loop.sh should track active observer session leases');
|
||||
assert.ok(content.includes('has_active_session_leases'), 'observer-loop.sh should define active session lease checks');
|
||||
assert.ok(content.includes('exit_if_idle_without_sessions'), 'observer-loop.sh should define idle self-termination helper');
|
||||
});
|
||||
|
||||
// ──────────────────────────────────────────────────────
|
||||
// Test group 4: Tail-based sampling (no full file load)
|
||||
// ──────────────────────────────────────────────────────
|
||||
|
||||
@@ -303,6 +303,101 @@ async function runTests() {
|
||||
assert.strictEqual(result.code, 0, 'Non-blocking hook should exit 0');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (await asyncTest('session-start registers an observer lease for the active session', async () => {
|
||||
const testDir = createTestDir();
|
||||
const projectDir = path.join(testDir, 'project');
|
||||
fs.mkdirSync(projectDir, { recursive: true });
|
||||
|
||||
try {
|
||||
const sessionId = `session-${Date.now()}`;
|
||||
const result = await runHookWithInput(
|
||||
path.join(scriptsDir, 'session-start.js'),
|
||||
{},
|
||||
{
|
||||
HOME: testDir,
|
||||
CLAUDE_PROJECT_DIR: projectDir,
|
||||
CLAUDE_SESSION_ID: sessionId
|
||||
}
|
||||
);
|
||||
|
||||
assert.strictEqual(result.code, 0, 'SessionStart should exit 0');
|
||||
const homunculusDir = path.join(testDir, '.claude', 'homunculus');
|
||||
const projectsDir = path.join(homunculusDir, 'projects');
|
||||
const projectEntries = fs.existsSync(projectsDir) ? fs.readdirSync(projectsDir) : [];
|
||||
assert.ok(projectEntries.length > 0, 'SessionStart should create a homunculus project directory');
|
||||
const leaseDir = path.join(projectsDir, projectEntries[0], '.observer-sessions');
|
||||
const leaseFiles = fs.existsSync(leaseDir) ? fs.readdirSync(leaseDir).filter(name => name.endsWith('.json')) : [];
|
||||
assert.ok(leaseFiles.length === 1, `Expected one observer lease file, found ${leaseFiles.length}`);
|
||||
} finally {
|
||||
cleanupTestDir(testDir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (await asyncTest('session-end-marker removes the last lease and stops the observer process', async () => {
|
||||
const testDir = createTestDir();
|
||||
const projectDir = path.join(testDir, 'project');
|
||||
fs.mkdirSync(projectDir, { recursive: true });
|
||||
|
||||
const sessionId = `session-${Date.now()}`;
|
||||
const sleeper = spawn(process.execPath, ['-e', "process.on('SIGTERM', () => process.exit(0)); setInterval(() => {}, 1000)"], {
|
||||
stdio: 'ignore'
|
||||
});
|
||||
|
||||
try {
|
||||
await runHookWithInput(
|
||||
path.join(scriptsDir, 'session-start.js'),
|
||||
{},
|
||||
{
|
||||
HOME: testDir,
|
||||
CLAUDE_PROJECT_DIR: projectDir,
|
||||
CLAUDE_SESSION_ID: sessionId
|
||||
}
|
||||
);
|
||||
|
||||
const homunculusDir = path.join(testDir, '.claude', 'homunculus');
|
||||
const projectsDir = path.join(homunculusDir, 'projects');
|
||||
const projectEntries = fs.existsSync(projectsDir) ? fs.readdirSync(projectsDir) : [];
|
||||
assert.ok(projectEntries.length > 0, 'Expected SessionStart to create a homunculus project directory');
|
||||
const projectStorageDir = path.join(projectsDir, projectEntries[0]);
|
||||
const pidFile = path.join(projectStorageDir, '.observer.pid');
|
||||
fs.writeFileSync(pidFile, `${sleeper.pid}\n`);
|
||||
|
||||
const markerInput = { hook_event_name: 'SessionEnd' };
|
||||
const result = await runHookWithInput(
|
||||
path.join(scriptsDir, 'session-end-marker.js'),
|
||||
markerInput,
|
||||
{
|
||||
HOME: testDir,
|
||||
CLAUDE_PROJECT_DIR: projectDir,
|
||||
CLAUDE_SESSION_ID: sessionId
|
||||
}
|
||||
);
|
||||
|
||||
assert.strictEqual(result.code, 0, 'SessionEnd marker should exit 0');
|
||||
assert.strictEqual(result.stdout, JSON.stringify(markerInput), 'SessionEnd marker should pass stdin through unchanged');
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 150));
|
||||
const exited = sleeper.exitCode !== null || sleeper.signalCode !== null;
|
||||
let processAlive = !exited;
|
||||
if (processAlive) {
|
||||
try {
|
||||
process.kill(sleeper.pid, 0);
|
||||
} catch {
|
||||
processAlive = false;
|
||||
}
|
||||
}
|
||||
assert.strictEqual(processAlive, false, 'SessionEnd marker should stop the observer process when the last lease ends');
|
||||
|
||||
const leaseDir = path.join(projectStorageDir, '.observer-sessions');
|
||||
const leaseFiles = fs.existsSync(leaseDir) ? fs.readdirSync(leaseDir).filter(name => name.endsWith('.json')) : [];
|
||||
assert.strictEqual(leaseFiles.length, 0, 'SessionEnd marker should remove the finished session lease');
|
||||
assert.strictEqual(fs.existsSync(pidFile), false, 'SessionEnd marker should remove the observer pid file after stopping it');
|
||||
} finally {
|
||||
sleeper.kill();
|
||||
cleanupTestDir(testDir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (await asyncTest('dev server hook transforms yarn dev to tmux session', async () => {
|
||||
// The auto-tmux dev hook transforms dev commands (yarn dev, npm run dev, etc.)
|
||||
const hookCommand = getHookCommandByDescription(
|
||||
|
||||
Reference in New Issue
Block a user