mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-30 22:13:28 +08:00
456 lines
15 KiB
JavaScript
456 lines
15 KiB
JavaScript
/**
|
|
* Tests for scripts/loop-status.js
|
|
*/
|
|
|
|
const assert = require('assert');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const { execFileSync } = require('child_process');
|
|
|
|
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'loop-status.js');
|
|
const { analyzeTranscript, buildStatus, parseArgs } = require('../../scripts/loop-status');
|
|
const NOW = '2026-04-30T10:00:00.000Z';
|
|
|
|
function run(args = [], options = {}) {
|
|
const envOverrides = {
|
|
...(options.env || {}),
|
|
};
|
|
|
|
if (typeof envOverrides.HOME === 'string' && !('USERPROFILE' in envOverrides)) {
|
|
envOverrides.USERPROFILE = envOverrides.HOME;
|
|
}
|
|
|
|
if (typeof envOverrides.USERPROFILE === 'string' && !('HOME' in envOverrides)) {
|
|
envOverrides.HOME = envOverrides.USERPROFILE;
|
|
}
|
|
|
|
try {
|
|
const stdout = execFileSync('node', [SCRIPT, ...args], {
|
|
encoding: 'utf8',
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
timeout: 10000,
|
|
cwd: options.cwd || process.cwd(),
|
|
env: {
|
|
...process.env,
|
|
...envOverrides,
|
|
},
|
|
});
|
|
return { code: 0, stdout, stderr: '' };
|
|
} catch (error) {
|
|
return {
|
|
code: error.status || 1,
|
|
stdout: error.stdout || '',
|
|
stderr: error.stderr || '',
|
|
};
|
|
}
|
|
}
|
|
|
|
function createTempHome() {
|
|
return fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-loop-status-home-'));
|
|
}
|
|
|
|
function writeTranscript(homeDir, projectSlug, fileName, entries) {
|
|
const transcriptDir = path.join(homeDir, '.claude', 'projects', projectSlug);
|
|
fs.mkdirSync(transcriptDir, { recursive: true });
|
|
const transcriptPath = path.join(transcriptDir, fileName);
|
|
fs.writeFileSync(
|
|
transcriptPath,
|
|
entries.map(entry => JSON.stringify(entry)).join('\n') + '\n',
|
|
'utf8'
|
|
);
|
|
return transcriptPath;
|
|
}
|
|
|
|
function toolUse(timestamp, sessionId, id, name, input = {}) {
|
|
return {
|
|
timestamp,
|
|
sessionId,
|
|
type: 'assistant',
|
|
message: {
|
|
role: 'assistant',
|
|
content: [
|
|
{
|
|
type: 'tool_use',
|
|
id,
|
|
name,
|
|
input,
|
|
},
|
|
],
|
|
},
|
|
};
|
|
}
|
|
|
|
function toolResult(timestamp, sessionId, toolUseId, content = 'ok') {
|
|
return {
|
|
timestamp,
|
|
sessionId,
|
|
type: 'user',
|
|
message: {
|
|
role: 'user',
|
|
content: [
|
|
{
|
|
type: 'tool_result',
|
|
tool_use_id: toolUseId,
|
|
content,
|
|
},
|
|
],
|
|
},
|
|
};
|
|
}
|
|
|
|
function assistantMessage(timestamp, sessionId, text) {
|
|
return {
|
|
timestamp,
|
|
sessionId,
|
|
type: 'assistant',
|
|
message: {
|
|
role: 'assistant',
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text,
|
|
},
|
|
],
|
|
},
|
|
};
|
|
}
|
|
|
|
function parsePayload(stdout) {
|
|
return JSON.parse(stdout.trim());
|
|
}
|
|
|
|
function test(name, fn) {
|
|
try {
|
|
fn();
|
|
console.log(` ✓ ${name}`);
|
|
return true;
|
|
} catch (error) {
|
|
console.log(` ✗ ${name}`);
|
|
console.error(` ${error.message}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function runTests() {
|
|
console.log('\n=== Testing loop-status.js ===\n');
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
if (test('reports overdue ScheduleWakeup calls from Claude transcripts', () => {
|
|
const homeDir = createTempHome();
|
|
|
|
try {
|
|
const transcriptPath = writeTranscript(homeDir, '-Users-affoon-project-a', 'session-a.jsonl', [
|
|
toolUse('2026-04-30T09:00:00.000Z', 'session-a', 'toolu_wake', 'ScheduleWakeup', {
|
|
delaySeconds: 300,
|
|
reason: 'Iter 15: continue autonomous loop',
|
|
}),
|
|
]);
|
|
|
|
const result = run(['--home', homeDir, '--now', NOW, '--json']);
|
|
|
|
assert.strictEqual(result.code, 0, result.stderr);
|
|
const payload = parsePayload(result.stdout);
|
|
assert.strictEqual(payload.schemaVersion, 'ecc.loop-status.v1');
|
|
assert.strictEqual(payload.sessions.length, 1);
|
|
assert.strictEqual(payload.sessions[0].sessionId, 'session-a');
|
|
assert.strictEqual(payload.sessions[0].transcriptPath, transcriptPath);
|
|
assert.strictEqual(payload.sessions[0].state, 'attention');
|
|
assert.ok(payload.sessions[0].signals.some(signal => signal.type === 'schedule_wakeup_overdue'));
|
|
assert.strictEqual(payload.sessions[0].latestWake.dueAt, '2026-04-30T09:05:00.000Z');
|
|
} finally {
|
|
fs.rmSync(homeDir, { recursive: true, force: true });
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('analyzeTranscript applies default thresholds when called directly', () => {
|
|
const homeDir = createTempHome();
|
|
|
|
try {
|
|
const transcriptPath = writeTranscript(homeDir, '-Users-affoon-project-direct', 'session-direct.jsonl', [
|
|
toolUse('2026-04-30T09:00:00.000Z', 'session-direct', 'toolu_direct_wake', 'ScheduleWakeup', {
|
|
delaySeconds: 300,
|
|
reason: 'Direct API default threshold check',
|
|
}),
|
|
]);
|
|
|
|
const session = analyzeTranscript(transcriptPath, { now: NOW });
|
|
|
|
assert.strictEqual(session.state, 'attention');
|
|
assert.ok(session.signals.some(signal => signal.type === 'schedule_wakeup_overdue'));
|
|
} finally {
|
|
fs.rmSync(homeDir, { recursive: true, force: true });
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('reports stale Bash tool_use entries without matching tool_result', () => {
|
|
const homeDir = createTempHome();
|
|
|
|
try {
|
|
writeTranscript(homeDir, '-Users-affoon-project-b', 'session-b.jsonl', [
|
|
toolUse('2026-04-30T09:10:00.000Z', 'session-b', 'toolu_bash', 'Bash', {
|
|
command: 'pytest tests/integration/test_pipeline.py',
|
|
}),
|
|
]);
|
|
|
|
const result = run(['--home', homeDir, '--now', NOW, '--json']);
|
|
|
|
assert.strictEqual(result.code, 0, result.stderr);
|
|
const payload = parsePayload(result.stdout);
|
|
assert.strictEqual(payload.sessions[0].state, 'attention');
|
|
assert.ok(payload.sessions[0].signals.some(signal => (
|
|
signal.type === 'pending_bash_tool_result'
|
|
&& signal.toolUseId === 'toolu_bash'
|
|
&& signal.ageSeconds === 3000
|
|
)));
|
|
} finally {
|
|
fs.rmSync(homeDir, { recursive: true, force: true });
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('does not flag Bash tool_use entries that have a matching tool_result', () => {
|
|
const homeDir = createTempHome();
|
|
|
|
try {
|
|
writeTranscript(homeDir, '-Users-affoon-project-c', 'session-c.jsonl', [
|
|
toolUse('2026-04-30T09:40:00.000Z', 'session-c', 'toolu_bash_ok', 'Bash', {
|
|
command: 'npm test',
|
|
}),
|
|
toolResult('2026-04-30T09:41:00.000Z', 'session-c', 'toolu_bash_ok', 'passed'),
|
|
]);
|
|
|
|
const result = run(['--home', homeDir, '--now', NOW, '--json']);
|
|
|
|
assert.strictEqual(result.code, 0, result.stderr);
|
|
const payload = parsePayload(result.stdout);
|
|
assert.strictEqual(payload.sessions[0].state, 'ok');
|
|
assert.deepStrictEqual(payload.sessions[0].signals, []);
|
|
assert.deepStrictEqual(payload.sessions[0].pendingTools, []);
|
|
} finally {
|
|
fs.rmSync(homeDir, { recursive: true, force: true });
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('does not flag ScheduleWakeup when later assistant progress exists', () => {
|
|
const homeDir = createTempHome();
|
|
|
|
try {
|
|
writeTranscript(homeDir, '-Users-affoon-project-d', 'session-d.jsonl', [
|
|
toolUse('2026-04-30T09:00:00.000Z', 'session-d', 'toolu_wake_ok', 'ScheduleWakeup', {
|
|
delaySeconds: 300,
|
|
reason: 'Loop checkpoint',
|
|
}),
|
|
assistantMessage('2026-04-30T09:06:00.000Z', 'session-d', 'Wake fired; continuing.'),
|
|
]);
|
|
|
|
const result = run(['--home', homeDir, '--now', NOW, '--json']);
|
|
|
|
assert.strictEqual(result.code, 0, result.stderr);
|
|
const payload = parsePayload(result.stdout);
|
|
assert.strictEqual(payload.sessions[0].state, 'ok');
|
|
assert.ok(!payload.sessions[0].signals.some(signal => signal.type === 'schedule_wakeup_overdue'));
|
|
} finally {
|
|
fs.rmSync(homeDir, { recursive: true, force: true });
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('supports inspecting one transcript path directly', () => {
|
|
const homeDir = createTempHome();
|
|
|
|
try {
|
|
const transcriptPath = writeTranscript(homeDir, '-Users-affoon-project-e', 'session-e.jsonl', [
|
|
toolUse('2026-04-30T09:00:00.000Z', 'session-e', 'toolu_direct', 'Bash', {
|
|
command: 'sleep 999',
|
|
}),
|
|
]);
|
|
|
|
const result = run(['--transcript', transcriptPath, '--now', NOW, '--json']);
|
|
|
|
assert.strictEqual(result.code, 0, result.stderr);
|
|
const payload = parsePayload(result.stdout);
|
|
assert.strictEqual(payload.sessions.length, 1);
|
|
assert.strictEqual(payload.sessions[0].transcriptPath, transcriptPath);
|
|
assert.ok(payload.sessions[0].signals.some(signal => signal.type === 'pending_bash_tool_result'));
|
|
} finally {
|
|
fs.rmSync(homeDir, { recursive: true, force: true });
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('prints text output with state and recommended action', () => {
|
|
const homeDir = createTempHome();
|
|
|
|
try {
|
|
writeTranscript(homeDir, '-Users-affoon-project-f', 'session-f.jsonl', [
|
|
toolUse('2026-04-30T09:00:00.000Z', 'session-f', 'toolu_text', 'ScheduleWakeup', {
|
|
delaySeconds: 600,
|
|
reason: 'Loop checkpoint',
|
|
}),
|
|
]);
|
|
|
|
const result = run(['--home', homeDir, '--now', NOW]);
|
|
|
|
assert.strictEqual(result.code, 0, result.stderr);
|
|
assert.match(result.stdout, /session-f/);
|
|
assert.match(result.stdout, /attention/);
|
|
assert.match(result.stdout, /schedule_wakeup_overdue/);
|
|
assert.match(result.stdout, /Open the transcript or interrupt the parked session/);
|
|
} finally {
|
|
fs.rmSync(homeDir, { recursive: true, force: true });
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('continues when an explicit transcript path cannot be read', () => {
|
|
const missingTranscript = path.join(os.tmpdir(), `missing-loop-status-${Date.now()}.jsonl`);
|
|
|
|
const result = run(['--transcript', missingTranscript, '--now', NOW, '--json']);
|
|
|
|
assert.strictEqual(result.code, 0, result.stderr);
|
|
const payload = parsePayload(result.stdout);
|
|
assert.deepStrictEqual(payload.sessions, []);
|
|
assert.strictEqual(payload.errors.length, 1);
|
|
assert.strictEqual(payload.errors[0].transcriptPath, missingTranscript);
|
|
})) passed++; else failed++;
|
|
|
|
if (test('text output distinguishes explicit transcript read failures from empty discovery', () => {
|
|
const missingTranscript = path.join(os.tmpdir(), `missing-loop-status-text-${Date.now()}.jsonl`);
|
|
|
|
const result = run(['--transcript', missingTranscript, '--now', NOW]);
|
|
|
|
assert.strictEqual(result.code, 0, result.stderr);
|
|
assert.match(result.stdout, /No readable Claude transcript JSONL files were found/);
|
|
assert.match(result.stdout, /Skipped transcript errors/);
|
|
assert.ok(!result.stdout.includes('No Claude transcript JSONL files found under'));
|
|
})) passed++; else failed++;
|
|
|
|
if (test('continues when one transcript directory cannot be read', () => {
|
|
const homeDir = createTempHome();
|
|
const blockedDir = path.join(homeDir, '.claude', 'projects', '-blocked-project');
|
|
const originalReaddirSync = fs.readdirSync;
|
|
|
|
try {
|
|
writeTranscript(homeDir, '-Users-affoon-project-readable', 'session-readable.jsonl', [
|
|
toolResult('2026-04-30T09:41:00.000Z', 'session-readable', 'toolu_done', 'done'),
|
|
]);
|
|
fs.mkdirSync(blockedDir, { recursive: true });
|
|
fs.readdirSync = (dir, options) => {
|
|
if (path.resolve(dir) === path.resolve(blockedDir)) {
|
|
const error = new Error('permission denied');
|
|
error.code = 'EACCES';
|
|
throw error;
|
|
}
|
|
return originalReaddirSync(dir, options);
|
|
};
|
|
|
|
const payload = buildStatus({ home: homeDir, now: NOW });
|
|
|
|
assert.strictEqual(payload.sessions.length, 1);
|
|
assert.strictEqual(payload.sessions[0].sessionId, 'session-readable');
|
|
assert.strictEqual(payload.errors.length, 1);
|
|
assert.strictEqual(payload.errors[0].code, 'EACCES');
|
|
assert.strictEqual(payload.errors[0].transcriptPath, blockedDir);
|
|
} finally {
|
|
fs.readdirSync = originalReaddirSync;
|
|
fs.rmSync(homeDir, { recursive: true, force: true });
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('reports malformed JSONL lines as an attention signal', () => {
|
|
const homeDir = createTempHome();
|
|
|
|
try {
|
|
const transcriptDir = path.join(homeDir, '.claude', 'projects', '-Users-affoon-project-malformed');
|
|
fs.mkdirSync(transcriptDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(transcriptDir, 'session-malformed.jsonl'),
|
|
[
|
|
JSON.stringify({
|
|
timestamp: '2026-04-30T09:55:00.000Z',
|
|
sessionId: 'session-malformed',
|
|
message: { role: 'assistant', content: [{ type: 'text', text: 'partial log' }] },
|
|
}),
|
|
'{"timestamp":',
|
|
].join('\n') + '\n',
|
|
'utf8'
|
|
);
|
|
|
|
const result = run(['--home', homeDir, '--now', NOW, '--json']);
|
|
|
|
assert.strictEqual(result.code, 0, result.stderr);
|
|
const payload = parsePayload(result.stdout);
|
|
assert.strictEqual(payload.sessions[0].state, 'attention');
|
|
assert.ok(payload.sessions[0].signals.some(signal => (
|
|
signal.type === 'transcript_parse_errors'
|
|
&& signal.count === 1
|
|
)));
|
|
} finally {
|
|
fs.rmSync(homeDir, { recursive: true, force: true });
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('rejects non-integer limit values', () => {
|
|
const result = run(['--limit', '1.5']);
|
|
|
|
assert.strictEqual(result.code, 1);
|
|
assert.match(result.stderr, /--limit must be a positive integer/);
|
|
})) passed++; else failed++;
|
|
|
|
if (test('parses watch mode controls', () => {
|
|
const options = parseArgs([
|
|
'node',
|
|
'scripts/loop-status.js',
|
|
'--watch',
|
|
'--watch-count',
|
|
'2',
|
|
'--watch-interval-seconds',
|
|
'0.01',
|
|
]);
|
|
|
|
assert.strictEqual(options.watch, true);
|
|
assert.strictEqual(options.watchCount, 2);
|
|
assert.strictEqual(options.watchIntervalSeconds, 0.01);
|
|
})) passed++; else failed++;
|
|
|
|
if (test('watch mode emits repeated JSON status frames', () => {
|
|
const homeDir = createTempHome();
|
|
|
|
try {
|
|
writeTranscript(homeDir, '-Users-affoon-project-watch', 'session-watch.jsonl', [
|
|
toolUse('2026-04-30T09:00:00.000Z', 'session-watch', 'toolu_watch', 'ScheduleWakeup', {
|
|
delaySeconds: 300,
|
|
reason: 'Loop checkpoint',
|
|
}),
|
|
]);
|
|
|
|
const result = run([
|
|
'--home',
|
|
homeDir,
|
|
'--now',
|
|
NOW,
|
|
'--json',
|
|
'--watch',
|
|
'--watch-count',
|
|
'2',
|
|
'--watch-interval-seconds',
|
|
'0.01',
|
|
]);
|
|
|
|
assert.strictEqual(result.code, 0, result.stderr);
|
|
const frames = result.stdout.trim().split(/\r?\n/).map(line => JSON.parse(line));
|
|
assert.strictEqual(frames.length, 2);
|
|
assert.strictEqual(frames[0].schemaVersion, 'ecc.loop-status.v1');
|
|
assert.strictEqual(frames[1].schemaVersion, 'ecc.loop-status.v1');
|
|
assert.strictEqual(frames[0].sessions[0].sessionId, 'session-watch');
|
|
assert.strictEqual(frames[1].sessions[0].sessionId, 'session-watch');
|
|
} finally {
|
|
fs.rmSync(homeDir, { recursive: true, force: true });
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
|
process.exit(failed > 0 ? 1 : 0);
|
|
}
|
|
|
|
runTests();
|