import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { execFileSync } from "node:child_process"; import { mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { describe, test } from "node:test"; import { expect } from "expect"; import { guidanceConfigHash } from "../src/config.js"; import { freezeProject, initProject, resumeProject, statusProject } from "../src/projects.js"; function tempRoot(prefix: string): string { return mkdtempSync(path.join(tmpdir(), prefix)); } function templateRoot(prefix: string): string { const root = tempRoot(prefix); for (const entry of ["AGENTS.md", ".codex/agents", ".codex/workflows", ".agents/skills"]) { cpSync(path.join(process.cwd(), entry), path.join(root, entry), { recursive: true }); } return root; } function writeAuthoringArtifacts(root: string): void { for (const dir of ["eval-framework", "references", "openspec", "scripts", "tooling", "research", ".truthmark", ".hermes", "src", "tests"]) { mkdirSync(path.join(root, dir), { recursive: true }); } writeFileSync(path.join(root, "eval-framework", "README.md"), "maintainer evals\n"); writeFileSync(path.join(root, "references", "matrix.md"), "maintainer matrix\n"); writeFileSync(path.join(root, "openspec", "change.md"), "maintainer spec\n"); writeFileSync(path.join(root, "scripts", "refresh.mjs"), "console.log('authoring');\n"); writeFileSync(path.join(root, "tooling", "audit.ts"), "export {};\n"); writeFileSync(path.join(root, "research", "stale.md"), "old research\n"); writeFileSync(path.join(root, ".truthmark", "config.json"), "{}\n"); writeFileSync(path.join(root, ".hermes", "plan.md"), "local agent plan\n"); writeFileSync(path.join(root, "src", "cli.ts"), "export {};\n"); writeFileSync(path.join(root, "tests", "validation.test.ts"), "export {};\n"); writeFileSync(path.join(root, "CONTRIBUTING.md"), "maintainer notes\n"); writeFileSync(path.join(root, "tsconfig.json"), "{}\n"); writeFileSync(path.join(root, "tsconfig.build.json"), "{}\n"); } describe("project workflow", () => { test("init configures the current repository root as the game root", () => { const cwd = templateRoot("ogs-root-project-"); const { projectRoot, config } = initProject({ name: "Test Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd); expect(projectRoot).toBe(cwd); expect(existsSync(path.join(cwd, "projects", "test-game"))).toBe(false); expect(existsSync(path.join(projectRoot, ".codex", "studio.json"))).toBe(true); expect(existsSync(path.join(projectRoot, ".codex", "context-manifest.json"))).toBe(true); expect(existsSync(path.join(projectRoot, ".codex", "context-manifest.meta.json"))).toBe(true); expect(existsSync(path.join(projectRoot, ".codex", "runs"))).toBe(true); expect(existsSync(path.join(projectRoot, ".gamestudio"))).toBe(false); expect(existsSync(path.join(projectRoot, "src", "project.godot"))).toBe(true); expect(existsSync(path.join(projectRoot, "assets", ".gitkeep"))).toBe(true); expect(existsSync(path.join(projectRoot, "design", "gdd.md"))).toBe(true); expect(existsSync(path.join(projectRoot, "docs", "architecture", "README.md"))).toBe(true); expect(existsSync(path.join(projectRoot, "docs", "market-overview.md"))).toBe(true); expect(existsSync(path.join(projectRoot, "tests", ".gitkeep"))).toBe(true); expect(existsSync(path.join(projectRoot, "tools", ".gitkeep"))).toBe(true); expect(existsSync(path.join(projectRoot, "production", "timeline.md"))).toBe(true); const contextManifest = JSON.parse(readFileSync(path.join(projectRoot, ".codex", "context-manifest.json"), "utf8")); expect(contextManifest.entries).toEqual( expect.arrayContaining([ expect.objectContaining({ sourcePath: "AGENTS.md", required: true, safety: "safe", status: "selected" }), expect.objectContaining({ sourcePath: "design/gdd.md", required: true, status: "selected" }), expect.objectContaining({ sourcePath: "src/project.godot", reason: expect.stringMatching(/engine/i), status: "selected" }) ]) ); const agents = readFileSync(path.join(projectRoot, "AGENTS.md"), "utf8"); expect(agents).toContain("# Codex Game Studio Template"); for (const section of ["## Project Goal", "## Engine", "## Commands", "## Coding Conventions", "## Asset Conventions", "## Studio Roles", "## Current Milestone", "## Verification", "## Rules"]) { expect(agents).toContain(section); } expect(agents).not.toContain("CODEX.md"); expect(agents).not.toContain("NodeNext"); expect(agents).not.toContain("Truthmark"); expect(agents).not.toContain(guidanceConfigHash(config)); expect(config.project.concept).toBe("Test Game concept"); expect(config.project.genre).toBe("Unspecified"); expect(config.project.platform).toBe("PC"); expect(config.project.audience).toBe("General players"); expect(config.project.competitors).toEqual([]); }); test("init prunes maintainer-only template authoring artifacts from game workspaces", () => { const cwd = templateRoot("ogs-prune-authoring-"); writeAuthoringArtifacts(cwd); const { projectRoot, prunedArtifacts } = initProject({ name: "Clean Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd); expect(prunedArtifacts).toEqual( expect.arrayContaining([ "eval-framework", "references", "openspec", "scripts", "tooling", "research", ".truthmark", ".hermes", "CONTRIBUTING.md", "tsconfig.json", "tsconfig.build.json", path.join("src", "cli.ts"), path.join("tests", "validation.test.ts") ]) ); for (const removed of ["eval-framework", "references", "openspec", "scripts", "tooling", "research", ".truthmark", ".hermes", "CONTRIBUTING.md", "tsconfig.json", "tsconfig.build.json"]) { expect(existsSync(path.join(projectRoot, removed))).toBe(false); } expect(existsSync(path.join(projectRoot, "src", "cli.ts"))).toBe(false); expect(existsSync(path.join(projectRoot, "tests", "validation.test.ts"))).toBe(false); expect(existsSync(path.join(projectRoot, "src", "project.godot"))).toBe(true); expect(existsSync(path.join(projectRoot, "tests", ".gitkeep"))).toBe(true); expect(existsSync(path.join(projectRoot, ".codex", "agents", "gameplay-programmer.toml"))).toBe(true); expect(existsSync(path.join(projectRoot, ".agents", "skills", "cgs-start", "SKILL.md"))).toBe(true); }); test("init can keep template authoring artifacts for maintainers", () => { const cwd = templateRoot("ogs-keep-authoring-"); writeAuthoringArtifacts(cwd); const { projectRoot, prunedArtifacts } = initProject({ name: "Maintainer Game", engine: "godot", mode: "prototype", nonInteractive: true, keepTemplateAuthoring: true }, cwd); expect(prunedArtifacts).toEqual([]); expect(existsSync(path.join(projectRoot, "eval-framework", "README.md"))).toBe(true); expect(existsSync(path.join(projectRoot, "src", "cli.ts"))).toBe(true); expect(existsSync(path.join(projectRoot, "src", "project.godot"))).toBe(true); expect(existsSync(path.join(projectRoot, "tests", "validation.test.ts"))).toBe(true); expect(existsSync(path.join(projectRoot, "tests", ".gitkeep"))).toBe(true); }); test("init preserves clone-visible Codex-native custom agents and repository skills", () => { const cwd = templateRoot("ogs-surfaces-"); const { projectRoot } = initProject({ name: "Surface Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd); const agent = readFileSync(path.join(projectRoot, ".codex", "agents", "gameplay-programmer.toml"), "utf8"); expect(agent).toContain('name = "gameplay_programmer"'); expect(agent).toContain("description = "); expect(agent).toContain("developer_instructions = "); expect(existsSync(path.join(projectRoot, ".codex", "agents", "gameplay-programmer.md"))).toBe(false); expect(existsSync(path.join(projectRoot, ".codex", "agents", "godot-specialist.toml"))).toBe(true); expect(existsSync(path.join(projectRoot, ".codex", "agents", "godot-gdscript-specialist.toml"))).toBe(true); expect(existsSync(path.join(projectRoot, ".codex", "agents", "godot-csharp-specialist.toml"))).toBe(true); expect(existsSync(path.join(projectRoot, ".codex", "agents", "godot-shader-specialist.toml"))).toBe(true); expect(existsSync(path.join(projectRoot, ".codex", "agents", "godot-gdextension-specialist.toml"))).toBe(true); expect(existsSync(path.join(projectRoot, ".codex", "agents", "unity-specialist.toml"))).toBe(true); expect(existsSync(path.join(projectRoot, ".codex", "agents", "unity-dots-specialist.toml"))).toBe(true); expect(existsSync(path.join(projectRoot, ".codex", "agents", "unity-shader-specialist.toml"))).toBe(true); expect(existsSync(path.join(projectRoot, ".codex", "agents", "unity-addressables-specialist.toml"))).toBe(true); expect(existsSync(path.join(projectRoot, ".codex", "agents", "unity-ui-specialist.toml"))).toBe(true); expect(existsSync(path.join(projectRoot, ".codex", "agents", "unreal-specialist.toml"))).toBe(true); expect(existsSync(path.join(projectRoot, ".codex", "agents", "ue-gas-specialist.toml"))).toBe(true); expect(existsSync(path.join(projectRoot, ".codex", "agents", "ue-blueprint-specialist.toml"))).toBe(true); expect(existsSync(path.join(projectRoot, ".codex", "agents", "ue-replication-specialist.toml"))).toBe(true); expect(existsSync(path.join(projectRoot, ".codex", "agents", "ue-umg-specialist.toml"))).toBe(true); expect(existsSync(path.join(projectRoot, ".codex", "hooks.json"))).toBe(false); expect(existsSync(path.join(projectRoot, ".codex", "rules"))).toBe(false); for (const skill of ["cgs-start", "cgs-setup-engine", "cgs-adopt", "cgs-bugfix", "cgs-vertical-slice", "cgs-ui-ux-review", "cgs-release-checklist", "cgs-standards-gameplay", "cgs-standards-tests", "cgs-standards-prototype", "cgs-standards-ui"]) { const body = readFileSync(path.join(projectRoot, ".agents", "skills", skill, "SKILL.md"), "utf8"); expect(body).toContain(`name: ${skill}`); expect(body).toContain("description: "); } }); test("init requires explicit non-interactive mode and supports optional overrides", () => { const cwd = tempRoot("ogs-required-"); expect(() => initProject({ name: "Missing Mode", engine: "godot", nonInteractive: true }, cwd)).toThrow(/--mode/); expect(() => initProject({ name: "Missing Noninteractive", engine: "godot", mode: "prototype" }, cwd)).toThrow(/--non-interactive/); const { config } = initProject({ name: "Override Game", engine: "godot", mode: "design", studioMode: "strict-studio", nonInteractive: true, competitors: ["terra nil", "mini metro"], engineVersion: "4.5.custom" }, cwd); expect(config.project.studio_mode).toBe("strict-studio"); expect(config.project.competitors).toEqual(["terra nil", "mini metro"]); expect(config.project.engine_version).toBe("4.5.custom"); }); test("default init protects an existing different root project", () => { const cwd = tempRoot("ogs-protect-"); initProject({ name: "First Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd); expect(() => initProject({ name: "Second Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd)).toThrow(/already contains/i); }); test("CLI init writes root studio state by default and accepts repeated competitor flags", () => { const cwd = tempRoot("ogs-cli-"); const cli = path.join(process.cwd(), "src", "cli.ts"); const tsx = path.join(process.cwd(), "node_modules", ".bin", "tsx"); expect(() => execFileSync(tsx, [cli, "init", "--name", "CLI Missing Mode", "--engine", "godot", "--non-interactive"], { cwd, encoding: "utf8", stdio: "pipe" })).toThrow(); execFileSync(tsx, [cli, "init", "--name", "CLI Game", "--engine", "godot", "--mode", "prototype", "--studio-mode", "fast-prototype", "--non-interactive", "--competitor", "terra nil", "--competitor", "mini metro", "--engine-version", "4.5.custom"], { cwd, encoding: "utf8" }); const studio = JSON.parse(readFileSync(path.join(cwd, ".codex", "studio.json"), "utf8")); expect(studio.engineVersion).toBe("4.5.custom"); expect(studio.studioMode).toBe("fast-prototype"); expect(existsSync(path.join(cwd, "projects", "cli-game"))).toBe(false); }); test("all engines initialize expected root files", () => { expect(existsSync(path.join(initProject({ name: "Godot Game", engine: "godot", mode: "prototype", nonInteractive: true }, tempRoot("ogs-godot-")).projectRoot, "src", "project.godot"))).toBe(true); expect(existsSync(path.join(initProject({ name: "Unity Game", engine: "unity", mode: "design", nonInteractive: true }, tempRoot("ogs-unity-")).projectRoot, "src", "Packages", "manifest.json"))).toBe(true); expect(existsSync(path.join(initProject({ name: "Unreal Game", engine: "Unreal Engine", mode: "development", nonInteractive: true }, tempRoot("ogs-unreal-")).projectRoot, "src", "UnrealGame.uproject"))).toBe(true); }); test("status resume are read-only and freeze only changes operational status", () => { const cwd = templateRoot("ogs-status-"); const { projectRoot } = initProject({ name: "Freeze Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd); const studioPath = path.join(projectRoot, ".codex", "studio.json"); const before = readFileSync(studioPath, "utf8"); expect(statusProject(undefined, cwd)).toContain("custom agents: .codex/agents/*.toml"); expect(resumeProject(undefined, cwd)).toContain("Suggested next command"); expect(readFileSync(studioPath, "utf8")).toBe(before); const agentsBefore = readFileSync(path.join(projectRoot, "AGENTS.md"), "utf8"); freezeProject(undefined, cwd); expect(JSON.parse(readFileSync(studioPath, "utf8")).status).toBe("frozen"); expect(readFileSync(path.join(projectRoot, "AGENTS.md"), "utf8")).toBe(agentsBefore); }); });