mirror of
https://github.com/merlinhu1/codex-game-studio.git
synced 2026-08-25 07:54:34 +02:00
* docs: remove implemented OpenSpec change Remove the completed eval framework OpenSpec change documents and add a CCGS gap task reference based on the parity reports and upstream CCGS surfaces. * feat: generate CCGS gap report Add an OpenSpec-backed implementation for deterministic remaining-gap reporting from the CCGS parity matrix, including tests and regenerated references. * feat: deepen accessibility role parity Add OpenSpec-backed CCGS-depth coverage for the accessibility specialist role, including role contract tests, parity regression tests, and regenerated gap reports. * feat: close direct role depth gaps Add OpenSpec-backed CCGS-depth contracts for the remaining direct role package gaps, plus parity tests and regenerated remaining-gap reports. Stop before engine sub-specialist, workflow, template, and rule work. * feat: add engine sub-specialist roles Add OpenSpec-backed CCGS engine sub-specialist parity, selected-engine role sets, wrong-engine gating, focused engine references, generated custom-agent surfaces, tests, and regenerated parity reports. * feat: close first workflow parity batch * feat: close remaining workflow parity gaps * feat: close final CCGS parity gaps * chore: remove implemented OpenSpec changes --------- Co-authored-by: MerlinH <merlinh221@gmail.com>
240 lines
10 KiB
TypeScript
240 lines
10 KiB
TypeScript
import { describe, test } from "node:test";
|
|
import { expect } from "expect";
|
|
import { engineForRole, engineRoleIdsForEngine, engineRoleSets, isRoleAvailableForEngine, isStudioRoleId, rolePackages, studioRoleIds, unknownStudioRoleMessage } from "../src/roles.js";
|
|
|
|
describe("Codex role packages", () => {
|
|
test("defines all canonical StudioRoleId packages", () => {
|
|
expect(studioRoleIds).toEqual([
|
|
"studio-orchestrator",
|
|
"producer",
|
|
"market-analyst",
|
|
"data-scientist",
|
|
"creative-director",
|
|
"senior-game-designer",
|
|
"game-designer",
|
|
"narrative-designer",
|
|
"writer",
|
|
"world-builder",
|
|
"level-designer",
|
|
"game-feel-designer",
|
|
"systems-designer",
|
|
"economy-designer",
|
|
"gameplay-programmer",
|
|
"ai-programmer",
|
|
"network-programmer",
|
|
"ui-programmer",
|
|
"engine-programmer",
|
|
"godot-specialist",
|
|
"godot-gdscript-specialist",
|
|
"godot-csharp-specialist",
|
|
"godot-shader-specialist",
|
|
"godot-gdextension-specialist",
|
|
"unity-specialist",
|
|
"unity-dots-specialist",
|
|
"unity-shader-specialist",
|
|
"unity-addressables-specialist",
|
|
"unity-ui-specialist",
|
|
"unreal-specialist",
|
|
"ue-gas-specialist",
|
|
"ue-blueprint-specialist",
|
|
"ue-replication-specialist",
|
|
"ue-umg-specialist",
|
|
"tools-programmer",
|
|
"technical-director",
|
|
"devops-engineer",
|
|
"security-engineer",
|
|
"performance-analyst",
|
|
"senior-game-artist",
|
|
"technical-artist",
|
|
"audio-director",
|
|
"sound-designer",
|
|
"ui-ux-designer",
|
|
"accessibility-specialist",
|
|
"qa-playtester",
|
|
"localization-lead",
|
|
"live-ops-designer",
|
|
"community-manager",
|
|
"release-manager"
|
|
]);
|
|
expect(Object.keys(rolePackages).sort()).toEqual([...studioRoleIds].sort());
|
|
});
|
|
|
|
test("role packages have non-empty Codex contracts", () => {
|
|
for (const role of Object.values(rolePackages)) {
|
|
expect(role.displayName).toMatch(/\S/);
|
|
expect(role.systemPrompt).toMatch(/\S/);
|
|
expect(role.expectedOutputs.length).toBeGreaterThan(0);
|
|
expect(role.handoffTemplate).toMatch(/\S/);
|
|
expect(role.reviewChecklist.length).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
|
|
test("role packages cover bounded specialist domain clusters", () => {
|
|
const clusters = {
|
|
audio: ["audio-director", "sound-designer"],
|
|
content: ["writer", "world-builder", "level-designer"],
|
|
systems: ["systems-designer", "economy-designer"],
|
|
operations: ["live-ops-designer", "community-manager", "localization-lead", "accessibility-specialist"],
|
|
technical: ["technical-director", "devops-engineer", "security-engineer", "performance-analyst", "network-programmer", "ai-programmer", "ui-programmer"]
|
|
} as const;
|
|
|
|
for (const roles of Object.values(clusters)) {
|
|
for (const role of roles) {
|
|
expect(studioRoleIds).toContain(role);
|
|
expect(rolePackages[role].expectedOutputs.length).toBeGreaterThanOrEqual(2);
|
|
expect(rolePackages[role].reviewChecklist.length).toBeGreaterThanOrEqual(2);
|
|
}
|
|
}
|
|
|
|
expect(rolePackages["audio-director"].systemPrompt).toContain("audio");
|
|
expect(rolePackages["network-programmer"].systemPrompt).toContain("network");
|
|
expect(rolePackages["accessibility-specialist"].systemPrompt).toContain("accessibility");
|
|
});
|
|
|
|
test("engine sub-specialists are first-class engine-scoped role packages", () => {
|
|
expect(engineRoleSets).toEqual({
|
|
godot: ["godot-specialist", "godot-gdscript-specialist", "godot-csharp-specialist", "godot-shader-specialist", "godot-gdextension-specialist"],
|
|
unity: ["unity-specialist", "unity-dots-specialist", "unity-shader-specialist", "unity-addressables-specialist", "unity-ui-specialist"],
|
|
unreal: ["unreal-specialist", "ue-gas-specialist", "ue-blueprint-specialist", "ue-replication-specialist", "ue-umg-specialist"]
|
|
});
|
|
const roleTerms = {
|
|
"godot-gdscript-specialist": ["GDScript", "static typing", "signals"],
|
|
"godot-csharp-specialist": ["C#", ".NET", "Signal"],
|
|
"godot-shader-specialist": ["shader", "rendering", "VFX"],
|
|
"godot-gdextension-specialist": ["GDExtension", "native", "bindings"],
|
|
"unity-dots-specialist": ["DOTS", "ECS", "Burst"],
|
|
"unity-shader-specialist": ["shader", "VFX", "rendering"],
|
|
"unity-addressables-specialist": ["Addressables", "asset", "memory"],
|
|
"unity-ui-specialist": ["UI Toolkit", "UGUI", "input"],
|
|
"ue-gas-specialist": ["Gameplay Ability System", "attributes", "prediction"],
|
|
"ue-blueprint-specialist": ["Blueprint", "C++", "graph"],
|
|
"ue-replication-specialist": ["replication", "RPC", "bandwidth"],
|
|
"ue-umg-specialist": ["UMG", "CommonUI", "widget"]
|
|
} as const;
|
|
for (const [roleId, terms] of Object.entries(roleTerms)) {
|
|
expect(studioRoleIds).toContain(roleId);
|
|
const role = rolePackages[roleId as keyof typeof roleTerms];
|
|
const contractText = [role.systemPrompt, ...role.responsibilities, ...(role.outputSchema ?? []), ...role.qualityGates, ...role.collaborationNotes, ...role.stopConditions, role.handoffTemplate].join("\n");
|
|
expect(role.responsibilities.length).toBeGreaterThanOrEqual(3);
|
|
expect(role.outputSchema?.length ?? 0).toBeGreaterThanOrEqual(4);
|
|
expect(role.qualityGates.length).toBeGreaterThanOrEqual(3);
|
|
expect(role.collaborationNotes.length).toBeGreaterThanOrEqual(2);
|
|
expect(role.stopConditions.length).toBeGreaterThanOrEqual(1);
|
|
expect(role.handoffTemplate).toContain(role.displayName);
|
|
for (const term of terms) expect(contractText).toContain(term);
|
|
}
|
|
});
|
|
|
|
test("engine role helpers expose selected-engine role sets and wrong-engine availability", () => {
|
|
expect(engineRoleIdsForEngine("godot")).toEqual(engineRoleSets.godot);
|
|
expect(engineForRole("godot-gdscript-specialist")).toBe("godot");
|
|
expect(engineForRole("unity-dots-specialist")).toBe("unity");
|
|
expect(engineForRole("ue-gas-specialist")).toBe("unreal");
|
|
expect(engineForRole("gameplay-programmer")).toBeUndefined();
|
|
expect(isRoleAvailableForEngine("godot-gdscript-specialist", "godot")).toBe(true);
|
|
expect(isRoleAvailableForEngine("unity-dots-specialist", "godot")).toBe(false);
|
|
expect(isRoleAvailableForEngine("gameplay-programmer", "godot")).toBe(true);
|
|
});
|
|
|
|
|
|
|
|
test("accessibility specialist exposes CCGS-depth accessibility contract", () => {
|
|
const role = rolePackages["accessibility-specialist"];
|
|
const contractText = [
|
|
...role.responsibilities,
|
|
...role.inputsToInspect,
|
|
...(role.outputSchema ?? []),
|
|
...role.qualityGates,
|
|
...role.collaborationNotes,
|
|
...role.stopConditions,
|
|
role.handoffTemplate
|
|
].join("\n");
|
|
|
|
expect(role.outputSchema).toEqual(expect.arrayContaining(["Finding", "WCAG criterion", "Severity", "Recommendation", "Verification steps", "Owner handoff"]));
|
|
for (const phrase of ["WCAG", "contrast", "colorblind", "subtitle", "input remapping", "motor", "cognitive", "structured findings"]) {
|
|
expect(contractText).toContain(phrase);
|
|
}
|
|
expect(contractText).toMatch(/UI Programmer|ui-programmer/);
|
|
expect(contractText).toMatch(/Audio Director|audio-director|Sound Designer|sound-designer/);
|
|
expect(contractText).toMatch(/QA Playtester|qa-playtester/);
|
|
expect(contractText).toMatch(/Localization Lead|localization-lead/);
|
|
expect(contractText).toMatch(/Producer|producer/);
|
|
});
|
|
|
|
test("remaining direct CCGS role gaps expose upgraded role contracts", () => {
|
|
const directRoleGapIds = [
|
|
"ai-programmer",
|
|
"audio-director",
|
|
"community-manager",
|
|
"devops-engineer",
|
|
"economy-designer",
|
|
"engine-programmer",
|
|
"godot-specialist",
|
|
"level-designer",
|
|
"live-ops-designer",
|
|
"localization-lead",
|
|
"network-programmer",
|
|
"security-engineer",
|
|
"sound-designer",
|
|
"technical-artist",
|
|
"tools-programmer",
|
|
"ui-programmer",
|
|
"unity-specialist",
|
|
"unreal-specialist",
|
|
"world-builder",
|
|
"writer"
|
|
] as const;
|
|
|
|
for (const roleId of directRoleGapIds) {
|
|
const role = rolePackages[roleId];
|
|
const contractText = [
|
|
...role.responsibilities,
|
|
...role.inputsToInspect,
|
|
...(role.outputSchema ?? []),
|
|
...role.qualityGates,
|
|
...role.collaborationNotes,
|
|
...role.stopConditions,
|
|
role.handoffTemplate
|
|
].join("\n");
|
|
|
|
expect(role.responsibilities.length).toBeGreaterThanOrEqual(3);
|
|
expect(role.outputSchema?.length ?? 0).toBeGreaterThanOrEqual(4);
|
|
expect(role.qualityGates.length).toBeGreaterThanOrEqual(3);
|
|
expect(role.collaborationNotes.length).toBeGreaterThanOrEqual(2);
|
|
expect(role.stopConditions.length).toBeGreaterThanOrEqual(1);
|
|
expect(role.handoffTemplate).toContain(role.displayName);
|
|
expect(contractText).toContain("verification");
|
|
expect(contractText).toContain("handoff");
|
|
}
|
|
});
|
|
|
|
test("role packages expose structured compact role contracts", () => {
|
|
for (const role of Object.values(rolePackages)) {
|
|
expect(role.responsibilities.length).toBeGreaterThanOrEqual(2);
|
|
expect(role.inputsToInspect.length).toBeGreaterThanOrEqual(2);
|
|
expect(role.qualityGates.length).toBeGreaterThanOrEqual(2);
|
|
expect(role.stopConditions.length).toBeGreaterThanOrEqual(1);
|
|
expect(role.sharedFragments).toBeDefined();
|
|
expect(new Set(role.sharedFragments).size).toBe(role.sharedFragments.length);
|
|
}
|
|
|
|
expect(rolePackages["gameplay-programmer"].outputSchema).toEqual(
|
|
expect.arrayContaining(["Changed files", "Implementation notes", "Verification evidence", "Risks or follow-ups"])
|
|
);
|
|
expect(rolePackages["qa-playtester"].outputSchema).toEqual(
|
|
expect.arrayContaining(["Issue ID", "Severity", "Reproduction steps", "Expected result", "Actual result", "Evidence"])
|
|
);
|
|
expect(rolePackages["release-manager"].outputSchema).toEqual(
|
|
expect.arrayContaining(["Release decision", "Blocking issues", "Warnings", "Validation evidence", "Rollback notes"])
|
|
);
|
|
});
|
|
|
|
test("legacy underscore aliases are rejected with guidance", () => {
|
|
for (const alias of ["producer_agent", "qa_agent", "master_orchestrator"]) {
|
|
expect(isStudioRoleId(alias)).toBe(false);
|
|
expect(unknownStudioRoleMessage(alias)).toContain("Codex-native hyphenated role IDs");
|
|
}
|
|
});
|
|
});
|