mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-01 22:45:02 +01:00
Free-suite repairs from the first integrated Ubicloud run
Product:
- Outside-voice fences carry runtimeRootPrelude(ctx) explicitly again, so the
resolver output runs on its own (the post-render pass skips fences that
already assign GSTACK_ROOT); outsideVoiceRuntime stays deleted.
- D5: an explicit CHROMIUM_PROFILE (gbrowser's per-workspace gbd) is never
seeded from the shared profile or registered for `browse profiles prune`;
this was also the free suite's write to the real ~/.gstack/browse-profiles.json.
- gstack-ios-qa-daemon: BASH_COMPAT=50 for its >512-byte --help heredoc.
Tests migrated to the wave's deliberate contract changes:
- B1 heading admits `unverified`; sandbox flag is ${_GSTACK_CODEX_SANDBOX:?};
verdict-form output (VERDICT/FINDINGS/REASON); fake codex follows the
preflight + stdin + -o form; Q2 notice marked shown in a private state root.
- C1: an exported root needs bin/ and lib/; a lone GSTACK_BIN is not honored.
- C4: plan-ceo-review is carved on every host (carrier, discovery and
methodology fixtures read sections/ when the render has it).
- E7: READY prints the resolved Aside path.
- INV-3: the cookie-approval reconstruction reverses the bun-install mktemp template.
- F1's `Plan:` binding read gets a reasoned tracker-guard exemption.
- Browse stubs stop their server instead of scheduling process.exit; the
headless launch check only counts browsers its own launch started.
This commit is contained in:
1 parent
cebc517964
commit
091f48a6f7
17 files changed
+114
-29
No files matched your search
@@ -21,6 +21,8 @@
|
||||
# session tokens are outstanding.
|
||||
|
||||
set -euo pipefail
|
||||
# The --help heredoc is over 512 bytes; keep it off a pipe (test/heredoc-pipe-deadlock.test.ts).
|
||||
BASH_COMPAT=50
|
||||
|
||||
case "${1:-}" in
|
||||
-h|--help)
|
||||
|
||||
@@ -103,6 +103,10 @@ function writeRegistry(list: string[]): void {
|
||||
*/
|
||||
export function ensureProjectProfile(profileDir: string, log: (msg: string) => void = console.log): void {
|
||||
if (fs.existsSync(profileDir)) return;
|
||||
// An explicit CHROMIUM_PROFILE belongs to its caller (gbrowser's per-workspace
|
||||
// gbd): never seed it from the shared profile or register it for pruning.
|
||||
const explicit = process.env.CHROMIUM_PROFILE;
|
||||
if (explicit && path.resolve(explicit) === path.resolve(profileDir)) return;
|
||||
const legacy = legacyProfileDir();
|
||||
if (path.resolve(profileDir) === path.resolve(legacy)) return;
|
||||
writeRegistry([...readRegistry(), path.resolve(profileDir)]);
|
||||
|
||||
@@ -390,3 +390,29 @@ describe.skipIf(process.platform === 'win32')('Chromium profile isolation (#2817
|
||||
}, 60_000);
|
||||
}
|
||||
});
|
||||
|
||||
describe('explicit CHROMIUM_PROFILE (D5)', () => {
|
||||
test('an explicit profile is neither seeded from the shared profile nor registered for pruning', async () => {
|
||||
const { ensureProjectProfile } = await import('../src/chromium-profiles');
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'browse-explicit-profile-'));
|
||||
const saved = { CHROMIUM_PROFILE: process.env.CHROMIUM_PROFILE, GSTACK_HOME: process.env.GSTACK_HOME };
|
||||
try {
|
||||
process.env.GSTACK_HOME = path.join(root, 'state');
|
||||
fs.mkdirSync(path.join(process.env.GSTACK_HOME, 'chromium-profile'), { recursive: true });
|
||||
fs.writeFileSync(path.join(process.env.GSTACK_HOME, 'chromium-profile', 'Cookies'), 'shared logins');
|
||||
const explicit = path.join(root, 'workspace-profile');
|
||||
process.env.CHROMIUM_PROFILE = explicit;
|
||||
const logs: string[] = [];
|
||||
ensureProjectProfile(explicit, msg => logs.push(msg));
|
||||
expect(fs.existsSync(explicit)).toBe(false);
|
||||
expect(fs.existsSync(path.join(process.env.GSTACK_HOME, 'browse-profiles.json'))).toBe(false);
|
||||
expect(logs).toEqual([]);
|
||||
} finally {
|
||||
for (const [k, v] of Object.entries(saved)) {
|
||||
if (v === undefined) delete process.env[k];
|
||||
else process.env[k] = v;
|
||||
}
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -49,7 +49,7 @@ describe('CLI → daemon behind a proxy that fails every request', () => {
|
||||
fs.writeFileSync(process.env.BROWSE_STATE_FILE!, JSON.stringify({
|
||||
pid: process.pid, port: server.port, token: 'no-proxy-test', mode: 'launched', noProxy: process.env.NO_PROXY,
|
||||
}));
|
||||
setTimeout(() => process.exit(0), 5000);
|
||||
setTimeout(() => server.stop(true), 5000);
|
||||
`);
|
||||
const env: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(process.env)) {
|
||||
|
||||
@@ -92,7 +92,7 @@ describe('D0: project .env and bunfig.toml are ignored', () => {
|
||||
pid: process.pid, port: server.port, token: 'dotenv-test', mode: 'launched',
|
||||
sandbox: process.env.GSTACK_CHROMIUM_NO_SANDBOX ?? null,
|
||||
}));
|
||||
setTimeout(() => process.exit(0), 5000);
|
||||
setTimeout(() => server.stop(true), 5000);
|
||||
`);
|
||||
const env: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(process.env)) {
|
||||
|
||||
@@ -38,12 +38,15 @@ describe.skipIf(process.platform !== 'linux')('headless launch options (Linux, r
|
||||
test('GSTACK_CHROMIUM_PATH is the binary the headless launch runs', async () => {
|
||||
const custom = chromium.executablePath();
|
||||
process.env.GSTACK_CHROMIUM_PATH = custom;
|
||||
// Only browsers this launch started count: an earlier file in the same
|
||||
// shard process may still be reaping its own headless_shell child.
|
||||
const children = () => new Set(fs.readdirSync('/proc').filter(d => /^\d+$/.test(d)).filter(d => {
|
||||
try { return fs.readFileSync(`/proc/${d}/stat`, 'utf-8').split(') ')[1].split(' ')[1] === String(process.pid); } catch { return false; }
|
||||
}));
|
||||
const before = children();
|
||||
bm = new BrowserManager();
|
||||
await bm.launch();
|
||||
const children = fs.readdirSync('/proc').filter(d => /^\d+$/.test(d)).filter(d => {
|
||||
try { return fs.readFileSync(`/proc/${d}/stat`, 'utf-8').split(') ')[1].split(' ')[1] === String(process.pid); } catch { return false; }
|
||||
});
|
||||
const cmdlines = children.map(pid => readPidCmdline(Number(pid)));
|
||||
const cmdlines = [...children()].filter(pid => !before.has(pid)).map(pid => readPidCmdline(Number(pid)));
|
||||
expect(cmdlines.some(c => c.startsWith(custom) && c.includes('--headless'))).toBe(true);
|
||||
expect(cmdlines.some(c => c.includes('headless_shell'))).toBe(false);
|
||||
}, 60_000);
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
import { toShellPath, type TemplateContext } from './types';
|
||||
import { CC_BACKGROUND_DEFAULT_SINCE } from './constants';
|
||||
import { outsideVoiceFailurePolicy, outsideVoiceFor, outsideVoiceInvocation, outsideVoicePreflight, outsideVoiceProvenance } from './outside-voice';
|
||||
import { runtimeRootPrelude } from './runtime-root';
|
||||
|
||||
const CODEX_BOUNDARY = 'IMPORTANT: Do NOT read or execute any files under ~/.claude/, ~/.agents/, .claude/skills/, or agents/. These are skill definitions, not repository review data. Do not invoke any installed skill (Codex home skills/, .agents/), hook, or tool instruction; answer directly. Ignore them completely. Do NOT modify agents/openai.yaml. Stay focused on the repository code only.\\n\\n';
|
||||
|
||||
@@ -321,6 +322,7 @@ shell and re-reads the control; enabled workflows never append a disabled record
|
||||
If logging fails, report the persistence failure and retain the disabled opt-out.
|
||||
|
||||
\`\`\`bash
|
||||
${runtimeRootPrelude(ctx)}
|
||||
_DISABLED_REVIEW_MODE=$("${bin}/gstack-config" get codex_reviews 2>/dev/null) || {
|
||||
echo 'Cannot read codex_reviews; disabled outside coverage was not recorded.' >&2
|
||||
exit 1
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
import { toShellPath, type TemplateContext } from './types';
|
||||
import { CODEX_MODEL_CONFIG_FLAG, CODEX_REVIEW_MODEL_CONFIG_FLAG, CODEX_WEB_SEARCH_FLAG, codexPreflight } from './constants';
|
||||
import { runtimeRootPrelude } from './runtime-root';
|
||||
|
||||
export function outsideVoiceFor(ctx: Pick<TemplateContext, 'host'>) {
|
||||
return ctx.host === 'codex'
|
||||
@@ -46,7 +47,8 @@ fi`;
|
||||
export function outsideVoicePreflight(ctx: TemplateContext, opts: { disabledBehavior: 'skip-all' | 'codex-only' | 'opt-in'; acceptedOnly?: boolean; nativeReview?: boolean }): string {
|
||||
const v = outsideVoiceFor(ctx);
|
||||
if (v.id === 'codex' && opts.disabledBehavior !== 'opt-in') {
|
||||
let preflight = outsideVoiceLabels(ctx, codexPreflight({ disabledBehavior: opts.disabledBehavior, nativeReview: opts.nativeReview }));
|
||||
let preflight = outsideVoiceLabels(ctx, codexPreflight({ disabledBehavior: opts.disabledBehavior, nativeReview: opts.nativeReview }))
|
||||
.replace('```bash\n', `\`\`\`bash\n${runtimeRootPrelude(ctx)}\n`);
|
||||
if (['plan-eng-review', 'plan-ceo-review'].includes(ctx.skillName)) {
|
||||
preflight = preflight.replace("follow the workflow's native-review instructions below",
|
||||
'construct the prompt below, then follow **Native fallback**');
|
||||
@@ -69,6 +71,7 @@ else
|
||||
echo 'CODEX_MODE: under_current_harness'
|
||||
fi`;
|
||||
return `\`\`\`bash
|
||||
${runtimeRootPrelude(ctx)}
|
||||
${opts.acceptedOnly ? '' : `${config}
|
||||
if [ "$_OUTSIDE_CFG" = disabled ]; then
|
||||
echo 'CODEX_MODE: disabled'
|
||||
@@ -120,6 +123,7 @@ if [ "$_OUTSIDE_EXIT" -eq 0 ]; then
|
||||
bun -e 'const r=await Bun.file(process.argv[1]).json(); if(r.status!=="completed" || typeof r.result!=="string" || !r.result.trim()) process.exit(1); await Bun.write(process.argv[2],r.result)' "$_OUTSIDE_TMP/result.json" "$_OUTSIDE_TMP/text" || _OUTSIDE_EXIT=1
|
||||
fi`;
|
||||
return `${outsideVoiceGuard(ctx)}
|
||||
${runtimeRootPrelude(ctx)}
|
||||
_REPO_ROOT=$(git rev-parse --show-toplevel) || { echo 'ERROR: not in a git repo' >&2; exit 1; }
|
||||
_OUTSIDE_TMP=$(mktemp -d "\${TMPDIR:-/tmp}/gstack-outside.XXXXXXXX") || exit 1
|
||||
trap 'rm -rf "$_OUTSIDE_TMP"' EXIT
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { afterEach, describe, expect, test } from 'bun:test';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { chmodSync, cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { chmodSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join, resolve } from 'node:path';
|
||||
import { createSnapshot, prepareMethodology } from '../bin/gstack-autoplan-snapshot';
|
||||
@@ -26,7 +26,9 @@ function fixture(phase: string, layout: string) {
|
||||
const target = join(dir, skill);
|
||||
mkdirSync(target);
|
||||
cpSync(join(source, 'SKILL.md'), join(target, 'SKILL.md'));
|
||||
if (layout !== 'inlineCodex') cpSync(join(source, 'sections'), join(target, 'sections'), { recursive: true });
|
||||
// C4: Codex carves plan-ceo-review too, so an install that has sections/ copies it.
|
||||
const carved = existsSync(join(source, 'sections'));
|
||||
if (carved) cpSync(join(source, 'sections'), join(target, 'sections'), { recursive: true });
|
||||
if (layout === 'patchedClaude') {
|
||||
const result = spawnSync('bash', [join(ROOT, 'bin/gstack-patch-names'), dir, 'true'], { encoding: 'utf8', timeout: 10_000 });
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
@@ -36,7 +38,7 @@ function fixture(phase: string, layout: string) {
|
||||
const restore = join(dir, 'restore.md');
|
||||
writeFileSync(active, '## Implementation plan\nBuild the widget.\n## Review record\n');
|
||||
writeFileSync(restore, 'Restore point.\n');
|
||||
return { dir, file: join(target, 'SKILL.md'), active, restore, skill };
|
||||
return { dir, file: join(target, 'SKILL.md'), active, restore, skill, carved };
|
||||
}
|
||||
|
||||
describe('installed methodology identities', () => {
|
||||
@@ -44,7 +46,8 @@ describe('installed methodology identities', () => {
|
||||
test(`${phase}: ${layout} prepares and consumes exact installed bytes`, () => {
|
||||
const f = fixture(phase, layout);
|
||||
const bundle = prepareMethodology(phase, f.file, f.restore);
|
||||
expect(bundle.sources).toHaveLength(layout === 'inlineCodex' ? 1 : 2);
|
||||
expect(bundle.sources).toHaveLength(f.carved ? 2 : 1);
|
||||
if (layout !== 'inlineCodex') expect(f.carved).toBe(true);
|
||||
const bytes = readFileSync(bundle.methodologyPath);
|
||||
for (const source of bundle.sources) expect(bytes.subarray(source.startByte, source.endByte)).toEqual(readFileSync(source.path));
|
||||
expect(readFileSync(createSnapshot(phase, f.active, f.restore, bundle.methodologyPath).snapshotPath, 'utf8')).toBe('Build the widget.\n');
|
||||
|
||||
@@ -99,7 +99,10 @@ describe('autoplan reads installed host methodology', () => {
|
||||
const loaded = fs.readFileSync(path.resolve(path.dirname(entry), reference), 'utf8');
|
||||
expect(loaded).toBe(fs.readFileSync(generatedReview, 'utf8'));
|
||||
expect(loaded).not.toContain('STALE FOREIGN HARNESS SKILL');
|
||||
if (host.name === 'codex') expect(loaded).toContain('"outside_provider":"claude-code"');
|
||||
// C4: a carved review (every host's plan-ceo-review; Claude's four) keeps its body in sections/.
|
||||
const sectionSource = path.join(generatedRoot, reviewName, 'sections/review-sections.md');
|
||||
const carved = fs.existsSync(sectionSource);
|
||||
if (host.name === 'codex') expect(loaded + (carved ? fs.readFileSync(sectionSource, 'utf8') : '')).toContain('"outside_provider":"claude-code"');
|
||||
const phase = review === 'plan-devex-review' ? 'dx' : review.split('-')[1]!;
|
||||
const phaseBody = host.name === 'claude'
|
||||
? fs.readFileSync(path.join(generatedRoot, 'autoplan', 'sections', `${phase}-phase.md`), 'utf8')
|
||||
@@ -117,8 +120,9 @@ describe('autoplan reads installed host methodology', () => {
|
||||
expect(directive).toContain(`methodology ${phase} `);
|
||||
expect(directive).toContain('`methodologyPath` from');
|
||||
expect(directive).toContain('"<REVIEW_SKILL>"');
|
||||
}
|
||||
if (carved) {
|
||||
const relative = 'sections/review-sections.md';
|
||||
const sectionSource = path.join(generatedRoot, review, relative);
|
||||
const sectionInstalled = path.resolve(path.dirname(entry), reference, '..', relative);
|
||||
installFile(sectionSource, sectionInstalled, mode);
|
||||
const section = fs.readFileSync(sectionInstalled, 'utf8');
|
||||
|
||||
@@ -24,13 +24,19 @@ const LATER_PREAMBLE_EDITS: Array<[string, string]> = [
|
||||
['~/.claude/skills/gstack/bin/gstack-skill-start --skill "setup-browser-cookies" --model "claude"\n', '_SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start"\n[ -x "$_SS" ] || _SS=".claude/skills/gstack/bin/gstack-skill-start"\n"$_SS" --skill "setup-browser-cookies" --model "claude" --parent-pid "$PPID" \\\n || echo "SKILL_START: unavailable — stale install; run ./setup or /gstack-upgrade (preamble degraded, continue the user\'s task)"\n'],
|
||||
];
|
||||
|
||||
// The v1.91.19.0 generated-bash lint (INV-3) gave the shared Bun-install block a
|
||||
// ${TMPDIR:-/tmp} mktemp template; same line, different bytes.
|
||||
const LATER_WAVE_EDITS: Array<[string, string]> = [
|
||||
['tmpfile=$(mktemp "${TMPDIR:-/tmp}/bun-install.XXXXXX")', 'tmpfile=$(mktemp)'],
|
||||
];
|
||||
|
||||
export function approvedCookieWorkflowSource(source: string): string {
|
||||
let removedLines = 0;
|
||||
let historical = source
|
||||
.replace('sha256sum < "$tmpfile" | awk \'{print $(1)}\'', 'sha256sum "$tmpfile" | awk \'{print $1}\'')
|
||||
.replace('shasum -a 256 < "$tmpfile" | awk \'{print $(1)}\'', 'shasum -a 256 "$tmpfile" | awk \'{print $1}\'')
|
||||
.replace(LATER_BROWSER_BLOCK, block => { removedLines = block.split('\n').length - 1; return ''; });
|
||||
for (const [later, earlier] of [...LATER_BROWSER_EDITS, ...LATER_PREAMBLE_EDITS]) historical = historical.replace(later, earlier);
|
||||
for (const [later, earlier] of [...LATER_BROWSER_EDITS, ...LATER_PREAMBLE_EDITS, ...LATER_WAVE_EDITS]) historical = historical.replace(later, earlier);
|
||||
return historical
|
||||
.replace(/(--- BEGIN FILE "BROWSER\.md" \(lines \d+-)(\d+)(; section\) ---)/, (_, head, end, tail) => `${head}${Number(end) - removedLines}${tail}`)
|
||||
.replace(/(--- BEGIN FILE "setup-browser-cookies\/SKILL\.md" \(lines )(\d+)-(\d+)(; entrypoint\) ---)/, (_, head, start, end, tail) => `${head}${APPROVED_ENTRYPOINT_START}-${APPROVED_ENTRYPOINT_START + Number(end) - Number(start)}${tail}`);
|
||||
|
||||
@@ -195,7 +195,8 @@ describe('outside reviewer runtime discovery in fresh shells', () => {
|
||||
f.install(f.local);
|
||||
const explicit = f.install(path.join(f.home, 'explicit runtime'));
|
||||
expect(f.preflight({ GSTACK_ROOT: explicit }).stdout).toContain(`RESOLVED_ROOT: ${explicit}`);
|
||||
expect(f.preflight({ GSTACK_BIN: path.join(explicit, 'bin') }).stdout).toContain(`RESOLVED_ROOT: ${explicit}`);
|
||||
// C1: only an exported GSTACK_ROOT (with bin/ and lib/) is honored; a lone GSTACK_BIN falls back to the repo-local install.
|
||||
expect(f.preflight({ GSTACK_BIN: path.join(explicit, 'bin') }).stdout).toContain(`RESOLVED_ROOT: ${f.local}`);
|
||||
const result = f.preflight({ GSTACK_ROOT: '/missing/gstack', GSTACK_BIN: '/missing/gstack/bin' });
|
||||
expect(result.stdout).toContain('CODEX_MODE: ready');
|
||||
expect(result.stdout).toContain(`RESOLVED_ROOT: ${f.local}`);
|
||||
|
||||
@@ -269,9 +269,15 @@ describe('plan report persistence precedes completion logging', () => {
|
||||
try {
|
||||
const generated = await runGeneration({ host: 'all', outputRoot, contentLinkRoot: null, log: () => {} });
|
||||
expect(generated.exitCode, JSON.stringify(generated.diagnostics)).toBe(0);
|
||||
const carriers = generated.artifacts.filter(artifact => artifact.host === 'claude'
|
||||
? artifact.kind === 'section' && plans.some(skill => artifact.relativePath === `${skill}/sections/review-sections.md`)
|
||||
: artifact.kind === 'skill' && plans.some(skill => artifact.relativePath.endsWith(`/gstack-${skill}/SKILL.md`)));
|
||||
// A carved skill carries the report in sections/review-sections.md (every
|
||||
// host since C4 for plan-ceo-review); an inlined one in its SKILL.md.
|
||||
const isSection = (artifact: typeof generated.artifacts[number]) => artifact.kind === 'section'
|
||||
&& plans.some(skill => artifact.relativePath === `${skill}/sections/review-sections.md`
|
||||
|| artifact.relativePath.endsWith(`/gstack-${skill}/sections/review-sections.md`));
|
||||
const carved = new Set(generated.artifacts.filter(isSection).map(a => `${a.host}:${a.relativePath.replace(/\/sections\/review-sections\.md$/, '')}`));
|
||||
const carriers = generated.artifacts.filter(artifact => isSection(artifact)
|
||||
|| (artifact.kind === 'skill' && plans.some(skill => artifact.relativePath.endsWith(`/gstack-${skill}/SKILL.md`))
|
||||
&& !carved.has(`${artifact.host}:${artifact.relativePath.replace(/\/SKILL\.md$/, '')}`)));
|
||||
expect(carriers).toHaveLength(plans.length * ALL_HOST_CONFIGS.length);
|
||||
for (const carrier of carriers) {
|
||||
const content = readFileSync(join(outputRoot, carrier.relativePath), 'utf8');
|
||||
@@ -660,7 +666,9 @@ describe('outside-voice commitment queue', () => {
|
||||
const eng = generateCodexPlanReview({ host: host.name, paths: HOST_PATHS[host.name]!, skillName: 'plan-eng-review' } as TemplateContext);
|
||||
const provider = host.name === 'codex' ? 'Claude Code' : 'Codex';
|
||||
const mismatch = host.name === 'codex' ? 'under_current_harness' : 'under_codex';
|
||||
expect(eng).toContain(`**If \`CODEX_MODE: ready\` — run ${provider}:**`);
|
||||
// B1: the heading also admits `unverified`.
|
||||
expect(eng).toContain('**If `CODEX_MODE: ready`');
|
||||
expect(eng).toContain(`— run ${provider}:**`);
|
||||
const fallback = compactProse(eng.slice(eng.indexOf('**Native fallback —'), eng.indexOf('**Bounded outside-voice wait')));
|
||||
const routing = eng.slice(eng.indexOf('**Outcome routing:**'), eng.indexOf('**Disabled is a terminal branch'));
|
||||
const preflight = eng.match(/```bash\n([\s\S]*?)\n```/)![1];
|
||||
|
||||
@@ -32,7 +32,8 @@ describe('generated Codex plan-review shell invocation', () => {
|
||||
const rendered = generateCodexPlanReview({ ...reviewContext('claude'),
|
||||
paths: { ...HOST_PATHS.claude, binDir: path.join(ROOT, 'bin'), skillRoot: ROOT },
|
||||
});
|
||||
const ready = rendered.slice(rendered.indexOf('**If `CODEX_MODE: ready` — run Codex:**'),
|
||||
// B1: the heading also admits `unverified`; locate it structurally.
|
||||
const ready = rendered.slice(rendered.search(/\*\*If `CODEX_MODE: ready`[^\n]*— run Codex:\*\*/),
|
||||
rendered.indexOf('Present the full output verbatim:'));
|
||||
const blocks = [...ready.matchAll(/```bash\n([\s\S]*?)\n```/g)].map(match => match[1]!);
|
||||
const quote = (value: string) => "'" + value.replaceAll("'", "'\\''") + "'";
|
||||
@@ -44,6 +45,10 @@ describe('generated Codex plan-review shell invocation', () => {
|
||||
const prompt = path.join(dir, 'review-prompt.txt');
|
||||
fs.writeFileSync(prompt, 'Review the current plan without edits.');
|
||||
const created = path.join(dir, 'created');
|
||||
// Q2's one-time notice has its own tests; mark it shown in a private state root.
|
||||
const state = path.join(dir, 'state');
|
||||
fs.mkdirSync(state);
|
||||
fs.writeFileSync(path.join(state, '.codex-review-notice-shown'), '');
|
||||
const calls = path.join(dir, 'calls');
|
||||
const stale = path.join(dir, 'codex-out-foreign');
|
||||
const staleError = path.join(dir, 'codex-planreview-foreign');
|
||||
@@ -57,10 +62,15 @@ p=$(${quote(Bun.which('mktemp')!)} "$@") || exit 1
|
||||
printf '%s\\n' "$p" >> "$FAKE_CREATED"
|
||||
printf '%s\\n' "$p"
|
||||
`);
|
||||
// B1/E5: the free sandbox preflight runs first; exec reads the prompt on stdin
|
||||
// and writes its final message to -o (stdout carries --json events).
|
||||
writeBin('codex', `
|
||||
[ "$1" = sandbox ] && exit 0
|
||||
printf '%s\\n' "$FAKE_REVIEW_ID" >> "$FAKE_CALLS"
|
||||
printf '%s\\n' "$FAKE_REVIEW_ID: current findings"
|
||||
printf '%s\\n' "Recommendation: fix $FAKE_REVIEW_ID because this is the current finding."
|
||||
out=; prev=; for a in "$@"; do [ "$prev" = -o ] && out=$a; prev=$a; done
|
||||
cat > /dev/null
|
||||
printf '%s\\n' "$FAKE_REVIEW_ID: current findings" "Recommendation: fix $FAKE_REVIEW_ID because this is the current finding." > "$out"
|
||||
printf '%s\\n' '{"type":"turn.completed"}'
|
||||
printf '%s\\n' "$FAKE_REVIEW_ID: current stderr" >&2
|
||||
exit "$FAKE_CODEX_STATUS"
|
||||
`);
|
||||
@@ -76,6 +86,7 @@ exec ${quote(Bun.which('cat')!)} "$@"
|
||||
FAKE_CREATED: created, FAKE_CALLS: calls, FAKE_REVIEW_ID: id,
|
||||
FAKE_CODEX_STATUS: String(code), FAKE_MKTEMP_FAIL: mktempFailure ? '1' : '0',
|
||||
FAKE_CAT_FAIL: catFailure ? '1' : '0', TMPDIR: dir, CODEX_HOME: dir, GSTACK_CODEX_MODEL: '',
|
||||
GSTACK_HOME: state, GSTACK_STATE_ROOT: '',
|
||||
CODEX_THREAD_ID: '', CODEX_SANDBOX: '', CLAUDECODE: '1', GSTACK_ACTIVE_HOST: 'claude' };
|
||||
// Each displayed block gets a fresh shell, as separate Bash tool calls do.
|
||||
return blocks.map(block => spawnSync('bash', ['-c', (errexit ? 'set -e\n' : '') + block.replace("'<prepared-prompt-file>'", quote(prompt))], {
|
||||
@@ -90,7 +101,9 @@ exec ${quote(Bun.which('cat')!)} "$@"
|
||||
}
|
||||
|
||||
const findings = (id: string) => `${id}: current findings\nRecommendation: fix ${id} because this is the current finding.\n`;
|
||||
const completed = (id: string) => `${findings(id)}OUTSIDE_STATUS: completed provider=codex host=claude\n`;
|
||||
// INV-1: the verdict-form validator prints its verdict before the completed status.
|
||||
const unavailable = (id: string) => `${findings(id)}VERDICT: unavailable\nFINDINGS: none\nREASON: execution_failed\n`;
|
||||
const completed = (id: string) => `${findings(id)}VERDICT: clean\nFINDINGS: none\nOUTSIDE_STATUS: completed provider=codex host=claude\n`;
|
||||
|
||||
test('fresh shells retain the current stderr and clean its exact temporary file', () => {
|
||||
const f = fixture();
|
||||
@@ -108,7 +121,7 @@ exec ${quote(Bun.which('cat')!)} "$@"
|
||||
try {
|
||||
const results = f.run('failed', 23, errexit);
|
||||
expect(results.map(result => result.status)).toEqual([23]);
|
||||
expect(results[0]!.stdout).toBe(findings('failed'));
|
||||
expect(results[0]!.stdout).toBe(unavailable('failed'));
|
||||
expect(results[0]!.stderr).toContain('failed: current stderr\n');
|
||||
expect(f.created()).toHaveLength(1);
|
||||
expect(f.created().every(file => !fs.existsSync(file))).toBe(true);
|
||||
@@ -120,7 +133,7 @@ exec ${quote(Bun.which('cat')!)} "$@"
|
||||
try {
|
||||
const results = f.run('display-failed', code, errexit, false, true);
|
||||
expect(results.map(result => result.status)).toEqual([code || 1]);
|
||||
expect(results[0]!.stdout).toBe(findings('display-failed'));
|
||||
expect(results[0]!.stdout).toBe(unavailable('display-failed'));
|
||||
expect(results[0]!.stderr).toContain('cat: simulated current-file read failure\n');
|
||||
expect(f.created()).toHaveLength(1);
|
||||
expect(f.created().every(file => !fs.existsSync(file))).toBe(true);
|
||||
@@ -247,7 +260,8 @@ describe('outside-voice dispatch contract', () => {
|
||||
const apply = rendered.indexOf('**4. Apply the answered row.**');
|
||||
expect(answer).toBeGreaterThan(0);
|
||||
expect(apply).toBeGreaterThan(answer);
|
||||
expect(rendered).toContain(`-s read-only ${CODEX_MODEL_CONFIG_FLAG} -c 'model_reasoning_effort="high"'`);
|
||||
// B1: the sandbox comes from _gstack_codex_select_model (read-only unless GSTACK_CODEX_NO_SANDBOX=1).
|
||||
expect(rendered).toContain(`-s "\${_GSTACK_CODEX_SANDBOX:?}" ${CODEX_MODEL_CONFIG_FLAG} -c 'model_reasoning_effort="high"'`);
|
||||
});
|
||||
|
||||
test('the generated-carrier exception rejects missing wait, cancellation or result guards', () => {
|
||||
|
||||
@@ -29,7 +29,8 @@ function runScan(host: 'claude' | 'codex', body: string, scanner: 'real' | 'brok
|
||||
const temps = join(scratch, 'tmp');
|
||||
const systemTemps = join(scratch, 'system-tmp');
|
||||
const shims = join(scratch, 'shims');
|
||||
for (const dir of [bin, sinks, temps, systemTemps, shims]) mkdirSync(dir, { recursive: true });
|
||||
// C1: the env-var-host prelude honors an exported GSTACK_ROOT only when it has bin/ and lib/.
|
||||
for (const dir of [bin, join(runtime, 'lib'), sinks, temps, systemTemps, shims]) mkdirSync(dir, { recursive: true });
|
||||
const realMktemp = Bun.which('mktemp');
|
||||
if (!realMktemp) throw new Error('mktemp is required for the spec redaction fixture');
|
||||
writeFileSync(join(shims, 'mktemp'), `#!/usr/bin/env bash
|
||||
|
||||
@@ -41,6 +41,12 @@ const READ_PATTERNS: { name: string; re: RegExp }[] = [
|
||||
|
||||
// (file, pattern-name) exemptions with reasons. Keep every entry REASONED.
|
||||
const SCANNER_EXEMPT: { file: string; pattern: string; reason: string }[] = [
|
||||
{
|
||||
file: 'scripts/resolvers/plan-gates.ts',
|
||||
pattern: 'gh pr body read',
|
||||
reason:
|
||||
'F1 plan binding: only the first `Plan: <path>` token survives (one whitespace-free path, CR and backticks stripped); the body never reaches model context, and the path is then read as a file the parent confirms',
|
||||
},
|
||||
{
|
||||
file: 'review/greptile-triage.md',
|
||||
pattern: 'gh comment-body api read',
|
||||
|
||||
@@ -324,8 +324,9 @@ describe('workflow judge file bundle', () => {
|
||||
expect(entrypoint.content).toContain('## Scope gate');
|
||||
expect(entrypoint.content.indexOf('## Scope gate')).toBeLessThan(entrypoint.content.indexOf('### Step 0: Scope Challenge'));
|
||||
expect(entrypoint.content).toContain('## Web research runs in Aside');
|
||||
expect(entrypoint.content).toContain('echo "READY: aside');
|
||||
expect(input.text.indexOf('echo "READY: aside')).toBeLessThan(input.text.indexOf('- **Search check:**'));
|
||||
// E7: the readiness probe prints the resolved Aside path (READY: $_A).
|
||||
expect(entrypoint.content).toContain('echo "READY: $_A');
|
||||
expect(input.text.indexOf('echo "READY: $_A')).toBeLessThan(input.text.indexOf('- **Search check:**'));
|
||||
expect(entrypoint.content).not.toContain('- **Search check:**');
|
||||
expect(occurrences(input.text, '- **Search check:**')).toBe(1);
|
||||
expect(occurrences(input.text, '## Scope gate')).toBe(1);
|
||||
|
||||
Reference in new issue
Block a user