mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 14:06:50 +01:00
E5: feed every Codex prompt on stdin instead of argv
Prompts went to Codex as one argv string ('codex exec "$(cat file)"'), which
fails over 32 KB on Windows (and over 128 KB on Linux, E2BIG) and mangles
quotes. Every codex exec site (outside-voice resolver, /codex review custom
instructions, challenge, consult new and resume) now runs 'codex exec -'
with the prepared prompt file on stdin; challenge and consult write the
prompt with a quoted heredoc so it is never shell-parsed. The bash-native
timeout watchdog (stock macOS) started the command in the background, which
gives it /dev/null as stdin; it now passes the caller's stdin through.
The 'byte for byte' dispatch test drops its old argv trailing-LF exception,
the consultation and autoplan fixtures follow the stdin form, and the static
site test requires stdin at every rendered codex exec.
This commit is contained in:
1 parent
ebab87bb11
commit
e2f9e144c0
11 files changed
+77
-22
No files matched your search
@@ -335,7 +335,9 @@ _gstack_codex_timeout_wrapper() {
|
||||
# mirror timeout(1)'s exit-124 contract. The watchdog's stdout is
|
||||
# detached so an early finish never blocks a caller's $(...) capture on
|
||||
# the orphaned sleep.
|
||||
"$@" &
|
||||
# Without job control a background command's stdin is /dev/null; keep the
|
||||
# caller's stdin so prompts fed with `codex exec -` arrive (#1674).
|
||||
"$@" <&0 &
|
||||
local _cmd_pid=$!
|
||||
(
|
||||
sleep "$_duration" && trap '' TERM && kill -TERM "$_cmd_pid" 2>/dev/null || exit 0 # best-effort: the command already exited, nothing to stop
|
||||
|
||||
+2
-2
@@ -184,8 +184,8 @@ Every prompt sent to Codex MUST be prefixed with this boundary instruction:
|
||||
|
||||
This applies to Challenge mode (prompt) and Consult mode (persona prompt), and to the
|
||||
custom-instructions path of Review mode — all three use `codex exec`, which still takes
|
||||
a free-form prompt argument. It does **not** apply to the default scoped `codex review`
|
||||
call in Step 2A: that command is invoked with **no prompt argument at all** (see "Scope
|
||||
a free-form prompt (fed on stdin with `codex exec -`, so size and quoting never break it). It does **not** apply to the default scoped `codex review`
|
||||
call in Step 2A: that command is invoked with **no prompt at all** (see "Scope
|
||||
flags exclude the prompt argument" in the Review mode section), so there is nowhere to put the preamble. That
|
||||
is acceptable — `codex review --base` hands the model a pre-computed diff rather than
|
||||
turning it loose on the filesystem, so the rabbit-hole risk the boundary guards against
|
||||
|
||||
@@ -18,6 +18,7 @@ With focus (e.g., "security"):
|
||||
Review the changes on this branch against the base branch. Run `git diff origin/<base>` to see the diff. Focus specifically on SECURITY. Your job is to find every way an attacker could exploit this code. Think about injection vectors, auth bypasses, privilege escalation, data exposure, and timing attacks. Be adversarial."
|
||||
|
||||
2. Run codex exec with **JSONL output** to capture reasoning traces and tool calls.
|
||||
Replace `<prompt>` with the full prompt from step 1, verbatim and unescaped; Codex reads it on stdin.
|
||||
Use `timeout: 600000` on the Bash call (the tool's maximum) — the gate sits ABOVE the
|
||||
540s wrapper so the wrapper fires first, ends Codex, and prints its explicit stall message:
|
||||
|
||||
@@ -36,7 +37,12 @@ fi
|
||||
TMPRESP=$(mktemp "$TMP_ROOT/codex-resp-XXXXXX") || { echo "ERROR: mktemp failed in TMP_ROOT=$TMP_ROOT; not running codex without its temp file" >&2; exit 1; }
|
||||
source ~/.claude/skills/gstack/bin/gstack-codex-probe || exit 1
|
||||
_gstack_codex_select_model exec || exit 1
|
||||
_gstack_codex_timeout_wrapper 540 codex exec "<prompt>" -C "$_REPO_ROOT" -s "${_GSTACK_CODEX_SANDBOX:?}" {{CODEX_MODEL_CONFIG_FLAG}} -c 'model_reasoning_effort="high"' {{CODEX_WEB_SEARCH_FLAG}} --json -o "$TMPRESP" < /dev/null 2>"$TMPERR" | tee "$TMPRESP.events" | PYTHONUNBUFFERED=1 "$PYTHON_CMD" -u -c "
|
||||
_PROMPT_FILE=$(mktemp "$TMP_ROOT/codex-prompt-XXXXXX") || { echo "ERROR: mktemp failed in TMP_ROOT=$TMP_ROOT; not running codex without its temp file" >&2; exit 1; }
|
||||
# The prompt goes to Codex on stdin (codex exec -), verbatim: no shell quoting, no argv size limit.
|
||||
cat > "$_PROMPT_FILE" <<'CODEX_PROMPT_END'
|
||||
<prompt>
|
||||
CODEX_PROMPT_END
|
||||
_gstack_codex_timeout_wrapper 540 codex exec - -C "$_REPO_ROOT" -s "${_GSTACK_CODEX_SANDBOX:?}" {{CODEX_MODEL_CONFIG_FLAG}} -c 'model_reasoning_effort="high"' {{CODEX_WEB_SEARCH_FLAG}} --json -o "$TMPRESP" < "$_PROMPT_FILE" 2>"$TMPERR" | tee "$TMPRESP.events" | PYTHONUNBUFFERED=1 "$PYTHON_CMD" -u -c "
|
||||
import sys, json
|
||||
turn_completed_count = 0
|
||||
turn_failed = False
|
||||
@@ -94,7 +100,7 @@ if grep -qiE "auth|login|unauthorized" "$TMPERR" 2>/dev/null; then
|
||||
_gstack_codex_log_event "codex_auth_failed"
|
||||
fi
|
||||
bun ~/.claude/skills/gstack/lib/outside-review-result.ts --label 'Codex challenge' --exit "$_CODEX_EXIT" --stderr "$TMPERR" --events "$TMPRESP.events" execution "$TMPRESP"
|
||||
rm -f "$TMPRESP" "$TMPRESP.events"
|
||||
rm -f "$TMPRESP" "$TMPRESP.events" "$_PROMPT_FILE"
|
||||
```
|
||||
|
||||
`VERDICT: unavailable` means the challenge did not run (its line names why, such as a
|
||||
|
||||
@@ -60,7 +60,8 @@ For non-plan consult prompts (user typed `/codex <question>`), still prepend the
|
||||
|
||||
<user's question>"
|
||||
|
||||
4. Run codex exec with **JSONL output** to capture reasoning traces. Use
|
||||
4. Run codex exec with **JSONL output** to capture reasoning traces. Replace `<prompt>`
|
||||
with the full prompt, verbatim and unescaped; Codex reads it on stdin. Use
|
||||
`timeout: 600000` on the Bash call (the tool's maximum; for both new and resumed
|
||||
sessions) — the gate sits ABOVE the 540s wrapper so the wrapper fires first, ends Codex,
|
||||
and prints its explicit stall message:
|
||||
@@ -78,7 +79,12 @@ fi
|
||||
source ~/.claude/skills/gstack/bin/gstack-codex-probe || exit 1
|
||||
_gstack_codex_select_model exec || exit 1
|
||||
# Fix 1: wrap with timeout (gtimeout/timeout fallback chain via probe helper)
|
||||
_gstack_codex_timeout_wrapper 540 codex exec "<prompt>" -C "$_REPO_ROOT" -s "${_GSTACK_CODEX_SANDBOX:?}" {{CODEX_MODEL_CONFIG_FLAG}} -c 'model_reasoning_effort="medium"' {{CODEX_WEB_SEARCH_FLAG}} --json -o "$TMPRESP" < /dev/null 2>"$TMPERR" | tee "$TMPRESP.events" | PYTHONUNBUFFERED=1 "$PYTHON_CMD" -u -c "
|
||||
_PROMPT_FILE=$(mktemp "$TMP_ROOT/codex-prompt-XXXXXX") || { echo "ERROR: mktemp failed in TMP_ROOT=$TMP_ROOT; not running codex without its temp file" >&2; exit 1; }
|
||||
# The prompt goes to Codex on stdin (codex exec -), verbatim: no shell quoting, no argv size limit.
|
||||
cat > "$_PROMPT_FILE" <<'CODEX_PROMPT_END'
|
||||
<prompt>
|
||||
CODEX_PROMPT_END
|
||||
_gstack_codex_timeout_wrapper 540 codex exec - -C "$_REPO_ROOT" -s "${_GSTACK_CODEX_SANDBOX:?}" {{CODEX_MODEL_CONFIG_FLAG}} -c 'model_reasoning_effort="medium"' {{CODEX_WEB_SEARCH_FLAG}} --json -o "$TMPRESP" < "$_PROMPT_FILE" 2>"$TMPERR" | tee "$TMPRESP.events" | PYTHONUNBUFFERED=1 "$PYTHON_CMD" -u -c "
|
||||
import sys, json
|
||||
turn_completed_count = 0
|
||||
turn_failed = False
|
||||
@@ -134,6 +140,7 @@ elif [ "$_CODEX_EXIT" != "0" ]; then
|
||||
_gstack_codex_log_event "codex_nonzero_exit" "consult:$_CODEX_EXIT"
|
||||
fi
|
||||
bun ~/.claude/skills/gstack/lib/outside-review-result.ts --label 'Codex consult' --exit "$_CODEX_EXIT" --stderr "$TMPERR" --events "$TMPRESP.events" execution "$TMPRESP"
|
||||
rm -f "$_PROMPT_FILE"
|
||||
```
|
||||
|
||||
**Session-cost reality (#2387, measured):** every `codex exec` call — resumed
|
||||
@@ -156,7 +163,12 @@ cd "$_REPO_ROOT" || exit 1
|
||||
source ~/.claude/skills/gstack/bin/gstack-codex-probe || exit 1
|
||||
_gstack_codex_select_model exec || exit 1
|
||||
# Fix 1: wrap with timeout (gtimeout/timeout fallback chain via probe helper)
|
||||
_gstack_codex_timeout_wrapper 540 codex exec resume <session-id> "<prompt>" -c "sandbox_mode=\"${_GSTACK_CODEX_SANDBOX:?}\"" {{CODEX_MODEL_CONFIG_FLAG}} -c 'model_reasoning_effort="medium"' {{CODEX_WEB_SEARCH_FLAG}} --json -o "$TMPRESP" < /dev/null 2>"$TMPERR" | tee "$TMPRESP.events" | PYTHONUNBUFFERED=1 "$PYTHON_CMD" -u -c "
|
||||
_PROMPT_FILE=$(mktemp "$TMP_ROOT/codex-prompt-XXXXXX") || { echo "ERROR: mktemp failed in TMP_ROOT=$TMP_ROOT; not running codex without its temp file" >&2; exit 1; }
|
||||
# The prompt goes to Codex on stdin (codex exec -), verbatim: no shell quoting, no argv size limit.
|
||||
cat > "$_PROMPT_FILE" <<'CODEX_PROMPT_END'
|
||||
<prompt>
|
||||
CODEX_PROMPT_END
|
||||
_gstack_codex_timeout_wrapper 540 codex exec resume <session-id> - -c "sandbox_mode=\"${_GSTACK_CODEX_SANDBOX:?}\"" {{CODEX_MODEL_CONFIG_FLAG}} -c 'model_reasoning_effort="medium"' {{CODEX_WEB_SEARCH_FLAG}} --json -o "$TMPRESP" < "$_PROMPT_FILE" 2>"$TMPERR" | tee "$TMPRESP.events" | PYTHONUNBUFFERED=1 "$PYTHON_CMD" -u -c "
|
||||
<same python streaming parser as above, with flush=True on all print() calls>
|
||||
"
|
||||
# Fix 1: same hang detection pattern as new-session block
|
||||
@@ -173,6 +185,7 @@ elif [ "$_CODEX_EXIT" != "0" ]; then
|
||||
_gstack_codex_log_event "codex_nonzero_exit" "consult-resume:$_CODEX_EXIT"
|
||||
fi
|
||||
bun ~/.claude/skills/gstack/lib/outside-review-result.ts --label 'Codex consult' --exit "$_CODEX_EXIT" --stderr "$TMPERR" --events "$TMPRESP.events" execution "$TMPRESP"
|
||||
rm -f "$_PROMPT_FILE"
|
||||
```
|
||||
|
||||
`VERDICT: unavailable` from either block means Codex did not answer from a working
|
||||
|
||||
@@ -96,7 +96,7 @@ _PROMPT_FILE=$(mktemp "$TMP_ROOT/codex-prompt-XXXXXX") || { echo "ERROR: mktemp
|
||||
git diff "<base>...HEAD" 2>/dev/null
|
||||
printf '\nDIFF_END\n'
|
||||
} > "$_PROMPT_FILE"
|
||||
_gstack_codex_timeout_wrapper 330 codex exec -s "${_GSTACK_CODEX_SANDBOX:?}" "$(cat "$_PROMPT_FILE")" {{CODEX_MODEL_CONFIG_FLAG}} -c 'model_reasoning_effort="high"' {{CODEX_WEB_SEARCH_FLAG}} --json -o "$TMPOUT" < /dev/null >"$TMPOUT.events" 2>"$TMPERR"
|
||||
_gstack_codex_timeout_wrapper 330 codex exec - -s "${_GSTACK_CODEX_SANDBOX:?}" {{CODEX_MODEL_CONFIG_FLAG}} -c 'model_reasoning_effort="high"' {{CODEX_WEB_SEARCH_FLAG}} --json -o "$TMPOUT" < "$_PROMPT_FILE" >"$TMPOUT.events" 2>"$TMPERR"
|
||||
_CODEX_EXIT=$?
|
||||
rm -f "$_PROMPT_FILE"
|
||||
cat "$TMPOUT"
|
||||
|
||||
@@ -129,11 +129,11 @@ export function outsideVoiceCommand(ctx: TemplateContext, opts: OutsideCommandOp
|
||||
const prompt = sh(opts.promptFile ?? '<prepared-prompt-file>');
|
||||
const codex = opts.structuredBase
|
||||
? `codex review --base ${sh(opts.structuredBase)} -c "sandbox_mode=\\"\${_GSTACK_CODEX_SANDBOX:?}\\"" ${CODEX_REVIEW_MODEL_CONFIG_FLAG} -c 'model_reasoning_effort="${opts.reasoningEffort ?? 'high'}"' ${CODEX_WEB_SEARCH_FLAG} < /dev/null`
|
||||
: `codex exec "$_OUTSIDE_PROMPT" -C "$_REPO_ROOT" -s "\${_GSTACK_CODEX_SANDBOX:?}" ${CODEX_MODEL_CONFIG_FLAG} -c 'model_reasoning_effort="${opts.reasoningEffort ?? 'high'}"' ${CODEX_WEB_SEARCH_FLAG} --json -o "$_OUTSIDE_TMP/text" < /dev/null`;
|
||||
: `codex exec - -C "$_REPO_ROOT" -s "\${_GSTACK_CODEX_SANDBOX:?}" ${CODEX_MODEL_CONFIG_FLAG} -c 'model_reasoning_effort="${opts.reasoningEffort ?? 'high'}"' ${CODEX_WEB_SEARCH_FLAG} --json -o "$_OUTSIDE_TMP/text" <"$_OUTSIDE_INPUT"`;
|
||||
const invocation = v.id === 'codex'
|
||||
? `source "${bin}/gstack-codex-probe" && _gstack_codex_select_model ${opts.structuredBase ? 'review' : 'exec'} || exit 1
|
||||
_gstack_codex_sandbox_preflight >/dev/null || exit 1
|
||||
${opts.structuredBase ? '' : '_OUTSIDE_PROMPT=$(cat "$_OUTSIDE_INPUT") || exit 1\n'}_OUTSIDE_EXIT=0
|
||||
_OUTSIDE_EXIT=0
|
||||
_gstack_codex_timeout_wrapper ${Math.ceil(opts.timeoutMs / 1000)} ${codex} >"$_OUTSIDE_TMP/${opts.structuredBase ? 'text' : 'events'}" 2>"$_OUTSIDE_TMP/stderr" || _OUTSIDE_EXIT=$?
|
||||
${opts.structuredBase ? 'cat "$_OUTSIDE_TMP/text"' : 'cat "$_OUTSIDE_TMP/text" 2>/dev/null || tail -n 20 "$_OUTSIDE_TMP/events"'}`
|
||||
: `_OUTSIDE_EXIT=0
|
||||
|
||||
@@ -265,7 +265,7 @@ test.each(['assignment-only','set-config','other-config','foreign-reader','or-co
|
||||
if(kind==='changed-harness')input.command=input.command.replace('exit 78','exit 0');
|
||||
if(kind==='changed-timeout')input.command=input.command.replace('_gstack_codex_timeout_wrapper 540','_gstack_codex_timeout_wrapper 1');
|
||||
if(kind==='changed-sandbox')input.command=input.command.replace('-s "${_GSTACK_CODEX_SANDBOX:?}"','-s danger-full-access');
|
||||
if(kind==='changed-prompt')input.command=input.command.replace('codex exec "$_OUTSIDE_PROMPT"','codex exec "Different plan"');
|
||||
if(kind==='changed-prompt')input.command=input.command.replace('codex exec - ','codex exec "Different plan" ');
|
||||
if(kind==='skipped-validator')input.command=input.command.replace(/^bun .*outside-review-result.*\n/m,'');
|
||||
if(kind==='suffix')input.command+='\ntrue';
|
||||
expect(f.read().codexVoiceFired,kind).toBe(false);
|
||||
|
||||
@@ -375,6 +375,21 @@ describe('gstack-codex-probe: timeout wrapper + namespace hygiene', () => {
|
||||
}
|
||||
});
|
||||
|
||||
for (const native of [false, true]) test(`E5: ${native ? 'bash-native watchdog' : 'timeout(1)'} passes the caller's stdin to the command (#1674)`, () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-watchdog-stdin-'));
|
||||
try {
|
||||
for (const tool of native ? ['bash', 'sleep', 'cat', 'printf'] : []) {
|
||||
const resolved = spawnSync('bash', ['-c', `command -v ${tool}`], { timeout: 5000 });
|
||||
const target = resolved.stdout.toString().trim();
|
||||
if (target.startsWith('/')) fs.symlinkSync(target, path.join(dir, tool));
|
||||
}
|
||||
const r = runProbe({ snippet: `printf 'prompt on stdin' | _gstack_codex_timeout_wrapper 5 cat; echo " rc=$?"`, env: native ? { PATH: dir } : {} });
|
||||
expect(r.stdout).toBe('prompt on stdin rc=0\n');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('bash-native watchdog reports its timeout while still retiring after TERM', () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-watchdog-race-'));
|
||||
try {
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
* B1 static coverage: every rendered fence that runs `codex exec` or
|
||||
* `codex review` as a command routes the result through the shared validator
|
||||
* (lib/outside-review-result.ts) later in the same fence, and takes its
|
||||
* sandbox from `_GSTACK_CODEX_SANDBOX` (read-only unless GSTACK_CODEX_NO_SANDBOX=1).
|
||||
* sandbox from `_GSTACK_CODEX_SANDBOX` (read-only unless GSTACK_CODEX_NO_SANDBOX=1);
|
||||
* every `codex exec` reads its prompt on stdin (E5).
|
||||
* Prose mentions of the commands are ignored: only command positions count.
|
||||
*/
|
||||
import { afterAll, describe, expect, test } from 'bun:test';
|
||||
@@ -66,6 +67,13 @@ describe('every rendered Codex invocation is classified by the shared validator'
|
||||
expect(missing).toEqual([]);
|
||||
});
|
||||
|
||||
test(`${host}: E5 every codex exec reads its prompt on stdin, never argv`, () => {
|
||||
const argv = rendered[host]!.filter(site => /codex\s+exec\b/.test(site.line))
|
||||
.filter(site => !/codex\s+exec\s+(?:resume\s+\S+\s+)?-\s/.test(site.line) || /<\s*\/dev\/null/.test(site.line))
|
||||
.map(s => `${s.file}: ${s.line}`);
|
||||
expect(argv).toEqual([]);
|
||||
});
|
||||
|
||||
test(`${host}: each codex exec/review command takes its sandbox from _GSTACK_CODEX_SANDBOX`, () => {
|
||||
const literal = rendered[host]!.filter(site => !site.line.includes('${_GSTACK_CODEX_SANDBOX:?}')).map(s => `${s.file}: ${s.line}`);
|
||||
expect(literal).toEqual([]);
|
||||
|
||||
@@ -2194,16 +2194,17 @@ describe('DESIGN_OUTSIDE_VOICES resolver', () => {
|
||||
const product = `A product with $(touch ${marker}), \`touch ${marker}\`, and "quotes".\nUsers: builders.\n`;
|
||||
fs.writeFileSync(brief, product);
|
||||
// Fake codex: the free sandbox preflight succeeds; exec writes its final message to -o.
|
||||
fs.writeFileSync(path.join(dir, 'codex'), `#!${process.execPath}\nconst fs = require('fs'); const a = process.argv.slice(2); if (a[0] === 'sandbox') process.exit(0);\nfs.writeFileSync(process.env.CAPTURE, JSON.stringify(a));\nconst msg = 'Recommendation: choose a clear hierarchy because builders need to find their work.';\nif (a.includes('-o')) fs.writeFileSync(a[a.indexOf('-o') + 1], msg);\nconsole.log(msg);\n`, { mode: 0o700 });
|
||||
fs.writeFileSync(path.join(dir, 'codex'), `#!${process.execPath}\nconst fs = require('fs'); const a = process.argv.slice(2); if (a[0] === 'sandbox') process.exit(0);\nfs.writeFileSync(process.env.CAPTURE, JSON.stringify({ args: a, stdin: a[1] === '-' ? fs.readFileSync(0, 'utf8') : '' }));\nconst msg = 'Recommendation: choose a clear hierarchy because builders need to find their work.';\nif (a.includes('-o')) fs.writeFileSync(a[a.indexOf('-o') + 1], msg);\nconsole.log(msg);\n`, { mode: 0o700 });
|
||||
const prepare = (file: string) => command.replace("'<prepared-prompt-file>'", quote(file))
|
||||
.replaceAll('$HOME/.claude/skills/gstack', ROOT);
|
||||
const env = { ...process.env, PATH: dir + path.delimiter + process.env.PATH, CAPTURE: capture,
|
||||
CODEX_THREAD_ID: '', CODEX_SANDBOX: '', CLAUDECODE: '1', GSTACK_ACTIVE_HOST: 'claude' };
|
||||
const result = spawnSync('bash', ['-c', prepare(brief)], { cwd: ROOT, env, encoding: 'utf8', timeout: 5_000 });
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
const args = JSON.parse(fs.readFileSync(capture, 'utf8'));
|
||||
const { args, stdin } = JSON.parse(fs.readFileSync(capture, 'utf8'));
|
||||
expect(args[0]).toBe('exec');
|
||||
expect(args[1]).toBe(product.trimEnd());
|
||||
expect(args[1]).toBe('-');
|
||||
expect(stdin).toBe(product);
|
||||
expect(args).toContain('read-only');
|
||||
expect(fs.readFileSync(brief, 'utf8')).toBe(product);
|
||||
expect(fs.existsSync(marker)).toBe(false);
|
||||
|
||||
@@ -27,7 +27,7 @@ const args = process.argv.slice(2);
|
||||
// The free sandbox preflight (\`codex sandbox ... true\`) succeeds unless a test plants its failure.
|
||||
if (args[0] === 'sandbox') { if (process.env.FAKE_SANDBOX_STDERR) { console.error(process.env.FAKE_SANDBOX_STDERR); process.exit(1); } process.exit(0); }
|
||||
const claude = process.env.FAKE_PROVIDER === 'claude-code';
|
||||
const prompt = claude ? await Bun.stdin.text() : args[0] === 'exec' ? args[1] : '';
|
||||
const prompt = claude || (args[0] === 'exec' && args[1] === '-') ? await Bun.stdin.text() : args[0] === 'exec' ? args[1] : '';
|
||||
writeFileSync(process.env.CAPTURE!, JSON.stringify({args,prompt,cwd:process.cwd()}));
|
||||
if (process.env.FAKE_MODE === 'timeout') {
|
||||
if (!claude) console.log('Partial finding before timeout');
|
||||
@@ -104,10 +104,9 @@ describe('generated outside-review dispatch', () => {
|
||||
test(`${host} dispatches the other CLI and keeps hostile prompt/path text literal`, () => {
|
||||
const result = invoke(host);
|
||||
expect(result.status).toBe(0);
|
||||
// Codex keeps its existing command-substitution argv contract, which
|
||||
// removes trailing LF; shell metacharacters within that value stay data.
|
||||
const expectedPrompt = host === 'codex' ? PROMPT_TEXT : PROMPT_TEXT.replace(/\n+$/,'');
|
||||
expect(capture().prompt).toBe(expectedPrompt);
|
||||
// Both CLIs read the prepared prompt on stdin, byte for byte (E5: no argv
|
||||
// size limit, no quoting); shell metacharacters stay data.
|
||||
expect(capture().prompt).toBe(PROMPT_TEXT);
|
||||
expect(capture().cwd).toBe(DIR);
|
||||
expect(fs.existsSync(path.join(DIR,'NEVER'))).toBe(false);
|
||||
expect(result.stdout).toContain(`OUTSIDE_STATUS: completed provider=${host === 'codex' ? 'claude-code' : 'codex'} host=${host}`);
|
||||
@@ -116,7 +115,7 @@ describe('generated outside-review dispatch', () => {
|
||||
expect(capture().args).not.toContain('--resume');
|
||||
expect(result.stdout).toContain('model-a');
|
||||
expect(result.stdout).toContain('model-b');
|
||||
} else expect(capture().args.slice(0,2)).toEqual(['exec',expectedPrompt]);
|
||||
} else expect(capture().args.slice(0,2)).toEqual(['exec','-']);
|
||||
});
|
||||
|
||||
test(`${host} rejects its own reviewer markers before any process starts`, () => {
|
||||
@@ -268,6 +267,17 @@ describe('generated outside-review dispatch', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('E5: a prompt larger than one argv string reaches Codex intact on stdin', () => {
|
||||
const big = path.join(TMP, 'big-prompt.txt');
|
||||
const text = `${'x'.repeat(200_000)}\n"quotes" 'single' $(touch NEVER) \\ end\n`;
|
||||
fs.writeFileSync(big, text);
|
||||
const ctx: TemplateContext = { skillName: 'review', tmplPath: 'review/SKILL.md.tmpl', host: 'claude', paths: HOST_PATHS.codex };
|
||||
const result = spawnSync('bash', ['-c', outsideVoiceCommand(ctx, { promptFile: big, timeoutMs: 5000 })], { cwd: DIR, env: environment('claude'), encoding: 'utf8', timeout: 15000 });
|
||||
expect(result.status).toBe(0);
|
||||
expect(capture().prompt).toBe(text);
|
||||
expect(fs.existsSync(path.join(DIR, 'NEVER'))).toBe(false);
|
||||
});
|
||||
|
||||
test('malformed Claude JSON cannot reach completion evaluation', () => {
|
||||
const result = invoke('codex',{}, {FAKE_MODE:'malformed'});
|
||||
expect(result.status).toBe(1);
|
||||
|
||||
Reference in new issue
Block a user