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:
djpjronline-netizen 2026-07-28 21:32:42 -04:00 committed by GitHub
parent 6be87a56ae
commit 837acaf20b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 517 additions and 53 deletions

View file

@ -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++;