mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-28 04:25:11 +02:00
fix(hooks): preserve complete bounded passthrough payloads
Forward-port #2925 for #2924 and verify ASCII and multibyte over-limit input suppression. Supersedes the overlapping direct-entrypoint fix in #2978. Co-authored-by: jackie-cqz <2557911191@qq.com>
This commit is contained in:
@@ -26,29 +26,31 @@ const EXCLUDED_PATTERNS = [
|
||||
/__mocks__\//,
|
||||
];
|
||||
|
||||
const MAX_STDIN = 1024 * 1024; // 1MB limit
|
||||
const MAX_DIRECT_STDIN_BYTES = 16 * 1024 * 1024;
|
||||
let data = '';
|
||||
let truncated = false;
|
||||
let stdinBytes = 0;
|
||||
let oversized = false;
|
||||
process.stdin.setEncoding('utf8');
|
||||
|
||||
process.stdin.on('data', chunk => {
|
||||
if (data.length < MAX_STDIN) {
|
||||
const remaining = MAX_STDIN - data.length;
|
||||
data += chunk.substring(0, remaining);
|
||||
if (chunk.length > remaining) truncated = true;
|
||||
} else {
|
||||
truncated = true;
|
||||
if (oversized) return;
|
||||
stdinBytes += Buffer.byteLength(chunk, 'utf8');
|
||||
if (stdinBytes > MAX_DIRECT_STDIN_BYTES) {
|
||||
data = '';
|
||||
oversized = true;
|
||||
return;
|
||||
}
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
/**
|
||||
* Echo stdin back (ECC pass-through convention), then exit once the pipe has
|
||||
* flushed. Truncated stdin is never echoed: a JSON document cut mid-stream is
|
||||
* reported by the harness as a Stop hook JSON validation failure (#2090).
|
||||
* flushed. Direct/legacy entrypoints preserve complete supported payloads up
|
||||
* to 16MiB; the production runner applies its stricter bounded-input policy.
|
||||
*/
|
||||
function passThroughAndExit() {
|
||||
if (truncated) {
|
||||
log('[Hook] check-console-log: stdin exceeded 1MB; suppressing pass-through (fail-open)');
|
||||
if (oversized) {
|
||||
log('[Hook] check-console-log: direct stdin exceeded 16MiB; suppressing pass-through');
|
||||
process.exit(0);
|
||||
}
|
||||
if (!data) {
|
||||
@@ -85,6 +87,6 @@ process.stdin.on('end', () => {
|
||||
log(`[Hook] check-console-log error: ${err.message}`);
|
||||
}
|
||||
|
||||
// Always output the original data (unless truncated)
|
||||
// Always output the complete original data.
|
||||
passThroughAndExit();
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
const path = require('path');
|
||||
const { buildPreToolUseAdditionalContext } = require('./pretooluse-visible-output');
|
||||
|
||||
const MAX_STDIN = 1024 * 1024;
|
||||
const MAX_DIRECT_STDIN_BYTES = 16 * 1024 * 1024;
|
||||
|
||||
// Known ad-hoc filenames that indicate impulse/scratch files (case-sensitive, uppercase only)
|
||||
const ADHOC_FILENAMES = /^(NOTES|TODO|SCRATCH|TEMP|DRAFT|BRAINSTORM|SPIKE|DEBUG|WIP)\.(md|txt)$/;
|
||||
@@ -71,21 +71,29 @@ function run(inputOrRaw, _options = {}) {
|
||||
|
||||
/**
|
||||
* Stdin entrypoint for direct/spawnSync execution: reads the hook payload from
|
||||
* stdin (capped at MAX_STDIN), runs the policy, and writes the PreToolUse result
|
||||
* to stdout. Must only run when invoked directly, never on require(), so the
|
||||
* stdin listeners are not leaked into a parent that loads this hook in-process.
|
||||
* stdin, runs the policy, and writes the PreToolUse result to stdout. Direct
|
||||
* and legacy entrypoints preserve complete supported payloads up to 16MiB;
|
||||
* the production runner applies its stricter bounded-input policy. Must only
|
||||
* run when invoked directly so stdin listeners are not leaked into a parent.
|
||||
*/
|
||||
function main() {
|
||||
let data = '';
|
||||
let stdinBytes = 0;
|
||||
let oversized = false;
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('data', c => {
|
||||
if (data.length < MAX_STDIN) {
|
||||
const remaining = MAX_STDIN - data.length;
|
||||
data += c.substring(0, remaining);
|
||||
if (oversized) return;
|
||||
stdinBytes += Buffer.byteLength(c, 'utf8');
|
||||
if (stdinBytes > MAX_DIRECT_STDIN_BYTES) {
|
||||
data = '';
|
||||
oversized = true;
|
||||
return;
|
||||
}
|
||||
data += c;
|
||||
});
|
||||
|
||||
process.stdin.on('end', () => {
|
||||
if (oversized) return;
|
||||
const result = run(data);
|
||||
|
||||
if (result.stderr) {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
const { readFile } = require('../lib/utils');
|
||||
|
||||
const MAX_STDIN = 1024 * 1024; // 1MB limit
|
||||
const MAX_DIRECT_STDIN_BYTES = 16 * 1024 * 1024;
|
||||
function run(data) {
|
||||
const warnings = [];
|
||||
try {
|
||||
@@ -47,14 +47,24 @@ function run(data) {
|
||||
|
||||
if (require.main === module) {
|
||||
let data = '';
|
||||
let stdinBytes = 0;
|
||||
let oversized = false;
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('data', chunk => {
|
||||
if (data.length < MAX_STDIN) {
|
||||
const remaining = MAX_STDIN - data.length;
|
||||
data += chunk.substring(0, remaining);
|
||||
if (oversized) return;
|
||||
stdinBytes += Buffer.byteLength(chunk, 'utf8');
|
||||
if (stdinBytes > MAX_DIRECT_STDIN_BYTES) {
|
||||
data = '';
|
||||
oversized = true;
|
||||
return;
|
||||
}
|
||||
data += chunk;
|
||||
});
|
||||
process.stdin.on('end', () => {
|
||||
if (oversized) {
|
||||
process.exitCode = 0;
|
||||
return;
|
||||
}
|
||||
const result = run(data);
|
||||
if (result.stderr) process.stderr.write(`${result.stderr}\n`);
|
||||
process.stdout.write(result.stdout);
|
||||
|
||||
@@ -25,7 +25,7 @@ const UNSAFE_PATH_CHARS = /[&|<>^%!;`()$]/;
|
||||
|
||||
const { findProjectRoot, detectFormatter, resolveFormatterBin } = require('../lib/resolve-formatter');
|
||||
|
||||
const MAX_STDIN = 1024 * 1024; // 1MB limit
|
||||
const MAX_DIRECT_STDIN_BYTES = 16 * 1024 * 1024;
|
||||
|
||||
/**
|
||||
* Core logic — exported so run-with-flags.js can call directly
|
||||
@@ -90,19 +90,28 @@ function run(rawInput) {
|
||||
// ── stdin entry point (backwards-compatible) ────────────────────
|
||||
if (require.main === module) {
|
||||
let data = '';
|
||||
let stdinBytes = 0;
|
||||
let oversized = false;
|
||||
process.stdin.setEncoding('utf8');
|
||||
|
||||
process.stdin.on('data', chunk => {
|
||||
if (data.length < MAX_STDIN) {
|
||||
const remaining = MAX_STDIN - data.length;
|
||||
data += chunk.substring(0, remaining);
|
||||
if (oversized) return;
|
||||
stdinBytes += Buffer.byteLength(chunk, 'utf8');
|
||||
if (stdinBytes > MAX_DIRECT_STDIN_BYTES) {
|
||||
data = '';
|
||||
oversized = true;
|
||||
return;
|
||||
}
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
process.stdin.on('end', () => {
|
||||
if (oversized) {
|
||||
process.exit(0);
|
||||
return;
|
||||
}
|
||||
data = run(data);
|
||||
process.stdout.write(data);
|
||||
process.exit(0);
|
||||
process.stdout.write(data, () => process.exit(0));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -13,18 +13,28 @@ const { execFileSync } = require("child_process");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const MAX_STDIN = 1024 * 1024; // 1MB limit
|
||||
const MAX_DIRECT_STDIN_BYTES = 16 * 1024 * 1024;
|
||||
let data = "";
|
||||
let stdinBytes = 0;
|
||||
let oversized = false;
|
||||
process.stdin.setEncoding("utf8");
|
||||
|
||||
process.stdin.on("data", (chunk) => {
|
||||
if (data.length < MAX_STDIN) {
|
||||
const remaining = MAX_STDIN - data.length;
|
||||
data += chunk.substring(0, remaining);
|
||||
if (oversized) return;
|
||||
stdinBytes += Buffer.byteLength(chunk, "utf8");
|
||||
if (stdinBytes > MAX_DIRECT_STDIN_BYTES) {
|
||||
data = "";
|
||||
oversized = true;
|
||||
return;
|
||||
}
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
process.stdin.on("end", () => {
|
||||
if (oversized) {
|
||||
process.exit(0);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const input = JSON.parse(data);
|
||||
const filePath = input.tool_input?.file_path;
|
||||
@@ -32,8 +42,8 @@ process.stdin.on("end", () => {
|
||||
if (filePath && /\.(ts|tsx)$/.test(filePath)) {
|
||||
const resolvedPath = path.resolve(filePath);
|
||||
if (!fs.existsSync(resolvedPath)) {
|
||||
process.stdout.write(data);
|
||||
process.exit(0);
|
||||
process.stdout.write(data, () => process.exit(0));
|
||||
return;
|
||||
}
|
||||
// Find nearest tsconfig.json by walking up (max 20 levels to prevent infinite loop)
|
||||
let dir = path.dirname(resolvedPath);
|
||||
@@ -91,6 +101,5 @@ process.stdin.on("end", () => {
|
||||
// Invalid input — pass through
|
||||
}
|
||||
|
||||
process.stdout.write(data);
|
||||
process.exit(0);
|
||||
process.stdout.write(data, () => process.exit(0));
|
||||
});
|
||||
|
||||
+32
-37
@@ -4310,9 +4310,13 @@ async function runTests() {
|
||||
else failed++;
|
||||
|
||||
if (
|
||||
await asyncTest('source calls process.exit(0) after writing output', async () => {
|
||||
await asyncTest('source exits only after stdout finishes writing', async () => {
|
||||
const formatSource = fs.readFileSync(path.join(scriptsDir, 'post-edit-format.js'), 'utf8');
|
||||
assert.ok(formatSource.includes('process.exit(0)'), 'Should call process.exit(0) for clean termination');
|
||||
assert.match(
|
||||
formatSource,
|
||||
/process\.stdout\.write\(data,\s*\(\)\s*=>\s*process\.exit\(0\)\)/,
|
||||
'Should exit from the stdout write callback'
|
||||
);
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
@@ -4321,7 +4325,7 @@ async function runTests() {
|
||||
if (
|
||||
await asyncTest('uses process.stdout.write instead of console.log for pass-through', async () => {
|
||||
const formatSource = fs.readFileSync(path.join(scriptsDir, 'post-edit-format.js'), 'utf8');
|
||||
assert.ok(formatSource.includes('process.stdout.write(data)'), 'Should use process.stdout.write to avoid trailing newline');
|
||||
assert.ok(formatSource.includes('process.stdout.write(data,'), 'Should use process.stdout.write to avoid trailing newline');
|
||||
// Verify no console.log(data) for pass-through (console.error for warnings is OK)
|
||||
const lines = formatSource.split('\n');
|
||||
const passThrough = lines.filter(l => /console\.log\(data\)/.test(l));
|
||||
@@ -4334,9 +4338,13 @@ async function runTests() {
|
||||
console.log('\nRound 29: post-edit-typecheck.js (exit and pass-through):');
|
||||
|
||||
if (
|
||||
await asyncTest('source calls process.exit(0) after writing output', async () => {
|
||||
await asyncTest('source exits only after stdout finishes writing', async () => {
|
||||
const tcSource = fs.readFileSync(path.join(scriptsDir, 'post-edit-typecheck.js'), 'utf8');
|
||||
assert.ok(tcSource.includes('process.exit(0)'), 'Should call process.exit(0) for clean termination');
|
||||
assert.match(
|
||||
tcSource,
|
||||
/process\.stdout\.write\(data,\s*\(\)\s*=>\s*process\.exit\(0\)\)/,
|
||||
'Should exit from the stdout write callback'
|
||||
);
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
@@ -4345,7 +4353,7 @@ async function runTests() {
|
||||
if (
|
||||
await asyncTest('uses process.stdout.write instead of console.log for pass-through', async () => {
|
||||
const tcSource = fs.readFileSync(path.join(scriptsDir, 'post-edit-typecheck.js'), 'utf8');
|
||||
assert.ok(tcSource.includes('process.stdout.write(data)'), 'Should use process.stdout.write');
|
||||
assert.ok(tcSource.includes('process.stdout.write(data,'), 'Should use process.stdout.write');
|
||||
const lines = tcSource.split('\n');
|
||||
const passThrough = lines.filter(l => /console\.log\(data\)/.test(l));
|
||||
assert.strictEqual(passThrough.length, 0, 'Should not use console.log(data) for pass-through');
|
||||
@@ -5446,18 +5454,17 @@ async function runTests() {
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
console.log('\nRound 59: check-console-log.js (stdin exceeding 1MB — truncation):');
|
||||
console.log('\nRound 59: check-console-log.js (large stdin pass-through):');
|
||||
|
||||
if (
|
||||
await asyncTest('suppresses pass-through for oversized stdin (fail-open, #2090)', async () => {
|
||||
// Send 1.2MB of data — exceeds the 1MB MAX_STDIN limit. Echoing the
|
||||
// truncated string would emit a JSON document cut mid-stream, which the
|
||||
// harness reports as a Stop hook JSON validation failure.
|
||||
await asyncTest('preserves complete oversized stdin (#2924)', async () => {
|
||||
// Direct/legacy entrypoints preserve the protocol payload. Production
|
||||
// wrappers continue to enforce their own bounded-input policy.
|
||||
const payload = 'x'.repeat(1024 * 1024 + 200000);
|
||||
const result = await runScript(path.join(scriptsDir, 'check-console-log.js'), payload);
|
||||
|
||||
assert.strictEqual(result.code, 0, 'Should exit 0 even with oversized stdin');
|
||||
assert.strictEqual(result.stdout, '', 'Truncated stdin must not be echoed (empty stdout = no opinion)');
|
||||
assert.strictEqual(result.stdout, payload, 'stdout should exactly match the complete stdin payload');
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
@@ -5548,20 +5555,16 @@ async function runTests() {
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
console.log('\nRound 60: post-edit-console-warn.js (stdin exceeding 1MB — truncation):');
|
||||
console.log('\nRound 60: post-edit-console-warn.js (large stdin pass-through):');
|
||||
|
||||
if (
|
||||
await asyncTest('truncates stdin at 1MB limit and still passes through data', async () => {
|
||||
// Send 1.2MB of data — exceeds the 1MB MAX_STDIN limit
|
||||
await asyncTest('preserves complete oversized stdin', async () => {
|
||||
const payload = 'x'.repeat(1024 * 1024 + 200000);
|
||||
const result = await runScript(path.join(scriptsDir, 'post-edit-console-warn.js'), payload);
|
||||
|
||||
assert.strictEqual(result.code, 0, 'Should exit 0 even with oversized stdin');
|
||||
// Data should be truncated — stdout significantly less than input
|
||||
assert.ok(result.stdout.length < payload.length, `stdout (${result.stdout.length}) should be shorter than input (${payload.length})`);
|
||||
// Should be approximately 1MB (last accepted chunk may push slightly over)
|
||||
assert.ok(result.stdout.length <= 1024 * 1024 + 65536, `stdout (${result.stdout.length}) should be near 1MB, not unbounded`);
|
||||
assert.ok(result.stdout.length > 0, 'Should still pass through truncated data');
|
||||
assert.strictEqual(result.stdout, payload, 'stdout should exactly match the complete stdin payload');
|
||||
assert.ok(result.stdout.length > 0, 'Should pass through complete data');
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
@@ -6074,40 +6077,32 @@ Some random content without the expected ### Context to Load section
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
// ── Round 87: post-edit-format.js and post-edit-typecheck.js stdin overflow (1MB) ──
|
||||
console.log('\nRound 87: post-edit-format.js (stdin exceeding 1MB — truncation):');
|
||||
// ── Round 87: post-edit-format.js and post-edit-typecheck.js large stdin pass-through ──
|
||||
console.log('\nRound 87: post-edit-format.js (large stdin pass-through):');
|
||||
|
||||
if (
|
||||
await asyncTest('truncates stdin at 1MB limit and still passes through data (post-edit-format)', async () => {
|
||||
// Send 1.2MB of data — exceeds the 1MB MAX_STDIN limit (lines 14-22)
|
||||
await asyncTest('preserves complete oversized stdin (post-edit-format)', async () => {
|
||||
const payload = 'x'.repeat(1024 * 1024 + 200000);
|
||||
const result = await runScript(path.join(scriptsDir, 'post-edit-format.js'), payload);
|
||||
|
||||
assert.strictEqual(result.code, 0, 'Should exit 0 even with oversized stdin');
|
||||
// Output should be truncated — significantly less than input
|
||||
assert.ok(result.stdout.length < payload.length, `stdout (${result.stdout.length}) should be shorter than input (${payload.length})`);
|
||||
// Output should be approximately 1MB (last accepted chunk may push slightly over)
|
||||
assert.ok(result.stdout.length <= 1024 * 1024 + 65536, `stdout (${result.stdout.length}) should be near 1MB, not unbounded`);
|
||||
assert.ok(result.stdout.length > 0, 'Should still pass through truncated data');
|
||||
assert.strictEqual(result.stdout, payload, 'stdout should exactly match the complete stdin payload');
|
||||
assert.ok(result.stdout.length > 0, 'Should pass through complete data');
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
console.log('\nRound 87: post-edit-typecheck.js (stdin exceeding 1MB — truncation):');
|
||||
console.log('\nRound 87: post-edit-typecheck.js (large stdin pass-through):');
|
||||
|
||||
if (
|
||||
await asyncTest('truncates stdin at 1MB limit and still passes through data (post-edit-typecheck)', async () => {
|
||||
// Send 1.2MB of data — exceeds the 1MB MAX_STDIN limit (lines 16-24)
|
||||
await asyncTest('preserves complete oversized stdin (post-edit-typecheck)', async () => {
|
||||
const payload = 'x'.repeat(1024 * 1024 + 200000);
|
||||
const result = await runScript(path.join(scriptsDir, 'post-edit-typecheck.js'), payload);
|
||||
|
||||
assert.strictEqual(result.code, 0, 'Should exit 0 even with oversized stdin');
|
||||
// Output should be truncated — significantly less than input
|
||||
assert.ok(result.stdout.length < payload.length, `stdout (${result.stdout.length}) should be shorter than input (${payload.length})`);
|
||||
// Output should be approximately 1MB (last accepted chunk may push slightly over)
|
||||
assert.ok(result.stdout.length <= 1024 * 1024 + 65536, `stdout (${result.stdout.length}) should be near 1MB, not unbounded`);
|
||||
assert.ok(result.stdout.length > 0, 'Should still pass through truncated data');
|
||||
assert.strictEqual(result.stdout, payload, 'stdout should exactly match the complete stdin payload');
|
||||
assert.ok(result.stdout.length > 0, 'Should pass through complete data');
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Regression coverage for #2924.
|
||||
*
|
||||
* Legacy direct hook entrypoints that echo stdin must preserve the complete
|
||||
* hook payload. Cutting the input at an arbitrary byte/character boundary
|
||||
* produces invalid JSON, while exiting before stdout drains loses everything
|
||||
* past the platform pipe buffer.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
|
||||
const repoRoot = path.join(__dirname, '..', '..');
|
||||
const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-passthrough-'));
|
||||
const DIRECT_STDIN_LIMIT_BYTES = 16 * 1024 * 1024;
|
||||
|
||||
const PASSTHROUGH_HOOKS = [
|
||||
'scripts/hooks/check-console-log.js',
|
||||
'scripts/hooks/post-edit-typecheck.js',
|
||||
'scripts/hooks/post-edit-console-warn.js',
|
||||
'scripts/hooks/post-edit-format.js',
|
||||
'scripts/hooks/pre-write-doc-warn.js'
|
||||
];
|
||||
|
||||
const PAYLOADS = [
|
||||
['1KB payload', 'x'.repeat(1024)],
|
||||
['200KB payload', 'x'.repeat(200 * 1024)],
|
||||
['2MB payload', 'x'.repeat(2 * 1024 * 1024)],
|
||||
['5MB payload', 'x'.repeat(5 * 1024 * 1024)],
|
||||
['multibyte payload beyond 1MB', '韩'.repeat(600 * 1024)]
|
||||
];
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(` ✓ ${name}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(` ✗ ${name}`);
|
||||
console.log(` Error: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function hookPayload(padding) {
|
||||
return JSON.stringify({
|
||||
session_id: `passthrough-${process.pid}`,
|
||||
hook_event_name: 'PostToolUse',
|
||||
tool_name: 'Edit',
|
||||
tool_input: { file_path: path.join(workDir, 'fixture.txt') },
|
||||
padding
|
||||
});
|
||||
}
|
||||
|
||||
function runDirect(script, input) {
|
||||
return spawnSync(process.execPath, [path.join(repoRoot, script)], {
|
||||
input,
|
||||
encoding: 'utf8',
|
||||
cwd: workDir,
|
||||
timeout: 30000,
|
||||
maxBuffer: 32 * 1024 * 1024,
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
}
|
||||
|
||||
console.log('\nPassthrough hook large-stdin tests (#2924):');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (const script of PASSTHROUGH_HOOKS) {
|
||||
for (const [label, padding] of PAYLOADS) {
|
||||
if (
|
||||
test(`${path.basename(script)} preserves the complete ${label}`, () => {
|
||||
const input = hookPayload(padding);
|
||||
const result = runDirect(script, input);
|
||||
|
||||
assert.strictEqual(
|
||||
result.status,
|
||||
0,
|
||||
`${script}: expected exit 0, got ${result.status}: ${result.stderr}`
|
||||
);
|
||||
assert.ok(
|
||||
result.stdout === input,
|
||||
`${script}: expected ${Buffer.byteLength(input)} bytes, got ${Buffer.byteLength(result.stdout || '')}`
|
||||
);
|
||||
assert.deepStrictEqual(JSON.parse(result.stdout), JSON.parse(input));
|
||||
})
|
||||
) {
|
||||
passed += 1;
|
||||
} else {
|
||||
failed += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const oversizedInputs = [
|
||||
['ASCII', hookPayload('x'.repeat(DIRECT_STDIN_LIMIT_BYTES))],
|
||||
['multibyte', hookPayload('韩'.repeat(6 * 1024 * 1024))]
|
||||
];
|
||||
for (const [encoding, overLimitInput] of oversizedInputs) {
|
||||
for (const script of PASSTHROUGH_HOOKS) {
|
||||
if (
|
||||
test(`${path.basename(script)} suppresses ${encoding} input beyond the 16MiB direct-entrypoint limit`, () => {
|
||||
assert.ok(Buffer.byteLength(overLimitInput) > DIRECT_STDIN_LIMIT_BYTES);
|
||||
const result = runDirect(script, overLimitInput);
|
||||
|
||||
assert.strictEqual(
|
||||
result.status,
|
||||
0,
|
||||
`${script}: expected exit 0, got ${result.status}: ${result.stderr || result.error || ''}`
|
||||
);
|
||||
assert.ok(result.stdout === '', 'oversized input must not be emitted as truncated JSON');
|
||||
})
|
||||
) {
|
||||
passed += 1;
|
||||
} else {
|
||||
failed += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
test('post-edit-typecheck.js flushes the nonexistent-TypeScript-file early return', () => {
|
||||
const input = JSON.stringify({
|
||||
hook_event_name: 'PostToolUse',
|
||||
tool_name: 'Edit',
|
||||
tool_input: { file_path: path.join(workDir, 'missing.ts') },
|
||||
padding: 'x'.repeat(2 * 1024 * 1024)
|
||||
});
|
||||
const result = runDirect('scripts/hooks/post-edit-typecheck.js', input);
|
||||
|
||||
assert.strictEqual(result.status, 0, result.stderr);
|
||||
assert.ok(result.stdout === input, 'early return must wait for the complete stdout payload');
|
||||
JSON.parse(result.stdout);
|
||||
})
|
||||
) {
|
||||
passed += 1;
|
||||
} else {
|
||||
failed += 1;
|
||||
}
|
||||
|
||||
if (
|
||||
test('pre-write-doc-warn.js returns valid structured output for a large warned payload', () => {
|
||||
const input = JSON.stringify({
|
||||
hook_event_name: 'PreToolUse',
|
||||
tool_name: 'Write',
|
||||
tool_input: { file_path: 'TODO.md', content: 'x'.repeat(2 * 1024 * 1024) }
|
||||
});
|
||||
const result = runDirect('scripts/hooks/pre-write-doc-warn.js', input);
|
||||
|
||||
assert.strictEqual(result.status, 0, result.stderr);
|
||||
const output = JSON.parse(result.stdout);
|
||||
assert.ok(
|
||||
output.hookSpecificOutput.additionalContext.includes('TODO.md'),
|
||||
'large warned payload should retain the doc warning'
|
||||
);
|
||||
})
|
||||
) {
|
||||
passed += 1;
|
||||
} else {
|
||||
failed += 1;
|
||||
}
|
||||
|
||||
try {
|
||||
fs.rmSync(workDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
/* best-effort cleanup */
|
||||
}
|
||||
|
||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}\n`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
@@ -142,7 +142,6 @@ const STOP_HOOKS = [
|
||||
// Direct-invocation legacy paths that echo stdin.
|
||||
const ECHOING_STOP_HOOKS = [
|
||||
'scripts/hooks/stop-format-typecheck.js',
|
||||
'scripts/hooks/check-console-log.js',
|
||||
'scripts/hooks/cost-tracker.js',
|
||||
'scripts/hooks/desktop-notify.js'
|
||||
];
|
||||
@@ -316,6 +315,17 @@ for (const script of ECHOING_STOP_HOOKS) {
|
||||
else failed++;
|
||||
}
|
||||
|
||||
if (
|
||||
test('check-console-log invoked directly echoes a >1MB payload uncut', () => {
|
||||
const result = runDirect('scripts/hooks/check-console-log.js', oversizedPayload);
|
||||
assert.strictEqual(result.status, 0);
|
||||
assert.strictEqual(result.stdout, oversizedPayload, 'direct pass-through must preserve the complete payload');
|
||||
JSON.parse(result.stdout);
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
if (
|
||||
test('check-console-log invoked directly echoes a sub-cap >64KB payload uncut', () => {
|
||||
const result = runDirect('scripts/hooks/check-console-log.js', realisticPayload);
|
||||
|
||||
@@ -854,8 +854,8 @@ async function runTests() {
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (await asyncTest('hooks survive stdin exceeding 1MB limit', async () => {
|
||||
// The post-edit-console-warn hook reads stdin up to 1MB then passes through
|
||||
// Send > 1MB to verify truncation doesn't crash the hook
|
||||
// Direct invocation preserves the complete payload. Send >1MB to verify
|
||||
// the pass-through path remains stable under backpressure.
|
||||
const oversizedInput = JSON.stringify({
|
||||
tool_input: { file_path: '/test.js' },
|
||||
tool_output: { output: 'x'.repeat(1200000) } // ~1.2MB
|
||||
|
||||
Reference in New Issue
Block a user