2026-06-29 14:11:18 +00:00
|
|
|
import { chmodSync, cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
2026-05-28 16:21:35 +00:00
|
|
|
import { tmpdir } from "node:os";
|
|
|
|
|
import path from "node:path";
|
2026-06-29 12:47:44 +00:00
|
|
|
import { describe, test } from "node:test";
|
|
|
|
|
import { expect } from "expect";
|
2026-05-28 16:21:35 +00:00
|
|
|
import { initProject } from "../src/projects.js";
|
|
|
|
|
import { executeRunLifecycle, prepareRun } from "../src/runner.js";
|
|
|
|
|
|
2026-06-29 14:11:18 +00:00
|
|
|
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<typeof initProject>[0], cwd: string): ReturnType<typeof initProject> {
|
|
|
|
|
seedTemplateRoot(cwd);
|
|
|
|
|
return initProject(options, cwd);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-28 16:21:35 +00:00
|
|
|
describe("runner", () => {
|
|
|
|
|
test("inspection modes do not write run cache files", () => {
|
|
|
|
|
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-runner-"));
|
2026-06-29 14:11:18 +00:00
|
|
|
const { projectRoot } = initTemplateProject({ name: "Inspect Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
|
2026-05-28 16:21:35 +00:00
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-17 18:47:15 +10:00
|
|
|
test("strict studio blocks unapproved mutating role runs before run metadata writes", () => {
|
|
|
|
|
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-runner-strict-"));
|
2026-06-29 14:11:18 +00:00
|
|
|
const { projectRoot } = initTemplateProject({ name: "Strict Game", engine: "godot", mode: "prototype", studioMode: "strict-studio", nonInteractive: true }, cwd);
|
2026-06-17 18:47:15 +10:00
|
|
|
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-"));
|
2026-06-29 14:11:18 +00:00
|
|
|
const guided = initTemplateProject({ name: "Guided Game", engine: "godot", mode: "prototype", studioMode: "guided-studio", nonInteractive: true }, cwd);
|
2026-06-17 18:47:15 +10:00
|
|
|
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 }
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-29 14:11:18 +00:00
|
|
|
const fast = initTemplateProject({ name: "Fast Game", engine: "godot", mode: "prototype", studioMode: "fast-prototype", nonInteractive: true }, mkdtempSync(path.join(tmpdir(), "ogs-runner-policy-")));
|
2026-06-17 18:47:15 +10:00
|
|
|
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-"));
|
2026-06-29 14:11:18 +00:00
|
|
|
const { projectRoot } = initTemplateProject({ name: "Sandbox Game", engine: "godot", mode: "prototype", studioMode: "fast-prototype", nonInteractive: true }, cwd);
|
2026-06-17 18:47:15 +10:00
|
|
|
|
|
|
|
|
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"]));
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-29 14:11:18 +00:00
|
|
|
test("inlines runtime role context without generating prompt files", () => {
|
2026-05-30 14:33:23 +00:00
|
|
|
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-runner-"));
|
2026-06-29 14:11:18 +00:00
|
|
|
const { projectRoot } = initTemplateProject({ name: "Prompt Game", engine: "godot", mode: "design", nonInteractive: true }, cwd);
|
2026-05-30 14:33:23 +00:00
|
|
|
|
|
|
|
|
const run = prepareRun("market-analyst", { project: projectRoot, task: "Assess competitors", printPrompt: true }, cwd);
|
|
|
|
|
|
2026-06-29 14:11:18 +00:00
|
|
|
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);
|
2026-05-30 14:33:23 +00:00
|
|
|
});
|
|
|
|
|
|
2026-07-10 18:49:20 +10:00
|
|
|
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");
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-29 14:11:18 +00:00
|
|
|
test("missing tracked custom agent is reported as context blocker", () => {
|
2026-05-30 14:33:23 +00:00
|
|
|
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-runner-"));
|
2026-06-29 14:11:18 +00:00
|
|
|
const { projectRoot } = initTemplateProject({ name: "Missing Agent Game", engine: "godot", mode: "design", nonInteractive: true }, cwd);
|
|
|
|
|
rmSync(path.join(projectRoot, ".codex", "agents", "market-analyst.toml"));
|
2026-05-30 14:33:23 +00:00
|
|
|
|
2026-06-29 14:11:18 +00:00
|
|
|
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");
|
2026-05-30 14:33:23 +00:00
|
|
|
});
|
|
|
|
|
|
2026-06-17 18:47:15 +10:00
|
|
|
test("wrong-engine specialist run fails clearly before producing a prompt", () => {
|
|
|
|
|
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-runner-specialist-"));
|
2026-06-29 14:11:18 +00:00
|
|
|
const { projectRoot } = initTemplateProject({ name: "Godot Specialist Run", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
|
2026-06-17 18:47:15 +10:00
|
|
|
|
|
|
|
|
expect(() => prepareRun("unity-specialist", { project: projectRoot, task: "Review engine setup", printPrompt: true }, cwd)).toThrow(
|
|
|
|
|
/unity-specialist is not available for godot projects/i
|
|
|
|
|
);
|
2026-07-07 14:09:47 +10:00
|
|
|
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
|
|
|
|
|
);
|
2026-06-17 18:47:15 +10:00
|
|
|
|
|
|
|
|
const run = prepareRun("godot-specialist", { project: projectRoot, task: "Review engine setup", printPrompt: true }, cwd);
|
2026-06-29 14:11:18 +00:00
|
|
|
expect(run.prompt).toContain("# Runtime Role Context: godot-specialist");
|
2026-06-17 18:47:15 +10:00
|
|
|
expect(run.prompt).toContain("docs/engine-reference/godot/specialist.md");
|
2026-07-07 14:09:47 +10:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
);
|
2026-06-17 18:47:15 +10:00
|
|
|
});
|
|
|
|
|
|
2026-06-29 14:11:18 +00:00
|
|
|
test("fix prompt includes runtime role context", () => {
|
2026-05-30 14:33:23 +00:00
|
|
|
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-runner-"));
|
2026-06-29 14:11:18 +00:00
|
|
|
const { projectRoot } = initTemplateProject({ name: "Fix Prompt Game", engine: "godot", mode: "design", nonInteractive: true }, cwd);
|
2026-05-30 14:33:23 +00:00
|
|
|
|
|
|
|
|
const run = prepareRun("market-analyst", { project: projectRoot, task: "Assess competitors", dryRun: true, fix: true }, cwd);
|
|
|
|
|
|
2026-06-29 14:11:18 +00:00
|
|
|
expect(run.fixPrompt).toContain("# Runtime Role Context: market-analyst");
|
2026-05-30 14:33:23 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("role runs inline selected templates", () => {
|
|
|
|
|
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-run-template-"));
|
2026-06-29 14:11:18 +00:00
|
|
|
const { projectRoot } = initTemplateProject({ name: "Template Run Game", engine: "godot", mode: "design", nonInteractive: true }, cwd);
|
2026-05-30 14:33:23 +00:00
|
|
|
|
|
|
|
|
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-"));
|
2026-06-29 14:11:18 +00:00
|
|
|
const { projectRoot } = initTemplateProject({ name: "Broad Context Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
|
2026-05-30 14:33:23 +00:00
|
|
|
|
|
|
|
|
const narrow = prepareRun("producer", { project: projectRoot, task: "Plan next milestone", dryRun: true }, cwd);
|
2026-06-29 09:46:45 +00:00
|
|
|
expect(narrow.contextFiles).not.toContain("design/gdd.md");
|
|
|
|
|
expect(narrow.contextFiles).not.toContain("production/timeline.md");
|
2026-05-30 14:33:23 +00:00
|
|
|
|
|
|
|
|
const broad = prepareRun("producer", { project: projectRoot, task: "Plan next milestone", dryRun: true, allowBroadContext: true }, cwd);
|
2026-06-29 09:46:45 +00:00
|
|
|
expect(broad.contextFiles).toContain("design/gdd.md");
|
|
|
|
|
expect(broad.contextFiles).toContain("production/timeline.md");
|
|
|
|
|
expect(broad.contextFiles).toContain("docs/market-overview.md");
|
2026-05-30 14:33:23 +00:00
|
|
|
expect(broad.contextFiles).not.toContain("Broad context explicitly allowed by CLI flag.");
|
2026-06-29 14:11:18 +00:00
|
|
|
expect(broad.contextFiles.filter((file) => file.startsWith(".codex/agents/"))).toHaveLength(1);
|
2026-05-30 14:33:23 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("broad context ignores directories and realpath escapes", () => {
|
|
|
|
|
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-runner-"));
|
2026-06-29 14:11:18 +00:00
|
|
|
const { projectRoot } = initTemplateProject({ name: "Bounded Context Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
|
2026-06-29 09:46:45 +00:00
|
|
|
const gdd = path.join(projectRoot, "design", "gdd.md");
|
2026-05-30 14:33:23 +00:00
|
|
|
rmSync(gdd);
|
|
|
|
|
mkdirSync(gdd);
|
2026-06-29 09:46:45 +00:00
|
|
|
const overview = path.join(projectRoot, "docs", "market-overview.md");
|
|
|
|
|
const outsideDir = mkdtempSync(path.join(tmpdir(), "ogs-outside-"));
|
|
|
|
|
const outside = path.join(outsideDir, "outside.md");
|
2026-05-30 14:33:23 +00:00
|
|
|
writeFileSync(outside, "outside");
|
|
|
|
|
rmSync(overview);
|
|
|
|
|
symlinkSync(outside, overview);
|
|
|
|
|
|
|
|
|
|
const broad = prepareRun("producer", { project: projectRoot, task: "Plan next milestone", dryRun: true, allowBroadContext: true }, cwd);
|
|
|
|
|
|
2026-06-29 09:46:45 +00:00
|
|
|
expect(broad.contextFiles).not.toContain("design/gdd.md");
|
|
|
|
|
expect(broad.contextFiles).not.toContain("docs/market-overview.md");
|
2026-05-30 14:33:23 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("include artifact renders canonical project-relative paths", () => {
|
|
|
|
|
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-runner-"));
|
2026-06-29 14:11:18 +00:00
|
|
|
const { projectRoot } = initTemplateProject({ name: "Artifact Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
|
2026-05-30 14:33:23 +00:00
|
|
|
|
2026-06-29 09:46:45 +00:00
|
|
|
const run = prepareRun("producer", { project: projectRoot, task: "Plan next milestone", printPrompt: true, includeArtifact: ["design/../design/gdd.md"] }, cwd);
|
2026-05-30 14:33:23 +00:00
|
|
|
|
2026-06-29 09:46:45 +00:00
|
|
|
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(
|
2026-05-30 14:33:23 +00:00
|
|
|
"--include-artifact cannot contain control characters"
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-17 18:47:15 +10:00
|
|
|
test("rejected included artifacts are not embedded in prompts", () => {
|
|
|
|
|
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-runner-artifact-rejected-"));
|
2026-06-29 14:11:18 +00:00
|
|
|
const { projectRoot } = initTemplateProject({ name: "Rejected Artifact Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
|
2026-06-17 18:47:15 +10:00
|
|
|
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-"));
|
2026-06-29 14:11:18 +00:00
|
|
|
const { projectRoot } = initTemplateProject({ name: "Oversized Artifact Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
|
2026-06-29 09:46:45 +00:00
|
|
|
const artifactPath = path.join(projectRoot, "design", "large-artifact.md");
|
2026-06-17 18:47:15 +10:00
|
|
|
writeFileSync(artifactPath, `OVERSIZED_ARTIFACT_BODY\n${"x".repeat(20_000)}`);
|
|
|
|
|
|
2026-06-29 09:46:45 +00:00
|
|
|
const run = prepareRun("producer", { project: projectRoot, task: "Plan next milestone", printPrompt: true, includeArtifact: ["design/large-artifact.md"] }, cwd);
|
2026-06-17 18:47:15 +10:00
|
|
|
|
2026-06-29 09:46:45 +00:00
|
|
|
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");
|
2026-06-17 18:47:15 +10:00
|
|
|
expect(run.prompt).not.toContain("OVERSIZED_ARTIFACT_BODY");
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-28 16:21:35 +00:00
|
|
|
test("review passes execute Codex with a read-only sandbox", async () => {
|
|
|
|
|
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-review-"));
|
2026-06-29 14:11:18 +00:00
|
|
|
const { projectRoot } = initTemplateProject({ name: "Review Game", engine: "godot", mode: "prototype", studioMode: "fast-prototype", nonInteractive: true }, cwd);
|
2026-05-28 16:21:35 +00:00
|
|
|
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);
|
2026-06-17 18:47:15 +10:00
|
|
|
expect(invocations[0].args).toEqual(expect.arrayContaining(["--sandbox", "danger-full-access"]));
|
2026-05-28 16:21:35 +00:00
|
|
|
expect(invocations[1].args).toEqual(expect.arrayContaining(["--sandbox", "read-only"]));
|
2026-06-29 14:11:18 +00:00
|
|
|
expect(invocations[1].input).toContain("# Runtime Role Context: qa-playtester");
|
2026-05-30 14:33:23 +00:00
|
|
|
expect(invocations[1].input).toContain("Template: playtest_report");
|
2026-05-28 16:21:35 +00:00
|
|
|
});
|
2026-06-17 18:47:15 +10:00
|
|
|
|
|
|
|
|
test("implementation, review, and fix prompts share a bounded Context Contract", () => {
|
|
|
|
|
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-contract-"));
|
2026-06-29 14:11:18 +00:00
|
|
|
const { projectRoot } = initTemplateProject({ name: "Contract Game", engine: "godot", mode: "prototype", studioMode: "fast-prototype", nonInteractive: true }, cwd);
|
2026-06-17 18:47:15 +10:00
|
|
|
|
|
|
|
|
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.");
|
2026-06-29 14:11:18 +00:00
|
|
|
expect(run.reviewPrompt).toContain(".codex/agents/qa-playtester.toml");
|
2026-06-17 18:47:15 +10:00
|
|
|
expect(run.fixPrompt).toContain("Bounded Blockers:");
|
|
|
|
|
expect(run.fixPrompt).toContain("- Review and verification blockers will be supplied at execution time.");
|
|
|
|
|
});
|
2026-05-28 16:21:35 +00:00
|
|
|
});
|