mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-05 08:43:29 +08:00
feat: deliver v1.8.0 harness reliability and parity updates
This commit is contained in:
@@ -1183,7 +1183,7 @@ async function runTests() {
|
||||
assert.ok(hooks.hooks.PreCompact, 'Should have PreCompact hooks');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('all hook commands use node or are skill shell scripts', () => {
|
||||
if (test('all hook commands use node or approved shell wrappers', () => {
|
||||
const hooksPath = path.join(__dirname, '..', '..', 'hooks', 'hooks.json');
|
||||
const hooks = JSON.parse(fs.readFileSync(hooksPath, 'utf8'));
|
||||
|
||||
@@ -1196,9 +1196,11 @@ async function runTests() {
|
||||
/^(bash|sh)\s/.test(hook.command) ||
|
||||
hook.command.startsWith('${CLAUDE_PLUGIN_ROOT}/skills/')
|
||||
);
|
||||
const isHookShellWrapper = /^(bash|sh)\s+["']?\$\{CLAUDE_PLUGIN_ROOT\}\/scripts\/hooks\/run-with-flags-shell\.sh/.test(hook.command);
|
||||
const isSessionStartFallback = hook.command.startsWith('bash -lc') && hook.command.includes('run-with-flags.js');
|
||||
assert.ok(
|
||||
isNode || isSkillScript,
|
||||
`Hook command should start with 'node' or be a skill shell script: ${hook.command.substring(0, 80)}...`
|
||||
isNode || isSkillScript || isHookShellWrapper || isSessionStartFallback,
|
||||
`Hook command should use node or approved shell wrapper: ${hook.command.substring(0, 100)}...`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1210,7 +1212,7 @@ async function runTests() {
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('script references use CLAUDE_PLUGIN_ROOT variable', () => {
|
||||
if (test('script references use CLAUDE_PLUGIN_ROOT variable (except SessionStart fallback)', () => {
|
||||
const hooksPath = path.join(__dirname, '..', '..', 'hooks', 'hooks.json');
|
||||
const hooks = JSON.parse(fs.readFileSync(hooksPath, 'utf8'));
|
||||
|
||||
@@ -1219,7 +1221,8 @@ async function runTests() {
|
||||
for (const hook of entry.hooks) {
|
||||
if (hook.type === 'command' && hook.command.includes('scripts/hooks/')) {
|
||||
// Check for the literal string "${CLAUDE_PLUGIN_ROOT}" in the command
|
||||
const hasPluginRoot = hook.command.includes('${CLAUDE_PLUGIN_ROOT}');
|
||||
const isSessionStartFallback = hook.command.startsWith('bash -lc') && hook.command.includes('run-with-flags.js');
|
||||
const hasPluginRoot = hook.command.includes('${CLAUDE_PLUGIN_ROOT}') || isSessionStartFallback;
|
||||
assert.ok(
|
||||
hasPluginRoot,
|
||||
`Script paths should use CLAUDE_PLUGIN_ROOT: ${hook.command.substring(0, 80)}...`
|
||||
|
||||
@@ -11,6 +11,7 @@ const path = require('path');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const { spawn } = require('child_process');
|
||||
const REPO_ROOT = path.join(__dirname, '..', '..');
|
||||
|
||||
// Test helper
|
||||
function _test(name, fn) {
|
||||
@@ -90,22 +91,20 @@ function runHookWithInput(scriptPath, input = {}, env = {}, timeoutMs = 10000) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Run an inline hook command (like those in hooks.json)
|
||||
* @param {string} command - The node -e "..." command
|
||||
* Run a hook command string exactly as declared in hooks.json.
|
||||
* Supports wrapped node script commands and shell wrappers.
|
||||
* @param {string} command - Hook command from hooks.json
|
||||
* @param {object} input - Hook input object
|
||||
* @param {object} env - Environment variables
|
||||
*/
|
||||
function _runInlineHook(command, input = {}, env = {}, timeoutMs = 10000) {
|
||||
function runHookCommand(command, input = {}, env = {}, timeoutMs = 10000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Extract the code from node -e "..."
|
||||
const match = command.match(/^node -e "(.+)"$/s);
|
||||
if (!match) {
|
||||
reject(new Error('Invalid inline hook command format'));
|
||||
return;
|
||||
}
|
||||
const isWindows = process.platform === 'win32';
|
||||
const shell = isWindows ? 'cmd' : 'bash';
|
||||
const shellArgs = isWindows ? ['/d', '/s', '/c', command] : ['-lc', command];
|
||||
|
||||
const proc = spawn('node', ['-e', match[1]], {
|
||||
env: { ...process.env, ...env },
|
||||
const proc = spawn(shell, shellArgs, {
|
||||
env: { ...process.env, CLAUDE_PLUGIN_ROOT: REPO_ROOT, ...env },
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
@@ -116,9 +115,9 @@ function _runInlineHook(command, input = {}, env = {}, timeoutMs = 10000) {
|
||||
proc.stdout.on('data', data => stdout += data);
|
||||
proc.stderr.on('data', data => stderr += data);
|
||||
|
||||
// Ignore EPIPE errors (process may exit before we finish writing)
|
||||
// Ignore EPIPE/EOF errors (process may exit before we finish writing)
|
||||
proc.stdin.on('error', (err) => {
|
||||
if (err.code !== 'EPIPE') {
|
||||
if (err.code !== 'EPIPE' && err.code !== 'EOF') {
|
||||
if (timer) clearTimeout(timer);
|
||||
reject(err);
|
||||
}
|
||||
@@ -130,8 +129,8 @@ function _runInlineHook(command, input = {}, env = {}, timeoutMs = 10000) {
|
||||
proc.stdin.end();
|
||||
|
||||
timer = setTimeout(() => {
|
||||
proc.kill('SIGKILL');
|
||||
reject(new Error(`Inline hook timed out after ${timeoutMs}ms`));
|
||||
proc.kill(isWindows ? undefined : 'SIGKILL');
|
||||
reject(new Error(`Hook command timed out after ${timeoutMs}ms`));
|
||||
}, timeoutMs);
|
||||
|
||||
proc.on('close', code => {
|
||||
@@ -239,35 +238,16 @@ async function runTests() {
|
||||
if (await asyncTest('blocking hooks output BLOCKED message', async () => {
|
||||
// Test the dev server blocking hook — must send a matching command
|
||||
const blockingCommand = hooks.hooks.PreToolUse[0].hooks[0].command;
|
||||
const match = blockingCommand.match(/^node -e "(.+)"$/s);
|
||||
|
||||
const proc = spawn('node', ['-e', match[1]], {
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
let stderr = '';
|
||||
let code = null;
|
||||
proc.stderr.on('data', data => stderr += data);
|
||||
|
||||
// Send a dev server command so the hook triggers the block
|
||||
proc.stdin.write(JSON.stringify({
|
||||
const result = await runHookCommand(blockingCommand, {
|
||||
tool_input: { command: 'npm run dev' }
|
||||
}));
|
||||
proc.stdin.end();
|
||||
|
||||
await new Promise(resolve => {
|
||||
proc.on('close', (c) => {
|
||||
code = c;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
// Hook only blocks on non-Windows platforms (tmux is Unix-only)
|
||||
if (process.platform === 'win32') {
|
||||
assert.strictEqual(code, 0, 'On Windows, hook should not block (exit 0)');
|
||||
assert.strictEqual(result.code, 0, 'On Windows, hook should not block (exit 0)');
|
||||
} else {
|
||||
assert.ok(stderr.includes('BLOCKED'), 'Blocking hook should output BLOCKED');
|
||||
assert.strictEqual(code, 2, 'Blocking hook should exit with code 2');
|
||||
assert.ok(result.stderr.includes('BLOCKED'), 'Blocking hook should output BLOCKED');
|
||||
assert.strictEqual(result.code, 2, 'Blocking hook should exit with code 2');
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
@@ -284,30 +264,15 @@ async function runTests() {
|
||||
if (await asyncTest('blocking hooks exit with code 2', async () => {
|
||||
// The dev server blocker blocks when a dev server command is detected
|
||||
const blockingCommand = hooks.hooks.PreToolUse[0].hooks[0].command;
|
||||
const match = blockingCommand.match(/^node -e "(.+)"$/s);
|
||||
|
||||
const proc = spawn('node', ['-e', match[1]], {
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
let code = null;
|
||||
proc.stdin.write(JSON.stringify({
|
||||
const result = await runHookCommand(blockingCommand, {
|
||||
tool_input: { command: 'yarn dev' }
|
||||
}));
|
||||
proc.stdin.end();
|
||||
|
||||
await new Promise(resolve => {
|
||||
proc.on('close', (c) => {
|
||||
code = c;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
// Hook only blocks on non-Windows platforms (tmux is Unix-only)
|
||||
if (process.platform === 'win32') {
|
||||
assert.strictEqual(code, 0, 'On Windows, hook should not block (exit 0)');
|
||||
assert.strictEqual(result.code, 0, 'On Windows, hook should not block (exit 0)');
|
||||
} else {
|
||||
assert.strictEqual(code, 2, 'Blocking hook should exit 2');
|
||||
assert.strictEqual(result.code, 2, 'Blocking hook should exit 2');
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
@@ -391,26 +356,13 @@ async function runTests() {
|
||||
|
||||
assert.ok(prHook, 'PR hook should exist');
|
||||
|
||||
const match = prHook.hooks[0].command.match(/^node -e "(.+)"$/s);
|
||||
|
||||
const proc = spawn('node', ['-e', match[1]], {
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
let stderr = '';
|
||||
proc.stderr.on('data', data => stderr += data);
|
||||
|
||||
// Simulate gh pr create output
|
||||
proc.stdin.write(JSON.stringify({
|
||||
const result = await runHookCommand(prHook.hooks[0].command, {
|
||||
tool_input: { command: 'gh pr create --title "Test"' },
|
||||
tool_output: { output: 'Creating pull request...\nhttps://github.com/owner/repo/pull/123' }
|
||||
}));
|
||||
proc.stdin.end();
|
||||
|
||||
await new Promise(resolve => proc.on('close', resolve));
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
stderr.includes('PR created') || stderr.includes('github.com'),
|
||||
result.stderr.includes('PR created') || result.stderr.includes('github.com'),
|
||||
'Should extract and log PR URL'
|
||||
);
|
||||
})) passed++; else failed++;
|
||||
@@ -678,8 +630,18 @@ async function runTests() {
|
||||
assert.strictEqual(typeof asyncHook.hooks[0].timeout, 'number', 'Timeout should be a number');
|
||||
assert.ok(asyncHook.hooks[0].timeout > 0, 'Timeout should be positive');
|
||||
|
||||
const match = asyncHook.hooks[0].command.match(/^node -e "(.+)"$/s);
|
||||
assert.ok(match, 'Async hook command should be node -e format');
|
||||
const command = asyncHook.hooks[0].command;
|
||||
const isNodeInline = command.startsWith('node -e');
|
||||
const isNodeScript = command.startsWith('node "');
|
||||
const isShellWrapper =
|
||||
command.startsWith('bash "') ||
|
||||
command.startsWith('sh "') ||
|
||||
command.startsWith('bash -lc ') ||
|
||||
command.startsWith('sh -c ');
|
||||
assert.ok(
|
||||
isNodeInline || isNodeScript || isShellWrapper,
|
||||
`Async hook command should be runnable (node -e, node script, or shell wrapper), got: ${command.substring(0, 80)}`
|
||||
);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (await asyncTest('all hook commands in hooks.json are valid format', async () => {
|
||||
@@ -692,11 +654,16 @@ async function runTests() {
|
||||
|
||||
const isInline = hook.command.startsWith('node -e');
|
||||
const isFilePath = hook.command.startsWith('node "');
|
||||
const isShellScript = hook.command.endsWith('.sh');
|
||||
const isShellWrapper =
|
||||
hook.command.startsWith('bash "') ||
|
||||
hook.command.startsWith('sh "') ||
|
||||
hook.command.startsWith('bash -lc ') ||
|
||||
hook.command.startsWith('sh -c ');
|
||||
const isShellScriptPath = hook.command.endsWith('.sh');
|
||||
|
||||
assert.ok(
|
||||
isInline || isFilePath || isShellScript,
|
||||
`Hook command in ${hookType} should be inline (node -e), file path (node "), or shell script (.sh), got: ${hook.command.substring(0, 80)}`
|
||||
isInline || isFilePath || isShellWrapper || isShellScriptPath,
|
||||
`Hook command in ${hookType} should be node -e, node script, or shell wrapper/script, got: ${hook.command.substring(0, 80)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -583,9 +583,10 @@ src/main.ts
|
||||
assert.strictEqual(result, null, 'Uppercase letters should be rejected');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('rejects filenames with extra segments', () => {
|
||||
if (test('accepts hyphenated short IDs (extra segments)', () => {
|
||||
const result = sessionManager.parseSessionFilename('2026-02-01-abc12345-extra-session.tmp');
|
||||
assert.strictEqual(result, null, 'Extra segments should be rejected');
|
||||
assert.ok(result, 'Hyphenated short IDs should be accepted');
|
||||
assert.strictEqual(result.shortId, 'abc12345-extra');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('rejects impossible month (13)', () => {
|
||||
|
||||
@@ -21,7 +21,12 @@ const {
|
||||
buildPrompt,
|
||||
askClaude,
|
||||
isValidSessionName,
|
||||
handleClear
|
||||
handleClear,
|
||||
getSessionMetrics,
|
||||
searchSessions,
|
||||
branchSession,
|
||||
exportSession,
|
||||
compactSession
|
||||
} = require(path.join(__dirname, '..', '..', 'scripts', 'claw.js'));
|
||||
|
||||
// Test helper — matches ECC's custom test pattern
|
||||
@@ -229,6 +234,92 @@ function runTests() {
|
||||
assert.strictEqual(isValidSessionName(undefined), false);
|
||||
})) passed++; else failed++;
|
||||
|
||||
console.log('\nNanoClaw v2:');
|
||||
|
||||
if (test('getSessionMetrics returns non-zero token estimate for populated history', () => {
|
||||
const tmpDir = makeTmpDir();
|
||||
const filePath = path.join(tmpDir, 'metrics.md');
|
||||
try {
|
||||
appendTurn(filePath, 'User', 'Implement auth');
|
||||
appendTurn(filePath, 'Assistant', 'Working on it');
|
||||
const metrics = getSessionMetrics(filePath);
|
||||
assert.strictEqual(metrics.turns, 2);
|
||||
assert.ok(metrics.tokenEstimate > 0);
|
||||
} finally {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('searchSessions finds query in saved session', () => {
|
||||
const tmpDir = makeTmpDir();
|
||||
const originalHome = process.env.HOME;
|
||||
process.env.HOME = tmpDir;
|
||||
try {
|
||||
const sessionPath = path.join(tmpDir, '.claude', 'claw', 'alpha.md');
|
||||
fs.mkdirSync(path.dirname(sessionPath), { recursive: true });
|
||||
appendTurn(sessionPath, 'User', 'Need oauth migration');
|
||||
const results = searchSessions('oauth');
|
||||
assert.strictEqual(results.length, 1);
|
||||
assert.strictEqual(results[0].session, 'alpha');
|
||||
} finally {
|
||||
process.env.HOME = originalHome;
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('branchSession copies history into new branch session', () => {
|
||||
const tmpDir = makeTmpDir();
|
||||
const originalHome = process.env.HOME;
|
||||
process.env.HOME = tmpDir;
|
||||
try {
|
||||
const source = path.join(tmpDir, '.claude', 'claw', 'base.md');
|
||||
fs.mkdirSync(path.dirname(source), { recursive: true });
|
||||
appendTurn(source, 'User', 'base content');
|
||||
const result = branchSession(source, 'feature-branch');
|
||||
assert.strictEqual(result.ok, true);
|
||||
const branched = fs.readFileSync(result.path, 'utf8');
|
||||
assert.ok(branched.includes('base content'));
|
||||
} finally {
|
||||
process.env.HOME = originalHome;
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('exportSession writes JSON export', () => {
|
||||
const tmpDir = makeTmpDir();
|
||||
const filePath = path.join(tmpDir, 'export.md');
|
||||
const outPath = path.join(tmpDir, 'export.json');
|
||||
try {
|
||||
appendTurn(filePath, 'User', 'hello');
|
||||
appendTurn(filePath, 'Assistant', 'world');
|
||||
const result = exportSession(filePath, 'json', outPath);
|
||||
assert.strictEqual(result.ok, true);
|
||||
const exported = JSON.parse(fs.readFileSync(outPath, 'utf8'));
|
||||
assert.strictEqual(Array.isArray(exported.turns), true);
|
||||
assert.strictEqual(exported.turns.length, 2);
|
||||
} finally {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('compactSession reduces long histories', () => {
|
||||
const tmpDir = makeTmpDir();
|
||||
const filePath = path.join(tmpDir, 'compact.md');
|
||||
try {
|
||||
for (let i = 0; i < 30; i++) {
|
||||
appendTurn(filePath, i % 2 ? 'Assistant' : 'User', `turn-${i}`);
|
||||
}
|
||||
const changed = compactSession(filePath, 10);
|
||||
assert.strictEqual(changed, true);
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
assert.ok(content.includes('NanoClaw Compaction'));
|
||||
assert.ok(!content.includes('turn-0'));
|
||||
assert.ok(content.includes('turn-29'));
|
||||
} finally {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
// ── Summary ───────────────────────────────────────────────────────────
|
||||
|
||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||
|
||||
Reference in New Issue
Block a user