2026-06-29 14:11:18 +00:00
import { cpSync , mkdirSync , mkdtempSync , rmSync , symlinkSync , writeFileSync } from "node:fs" ;
2026-05-28 15:11:30 +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 15:11:30 +00:00
import { initProject } from "../src/projects.js" ;
import { prepareRun } from "../src/runner.js" ;
import { createTask , renderTaskRun } from "../src/tasks.js" ;
import { renderWorkflowPrompt } from "../src/workflows.js" ;
2026-06-17 18:47:15 +10:00
import { createContextManifest , selectContextEntries } from "../src/context-manifest.js" ;
2026-05-28 15:11:30 +00:00
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 15:11:30 +00:00
describe ( "Codex context files" , () => {
test ( "runner, workflows, and task prompts use AGENTS.md" , () => {
const cwd = mkdtempSync ( path . join ( tmpdir (), "ogs-context-" ));
2026-06-29 14:11:18 +00:00
const { projectRoot } = initTemplateProject ({ name : "Context Game" , engine : "godot" , mode : "prototype" , nonInteractive : true }, cwd );
2026-05-28 15:11:30 +00:00
const prepared = prepareRun ( "gameplay-programmer" , { project : projectRoot , task : "Implement movement" , dryRun : true }, cwd );
expect ( prepared . contextFiles [ 0 ]). toBe ( "AGENTS.md" );
expect ( prepared . prompt ). toContain ( "AGENTS.md" );
expect ( prepared . output ). toContain ( "- AGENTS.md" );
expect ( prepared . prompt ). not . toContain ( "CODEX.md" );
const workflow = renderWorkflowPrompt ( projectRoot , "vertical-slice" );
expect ( workflow ). toContain ( "AGENTS.md" );
expect ( workflow ). not . toContain ( "CODEX.md" );
const task = createTask ( projectRoot , { title : "Wire jump controls" , role : "gameplay-programmer" });
const taskRun = renderTaskRun ( projectRoot , task . id );
expect ( taskRun . prompt ). toContain ( "AGENTS.md" );
expect ( taskRun . prompt ). not . toContain ( "CODEX.md" );
});
2026-06-17 18:47:15 +10:00
test ( "path-safe selector records required, missing, unsafe, and budgeted context" , () => {
const cwd = mkdtempSync ( path . join ( tmpdir (), "ogs-context-select-" ));
2026-06-29 14:11:18 +00:00
const { projectRoot } = initTemplateProject ({ name : "Selector Game" , engine : "godot" , mode : "prototype" , nonInteractive : true }, cwd );
2026-06-29 09:46:45 +00:00
const outsideDir = mkdtempSync ( path . join ( tmpdir (), "ogs-outside-" ));
const outside = path . join ( outsideDir , "outside.md" );
2026-06-17 18:47:15 +10:00
writeFileSync ( outside , "outside" );
2026-06-29 09:46:45 +00:00
symlinkSync ( outside , path . join ( projectRoot , "design" , "escape.md" ));
writeFileSync ( path . join ( projectRoot , "design" , "large.md" ), "x" . repeat ( 20 _000 ));
2026-06-17 18:47:15 +10:00
const result = selectContextEntries ( projectRoot , [
{ sourcePath : "AGENTS.md" , reason : "project instructions" , required : true },
2026-06-29 09:46:45 +00:00
{ sourcePath : "design/missing.md" , reason : "missing required" , required : true },
{ sourcePath : "design/gdd.md" , reason : "design reference" },
{ sourcePath : "production/timeline.md" , reason : "production reference" },
{ sourcePath : "design/large.md" , reason : "large optional" },
2026-06-17 18:47:15 +10:00
{ sourcePath : "/absolute.md" , reason : "absolute" , required : true },
{ sourcePath : "../escape.md" , reason : "traversal" , required : true },
{ sourcePath : ".env" , reason : "secret" , required : true },
2026-06-29 09:46:45 +00:00
{ sourcePath : "design/escape.md" , reason : "symlink escape" , required : true }
2026-06-17 18:47:15 +10:00
], { maxFiles : 2 , maxChars : 50_000 , maxEntryChars : 10_000 });
2026-06-29 09:46:45 +00:00
expect ( result . selected . map (( entry ) => entry . sourcePath )). toEqual ([ "AGENTS.md" , "design/gdd.md" ]);
2026-06-17 18:47:15 +10:00
expect ( result . entries ). toContainEqual ( expect . objectContaining ({ sourcePath : "AGENTS.md" , status : "selected" , required : true , reason : "project instructions" }));
2026-06-29 09:46:45 +00:00
expect ( result . entries ). toContainEqual ( expect . objectContaining ({ sourcePath : "design/missing.md" , status : "missing" }));
2026-06-17 18:47:15 +10:00
expect ( result . entries ). toContainEqual ( expect . objectContaining ({ sourcePath : "/absolute.md" , status : "rejected" , safety : "unsafe" , statusReason : expect.stringMatching ( /absolute/i ) }));
expect ( result . entries ). toContainEqual ( expect . objectContaining ({ sourcePath : "../escape.md" , status : "rejected" , safety : "unsafe" , statusReason : expect.stringMatching ( /traversal/i ) }));
expect ( result . entries ). toContainEqual ( expect . objectContaining ({ sourcePath : ".env" , status : "rejected" , safety : "secret" }));
2026-06-29 09:46:45 +00:00
expect ( result . entries ). toContainEqual ( expect . objectContaining ({ sourcePath : "design/escape.md" , status : "rejected" , statusReason : expect.stringMatching ( /symlink/i ) }));
expect ( result . entries ). toContainEqual ( expect . objectContaining ({ sourcePath : "design/large.md" , status : "omitted" , statusReason : expect.stringMatching ( /entry character budget/i ) }));
expect ( result . entries ). toContainEqual ( expect . objectContaining ({ sourcePath : "production/timeline.md" , status : "omitted" , statusReason : expect.stringMatching ( /file count budget/i ) }));
2026-06-17 18:47:15 +10:00
});
test ( "selector prioritizes required context over earlier optional context within file budget" , () => {
const cwd = mkdtempSync ( path . join ( tmpdir (), "ogs-context-required-budget-" ));
2026-06-29 14:11:18 +00:00
const { projectRoot } = initTemplateProject ({ name : "Required Budget Game" , engine : "godot" , mode : "prototype" , nonInteractive : true }, cwd );
2026-06-17 18:47:15 +10:00
const result = selectContextEntries ( projectRoot , [
2026-06-29 09:46:45 +00:00
{ sourcePath : "design/gdd.md" , reason : "optional design reference" },
2026-06-17 18:47:15 +10:00
{ sourcePath : "AGENTS.md" , reason : "project instructions" , required : true }
], { maxFiles : 1 , maxChars : 10_000 , maxEntryChars : 10_000 });
expect ( result . selected . map (( entry ) => entry . sourcePath )). toEqual ([ "AGENTS.md" ]);
expect ( result . budget . usedFiles ). toBeLessThanOrEqual ( 1 );
2026-06-29 09:46:45 +00:00
expect ( result . entries ). toContainEqual ( expect . objectContaining ({ sourcePath : "design/gdd.md" , status : "omitted" , statusReason : expect.stringMatching ( /file count budget/i ) }));
2026-06-17 18:47:15 +10:00
});
test ( "selector omits oversized required context instead of bypassing character budgets" , () => {
const cwd = mkdtempSync ( path . join ( tmpdir (), "ogs-context-required-size-" ));
2026-06-29 14:11:18 +00:00
const { projectRoot } = initTemplateProject ({ name : "Required Size Game" , engine : "godot" , mode : "prototype" , nonInteractive : true }, cwd );
2026-06-29 09:46:45 +00:00
writeFileSync ( path . join ( projectRoot , "design" , "huge-required.md" ), "x" . repeat ( 20 _000 ));
2026-06-17 18:47:15 +10:00
const result = selectContextEntries ( projectRoot , [
2026-06-29 09:46:45 +00:00
{ sourcePath : "design/huge-required.md" , reason : "required oversized reference" , required : true },
2026-06-17 18:47:15 +10:00
{ sourcePath : "AGENTS.md" , reason : "project instructions" , required : true }
], { maxFiles : 2 , maxChars : 50_000 , maxEntryChars : 10_000 });
expect ( result . selected . map (( entry ) => entry . sourcePath )). toEqual ([ "AGENTS.md" ]);
expect ( result . budget . usedChars ). toBeLessThanOrEqual ( 50 _000 );
2026-06-29 09:46:45 +00:00
expect ( result . entries ). toContainEqual ( expect . objectContaining ({ sourcePath : "design/huge-required.md" , status : "omitted" , statusReason : expect.stringMatching ( /entry character budget/i ) }));
2026-06-17 18:47:15 +10:00
});
test ( "selector rejects dotenv variants as secret-like paths" , () => {
const cwd = mkdtempSync ( path . join ( tmpdir (), "ogs-context-env-local-" ));
2026-06-29 14:11:18 +00:00
const { projectRoot } = initTemplateProject ({ name : "Env Local Game" , engine : "godot" , mode : "prototype" , nonInteractive : true }, cwd );
2026-06-17 18:47:15 +10:00
writeFileSync ( path . join ( projectRoot , ".env.local" ), "TOKEN=secret\n" );
const result = selectContextEntries ( projectRoot , [
{ sourcePath : ".env.local" , reason : "local secrets" , required : true }
]);
expect ( result . selected ). toHaveLength ( 0 );
expect ( result . entries ). toContainEqual ( expect . objectContaining ({ sourcePath : ".env.local" , status : "rejected" , safety : "secret" }));
});
test ( "implement workflow context contract does not mix read-only policy with writable permissions" , () => {
const cwd = mkdtempSync ( path . join ( tmpdir (), "ogs-workflow-contract-" ));
2026-06-29 14:11:18 +00:00
const { projectRoot } = initTemplateProject ({ name : "Workflow Contract Game" , engine : "godot" , mode : "prototype" , studioMode : "fast-prototype" , nonInteractive : true }, cwd );
2026-06-17 18:47:15 +10:00
const workflow = renderWorkflowPrompt ( projectRoot , "bugfix" );
expect ( workflow ). toContain ( "Phase: implement" );
expect ( workflow ). toContain ( "Write Policy: advisory-write" );
expect ( workflow ). toContain ( "Sandbox: danger-full-access" );
expect ( workflow ). toContain ( "File Edits: allowed" );
expect ( workflow ). not . toContain ( "Write Policy: read-only\nSandbox: danger-full-access\nFile Edits: allowed" );
});
test ( "broad context selection records missing required files instead of widening reads" , () => {
const cwd = mkdtempSync ( path . join ( tmpdir (), "ogs-context-missing-" ));
2026-06-29 14:11:18 +00:00
const { projectRoot } = initTemplateProject ({ name : "Missing Context Game" , engine : "godot" , mode : "prototype" , nonInteractive : true }, cwd );
2026-06-29 09:46:45 +00:00
rmSync ( path . join ( projectRoot , "design" , "gdd.md" ));
2026-06-17 18:47:15 +10:00
mkdirSync ( path . join ( projectRoot , "notes" ), { recursive : true });
writeFileSync ( path . join ( projectRoot , "notes" , "unrequested.md" ), "do not include" );
const result = selectContextEntries ( projectRoot , [
2026-06-29 09:46:45 +00:00
{ sourcePath : "design/gdd.md" , reason : "required design reference" , required : true },
2026-06-17 18:47:15 +10:00
{ sourcePath : "AGENTS.md" , reason : "project instructions" , required : true }
]);
expect ( result . selected . map (( entry ) => entry . sourcePath )). toEqual ([ "AGENTS.md" ]);
2026-06-29 09:46:45 +00:00
expect ( result . entries ). toContainEqual ( expect . objectContaining ({ sourcePath : "design/gdd.md" , status : "missing" }));
2026-06-17 18:47:15 +10:00
expect ( result . entries . map (( entry ) => entry . sourcePath )). not . toContain ( "notes/unrequested.md" );
});
test ( "context manifest selects active engine references without unrelated engine docs" , () => {
const cwd = mkdtempSync ( path . join ( tmpdir (), "ogs-context-engine-reference-" ));
2026-06-29 14:11:18 +00:00
const { projectRoot } = initTemplateProject ({ name : "Unity Context Game" , engine : "unity" , mode : "prototype" , nonInteractive : true }, cwd );
2026-06-17 18:47:15 +10:00
const manifest = createContextManifest ( projectRoot , {
schemaVersion : 1 ,
product : "codex-game-studio" ,
name : "Unity Context Game" ,
slug : "unity-context-game" ,
concept : "Unity Context Game concept" ,
genre : "Unspecified" ,
platform : "PC" ,
audience : "General players" ,
competitors : [],
monetization : "undecided" ,
timeline : "TBD" ,
engine : "unity" ,
engineVersion : "2022 LTS" ,
mode : "prototype" ,
studioMode : "guided-studio" ,
phase : "Initialization" ,
status : "active" ,
currentMilestone : "prototype" ,
roles : [],
activeRoles : [ "gameplay-programmer" ],
workflows : []
});
const sources = manifest . manifest . entries . map (( entry ) => entry . sourcePath );
expect ( sources ). toContain ( "docs/engine-reference/unity/VERSION.md" );
2026-06-17 20:19:36 +00:00
expect ( sources ). toContain ( "docs/engine-reference/unity/current-best-practices.md" );
2026-06-17 18:47:15 +10:00
expect ( sources ). toContain ( "docs/engine-reference/unity/gameplay.md" );
expect ( sources . some (( source ) => source . startsWith ( "docs/engine-reference/unreal/" ))). toBe ( false );
});
2026-06-17 20:19:36 +00:00
test ( "role runs and workflow prompts select task-relevant engine references without broad loading" , () => {
const cwd = mkdtempSync ( path . join ( tmpdir (), "ogs-context-engine-task-" ));
2026-06-29 14:11:18 +00:00
const { projectRoot } = initTemplateProject ({ name : "Godot Netcode Game" , engine : "godot" , mode : "prototype" , nonInteractive : true }, cwd );
2026-06-17 20:19:36 +00:00
const run = prepareRun ( "gameplay-programmer" , { project : projectRoot , task : "Implement rollback networking input sync" , printPrompt : true }, cwd );
expect ( run . contextFiles ). toContain ( "docs/engine-reference/godot/modules/networking.md" );
expect ( run . contextFiles ). toContain ( "docs/engine-reference/godot/modules/input.md" );
expect ( run . contextFiles ). not . toContain ( "docs/engine-reference/godot/modules/audio.md" );
expect ( run . contextFiles . some (( source ) => source . startsWith ( "docs/engine-reference/unity/" ))). toBe ( false );
const workflow = renderWorkflowPrompt ( projectRoot , "bugfix" );
expect ( workflow ). toContain ( "docs/engine-reference/godot/current-best-practices.md" );
expect ( workflow ). not . toContain ( "docs/engine-reference/godot/modules/audio.md" );
});
2026-05-28 15:11:30 +00:00
});