mirror of
https://github.com/merlinhu1/codex-game-studio.git
synced 2026-08-25 07:54:34 +02:00
114 lines
6.6 KiB
TypeScript
114 lines
6.6 KiB
TypeScript
import { existsSync, readFileSync } 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, expect, test } from "vitest";
|
|
import { guidanceConfigHash, readProjectConfig } from "../src/config.js";
|
|
import { freezeProject, initProject, resumeProject, statusProject } from "../src/projects.js";
|
|
|
|
describe("project workflow", () => {
|
|
test("init creates project docs, config, agents, and engine files", () => {
|
|
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-project-"));
|
|
const { projectRoot, config } = initProject({ name: "Test Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
|
|
expect(existsSync(path.join(projectRoot, "project-config.json"))).toBe(true);
|
|
expect(existsSync(path.join(projectRoot, "source", "project-test-game", "project.godot"))).toBe(true);
|
|
expect(existsSync(path.join(projectRoot, "resources", "market-research", "market-overview.md"))).toBe(true);
|
|
expect(existsSync(path.join(projectRoot, "documentation", "design", "gdd.md"))).toBe(true);
|
|
expect(existsSync(path.join(projectRoot, ".gamestudio", "agents", "master_orchestrator.md"))).toBe(true);
|
|
expect(readFileSync(path.join(projectRoot, "AGENTS.md"), "utf8")).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([]);
|
|
expect(config.project.monetization).toBe("undecided");
|
|
expect(config.project.timeline).toBe("TBD");
|
|
expect(existsSync(path.join(projectRoot, "resources", "market-research", "mini-metro.md"))).toBe(false);
|
|
});
|
|
|
|
test("init requires explicit non-interactive mode and supports optional overrides", () => {
|
|
const cwd = mkdtempSync(path.join(tmpdir(), "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",
|
|
nonInteractive: true,
|
|
competitors: ["terra nil", "mini metro"],
|
|
engineVersion: "4.5.custom"
|
|
}, cwd);
|
|
expect(config.project.competitors).toEqual(["terra nil", "mini metro"]);
|
|
expect(config.project.engine_version).toBe("4.5.custom");
|
|
});
|
|
|
|
test("CLI init requires mode and non-interactive and accepts repeated competitor flags", () => {
|
|
const cwd = mkdtempSync(path.join(tmpdir(), "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",
|
|
"--non-interactive",
|
|
"--competitor",
|
|
"terra nil",
|
|
"--competitor",
|
|
"mini metro",
|
|
"--engine-version",
|
|
"4.5.custom"
|
|
], { cwd, encoding: "utf8" });
|
|
const config = readProjectConfig(path.join(cwd, "projects", "cli-game", "project-config.json"));
|
|
expect(config.project.competitors).toEqual(["terra nil", "mini metro"]);
|
|
expect(config.project.engine_version).toBe("4.5.custom");
|
|
});
|
|
|
|
test("CLI init does not expose arbitrary project root override", () => {
|
|
const cli = path.join(process.cwd(), "src", "cli.ts");
|
|
const tsx = path.join(process.cwd(), "node_modules", ".bin", "tsx");
|
|
const help = execFileSync(tsx, [cli, "init", "--help"], { encoding: "utf8" });
|
|
expect(help).not.toContain("--root");
|
|
});
|
|
|
|
test("init ignores arbitrary root override and stays under projects slug", () => {
|
|
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-root-"));
|
|
const outsideRoot = path.join(cwd, "outside-root");
|
|
const { projectRoot } = initProject({ name: "Root Escape", engine: "godot", mode: "prototype", nonInteractive: true, root: outsideRoot } as Parameters<typeof initProject>[0], cwd);
|
|
expect(projectRoot).toBe(path.join(cwd, "projects", "root-escape"));
|
|
expect(existsSync(outsideRoot)).toBe(false);
|
|
});
|
|
|
|
test("all engines initialize expected files", () => {
|
|
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-engines-"));
|
|
expect(existsSync(path.join(initProject({ name: "Godot Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd).projectRoot, "source", "project-godot-game", "project.godot"))).toBe(true);
|
|
expect(existsSync(path.join(initProject({ name: "Unity Game", engine: "unity", mode: "design", nonInteractive: true }, cwd).projectRoot, "source", "project-unity-game", "Packages", "manifest.json"))).toBe(true);
|
|
expect(existsSync(path.join(initProject({ name: "Unreal Game", engine: "Unreal Engine", mode: "development", nonInteractive: true }, cwd).projectRoot, "source", "project-unreal-game", "UnrealGame.uproject"))).toBe(true);
|
|
});
|
|
|
|
test("same-parent init rejects Unreal class-name collisions", () => {
|
|
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-collision-"));
|
|
initProject({ name: "Foo1", engine: "unreal", mode: "prototype", nonInteractive: true }, cwd);
|
|
expect(() => initProject({ name: "Foo 1", engine: "unreal", mode: "prototype", nonInteractive: true }, cwd)).toThrow(/collides/i);
|
|
});
|
|
|
|
test("status resume are read-only and freeze only changes operational status", () => {
|
|
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-status-"));
|
|
const { projectRoot } = initProject({ name: "Freeze Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
|
|
const before = readFileSync(path.join(projectRoot, "project-config.json"), "utf8");
|
|
expect(statusProject(projectRoot, cwd)).toContain("status: active");
|
|
expect(resumeProject(projectRoot, cwd)).toContain("Suggested next command");
|
|
expect(readFileSync(path.join(projectRoot, "project-config.json"), "utf8")).toBe(before);
|
|
const hash = guidanceConfigHash(readProjectConfig(path.join(projectRoot, "project-config.json")));
|
|
freezeProject(projectRoot, cwd);
|
|
expect(readProjectConfig(path.join(projectRoot, "project-config.json")).project.status).toBe("frozen");
|
|
expect(guidanceConfigHash(readProjectConfig(path.join(projectRoot, "project-config.json")))).toBe(hash);
|
|
});
|
|
});
|