nccgs 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/nccgs-adversarial-reviewer.md +7 -1
- package/.claude/agents/nccgs-engine-programmer.md +2 -1
- package/.claude/agents/nccgs-gameplay-programmer.md +2 -1
- package/.claude/agents/nccgs-qa-engineer.md +2 -1
- package/.claude/agents/nccgs-ui-programmer.md +2 -1
- package/.claude/agents/nccgs-unity-build-specialist.md +2 -1
- package/.claude/agents/nccgs-unity-implementer.md +2 -1
- package/.claude/agents/nccgs-unity-systems-specialist.md +2 -1
- package/.claude/agents/nccgs-unity-ui-specialist.md +2 -1
- package/.claude/agents/nccgs-verification-engineer.md +2 -1
- package/.claude/nccgs/VERSION +1 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-accessibility-specialist.md +0 -1
- package/.claude/nccgs/agent-library/nccgs-adversarial-reviewer.md +30 -0
- package/.claude/{agents → nccgs/agent-library}/nccgs-ai-programmer.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-analytics-engineer.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-art-direction-lead.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-audio-direction-lead.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-creative-director.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-documentation-manager.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-economy-designer.md +0 -1
- package/.claude/nccgs/agent-library/nccgs-engine-programmer.md +25 -0
- package/.claude/{agents → nccgs/agent-library}/nccgs-game-design-lead.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-game-designer.md +0 -1
- package/.claude/nccgs/agent-library/nccgs-gameplay-programmer.md +25 -0
- package/.claude/{agents → nccgs/agent-library}/nccgs-level-designer.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-live-ops-designer.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-localization-lead.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-narrative-lead.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-network-programmer.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-performance-analyst.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-producer.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-production-coordinator.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-programming-lead.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-prototyper.md +0 -1
- package/.claude/nccgs/agent-library/nccgs-qa-engineer.md +25 -0
- package/.claude/{agents → nccgs/agent-library}/nccgs-qa-lead.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-release-engineer.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-release-lead.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-security-engineer.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-sound-designer.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-systems-designer.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-technical-architect.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-technical-artist.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-technical-director.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-tools-programmer.md +0 -1
- package/.claude/nccgs/agent-library/nccgs-ui-programmer.md +25 -0
- package/.claude/nccgs/agent-library/nccgs-unity-build-specialist.md +25 -0
- package/.claude/{agents → nccgs/agent-library}/nccgs-unity-content-specialist.md +0 -1
- package/.claude/nccgs/agent-library/nccgs-unity-implementer.md +25 -0
- package/.claude/{agents → nccgs/agent-library}/nccgs-unity-rendering-specialist.md +0 -1
- package/.claude/nccgs/agent-library/nccgs-unity-systems-specialist.md +25 -0
- package/.claude/nccgs/agent-library/nccgs-unity-ui-specialist.md +25 -0
- package/.claude/{agents → nccgs/agent-library}/nccgs-ux-designer.md +0 -1
- package/.claude/nccgs/agent-library/nccgs-verification-engineer.md +25 -0
- package/.claude/{agents → nccgs/agent-library}/nccgs-world-builder.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-writer.md +0 -1
- package/.claude/nccgs/constitution.md +13 -0
- package/.claude/nccgs/hooks/agent-audit.mjs +6 -0
- package/.claude/nccgs/hooks/closure-guard.mjs +34 -0
- package/.claude/nccgs/hooks/runtime-observation.mjs +11 -0
- package/.claude/nccgs/hooks/session-start.mjs +8 -1
- package/.claude/nccgs/pipeline.json +176 -0
- package/.claude/nccgs/protocols/agent-contract.md +5 -0
- package/.claude/nccgs/protocols/astra-claude.md +77 -0
- package/.claude/nccgs/protocols/evidence.md +6 -0
- package/.claude/nccgs/protocols/model-routing.md +27 -5
- package/.claude/nccgs/protocols/orchestration.md +5 -0
- package/.claude/nccgs/protocols/task-gates.md +66 -0
- package/.claude/nccgs/routing.generated.md +11 -0
- package/.claude/nccgs/settings.fragment.json +73 -9
- package/.claude/nccgs/studio.json +187 -96
- package/.claude/nccgs/tools/approve-overrides.mjs +81 -0
- package/.claude/nccgs/tools/compile-policy.mjs +83 -0
- package/.claude/nccgs/tools/configure-models.mjs +76 -31
- package/.claude/nccgs/tools/gates.mjs +167 -0
- package/.claude/nccgs/tools/integrity.mjs +69 -0
- package/.claude/nccgs/tools/policy.mjs +62 -0
- package/.claude/nccgs/tools/runtime.mjs +40 -0
- package/.claude/nccgs/tools/task.mjs +77 -0
- package/.claude/nccgs/workflow-catalog.json +91 -14
- package/.claude/skills/accessibility-review/SKILL.md +6 -5
- package/.claude/skills/accessibility-review/references/astra.md +17 -0
- package/.claude/skills/accessibility-review/references/legacy.md +7 -0
- package/.claude/skills/architecture-decision/SKILL.md +6 -5
- package/.claude/skills/architecture-decision/references/astra.md +12 -0
- package/.claude/skills/architecture-decision/references/legacy.md +7 -0
- package/.claude/skills/asset-audit/SKILL.md +6 -5
- package/.claude/skills/asset-audit/references/astra.md +17 -0
- package/.claude/skills/asset-audit/references/legacy.md +7 -0
- package/.claude/skills/audit/SKILL.md +6 -7
- package/.claude/skills/audit/references/astra.md +17 -0
- package/.claude/skills/audit/references/legacy.md +9 -0
- package/.claude/skills/balance-review/SKILL.md +6 -5
- package/.claude/skills/balance-review/references/astra.md +17 -0
- package/.claude/skills/balance-review/references/legacy.md +7 -0
- package/.claude/skills/bug-triage/SKILL.md +6 -5
- package/.claude/skills/bug-triage/references/astra.md +20 -0
- package/.claude/skills/bug-triage/references/legacy.md +7 -0
- package/.claude/skills/closure/SKILL.md +5 -4
- package/.claude/skills/closure/references/astra.md +15 -0
- package/.claude/skills/closure/references/legacy.md +7 -0
- package/.claude/skills/compatibility-review/SKILL.md +6 -5
- package/.claude/skills/compatibility-review/references/astra.md +17 -0
- package/.claude/skills/compatibility-review/references/legacy.md +7 -0
- package/.claude/skills/context-pack/SKILL.md +5 -4
- package/.claude/skills/context-pack/references/astra.md +12 -0
- package/.claude/skills/context-pack/references/legacy.md +7 -0
- package/.claude/skills/dependency-review/SKILL.md +6 -5
- package/.claude/skills/dependency-review/references/astra.md +17 -0
- package/.claude/skills/dependency-review/references/legacy.md +7 -0
- package/.claude/skills/design/SKILL.md +6 -11
- package/.claude/skills/design/references/astra.md +12 -0
- package/.claude/skills/design/references/legacy.md +13 -0
- package/.claude/skills/design-review/SKILL.md +6 -5
- package/.claude/skills/design-review/references/astra.md +17 -0
- package/.claude/skills/design-review/references/legacy.md +7 -0
- package/.claude/skills/evidence-review/SKILL.md +6 -5
- package/.claude/skills/evidence-review/references/astra.md +17 -0
- package/.claude/skills/evidence-review/references/legacy.md +7 -0
- package/.claude/skills/hotfix/SKILL.md +6 -5
- package/.claude/skills/hotfix/references/astra.md +23 -0
- package/.claude/skills/hotfix/references/legacy.md +7 -0
- package/.claude/skills/incident-recovery/SKILL.md +6 -5
- package/.claude/skills/incident-recovery/references/astra.md +20 -0
- package/.claude/skills/incident-recovery/references/legacy.md +7 -0
- package/.claude/skills/localize-game/SKILL.md +6 -5
- package/.claude/skills/localize-game/references/astra.md +20 -0
- package/.claude/skills/localize-game/references/legacy.md +7 -0
- package/.claude/skills/migrate-project/SKILL.md +6 -14
- package/.claude/skills/migrate-project/references/astra.md +23 -0
- package/.claude/skills/migrate-project/references/legacy.md +19 -0
- package/.claude/skills/migrate-project/references/procedure.md +21 -1
- package/.claude/skills/milestone-review/SKILL.md +6 -5
- package/.claude/skills/milestone-review/references/astra.md +12 -0
- package/.claude/skills/milestone-review/references/legacy.md +7 -0
- package/.claude/skills/nccgs-code-review/SKILL.md +15 -0
- package/.claude/skills/nccgs-code-review/references/astra.md +15 -0
- package/.claude/skills/{code-review/SKILL.md → nccgs-code-review/references/legacy.md} +0 -7
- package/.claude/skills/performance-audit/SKILL.md +6 -5
- package/.claude/skills/performance-audit/references/astra.md +17 -0
- package/.claude/skills/performance-audit/references/legacy.md +7 -0
- package/.claude/skills/plan-feature/SKILL.md +6 -5
- package/.claude/skills/plan-feature/references/astra.md +12 -0
- package/.claude/skills/plan-feature/references/legacy.md +7 -0
- package/.claude/skills/playtest/SKILL.md +5 -4
- package/.claude/skills/playtest/references/astra.md +20 -0
- package/.claude/skills/playtest/references/legacy.md +7 -0
- package/.claude/skills/project-stage/SKILL.md +5 -4
- package/.claude/skills/project-stage/references/astra.md +12 -0
- package/.claude/skills/project-stage/references/legacy.md +7 -0
- package/.claude/skills/prototype-feature/SKILL.md +6 -5
- package/.claude/skills/prototype-feature/references/astra.md +20 -0
- package/.claude/skills/prototype-feature/references/legacy.md +7 -0
- package/.claude/skills/qa-plan/SKILL.md +6 -5
- package/.claude/skills/qa-plan/references/astra.md +12 -0
- package/.claude/skills/qa-plan/references/legacy.md +7 -0
- package/.claude/skills/release/SKILL.md +6 -9
- package/.claude/skills/release/references/astra.md +23 -0
- package/.claude/skills/release/references/legacy.md +11 -0
- package/.claude/skills/release-readiness/SKILL.md +6 -5
- package/.claude/skills/release-readiness/references/astra.md +17 -0
- package/.claude/skills/release-readiness/references/legacy.md +7 -0
- package/.claude/skills/retrospective/SKILL.md +6 -5
- package/.claude/skills/retrospective/references/astra.md +12 -0
- package/.claude/skills/retrospective/references/legacy.md +7 -0
- package/.claude/skills/review/SKILL.md +6 -7
- package/.claude/skills/review/references/astra.md +15 -0
- package/.claude/skills/review/references/legacy.md +9 -0
- package/.claude/skills/security-audit/SKILL.md +6 -5
- package/.claude/skills/security-audit/references/astra.md +17 -0
- package/.claude/skills/security-audit/references/legacy.md +7 -0
- package/.claude/skills/sprint-plan/SKILL.md +6 -5
- package/.claude/skills/sprint-plan/references/astra.md +12 -0
- package/.claude/skills/sprint-plan/references/legacy.md +7 -0
- package/.claude/skills/status/SKILL.md +5 -6
- package/.claude/skills/status/references/astra.md +13 -0
- package/.claude/skills/status/references/legacy.md +9 -0
- package/.claude/skills/story-readiness/SKILL.md +5 -4
- package/.claude/skills/story-readiness/references/astra.md +17 -0
- package/.claude/skills/story-readiness/references/legacy.md +7 -0
- package/.claude/skills/test/SKILL.md +6 -7
- package/.claude/skills/test/references/astra.md +20 -0
- package/.claude/skills/test/references/legacy.md +9 -0
- package/.claude/skills/ui-review/SKILL.md +6 -5
- package/.claude/skills/ui-review/references/astra.md +17 -0
- package/.claude/skills/ui-review/references/legacy.md +7 -0
- package/.claude/skills/work/SKILL.md +6 -27
- package/.claude/skills/work/references/astra.md +20 -0
- package/.claude/skills/work/references/closure.md +10 -0
- package/.claude/skills/work/references/feature-contracts.md +4 -0
- package/.claude/skills/work/references/legacy.md +29 -0
- package/README.md +67 -29
- package/UPGRADING.md +54 -4
- package/VERSION +1 -1
- package/docs/ARCHITECTURE.md +30 -13
- package/docs/HUONG-DAN-MIGRATE-VA-SU-DUNG.md +34 -322
- package/docs/MIGRATION-MATRIX.md +1 -1
- package/docs/NCCGS-1.3.md +125 -0
- package/docs/PROJECT-POLICY.md +18 -3
- package/docs/WORKFLOWS.md +20 -14
- package/package-assets/nccgs-project.gitignore +2 -0
- package/package.json +5 -3
- package/scaffold/.nccgs/gates.json +7 -0
- package/scaffold/.nccgs/project.yaml +6 -3
- package/scaffold/.nccgs/state.md +1 -1
- package/scaffold/.nccgs/templates/agent-handoff.md +10 -0
- package/scaffold/.nccgs/templates/closure-record.md +11 -0
- package/scaffold/.nccgs/templates/feature-contract.md +9 -0
- package/scaffold/.nccgs/templates/implementation-report.md +49 -0
- package/scripts/cli.mjs +10 -5
- package/scripts/doctor.mjs +102 -18
- package/scripts/install.mjs +221 -88
- package/scripts/validate.mjs +34 -6
- package/tests/framework.test.mjs +213 -8
- package/tests/gates.test.mjs +170 -0
package/tests/framework.test.mjs
CHANGED
|
@@ -6,13 +6,46 @@ import path from "node:path";
|
|
|
6
6
|
import { spawnSync } from "node:child_process";
|
|
7
7
|
import test from "node:test";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { compile, renderAgent } from "../.claude/nccgs/tools/compile-policy.mjs";
|
|
9
10
|
|
|
10
11
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
12
|
const studio = JSON.parse(fs.readFileSync(path.join(root, ".claude/nccgs/studio.json"), "utf8"));
|
|
12
13
|
const workflows = JSON.parse(fs.readFileSync(path.join(root, ".claude/nccgs/workflow-catalog.json"), "utf8"));
|
|
14
|
+
const pipeline = JSON.parse(fs.readFileSync(path.join(root, ".claude/nccgs/pipeline.json"), "utf8"));
|
|
15
|
+
function copyAgentFixture(temp) {
|
|
16
|
+
fs.cpSync(path.join(root, ".claude/nccgs"), path.join(temp, ".claude/nccgs"), { recursive: true });
|
|
17
|
+
fs.mkdirSync(path.join(temp, ".claude/agents"), { recursive: true });
|
|
18
|
+
for (const agent of studio.agents) {
|
|
19
|
+
const template = fs.readFileSync(path.join(root, ".claude/nccgs/agent-library", `${agent.name}.md`), "utf8");
|
|
20
|
+
fs.writeFileSync(path.join(temp, ".claude/agents", `${agent.name}.md`), renderAgent(template, pipeline.profiles["astra-claude"].models[agent.modelClass]));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
13
23
|
const frameworkVersion = fs.readFileSync(path.join(root, "VERSION"), "utf8").trim();
|
|
14
24
|
const sha256 = (value) => crypto.createHash("sha256").update(value).digest("hex");
|
|
15
25
|
|
|
26
|
+
test("canonical policy changes invalidate generated routing until compilation", () => {
|
|
27
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), "nccgs-policy-test-"));
|
|
28
|
+
try {
|
|
29
|
+
copyAgentFixture(temp);
|
|
30
|
+
compile(temp, true);
|
|
31
|
+
assert.deepEqual(compile(temp), []);
|
|
32
|
+
const file = path.join(temp, ".claude/nccgs/pipeline.json");
|
|
33
|
+
const policy = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
34
|
+
policy.profiles["astra-claude"].models.review.effort = "medium";
|
|
35
|
+
fs.writeFileSync(file, JSON.stringify(policy));
|
|
36
|
+
const stale = compile(temp);
|
|
37
|
+
assert.ok(stale.includes(".claude/agents/nccgs-adversarial-reviewer.md"));
|
|
38
|
+
assert.ok(stale.includes(".claude/nccgs/workflow-catalog.json"));
|
|
39
|
+
compile(temp, true);
|
|
40
|
+
assert.deepEqual(compile(temp), []);
|
|
41
|
+
const catalog = JSON.parse(fs.readFileSync(path.join(temp, ".claude/nccgs/studio.json"), "utf8"));
|
|
42
|
+
assert.equal(catalog.effortProfiles["astra-claude"].review, "medium");
|
|
43
|
+
} finally {
|
|
44
|
+
assert.ok(path.resolve(temp).startsWith(path.resolve(os.tmpdir()) + path.sep + "nccgs-policy-test-"));
|
|
45
|
+
fs.rmSync(temp, { recursive: true, force: true });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
16
49
|
test("studio roles are unique and workflow references resolve", () => {
|
|
17
50
|
const roles = new Set(studio.agents.map((agent) => agent.name));
|
|
18
51
|
assert.equal(roles.size, studio.agents.length);
|
|
@@ -27,8 +60,116 @@ test("quality profile assigns all model classes", () => {
|
|
|
27
60
|
}
|
|
28
61
|
});
|
|
29
62
|
|
|
63
|
+
test("optional specialists survive updates and can be disabled without changing core agents", () => {
|
|
64
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), "nccgs-optional-agents-"));
|
|
65
|
+
try {
|
|
66
|
+
for (const marker of ["Assets", "Packages", "ProjectSettings"]) fs.mkdirSync(path.join(temp, marker));
|
|
67
|
+
const run = (...args) => spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), ...args], { cwd: temp, encoding: "utf8" });
|
|
68
|
+
const install = run("install");
|
|
69
|
+
assert.equal(install.status, 0, install.stderr || install.stdout);
|
|
70
|
+
const role = "nccgs-network-programmer";
|
|
71
|
+
const file = path.join(temp, ".claude/agents", `${role}.md`);
|
|
72
|
+
assert.ok(!fs.existsSync(file));
|
|
73
|
+
assert.equal(run("agents", "--enable", role).status, 0);
|
|
74
|
+
assert.match(fs.readFileSync(file, "utf8"), /model: claude-opus-5-5/);
|
|
75
|
+
const update = run("update");
|
|
76
|
+
assert.equal(update.status, 0, update.stderr || update.stdout);
|
|
77
|
+
assert.ok(fs.existsSync(file));
|
|
78
|
+
assert.equal(run("doctor").status, 0);
|
|
79
|
+
assert.equal(run("agents", "--disable", role).status, 0);
|
|
80
|
+
assert.ok(!fs.existsSync(file));
|
|
81
|
+
assert.notEqual(run("agents", "--disable", "nccgs-adversarial-reviewer").status, 0);
|
|
82
|
+
assert.equal(fs.readdirSync(path.join(temp, ".claude/agents")).length, pipeline.coreAgents.length);
|
|
83
|
+
} finally { fs.rmSync(temp, { recursive: true, force: true }); }
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("default install configures Opus coding and independent Fable review without erasing settings", () => {
|
|
87
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), "nccgs-astra-install-"));
|
|
88
|
+
try {
|
|
89
|
+
for (const marker of ["Assets", "Packages", "ProjectSettings", ".claude"]) fs.mkdirSync(path.join(temp, marker));
|
|
90
|
+
const settingsPath = path.join(temp, ".claude/settings.json");
|
|
91
|
+
fs.writeFileSync(settingsPath, JSON.stringify({ model: "sonnet", effortLevel: "medium", modelSettings: { "claude-opus-5-5": { effortLevel: "low" }, "claude-sonnet-4-6": { effortLevel: "medium" } }, permissions: { allow: ["Bash(git status*)"] }, env: { NCCGS_TEST: "preserved" } }));
|
|
92
|
+
const run = (...args) => spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), ...args], { cwd: temp, encoding: "utf8" });
|
|
93
|
+
const dry = run("install", "--dry-run");
|
|
94
|
+
assert.equal(dry.status, 0, dry.stderr || dry.stdout);
|
|
95
|
+
assert.equal(JSON.parse(fs.readFileSync(settingsPath)).model, "sonnet");
|
|
96
|
+
assert.ok(!fs.existsSync(path.join(temp, ".claude/agents")));
|
|
97
|
+
const install = run("install");
|
|
98
|
+
assert.equal(install.status, 0, install.stderr || install.stdout);
|
|
99
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath));
|
|
100
|
+
assert.equal(settings.model, "claude-opus-5-5");
|
|
101
|
+
assert.equal(settings.effortLevel, "xhigh");
|
|
102
|
+
assert.equal(settings.modelSettings["claude-opus-5-5"].effortLevel, "xhigh");
|
|
103
|
+
assert.equal(settings.modelSettings["claude-sonnet-4-6"].effortLevel, "medium");
|
|
104
|
+
assert.equal(settings.env.NCCGS_TEST, "preserved");
|
|
105
|
+
assert.deepEqual(settings.permissions.allow, ["Bash(git status*)"]);
|
|
106
|
+
for (const agent of studio.agents) {
|
|
107
|
+
if (!pipeline.coreAgents.includes(agent.name)) {
|
|
108
|
+
assert.ok(!fs.existsSync(path.join(temp, ".claude/agents", `${agent.name}.md`)));
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const text = fs.readFileSync(path.join(temp, ".claude/agents", `${agent.name}.md`), "utf8");
|
|
112
|
+
const expected = agent.name === "nccgs-adversarial-reviewer" ? "fable" : "claude-opus-5-5";
|
|
113
|
+
assert.equal(text.match(/^model:\s*(\S+)/m)?.[1], expected, agent.name);
|
|
114
|
+
const expectedEffort = agent.name === "nccgs-adversarial-reviewer" ? "high" : "xhigh";
|
|
115
|
+
assert.equal(text.match(/^effort:\s*(\S+)/m)?.[1], expectedEffort, agent.name);
|
|
116
|
+
}
|
|
117
|
+
assert.match(fs.readFileSync(path.join(temp, ".nccgs/project.yaml"), "utf8"), /profile: astra-claude/);
|
|
118
|
+
assert.ok(fs.existsSync(path.join(temp, ".nccgs/templates/implementation-report.md")));
|
|
119
|
+
const update = run("update");
|
|
120
|
+
assert.equal(update.status, 0, update.stderr || update.stdout);
|
|
121
|
+
assert.match(update.stdout, /0 preserved override/);
|
|
122
|
+
const doctor = run("doctor");
|
|
123
|
+
assert.equal(doctor.status, 0, doctor.stderr || doctor.stdout);
|
|
124
|
+
} finally {
|
|
125
|
+
fs.rmSync(temp, { recursive: true, force: true });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("profile changes clear effort pins and preserve skipped custom agents", () => {
|
|
130
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), "nccgs-effort-profile-"));
|
|
131
|
+
try {
|
|
132
|
+
fs.mkdirSync(path.join(temp, ".claude/nccgs"), { recursive: true });
|
|
133
|
+
fs.mkdirSync(path.join(temp, ".claude/agents"));
|
|
134
|
+
fs.mkdirSync(path.join(temp, ".nccgs"));
|
|
135
|
+
fs.copyFileSync(path.join(root, ".claude/nccgs/studio.json"), path.join(temp, ".claude/nccgs/studio.json"));
|
|
136
|
+
fs.writeFileSync(path.join(temp, ".nccgs/project.yaml"), "models:\n profile: astra-claude\n");
|
|
137
|
+
copyAgentFixture(temp);
|
|
138
|
+
const run = (profile, ...args) => spawnSync(process.execPath, [path.join(root, ".claude/nccgs/tools/configure-models.mjs"), "--project", temp, "--profile", profile, ...args], { encoding: "utf8" });
|
|
139
|
+
for (const profile of ["inherit", "quality", "balanced", "astra-claude"]) {
|
|
140
|
+
const result = run(profile);
|
|
141
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
142
|
+
for (const agent of studio.agents) {
|
|
143
|
+
if (profile === "astra-claude" && !pipeline.coreAgents.includes(agent.name)) {
|
|
144
|
+
assert.ok(!fs.existsSync(path.join(temp, ".claude/agents", `${agent.name}.md`)));
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const text = fs.readFileSync(path.join(temp, ".claude/agents", `${agent.name}.md`), "utf8");
|
|
148
|
+
assert.equal(text.match(/^model:\s*(\S+)/m)?.[1] ?? null, studio.modelProfiles[profile][agent.modelClass], agent.name);
|
|
149
|
+
const expectedEffort = profile === "astra-claude"
|
|
150
|
+
? (agent.name === "nccgs-adversarial-reviewer" ? "high" : "xhigh") : null;
|
|
151
|
+
assert.equal(text.match(/^effort:\s*(\S+)/m)?.[1] ?? null, expectedEffort, agent.name);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const file = path.join(temp, ".claude/agents/nccgs-adversarial-reviewer.md");
|
|
155
|
+
const before = fs.readFileSync(file, "utf8");
|
|
156
|
+
assert.equal(run("inherit", "--skip-path", ".claude/agents/nccgs-adversarial-reviewer.md").status, 0);
|
|
157
|
+
assert.equal(fs.readFileSync(file, "utf8"), before);
|
|
158
|
+
const invalid = run("unknown");
|
|
159
|
+
assert.notEqual(invalid.status, 0);
|
|
160
|
+
assert.equal(fs.readFileSync(file, "utf8"), before);
|
|
161
|
+
fs.writeFileSync(path.join(temp, ".claude/settings.json"), "invalid JSON");
|
|
162
|
+
assert.notEqual(run("astra-claude").status, 0);
|
|
163
|
+
assert.equal(fs.readFileSync(file, "utf8"), before);
|
|
164
|
+
const other = fs.readFileSync(path.join(temp, ".claude/agents/nccgs-unity-implementer.md"), "utf8");
|
|
165
|
+
assert.ok(!/^model:/m.test(other), "invalid settings must fail before rewriting agents");
|
|
166
|
+
} finally {
|
|
167
|
+
fs.rmSync(temp, { recursive: true, force: true });
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
30
171
|
test("migrate-project defaults to a single full run", () => {
|
|
31
|
-
const skill = fs.readFileSync(path.join(root, ".claude/skills/migrate-project/
|
|
172
|
+
const skill = fs.readFileSync(path.join(root, ".claude/skills/migrate-project/references/legacy.md"), "utf8");
|
|
32
173
|
assert.match(skill, /Default to `full`/);
|
|
33
174
|
assert.match(skill, /review, plan, apply, and verify in one invocation/i);
|
|
34
175
|
});
|
|
@@ -42,7 +183,7 @@ test("model configurator can remove and restore model pins", () => {
|
|
|
42
183
|
fs.copyFileSync(path.join(root, ".claude/nccgs/studio.json"), path.join(temp, ".claude/nccgs/studio.json"));
|
|
43
184
|
fs.copyFileSync(path.join(root, ".claude/nccgs/tools/configure-models.mjs"), path.join(temp, ".claude/nccgs/tools/configure-models.mjs"));
|
|
44
185
|
fs.writeFileSync(path.join(temp, ".nccgs/project.yaml"), "models:\n profile: quality\n", "utf8");
|
|
45
|
-
|
|
186
|
+
copyAgentFixture(temp);
|
|
46
187
|
const tool = path.join(temp, ".claude/nccgs/tools/configure-models.mjs");
|
|
47
188
|
assert.equal(spawnSync(process.execPath, [tool, "--project", temp, "--profile", "inherit"]).status, 0);
|
|
48
189
|
assert.ok(!/^model:/m.test(fs.readFileSync(path.join(temp, ".claude/agents", `${studio.agents[0].name}.md`), "utf8")));
|
|
@@ -129,7 +270,7 @@ test("installer preserves project truth, merges settings, and applies inherit pr
|
|
|
129
270
|
}
|
|
130
271
|
});
|
|
131
272
|
|
|
132
|
-
test("update replaces untouched
|
|
273
|
+
test("update replaces untouched files, preserves local overrides, and doctor gates review", () => {
|
|
133
274
|
const temp = fs.mkdtempSync(path.join(os.tmpdir(), "nccgs-idempotent-update-test-"));
|
|
134
275
|
try {
|
|
135
276
|
for (const marker of ["Assets", "Packages", "ProjectSettings"]) fs.mkdirSync(path.join(temp, marker));
|
|
@@ -148,20 +289,84 @@ test("update replaces untouched managed files, blocks local edits, and doctor ve
|
|
|
148
289
|
|
|
149
290
|
const update = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "update"], { cwd: temp, encoding: "utf8" });
|
|
150
291
|
assert.equal(update.status, 0, update.stderr || update.stdout);
|
|
151
|
-
assert.match(update.stdout,
|
|
292
|
+
assert.match(update.stdout, new RegExp(`1\\.0\\.0 -> ${frameworkVersion.replace(/\./g, "\\.")}`));
|
|
152
293
|
assert.match(update.stdout, /automatic update/);
|
|
153
294
|
assert.match(update.stdout, /No project migration required/);
|
|
154
295
|
assert.equal(fs.readFileSync(managedPath, "utf8"), fs.readFileSync(path.join(root, managedRelative), "utf8"));
|
|
155
296
|
|
|
156
|
-
|
|
297
|
+
let doctor = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "doctor"], { cwd: temp, encoding: "utf8" });
|
|
157
298
|
assert.equal(doctor.status, 0, doctor.stderr || doctor.stdout);
|
|
158
299
|
assert.match(doctor.stdout, /NCCGS doctor passed/);
|
|
159
300
|
|
|
160
301
|
fs.writeFileSync(managedPath, "project-local override\n", "utf8");
|
|
161
|
-
const
|
|
162
|
-
assert.equal(
|
|
163
|
-
assert.match(
|
|
302
|
+
const preserved = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "update"], { cwd: temp, encoding: "utf8" });
|
|
303
|
+
assert.equal(preserved.status, 0, preserved.stderr || preserved.stdout);
|
|
304
|
+
assert.match(preserved.stdout, /preserved override/);
|
|
305
|
+
assert.match(preserved.stdout, /Project migration required/);
|
|
164
306
|
assert.equal(fs.readFileSync(managedPath, "utf8"), "project-local override\n");
|
|
307
|
+
|
|
308
|
+
const overridePath = path.join(temp, ".nccgs/framework-overrides.json");
|
|
309
|
+
const overrides = JSON.parse(fs.readFileSync(overridePath, "utf8"));
|
|
310
|
+
const override = overrides.overrides.find((item) => item.path === managedRelative);
|
|
311
|
+
assert.equal(override.status, "pending_review");
|
|
312
|
+
assert.equal(override.disposition, "preserve");
|
|
313
|
+
|
|
314
|
+
doctor = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "doctor"], { cwd: temp, encoding: "utf8" });
|
|
315
|
+
assert.equal(doctor.status, 1);
|
|
316
|
+
assert.match(doctor.stderr, /awaits \/migrate-project review/);
|
|
317
|
+
|
|
318
|
+
const approve = spawnSync(process.execPath, [path.join(temp, ".claude/nccgs/tools/approve-overrides.mjs"), "--project", temp, "--approve-pending"], { cwd: temp, encoding: "utf8" });
|
|
319
|
+
assert.equal(approve.status, 0, approve.stderr || approve.stdout);
|
|
320
|
+
doctor = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "doctor"], { cwd: temp, encoding: "utf8" });
|
|
321
|
+
assert.equal(doctor.status, 0, doctor.stderr || doctor.stdout);
|
|
322
|
+
} finally {
|
|
323
|
+
fs.rmSync(temp, { recursive: true, force: true });
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("doctor accepts a legacy CRLF raw hash after Git normalizes the file to LF", () => {
|
|
328
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), "nccgs-eol-test-"));
|
|
329
|
+
try {
|
|
330
|
+
for (const marker of ["Assets", "Packages", "ProjectSettings"]) fs.mkdirSync(path.join(temp, marker));
|
|
331
|
+
const install = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "install"], { cwd: temp, encoding: "utf8" });
|
|
332
|
+
assert.equal(install.status, 0, install.stderr || install.stdout);
|
|
333
|
+
|
|
334
|
+
const relative = ".claude/nccgs/constitution.md";
|
|
335
|
+
const file = path.join(temp, ...relative.split("/"));
|
|
336
|
+
const lf = fs.readFileSync(file, "utf8").replace(/\r\n?/g, "\n");
|
|
337
|
+
fs.writeFileSync(file, lf, "utf8");
|
|
338
|
+
const statePath = path.join(temp, ".claude/nccgs/install-state.json");
|
|
339
|
+
const state = JSON.parse(fs.readFileSync(statePath, "utf8"));
|
|
340
|
+
const record = state.files.find((item) => item.relative === relative);
|
|
341
|
+
record.sha256 = sha256(lf.replace(/\n/g, "\r\n"));
|
|
342
|
+
delete record.canonicalSha256;
|
|
343
|
+
state.schemaVersion = 2;
|
|
344
|
+
fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
345
|
+
|
|
346
|
+
const doctor = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "doctor"], { cwd: temp, encoding: "utf8" });
|
|
347
|
+
assert.equal(doctor.status, 0, doctor.stderr || doctor.stdout);
|
|
348
|
+
} finally {
|
|
349
|
+
fs.rmSync(temp, { recursive: true, force: true });
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test("update preserves an intentionally missing managed file until migration review", () => {
|
|
354
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), "nccgs-absent-override-test-"));
|
|
355
|
+
try {
|
|
356
|
+
for (const marker of ["Assets", "Packages", "ProjectSettings"]) fs.mkdirSync(path.join(temp, marker));
|
|
357
|
+
const install = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "install"], { cwd: temp, encoding: "utf8" });
|
|
358
|
+
assert.equal(install.status, 0, install.stderr || install.stdout);
|
|
359
|
+
|
|
360
|
+
const relative = ".claude/skills/status/SKILL.md";
|
|
361
|
+
const file = path.join(temp, ...relative.split("/"));
|
|
362
|
+
fs.unlinkSync(file);
|
|
363
|
+
const update = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "update"], { cwd: temp, encoding: "utf8" });
|
|
364
|
+
assert.equal(update.status, 0, update.stderr || update.stdout);
|
|
365
|
+
assert.ok(!fs.existsSync(file));
|
|
366
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(temp, ".nccgs/framework-overrides.json"), "utf8"));
|
|
367
|
+
const override = manifest.overrides.find((item) => item.path === relative);
|
|
368
|
+
assert.equal(override.disposition, "absent");
|
|
369
|
+
assert.equal(override.status, "pending_review");
|
|
165
370
|
} finally {
|
|
166
371
|
fs.rmSync(temp, { recursive: true, force: true });
|
|
167
372
|
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import test from "node:test";
|
|
8
|
+
import { initialize, snapshot, record, check, close, taskPath } from "../.claude/nccgs/tools/gates.mjs";
|
|
9
|
+
import { artifact, atomicJson, json, hash } from "../.claude/nccgs/tools/policy.mjs";
|
|
10
|
+
import { run } from "../.claude/nccgs/tools/task.mjs";
|
|
11
|
+
import { observe } from "../.claude/nccgs/tools/runtime.mjs";
|
|
12
|
+
|
|
13
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
14
|
+
function fixture(options = {}) {
|
|
15
|
+
const project = fs.mkdtempSync(path.join(os.tmpdir(), "nccgs-gates-"));
|
|
16
|
+
const write = (name, value) => {
|
|
17
|
+
const file = path.join(project, name);
|
|
18
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
19
|
+
fs.writeFileSync(file, typeof value === "string" ? value : JSON.stringify(value));
|
|
20
|
+
return artifact(project, name);
|
|
21
|
+
};
|
|
22
|
+
for (const file of ["pipeline.json", "studio.json"]) write(`.claude/nccgs/${file}`, fs.readFileSync(path.join(root, ".claude/nccgs", file), "utf8"));
|
|
23
|
+
const policy = write(".nccgs/project.yaml", "schema_version: 2\nmodels:\n profile: astra-claude\n");
|
|
24
|
+
write(".nccgs/gates.json", { schemaVersion: 1, policySha256: policy.sha256, requiredChecks: ["regression"], requiredArtifacts: options.artifacts ?? [], manualGates: options.manual ?? [] });
|
|
25
|
+
write("Assets/game.cs", "// initial implementation\n");
|
|
26
|
+
write(".nccgs/plan.md", "Astra plan: preserve health invariant and verify regression.");
|
|
27
|
+
initialize(project, "feature", ".nccgs/plan.md");
|
|
28
|
+
let index = 0;
|
|
29
|
+
const snap = () => snapshot(project).sha256;
|
|
30
|
+
const task = () => json(taskPath(project, "feature"));
|
|
31
|
+
function runtime(stage, overrides = {}) {
|
|
32
|
+
const source = `.nccgs/evidence/raw-${++index}.json`;
|
|
33
|
+
write(source, { taskId: "feature", snapshot: snap(), sessionId: "session", agentId: stage,
|
|
34
|
+
observedModel: stage === "implementation" ? "claude-opus-5-5" : "claude-fable-5-1",
|
|
35
|
+
observedEffort: stage === "implementation" ? "xhigh" : "high", capturedAt: new Date().toISOString(),
|
|
36
|
+
status: "completed", provenance: { kind: "provider-log", reference: "test fixture: captured runtime request metadata" }, ...overrides });
|
|
37
|
+
return run(["observe", "--project", project, "--id", "feature", "--file", source]);
|
|
38
|
+
}
|
|
39
|
+
const impl = (overrides = {}) => record(project, "feature", "implementation", { snapshot: snap(), runtime: runtime("implementation"),
|
|
40
|
+
checks: [{ name: "regression", status: "PASS", evidence: write(`.nccgs/evidence/test-${++index}.log`, "fixture regression: PASS") }], ...overrides });
|
|
41
|
+
const review = (overrides = {}) => record(project, "feature", "review", { snapshot: snap(), runtime: runtime("review"), verdict: "PASS", criteriaMet: true, findings: [], evidence: write(`.nccgs/evidence/review-${++index}.md`, "Fixture independent review findings: none"), ...overrides });
|
|
42
|
+
const report = (overrides = {}) => record(project, "feature", "report", { snapshot: snap(), evidence: write(`.nccgs/evidence/report-${++index}.md`, "Fixture report, review reference and evidence"), ...overrides });
|
|
43
|
+
const approval = (stage, overrides = {}) => record(project, "feature", stage, { snapshot: snap(), evidence: write(`.nccgs/approvals/${stage}-${++index}.json`, {
|
|
44
|
+
actor: stage === "astra" ? "Astra" : "Product Owner", decision: stage === "astra" ? "PASS" : "ACCEPTED",
|
|
45
|
+
taskId: "feature", snapshot: snap(), decidedAt: new Date().toISOString(), statement: "Explicit TEST FIXTURE approval", reference: "test-only external decision event", ...overrides }) });
|
|
46
|
+
return { project, write, snap, task, runtime, impl, review, report, approval,
|
|
47
|
+
dispose() {
|
|
48
|
+
const resolved = path.resolve(project);
|
|
49
|
+
assert.ok(resolved.startsWith(path.resolve(os.tmpdir()) + path.sep + "nccgs-gates-"));
|
|
50
|
+
fs.rmSync(resolved, { recursive: true, force: true });
|
|
51
|
+
} };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
test("end-to-end gates require Fable, Astra and explicit acceptance in order", () => {
|
|
55
|
+
const f = fixture();
|
|
56
|
+
try {
|
|
57
|
+
assert.throws(() => close(f.project, "feature"), /implementation: missing/);
|
|
58
|
+
assert.throws(() => f.report(), /implementation: missing/);
|
|
59
|
+
f.impl();
|
|
60
|
+
assert.throws(() => f.report(), /review: missing/);
|
|
61
|
+
f.review();
|
|
62
|
+
assert.equal(check(f.project, f.task(), "report").allowed, true);
|
|
63
|
+
f.report();
|
|
64
|
+
assert.throws(() => close(f.project, "feature"), /astra: missing/);
|
|
65
|
+
f.approval("astra");
|
|
66
|
+
assert.throws(() => close(f.project, "feature"), /acceptance: missing/);
|
|
67
|
+
f.approval("acceptance");
|
|
68
|
+
assert.equal(close(f.project, "feature").phase, "DONE");
|
|
69
|
+
} finally { f.dispose(); }
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("source additions, deletions and edits invalidate review and approvals", () => {
|
|
73
|
+
const f = fixture();
|
|
74
|
+
try {
|
|
75
|
+
f.impl(); f.review(); f.report(); f.approval("astra"); f.approval("acceptance"); close(f.project, "feature");
|
|
76
|
+
const before = f.snap();
|
|
77
|
+
f.write("Assets/new.cs", "untracked new source");
|
|
78
|
+
assert.notEqual(f.snap(), before);
|
|
79
|
+
assert.throws(() => close(f.project, "feature"), /stale/);
|
|
80
|
+
fs.unlinkSync(path.join(f.project, "Assets/game.cs"));
|
|
81
|
+
assert.throws(() => check(f.project, f.task(), "report"), /stale/);
|
|
82
|
+
f.impl();
|
|
83
|
+
assert.deepEqual(Object.keys(f.task().stages), ["implementation"]);
|
|
84
|
+
assert.equal(f.task().closed, undefined);
|
|
85
|
+
assert.ok(f.task().history.length > 0);
|
|
86
|
+
assert.throws(() => f.report(), /review: missing/);
|
|
87
|
+
} finally { f.dispose(); }
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("unknown effort, wrong model, fallback, and same-agent review cannot pass", () => {
|
|
91
|
+
const f = fixture();
|
|
92
|
+
try {
|
|
93
|
+
assert.throws(() => f.impl({ runtime: f.runtime("implementation", { observedEffort: "high" }) }), /effort/);
|
|
94
|
+
assert.throws(() => f.impl({ runtime: f.runtime("implementation", { observedModel: "claude-sonnet-4-6" }) }), /model/);
|
|
95
|
+
f.impl();
|
|
96
|
+
assert.throws(() => f.review({ runtime: f.runtime("review", { agentId: "implementation" }) }), /independent/);
|
|
97
|
+
assert.throws(() => f.review({ runtime: f.runtime("review", { modelsUsed: ["claude-fable-5-1", "claude-opus-5-5"] }) }), /fallback/);
|
|
98
|
+
const event = observe(f.project, { session_id: "session", tool_input: { subagent_type: "nccgs-adversarial-reviewer" }, tool_response: { resolvedModel: "claude-fable-5-1", status: "completed", agentId: "native-review" } }, "agent_result");
|
|
99
|
+
assert.equal(event.observedEffort, null);
|
|
100
|
+
assert.equal(event.requestedEffort, "high");
|
|
101
|
+
assert.ok(event.missing.includes("effort"));
|
|
102
|
+
const ref = f.write(".nccgs/evidence/unknown-effort.json", { ...event, taskId: "feature", snapshot: f.snap() });
|
|
103
|
+
assert.throws(() => f.review({ runtime: ref }), /effort/);
|
|
104
|
+
} finally { f.dispose(); }
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("negative review and rejection remain blocking; amended evidence invalidates results", () => {
|
|
108
|
+
const f = fixture();
|
|
109
|
+
try {
|
|
110
|
+
f.impl(); f.review({ verdict: "CHANGES_REQUIRED", findings: [{ severity: "HIGH", status: "OPEN" }] });
|
|
111
|
+
assert.throws(() => f.report(), /not passed/);
|
|
112
|
+
assert.throws(() => f.review({ findings: [{ severity: "HIGH", status: "OPEN" }] }), /blocking findings/);
|
|
113
|
+
f.review(); f.report(); f.approval("astra"); f.approval("acceptance");
|
|
114
|
+
f.approval("acceptance", { decision: "REJECTED" });
|
|
115
|
+
assert.throws(() => close(f.project, "feature"), /explicit Product Owner/);
|
|
116
|
+
const evidence = f.task().stages.review.evidence;
|
|
117
|
+
f.write(evidence.path, "altered review");
|
|
118
|
+
assert.throws(() => check(f.project, f.task(), "report"), /stale artifact/);
|
|
119
|
+
} finally { f.dispose(); }
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("required checks, closure artifacts, manual gates and policy hashes are enforced", () => {
|
|
123
|
+
const f = fixture({ artifacts: [".nccgs/evidence/build.log"], manual: ["playtest"] });
|
|
124
|
+
try {
|
|
125
|
+
assert.throws(() => f.impl({ checks: [{ name: "unrelated", status: "PASS", evidence: f.write(".nccgs/evidence/unrelated.log", "PASS") }] }), /Required check missing/);
|
|
126
|
+
f.impl(); f.review();
|
|
127
|
+
assert.throws(() => f.report(), /closure artifact missing/);
|
|
128
|
+
f.report({ artifacts: [f.write(".nccgs/evidence/build.log", "Build successful")] });
|
|
129
|
+
f.approval("astra");
|
|
130
|
+
assert.throws(() => f.approval("acceptance"), /Manual gate missing/);
|
|
131
|
+
f.approval("acceptance", { gates: [{ id: "playtest", decision: "ACCEPTED", reference: "test fixture playtest decision" }] });
|
|
132
|
+
close(f.project, "feature");
|
|
133
|
+
f.write(".nccgs/plan.md", "Changed plan");
|
|
134
|
+
assert.throws(() => close(f.project, "feature"), /stale artifact/);
|
|
135
|
+
} finally { f.dispose(); }
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("path traversal and premature/fabricated external sign-offs fail", () => {
|
|
139
|
+
const f = fixture();
|
|
140
|
+
try {
|
|
141
|
+
assert.throws(() => taskPath(f.project, "../escape"), /Invalid task/);
|
|
142
|
+
assert.throws(() => artifact(f.project, "../outside.txt"), /escapes/);
|
|
143
|
+
f.impl(); f.review(); f.report();
|
|
144
|
+
assert.throws(() => f.approval("astra", { actor: "Claude" }), /explicit Astra/);
|
|
145
|
+
assert.throws(() => f.approval("astra", { decidedAt: "2000-01-01T00:00:00.000Z" }), /predates/);
|
|
146
|
+
const old = process.env.CLAUDECODE;
|
|
147
|
+
try { process.env.CLAUDECODE = "1"; assert.throws(() => f.approval("astra"), /outside the Claude/); }
|
|
148
|
+
finally { if (old === undefined) delete process.env.CLAUDECODE; else process.env.CLAUDECODE = old; }
|
|
149
|
+
const guard = spawnSync(process.execPath, [path.join(root, ".claude/nccgs/hooks/closure-guard.mjs"), "pre"], {
|
|
150
|
+
cwd: f.project, encoding: "utf8", input: JSON.stringify({ session_id: "session", tool_input: { file_path: path.join(f.project, ".nccgs/state.md"), content: "Final state: DONE" } }) });
|
|
151
|
+
assert.equal(guard.status, 2);
|
|
152
|
+
assert.match(guard.stderr, /bound to an NCCGS task/);
|
|
153
|
+
} finally { f.dispose(); }
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("verification runner captures real exit status and rejects changed logs", () => {
|
|
157
|
+
const f = fixture();
|
|
158
|
+
try {
|
|
159
|
+
const execute = source => run(["verify", "--project", f.project, "--id", "feature", "--name", "regression", "--", process.execPath, "-e", source]);
|
|
160
|
+
const failed = execute("process.exit(3)");
|
|
161
|
+
assert.equal(failed.status, "FAIL");
|
|
162
|
+
assert.throws(() => f.impl({ checks: [failed] }), /failed\/unproven/);
|
|
163
|
+
const passed = execute("console.log('fixture verification passed')");
|
|
164
|
+
assert.equal(passed.status, "PASS");
|
|
165
|
+
f.impl({ checks: [passed] });
|
|
166
|
+
const execution = json(path.join(f.project, passed.evidence.path));
|
|
167
|
+
f.write(execution.log.path, "tampered log");
|
|
168
|
+
assert.throws(() => check(f.project, f.task(), "review"), /stale artifact/);
|
|
169
|
+
} finally { f.dispose(); }
|
|
170
|
+
});
|