diff --git a/browse/test/gstack-config.test.ts b/browse/test/gstack-config.test.ts index a00af6096..958ce88dc 100644 --- a/browse/test/gstack-config.test.ts +++ b/browse/test/gstack-config.test.ts @@ -151,6 +151,29 @@ describe('gstack-config', () => { expect(content).toContain('skip_eng_review:'); }); + // ─── codex_reviews (paid-calls switch: reject-on-set, preserve existing) ── + test('codex_reviews defaults to enabled', () => { + const { exitCode, stdout } = run(['get', 'codex_reviews']); + expect(exitCode).toBe(0); + expect(stdout).toBe('enabled'); + }); + + test('codex_reviews accepts enabled and disabled', () => { + expect(run(['set', 'codex_reviews', 'disabled']).exitCode).toBe(0); + expect(run(['get', 'codex_reviews']).stdout).toBe('disabled'); + expect(run(['set', 'codex_reviews', 'enabled']).exitCode).toBe(0); + expect(run(['get', 'codex_reviews']).stdout).toBe('enabled'); + }); + + test('codex_reviews rejects an invalid value and preserves the existing one', () => { + run(['set', 'codex_reviews', 'disabled']); + const { exitCode, stderr } = run(['set', 'codex_reviews', 'disabledd']); + expect(exitCode).not.toBe(0); // rejected, not warn-and-default + expect(stderr).toContain('not recognized'); + // existing value must be untouched — a typo never silently flips paid Codex on/off + expect(run(['get', 'codex_reviews']).stdout).toBe('disabled'); + }); + test('header written only once, not duplicated on second set', () => { run(['set', 'foo', 'bar']); run(['set', 'baz', 'qux']); diff --git a/test/skill-validation.test.ts b/test/skill-validation.test.ts index fb1ec5bf4..e7def7dfa 100644 --- a/test/skill-validation.test.ts +++ b/test/skill-validation.test.ts @@ -1386,15 +1386,16 @@ describe('Codex skill', () => { expect(content).toContain('Adversarial review (always-on)'); // Always-on: both Claude and Codex adversarial expect(content).toContain('Claude adversarial subagent (always runs)'); - expect(content).toContain('Codex adversarial challenge (always runs when available)'); + expect(content).toContain('Codex adversarial challenge (runs whenever'); // Claude adversarial subagent dispatch expect(content).toContain('Agent tool'); expect(content).toContain('FIXABLE'); expect(content).toContain('INVESTIGATE'); - // Codex availability check - expect(content).toContain('CODEX_NOT_AVAILABLE'); - // OLD_CFG only gates Codex, not Claude - expect(content).toContain('skip Codex passes only'); + // Probe-based availability via the shared codexPreflight() (install + auth) + expect(content).toContain('CODEX_MODE'); + expect(content).toContain('command -v codex'); // install check kept literal + // codex_reviews=disabled gates Codex passes only; Claude adversarial still runs + expect(content).toContain('skip the Codex passes ONLY'); // Review log expect(content).toContain('adversarial-review'); expect(content).toContain('reasoning_effort="high"'); @@ -1449,6 +1450,43 @@ describe('Codex skill', () => { expect(content).toContain('codex exec'); }); + // D5 regression guard: the Codex outside voice is default-on, not opt-in. A future + // gen-skill-docs change must not silently reintroduce the "Want an outside voice?" + // AskUserQuestion. The CODEX_PLAN_REVIEW content renders into each skill's + // sections/review-sections.md (the skeleton points at it). plan-design-review uses + // DESIGN_OUTSIDE_VOICES, not CODEX_PLAN_REVIEW, so it is excluded here. + test('plan reviews run the Codex outside voice default-on (no opt-in question)', () => { + for (const skill of ['plan-eng-review', 'plan-ceo-review', 'plan-devex-review']) { + const content = fs.readFileSync( + path.join(ROOT, skill, 'sections', 'review-sections.md'), 'utf-8'); + expect(content).not.toContain('Want an outside voice'); + expect(content).toContain('Outside Voice — Independent Plan Challenge (default-on)'); + expect(content).toContain('CODEX_MODE'); + expect(content).toContain('command -v codex'); // preflight install check (e2e relies on it) + } + }); + + test('/document-release includes the default-on Codex documentation review', () => { + // The doc-review renders into the carved release-body section (kept out of the + // always-loaded skeleton to respect the skeleton-byte budget). + const content = fs.readFileSync( + path.join(ROOT, 'document-release', 'sections', 'release-body.md'), 'utf-8'); + expect(content).toContain('Codex Documentation Review (default-on)'); + expect(content).toContain('CODEX_MODE'); + expect(content).toContain('codex-doc-review'); + }); + + test('codex-host document-release does NOT contain the Codex doc review', () => { + // .agents/ is gitignored — generate on demand (codex never invokes itself) + Bun.spawnSync(['bun', 'run', 'scripts/gen-skill-docs.ts', '--host', 'codex'], { + cwd: ROOT, stdout: 'pipe', stderr: 'pipe', + }); + const content = fs.readFileSync( + path.join(ROOT, '.agents', 'skills', 'gstack-document-release', 'SKILL.md'), 'utf-8'); + expect(content).not.toContain('Codex Documentation Review'); + expect(content).not.toContain('codex-doc-review'); + }); + test('codex review invocations avoid the prompt plus --base argument shape', () => { for (const rel of ['codex/SKILL.md', 'review/SKILL.md', 'ship/SKILL.md']) { // ship's codex command moved into sections/adversarial.md (T9 carve).