Files
gstack/test/outside-voice-routing.test.ts
Garry Tanandcapy-ai[bot] 5d647455f2 v1.91.79.0 feat(eval-plan): /eval-plan tier 1, lane-check, evidence verify, coordinator contract, owner brief, contributor-PR mode, banned terms (multi-agent wave PR E) (#3123)
* v1.91.79.0 feat(eval-plan): /eval-plan tier 1, lane-check, evidence verify, coordinator contract, owner brief, contributor-PR mode, banned terms (multi-agent wave PR E)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(spec,review): fail closed when the coordinator contract cannot be read; restore the omitted-Army sentence

The spec --execute spawn captures `gstack-autoplan contract` before piping
the archive, so a lane never starts without the contract; the C9 fixture
installs its gstack-like checkout (bin, lib, a contract stub) at every
host's global root. Review Step 5.8 keeps the pinned
'without claiming specialist coverage' wording.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-10-11 03:10:06 +00:00

150 lines
7.7 KiB
TypeScript

/** Outside reviews follow harness identity, including lazily loaded workflows. */
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
import { mkdtempSync, readFileSync, readdirSync, existsSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { ALL_HOST_CONFIGS } from '../hosts';
import { ALL_MODEL_NAMES } from '../scripts/models';
import { HOST_PATHS, type TemplateContext } from '../scripts/resolvers/types';
import { outsideVoiceFor, outsideVoiceCommand } from '../scripts/resolvers/outside-voice';
import { RESOLVERS } from '../scripts/resolvers';
import { installOutsideReviewFixture } from './helpers/outside-voice-fixture';
const ROOT = resolve(import.meta.dir, '..');
let output: string;
function skillDir(host: typeof ALL_HOST_CONFIGS[number], skill: string): string {
return host.name === 'claude'
? join(output, skill === 'gstack' ? '' : skill)
: join(output, host.hostSubdir, 'skills', skill === 'gstack' ? 'gstack' : `gstack-${skill}`);
}
function readUnion(dir: string): string {
const sectionDir = join(dir, 'sections');
return readFileSync(join(dir, 'SKILL.md'), 'utf8')
+ (existsSync(sectionDir) ? readdirSync(sectionDir).sort()
.filter(file => file.endsWith('.md'))
.map(file => '\n' + readFileSync(join(sectionDir, file), 'utf8')).join('') : '');
}
beforeAll(() => {
output = mkdtempSync(join(tmpdir(), 'gstack-outside-routing-'));
const generated = Bun.spawnSync(['bun', 'run', 'scripts/gen-skill-docs.ts', '--host', 'all', '--out-dir', output], {
cwd: ROOT, stdout: 'pipe', stderr: 'pipe', timeout: 120_000,
});
if (generated.exitCode !== 0) throw new Error(generated.stderr.toString());
});
afterAll(() => { if (output) rmSync(output, { recursive: true, force: true }); });
describe('outside-review host and model matrix', () => {
for (const host of ALL_HOST_CONFIGS) {
test(`${host.name}: only its own wrapper is excluded`, () => {
expect(existsSync(join(skillDir(host, 'codex'), 'SKILL.md'))).toBe(host.name !== 'codex');
expect(existsSync(join(skillDir(host, 'claude-code'), 'SKILL.md'))).toBe(host.name !== 'claude');
expect(existsSync(join(skillDir(host, 'claude'), 'SKILL.md'))).toBe(false);
for (const wrapper of ['codex', 'claude-code']) {
if (!existsSync(join(skillDir(host, wrapper), 'SKILL.md'))) continue;
const text = readUnion(skillDir(host, wrapper));
// Keep all three modes reachable, including carved section files.
expect(text).toMatch(/\breview\b/i);
expect(text).toMatch(/\bchallenge\b/i);
expect(text).toMatch(/\bconsult\b/i);
}
});
test(`${host.name}: every model overlay retains the same provider and routing`, () => {
const providers = ALL_MODEL_NAMES.map(model => {
const ctx: TemplateContext = { host: host.name, model, skillName: 'review', tmplPath: 'review/SKILL.md.tmpl', paths: HOST_PATHS[host.name] };
const voice = outsideVoiceFor(ctx);
expect(voice.id).toBe(host.name === 'codex' ? 'claude-code' : 'codex');
expect(voice.skillName).toBe(host.name === 'codex' ? 'claude-code' : 'codex');
const command = outsideVoiceCommand(ctx, { promptFile: '"$PROMPT_FILE"', timeoutMs: 120_000 });
expect(command).toContain(host.name === 'codex' ? 'gstack-claude-code' : 'codex');
if (host.name === 'codex') expect(command).not.toMatch(/\bcodex\s+(?:exec|review)\s/);
return RESOLVERS.OUTSIDE_VOICE_ROUTING(ctx);
});
expect(new Set(providers).size).toBe(1);
});
test(`${host.name}: generic and explicit second-opinion routing is installed`, () => {
const router = readUnion(skillDir(host, 'gstack'));
expect(router).toMatch(/second opinion/i);
expect(router).toContain(host.name === 'codex' ? 'claude-code' : 'codex');
expect(router).toMatch(/explicit/i);
expect(router).toMatch(/(?:do not|never|without).*?(?:substitut|switch|replac)|(?:keep|honor|respect).*?(?:provider|request)/i);
});
}
});
describe('generated automatic review coverage', () => {
const skills = ['office-hours', 'plan-ceo-review', 'plan-eng-review', 'plan-devex-review', 'plan-design-review', 'design-review', 'design-consultation', 'review', 'ship', 'document-release', 'autoplan', 'spec'];
for (const skill of skills) {
test(`Codex /${skill} invokes Claude Code, including lazy sections`, () => {
const host = ALL_HOST_CONFIGS.find(h => h.name === 'codex')!;
const text = readUnion(skillDir(host, skill));
expect(text).toContain('gstack-claude-code');
// Prose about the CLI is permitted; executable codex review commands are not.
expect(shellLines(text).filter(line => /\bcodex\s+(?:exec|review)\b/.test(line))).toEqual([]);
expect(text).toContain('Claude Code');
});
}
test('Codex still omits Review Army specialist dispatch', () => {
const host = ALL_HOST_CONFIGS.find(h => h.name === 'codex')!;
for (const skill of ['review', 'ship']) {
expect(readUnion(skillDir(host, skill))).not.toContain('Step 4.5: Review Army — Specialist Dispatch');
}
});
});
/** Only shell fences count as invocation sites: explanatory prose cannot spawn. */
function shellLines(text: string): string[] {
let inShell = false;
return text.split('\n').filter(line => {
if (/^```(?:bash|sh|shell)\s*$/.test(line)) { inShell = true; return false; }
if (/^```\s*$/.test(line)) { inShell = false; return false; }
return inShell && !/^\s*#/.test(line);
});
}
function templateFiles(dir: string): string[] {
return readdirSync(dir, { withFileTypes: true }).flatMap(entry => {
const full = join(dir, entry.name);
if (entry.isDirectory() && entry.name === 'sections') return templateFiles(full);
return entry.isFile() && entry.name.endsWith('.tmpl') ? [full] : [];
});
}
test('automatic workflow templates delegate reviewer commands to shared resolvers', () => {
const skills = ['office-hours', 'plan-ceo-review', 'plan-eng-review', 'plan-devex-review', 'plan-design-review', 'design-review', 'design-consultation', 'design-shotgun', 'review', 'ship', 'document-release', 'autoplan', 'spec'];
const violations: string[] = [];
for (const skill of skills) {
for (const file of templateFiles(join(ROOT, skill))) {
for (const line of shellLines(readFileSync(file, 'utf8'))) {
// /spec's --execute worker implements the filed spec (archive + coordinator contract on stdin); it is not a reviewer.
if (file === join(ROOT, 'spec', 'sections', 'gate-and-file.md.tmpl') && line.trim() === '{ cat "$ARCHIVE_PATH"; printf \'\\n\\n%s\\n\' "$CONTRACT"; } | (cd "$SPAWN_PATH" && GSTACK_SESSION_KIND=spawned claude -p 2>&1) &') continue;
if (/\bcodex\s+(?:exec|review)\b|\bclaude\s+(?:-p|--print)\b|\bgstack-claude-code\s+--/.test(line)) {
violations.push(`${file.slice(ROOT.length + 1)}: ${line.trim()}`);
}
}
}
}
expect(violations).toEqual([]);
});
for (const host of ['claude', 'codex'] as const) {
test(`${host}: live E2E installs an executable extracted workflow from the actual host render`, () => {
const repo = mkdtempSync(join(tmpdir(), 'gstack-outside-fixture-'));
try {
const dir = installOutsideReviewFixture(output, host, repo, ROOT);
const text = readFileSync(join(dir, 'SKILL.md'), 'utf8');
expect(text).toContain('Step 0: Detect platform and base branch');
expect(text).toContain('Step 3: Get the diff');
expect(text).toContain('Step 4.8: Adversarial review (always-on)');
expect(text).toContain(host === 'codex' ? 'gstack-claude-code' : 'codex exec');
expect(text).not.toContain('## Preamble (run first)');
expect(text.match(/^name:/gm)).toHaveLength(1);
} finally { rmSync(repo, { recursive: true, force: true }); }
});
}