import { chmodSync, cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { describe, test } from "node:test"; import { expect } from "expect"; import { initProject } from "../src/projects.js"; import { executeRunLifecycle, prepareRun } from "../src/runner.js"; function seedTemplateRoot(root: string): void { for (const entry of ["AGENTS.md", ".codex/agents", ".codex/workflows", ".agents/skills"]) { cpSync(path.join(process.cwd(), entry), path.join(root, entry), { recursive: true }); } } function initTemplateProject(options: Parameters[0], cwd: string): ReturnType { seedTemplateRoot(cwd); return initProject(options, cwd); } describe("runner", () => { test("inspection modes do not write run cache files", () => { const cwd = mkdtempSync(path.join(tmpdir(), "ogs-runner-")); const { projectRoot } = initTemplateProject({ name: "Inspect Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd); const runsDir = path.join(projectRoot, ".codex", "runs"); const before = readdirSync(runsDir); const dryRun = prepareRun("gameplay-programmer", { project: projectRoot, task: "Inspect movement", dryRun: true }, cwd); const printPrompt = prepareRun("gameplay-programmer", { project: projectRoot, task: "Inspect movement", printPrompt: true }, cwd); expect(readdirSync(runsDir)).toEqual(before); expect(existsSync(dryRun.promptPath)).toBe(false); expect(existsSync(dryRun.metadataPath)).toBe(false); expect(existsSync(printPrompt.promptPath)).toBe(false); expect(existsSync(printPrompt.metadataPath)).toBe(false); }); test("strict studio blocks unapproved mutating role runs before run metadata writes", () => { const cwd = mkdtempSync(path.join(tmpdir(), "ogs-runner-strict-")); const { projectRoot } = initTemplateProject({ name: "Strict Game", engine: "godot", mode: "prototype", studioMode: "strict-studio", nonInteractive: true }, cwd); const runsDir = path.join(projectRoot, ".codex", "runs"); const before = readdirSync(runsDir); expect(() => prepareRun("gameplay-programmer", { project: projectRoot, task: "Implement movement", codexBin: path.join(cwd, "missing-codex") }, cwd)).toThrow( /matching approval/i ); expect(readdirSync(runsDir)).toEqual(before); }); test("guided override and fast prototype record provenance metadata for allowed runs", () => { const cwd = mkdtempSync(path.join(tmpdir(), "ogs-runner-policy-")); const guided = initTemplateProject({ name: "Guided Game", engine: "godot", mode: "prototype", studioMode: "guided-studio", nonInteractive: true }, cwd); const guidedDryRun = prepareRun("gameplay-programmer", { project: guided.projectRoot, task: "Implement movement", dryRun: true, approvedByUser: true }, cwd); expect(guidedDryRun.output).toContain("Write policy: override-write"); expect(guidedDryRun.output).toContain("Sandbox: danger-full-access"); expect(guidedDryRun.output).toContain("Provenance: override"); expect(guidedDryRun.output).toContain("Approved by user: true"); const guidedRun = prepareRun("gameplay-programmer", { project: guided.projectRoot, task: "Implement movement", approvedByUser: true, codexBin: path.join(cwd, "missing-codex") }, cwd); const guidedMetadata = JSON.parse(readFileSync(guidedRun.metadataPath, "utf8")) as { eligibility: { writePolicy: string; codexSandbox: string; metadata: { provenance: string; approvedByUser: boolean } } }; expect(guidedMetadata.eligibility).toMatchObject({ writePolicy: "override-write", codexSandbox: "danger-full-access", metadata: { provenance: "override", approvedByUser: true } }); const fast = initTemplateProject({ name: "Fast Game", engine: "godot", mode: "prototype", studioMode: "fast-prototype", nonInteractive: true }, mkdtempSync(path.join(tmpdir(), "ogs-runner-policy-"))); const fastRun = prepareRun("gameplay-programmer", { project: fast.projectRoot, task: "Implement movement", codexBin: path.join(cwd, "missing-codex") }, cwd); const fastMetadata = JSON.parse(readFileSync(fastRun.metadataPath, "utf8")) as { eligibility: { writePolicy: string; metadata: { provenance: string } } }; expect(fastRun.output).toContain("Write policy: advisory-write"); expect(fastMetadata.eligibility).toMatchObject({ writePolicy: "advisory-write", metadata: { provenance: "advisory" } }); }); test("mutating runs default to full access and require explicit constrained sandbox for workspace-write", () => { const cwd = mkdtempSync(path.join(tmpdir(), "ogs-runner-sandbox-")); const { projectRoot } = initTemplateProject({ name: "Sandbox Game", engine: "godot", mode: "prototype", studioMode: "fast-prototype", nonInteractive: true }, cwd); const defaultRun = prepareRun("gameplay-programmer", { project: projectRoot, task: "Implement movement", dryRun: true }, cwd); expect(defaultRun.codexCommand.args).toEqual(expect.arrayContaining(["--sandbox", "danger-full-access"])); expect(defaultRun.prompt).toContain("Sandbox: danger-full-access"); expect(defaultRun.prompt).toContain("Write Policy: advisory-write"); const constrainedRun = prepareRun("gameplay-programmer", { project: projectRoot, task: "Implement movement", dryRun: true, constrainedSandbox: true }, cwd); expect(constrainedRun.codexCommand.args).toEqual(expect.arrayContaining(["--sandbox", "workspace-write"])); }); test("inlines runtime role context without generating prompt files", () => { const cwd = mkdtempSync(path.join(tmpdir(), "ogs-runner-")); const { projectRoot } = initTemplateProject({ name: "Prompt Game", engine: "godot", mode: "design", nonInteractive: true }, cwd); const run = prepareRun("market-analyst", { project: projectRoot, task: "Assess competitors", printPrompt: true }, cwd); expect(run.prompt).toContain("# Runtime Role Context: market-analyst"); expect(run.prompt).toContain("Project: Prompt Game"); expect(run.contextFiles).toContain(".codex/agents/market-analyst.toml"); expect(existsSync(path.join(projectRoot, ".codex", "prompts"))).toBe(false); }); test("infers the agent surface tier from its model and preserves override precedence", () => { const cwd = mkdtempSync(path.join(tmpdir(), "codex-game-studio-runner-model-tier-")); const { projectRoot } = initTemplateProject({ name: "Model Tier", engine: "godot", mode: "prototype", studioMode: "fast-prototype", nonInteractive: true }, cwd); const agentPath = path.join(projectRoot, ".codex", "agents", "gameplay-programmer.toml"); const agent = readFileSync(agentPath, "utf8") .replace(/^model = ".*"$/m, 'model = "gpt-5.6-luna"') .replace(/^model_reasoning_effort = ".*"$/m, 'model_reasoning_effort = "low"'); writeFileSync(agentPath, agent); const surfaceRun = prepareRun("gameplay-programmer", { project: projectRoot, task: "Rename one label", dryRun: true }, cwd); expect(surfaceRun.selectedModelTier).toBe("luna"); expect(surfaceRun.selectedModel).toBe("gpt-5.6-luna"); expect(surfaceRun.modelReasoningEffort).toBe("low"); expect(surfaceRun.fallbackModel).toBe("gpt-5.4-mini"); expect(surfaceRun.fallbackReasoningEffort).toBe("low"); expect(surfaceRun.prompt).toContain("Selected tier: luna"); const lunaFixRun = prepareRun("gameplay-programmer", { project: projectRoot, task: "Format the inventory", dryRun: true, fix: true }, cwd); expect(lunaFixRun.fixCodexCommand?.args).toContain("gpt-5.6-terra"); expect(lunaFixRun.fixCodexCommand?.args).toEqual(expect.arrayContaining(["-c", 'model_reasoning_effort="medium"'])); expect(lunaFixRun.fixFallbackCodexCommand?.args).toContain("gpt-5.4"); const overrideRun = prepareRun("gameplay-programmer", { project: projectRoot, task: "Review architecture", dryRun: true, modelTier: "sol" }, cwd); expect(overrideRun.selectedModelTier).toBe("sol"); expect(overrideRun.selectedModel).toBe("gpt-5.6-sol"); expect(overrideRun.modelReasoningEffort).toBe("low"); expect(overrideRun.modelSelectionSource).toBe("explicit-tier"); }); test("missing tracked custom agent is reported as context blocker", () => { const cwd = mkdtempSync(path.join(tmpdir(), "ogs-runner-")); const { projectRoot } = initTemplateProject({ name: "Missing Agent Game", engine: "godot", mode: "design", nonInteractive: true }, cwd); rmSync(path.join(projectRoot, ".codex", "agents", "market-analyst.toml")); const run = prepareRun("market-analyst", { project: projectRoot, task: "Assess competitors", printPrompt: true }, cwd); expect(run.prompt).toContain(".codex/agents/market-analyst.toml: missing"); expect(run.contextFiles).not.toContain(".codex/agents/market-analyst.toml"); }); test("wrong-engine specialist run fails clearly before producing a prompt", () => { const cwd = mkdtempSync(path.join(tmpdir(), "ogs-runner-specialist-")); const { projectRoot } = initTemplateProject({ name: "Godot Specialist Run", engine: "godot", mode: "prototype", nonInteractive: true }, cwd); expect(() => prepareRun("unity-specialist", { project: projectRoot, task: "Review engine setup", printPrompt: true }, cwd)).toThrow( /unity-specialist is not available for godot projects/i ); expect(() => prepareRun("unity-dots-specialist", { project: projectRoot, task: "Review ECS architecture", printPrompt: true }, cwd)).toThrow( /unity-dots-specialist is not available for godot projects/i ); const run = prepareRun("godot-specialist", { project: projectRoot, task: "Review engine setup", printPrompt: true }, cwd); expect(run.prompt).toContain("# Runtime Role Context: godot-specialist"); expect(run.prompt).toContain("docs/engine-reference/godot/specialist.md"); const sub = prepareRun("godot-gdscript-specialist", { project: projectRoot, task: "Review typed GDScript signals", printPrompt: true }, cwd); expect(sub.prompt).toContain("# Runtime Role Context: godot-gdscript-specialist"); expect(sub.prompt).toContain("docs/engine-reference/godot/specialist.md"); expect(sub.prompt).toContain("docs/engine-reference/godot/gameplay.md"); }); test("unreal GAS sub-specialist selects focused plugin context", () => { const cwd = mkdtempSync(path.join(tmpdir(), "ogs-runner-gas-")); const { projectRoot } = initTemplateProject({ name: "Unreal GAS Run", engine: "unreal", mode: "development", nonInteractive: true }, cwd); const run = prepareRun("ue-gas-specialist", { project: projectRoot, task: "Review Gameplay Ability System attributes", printPrompt: true }, cwd); expect(run.prompt).toContain("# Runtime Role Context: ue-gas-specialist"); expect(run.prompt).toContain("docs/engine-reference/unreal/plugins/gas.md"); expect(() => prepareRun("godot-gdscript-specialist", { project: projectRoot, task: "Review script", printPrompt: true }, cwd)).toThrow( /godot-gdscript-specialist is not available for unreal projects/i ); }); test("fix prompt includes runtime role context", () => { const cwd = mkdtempSync(path.join(tmpdir(), "ogs-runner-")); const { projectRoot } = initTemplateProject({ name: "Fix Prompt Game", engine: "godot", mode: "design", nonInteractive: true }, cwd); const run = prepareRun("market-analyst", { project: projectRoot, task: "Assess competitors", dryRun: true, fix: true }, cwd); expect(run.fixPrompt).toContain("# Runtime Role Context: market-analyst"); }); test("role runs inline selected templates", () => { const cwd = mkdtempSync(path.join(tmpdir(), "ogs-run-template-")); const { projectRoot } = initTemplateProject({ name: "Template Run Game", engine: "godot", mode: "design", nonInteractive: true }, cwd); const market = prepareRun("market-analyst", { project: projectRoot, task: "Assess positioning", printPrompt: true }, cwd); expect(market.prompt).toContain("## Selected Templates"); expect(market.prompt).toContain("Template: market_analysis"); expect(market.prompt).toContain("Source: package:templates/market_analysis_template.md"); expect(market.prompt).not.toContain("Template: analytics_setup"); const analytics = prepareRun("data-scientist", { project: projectRoot, task: "Plan evidence loop", printPrompt: true }, cwd); expect(analytics.prompt).toContain("Template: analytics_setup"); expect(analytics.prompt).not.toContain("Template: market_analysis"); const gameplay = prepareRun("gameplay-programmer", { project: projectRoot, task: "Tune movement", printPrompt: true }, cwd); expect(gameplay.prompt).not.toContain("Template: market_analysis"); expect(gameplay.prompt).not.toContain("Template: analytics_setup"); const ui = prepareRun("ui-ux-designer", { project: projectRoot, task: "Review menus", printPrompt: true }, cwd); expect(ui.prompt).toContain("Template: ui_ux_review"); expect(ui.prompt).not.toContain("Template: ship_check"); const release = prepareRun("release-manager", { project: projectRoot, task: "Assess readiness", dryRun: true, fix: true }, cwd); expect(release.prompt).toContain("Template: ship_check"); expect(release.fixPrompt).toContain("Template: ship_check"); expect(release.prompt).not.toContain("Template: ui_ux_review"); }); test("broad context discovers bounded existing project files", () => { const cwd = mkdtempSync(path.join(tmpdir(), "ogs-runner-")); const { projectRoot } = initTemplateProject({ name: "Broad Context Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd); const narrow = prepareRun("producer", { project: projectRoot, task: "Plan next milestone", dryRun: true }, cwd); expect(narrow.contextFiles).not.toContain("design/gdd.md"); expect(narrow.contextFiles).not.toContain("production/timeline.md"); const broad = prepareRun("producer", { project: projectRoot, task: "Plan next milestone", dryRun: true, allowBroadContext: true }, cwd); expect(broad.contextFiles).toContain("design/gdd.md"); expect(broad.contextFiles).toContain("production/timeline.md"); expect(broad.contextFiles).toContain("docs/market-overview.md"); expect(broad.contextFiles).not.toContain("Broad context explicitly allowed by CLI flag."); expect(broad.contextFiles.filter((file) => file.startsWith(".codex/agents/"))).toHaveLength(1); }); test("broad context ignores directories and realpath escapes", () => { const cwd = mkdtempSync(path.join(tmpdir(), "ogs-runner-")); const { projectRoot } = initTemplateProject({ name: "Bounded Context Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd); const gdd = path.join(projectRoot, "design", "gdd.md"); rmSync(gdd); mkdirSync(gdd); const overview = path.join(projectRoot, "docs", "market-overview.md"); const outsideDir = mkdtempSync(path.join(tmpdir(), "ogs-outside-")); const outside = path.join(outsideDir, "outside.md"); writeFileSync(outside, "outside"); rmSync(overview); symlinkSync(outside, overview); const broad = prepareRun("producer", { project: projectRoot, task: "Plan next milestone", dryRun: true, allowBroadContext: true }, cwd); expect(broad.contextFiles).not.toContain("design/gdd.md"); expect(broad.contextFiles).not.toContain("docs/market-overview.md"); }); test("include artifact renders canonical project-relative paths", () => { const cwd = mkdtempSync(path.join(tmpdir(), "ogs-runner-")); const { projectRoot } = initTemplateProject({ name: "Artifact Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd); const run = prepareRun("producer", { project: projectRoot, task: "Plan next milestone", printPrompt: true, includeArtifact: ["design/../design/gdd.md"] }, cwd); expect(run.contextFiles).toContain("design/gdd.md"); expect(run.prompt).toContain("# Included Artifact: design/gdd.md"); expect(() => prepareRun("producer", { project: projectRoot, task: "Plan next milestone", printPrompt: true, includeArtifact: ["design/gdd.md\n# injected"] }, cwd)).toThrow( "--include-artifact cannot contain control characters" ); }); test("rejected included artifacts are not embedded in prompts", () => { const cwd = mkdtempSync(path.join(tmpdir(), "ogs-runner-artifact-rejected-")); const { projectRoot } = initTemplateProject({ name: "Rejected Artifact Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd); mkdirSync(path.join(projectRoot, "dist"), { recursive: true }); writeFileSync(path.join(projectRoot, "dist", "bundle.js"), "REJECTED_BUNDLE_BODY"); const run = prepareRun("producer", { project: projectRoot, task: "Plan next milestone", printPrompt: true, includeArtifact: ["dist/bundle.js"] }, cwd); expect(run.contextFiles).not.toContain("dist/bundle.js"); expect(run.prompt).toContain("- dist/bundle.js: rejected"); expect(run.prompt).not.toContain("# Included Artifact: dist/bundle.js"); expect(run.prompt).not.toContain("REJECTED_BUNDLE_BODY"); }); test("oversized included artifacts are not embedded in prompts", () => { const cwd = mkdtempSync(path.join(tmpdir(), "ogs-runner-artifact-oversized-")); const { projectRoot } = initTemplateProject({ name: "Oversized Artifact Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd); const artifactPath = path.join(projectRoot, "design", "large-artifact.md"); writeFileSync(artifactPath, `OVERSIZED_ARTIFACT_BODY\n${"x".repeat(20_000)}`); const run = prepareRun("producer", { project: projectRoot, task: "Plan next milestone", printPrompt: true, includeArtifact: ["design/large-artifact.md"] }, cwd); expect(run.contextFiles).not.toContain("design/large-artifact.md"); expect(run.prompt).toContain("- design/large-artifact.md: omitted"); expect(run.prompt).not.toContain("# Included Artifact: design/large-artifact.md"); expect(run.prompt).not.toContain("OVERSIZED_ARTIFACT_BODY"); }); test("review passes execute Codex with a read-only sandbox", async () => { const cwd = mkdtempSync(path.join(tmpdir(), "ogs-review-")); const { projectRoot } = initTemplateProject({ name: "Review Game", engine: "godot", mode: "prototype", studioMode: "fast-prototype", nonInteractive: true }, cwd); const log = path.join(cwd, "codex-invocations.jsonl"); const stub = path.join(cwd, "codex-stub.mjs"); writeFileSync( stub, `#!/usr/bin/env node import { appendFileSync, readFileSync } from "node:fs"; const input = readFileSync(0, "utf8"); appendFileSync(${JSON.stringify(log)}, JSON.stringify({ args: process.argv.slice(2), input }) + "\\n"); console.log(JSON.stringify({ blockers: [], warnings: [], summary: "ok", needsFix: false })); ` ); chmodSync(stub, 0o755); const run = prepareRun("gameplay-programmer", { project: projectRoot, task: "Implement movement", review: true, codexBin: stub }, cwd); await executeRunLifecycle(run); const invocations = readFileSync(log, "utf8") .trim() .split("\n") .map((line) => JSON.parse(line) as { args: string[]; input: string }); expect(invocations).toHaveLength(2); expect(invocations[0].args).toEqual(expect.arrayContaining(["--sandbox", "danger-full-access"])); expect(invocations[1].args).toEqual(expect.arrayContaining(["--sandbox", "read-only"])); expect(invocations[1].input).toContain("# Runtime Role Context: qa-playtester"); expect(invocations[1].input).toContain("Template: playtest_report"); }); test("implementation, review, and fix prompts share a bounded Context Contract", () => { const cwd = mkdtempSync(path.join(tmpdir(), "ogs-contract-")); const { projectRoot } = initTemplateProject({ name: "Contract Game", engine: "godot", mode: "prototype", studioMode: "fast-prototype", nonInteractive: true }, cwd); const run = prepareRun("gameplay-programmer", { project: projectRoot, task: "Implement movement", dryRun: true, review: true, fix: true }, cwd); for (const prompt of [run.prompt, run.reviewPrompt, run.fixPrompt]) { expect(prompt).toContain("# Context Contract"); expect(prompt).toContain("Project Stage: prototype"); expect(prompt).toContain("Studio Mode: fast-prototype"); expect(prompt).toMatch(/Phase: (implement|review|fix)/); expect(prompt).toContain("Write Policy:"); expect(prompt).toContain("Sandbox:"); expect(prompt).toContain("File Edits:"); expect(prompt).toContain("Selected Context:"); expect(prompt).toContain("- AGENTS.md (required; project instructions)"); expect(prompt).toContain("Omissions and Blockers:"); } expect(run.reviewPrompt).toContain("Read-only review: inspect diff and verification output; do not edit files."); expect(run.reviewPrompt).toContain(".codex/agents/qa-playtester.toml"); expect(run.fixPrompt).toContain("Bounded Blockers:"); expect(run.fixPrompt).toContain("- Review and verification blockers will be supplied at execution time."); }); });