nccgs 2.1.0 → 2.1.1
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 +1 -1
- package/.claude/agents/nccgs-engine-programmer.md +1 -1
- package/.claude/agents/nccgs-fast-implementer.md +1 -1
- package/.claude/agents/nccgs-game-designer.md +1 -1
- package/.claude/agents/nccgs-gameplay-programmer.md +1 -1
- package/.claude/agents/nccgs-qa-engineer.md +1 -1
- package/.claude/agents/nccgs-task-scout.md +1 -1
- package/.claude/agents/nccgs-ui-programmer.md +1 -1
- package/.claude/agents/nccgs-unity-build-specialist.md +1 -1
- package/.claude/agents/nccgs-unity-implementer.md +1 -1
- package/.claude/agents/nccgs-unity-systems-specialist.md +1 -1
- package/.claude/agents/nccgs-unity-ui-specialist.md +1 -1
- package/.claude/agents/nccgs-verification-engineer.md +1 -1
- package/.claude/nccgs/MEMORY.md +3 -1
- package/.claude/nccgs/VERSION +1 -1
- package/.claude/nccgs/constitution.md +187 -176
- package/.claude/nccgs/hooks/closure-guard.mjs +2 -0
- package/.claude/nccgs/hooks/unity-transport-guard.mjs +11 -0
- package/.claude/nccgs/pipeline.json +260 -260
- package/.claude/nccgs/protocols/context-packets.md +5 -0
- package/.claude/nccgs/protocols/model-routing.md +46 -46
- package/.claude/nccgs/protocols/review-handoff.md +21 -1
- package/.claude/nccgs/protocols/shared-workflow-context.md +33 -0
- package/.claude/nccgs/protocols/unity-boundary.md +20 -0
- package/.claude/nccgs/routing.generated.md +4 -4
- package/.claude/nccgs/settings.fragment.json +84 -24
- package/.claude/nccgs/studio.json +53 -53
- package/.claude/nccgs/tools/autonomy.mjs +13 -0
- package/.claude/nccgs/tools/command-tokens.mjs +22 -0
- package/.claude/nccgs/tools/hook-transport.mjs +34 -0
- package/.claude/nccgs/tools/native-completion.mjs +10 -1
- package/.claude/nccgs/tools/returned-handoff.mjs +11 -2
- package/.claude/nccgs/tools/shared-context-guard.mjs +60 -0
- package/.claude/nccgs/tools/shared-context.mjs +25 -0
- package/.claude/nccgs/tools/tool-guard.mjs +120 -131
- package/.claude/nccgs/tools/unity-transport.mjs +170 -0
- package/.claude/nccgs/workflow-catalog.json +4 -4
- package/CHANGELOG.md +235 -200
- package/CLAUDE.md +10 -8
- package/MEMORY.md +109 -60
- package/README.md +206 -191
- package/UPGRADING.md +130 -118
- package/VERSION +1 -1
- package/docs/ARCHITECTURE.md +131 -131
- package/docs/ASSET-PRODUCTION-GRAPHIFY-PLAN.md +230 -0
- package/docs/HUONG-DAN-MIGRATE-VA-SU-DUNG.md +117 -116
- package/docs/NCCGS-1.5.md +237 -237
- package/docs/PROJECT-POLICY.md +94 -94
- package/docs/RELEASE-STATUS.md +17 -9
- package/docs/UNINSTALL.md +3 -3
- package/docs/WFK17-HOOK-REPAIR.md +71 -0
- package/docs/WORKFLOWS.md +85 -85
- package/package.json +1 -1
- package/scaffold/.nccgs/project.yaml +109 -108
- package/scripts/benchmark-v15.mjs +173 -173
- package/scripts/doctor.mjs +261 -261
- package/scripts/install.mjs +530 -529
- package/scripts/run.mjs +5 -4
- package/scripts/supervise.mjs +4 -0
- package/scripts/uninstall.mjs +292 -292
- package/scripts/validate.mjs +285 -280
- package/tests/autonomy.test.mjs +126 -113
- package/tests/design-readiness.test.mjs +199 -199
- package/tests/fixtures/compiler-shutdown.cs +67 -64
- package/tests/fixtures/native-provider.mjs +61 -0
- package/tests/framework.test.mjs +740 -740
- package/tests/hook-repair.test.mjs +106 -0
- package/tests/lifecycle-v15.test.mjs +346 -300
- package/tests/native-completion-notices.test.mjs +181 -0
- package/tests/phase1.test.mjs +427 -416
- package/tests/phase2.test.mjs +250 -250
- package/tests/policy-v15.test.mjs +234 -234
- package/tests/returned-handoff-notices.test.mjs +170 -0
- package/tests/runtime-v15.test.mjs +1197 -1191
- package/tests/task-slices.test.mjs +285 -285
- package/tests/unity-transport.test.mjs +348 -0
package/tests/phase2.test.mjs
CHANGED
|
@@ -1,250 +1,250 @@
|
|
|
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 { fileURLToPath } from "node:url";
|
|
6
|
-
import test from "node:test";
|
|
7
|
-
import { artifact, json } from "../.claude/nccgs/tools/policy.mjs";
|
|
8
|
-
import { initialize, snapshot, record, close, taskPath, refreshInputs, reclassify } from "../.claude/nccgs/tools/gates.mjs";
|
|
9
|
-
import { reserve, claim, attachAgent, complete, status, reconcile } from "../.claude/nccgs/tools/orchestration.mjs";
|
|
10
|
-
import { startVerification, waitVerification } from "../.claude/nccgs/tools/verification-jobs.mjs";
|
|
11
|
-
import { assessCriteria } from "../.claude/nccgs/tools/criteria.mjs";
|
|
12
|
-
import { guardTool, safeTaskCommand } from "../.claude/nccgs/tools/tool-guard.mjs";
|
|
13
|
-
import { run } from "../.claude/nccgs/tools/task.mjs";
|
|
14
|
-
|
|
15
|
-
// Provider observations in these tests are SYNTHETIC fixtures, never Claude evidence.
|
|
16
|
-
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
17
|
-
const measurement = ".nccgs/evidence/measurements/runtime.json";
|
|
18
|
-
const criteria = ["needle", "orbit"].map(id => ({ id, description: `${id} wins unassisted`, check: "runtime", checkpoint: "early",
|
|
19
|
-
assertions: [{ artifact: measurement, pointer: `/builds/${id}/won`, op: "eq", value: true },
|
|
20
|
-
{ artifact: measurement, pointer: "/assisted", op: "eq", value: false },
|
|
21
|
-
{ artifact: measurement, pointer: "/reportOnly", op: "eq", value: false }] }));
|
|
22
|
-
const passed = { builds: { needle: { won: true }, orbit: { won: true } }, assisted: false, reportOnly: false };
|
|
23
|
-
|
|
24
|
-
function fixture(acceptance = criteria, contractOverrides = {}) {
|
|
25
|
-
const temporaryRoot = fs.realpathSync(os.tmpdir()), project = fs.mkdtempSync(path.join(temporaryRoot, "nccgs-phase2-"));
|
|
26
|
-
const write = (relative, value) => {
|
|
27
|
-
const file = path.join(project, relative); fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
28
|
-
fs.writeFileSync(file, typeof value === "string" ? value : JSON.stringify(value)); return artifact(project, relative);
|
|
29
|
-
};
|
|
30
|
-
fs.cpSync(path.join(root, ".claude/nccgs/tools"), path.join(project, ".claude/nccgs/tools"), { recursive: true });
|
|
31
|
-
for (const file of ["pipeline.json", "studio.json"]) write(`.claude/nccgs/${file}`, fs.readFileSync(path.join(root, ".claude/nccgs", file), "utf8"));
|
|
32
|
-
write("Assets/game.cs", "// fixture baseline");
|
|
33
|
-
write("Tools/probe.mjs", `import fs from 'node:fs';fs.mkdirSync('.nccgs/evidence/measurements',{recursive:true});const input=JSON.parse(process.argv[2]);if(input.delayMs)await new Promise(r=>setTimeout(r,input.delayMs));if(!input.skip)fs.writeFileSync('${measurement}',input.malformed?'bad JSON':JSON.stringify(input.data));console.log('Synthetic gameplay measurement fixture');process.exitCode=input.exitCode||0;`);
|
|
34
|
-
write(".nccgs/project.yaml", "schema_version: 2\nmodels:\n profile: claude-standalone\n");
|
|
35
|
-
write(".nccgs/execution.json", { schemaVersion: 1, name: "standalone", externalReview: { mode: "off" } });
|
|
36
|
-
write(".nccgs/inputs.json", { schemaVersion: 1, canonicalInputs: [], outputDirectories: [], checks: { runtime: { timeoutMs: 15000 } } });
|
|
37
|
-
const intake = { objective: "Synthetic test of acceptance mapping and repair budget", mode: "implement", readiness: "ready", requiredChecks: ["runtime"], acceptance, ...contractOverrides };
|
|
38
|
-
write(".nccgs/intake.json", intake);
|
|
39
|
-
initialize(project, "fixture", null, null, null, { intake: ".nccgs/intake.json" });
|
|
40
|
-
let serial = 0;
|
|
41
|
-
const task = () => json(taskPath(project, "fixture"));
|
|
42
|
-
const snap = () => snapshot(project, task()).sha256;
|
|
43
|
-
const assess = () => assessCriteria(project, task(), snap());
|
|
44
|
-
function verify(data = passed, options = {}) {
|
|
45
|
-
const job = startVerification(project, "fixture", { name: "runtime", executable: process.execPath, arguments: ["Tools/probe.mjs", JSON.stringify({ data, ...options })], requestKey: `probe-${++serial}` });
|
|
46
|
-
return waitVerification(project, job.jobId, "fixture", 20000);
|
|
47
|
-
}
|
|
48
|
-
function request(kind = "implementation", overrides = {}) {
|
|
49
|
-
return { kind, agent: kind === "review" ? "nccgs-adversarial-reviewer" : kind === "analysis" ? "nccgs-qa-engineer" : "nccgs-gameplay-programmer",
|
|
50
|
-
requestKey: `dispatch-${++serial}`, reason: "SYNTHETIC TEST FIXTURE", writePaths: kind === "implementation" ? ["Assets/game.cs"] : [], ...overrides };
|
|
51
|
-
}
|
|
52
|
-
function worker(kind = "implementation", overrides = {}, mutate = null) {
|
|
53
|
-
const req = request(kind, overrides);
|
|
54
|
-
if (req.purpose === "repair") {
|
|
55
|
-
req.workPacket = `.nccgs/evidence/work-packets/${req.requestKey}.json`;
|
|
56
|
-
req.handoffPaths = [`.nccgs/evidence/analysis/${req.requestKey}.md`];
|
|
57
|
-
write(req.workPacket, { schemaVersion: 1, taskId: "fixture", contractHash: task().contractHash, sourceSnapshot: snap(),
|
|
58
|
-
problem: "Synthetic acceptance failure", solution: "Apply the predetermined fixture correction", changes: [{ path: "Assets/game.cs", instruction: "Apply the synthetic bounded correction" }],
|
|
59
|
-
context: [], verification: ["runtime"] });
|
|
60
|
-
}
|
|
61
|
-
const d = reserve(project, "fixture", req), count = ++serial;
|
|
62
|
-
claim(project, d.id, { sessionId: "fixture-session", toolUseId: `tool-${count}`, agent: req.agent });
|
|
63
|
-
if (req.workPacket) {
|
|
64
|
-
attachAgent(project, d.id, `${kind}-${count}`);
|
|
65
|
-
guardTool(project, { session_id: "fixture-session", agent_id: `${kind}-${count}`, tool_use_id: `edit-${count}`, tool_name: "Edit", tool_input: { file_path: "Assets/game.cs", old_string: "fixture", new_string: "correction" } });
|
|
66
|
-
}
|
|
67
|
-
if (mutate) mutate();
|
|
68
|
-
if (req.workPacket) {
|
|
69
|
-
const content = "NCCGS-HANDOFF: COMPLETE\nSynthetic bounded correction; coordinator verification pending.";
|
|
70
|
-
guardTool(project, { session_id: "fixture-session", agent_id: `${kind}-${count}`, tool_use_id: `handoff-${count}`, tool_name: "Write", tool_input: { file_path: req.handoffPaths[0], content } });
|
|
71
|
-
write(req.handoffPaths[0], content);
|
|
72
|
-
}
|
|
73
|
-
const current = task(), route = kind === "review" ? current.execution.profile.models.review : current.implementationRoute;
|
|
74
|
-
const source = `.nccgs/evidence/raw-${count}.json`;
|
|
75
|
-
write(source, { taskId: "fixture", contractHash: current.contractHash, snapshot: snap(), dispatchId: d.id,
|
|
76
|
-
sessionId: "fixture-session", toolUseId: `tool-${count}`, agentId: `${kind}-${count}`, status: "completed", capturedAt: new Date().toISOString(),
|
|
77
|
-
observedProvider: "claude-code", observedModel: route.model === "sonnet" ? "claude-sonnet-4-6" : "claude-opus-5-5", observedEffort: null,
|
|
78
|
-
provenance: { kind: "provider-log", reference: "SYNTHETIC TEST FIXTURE, not real Claude" } });
|
|
79
|
-
const runtime = run(["observe", "--project", project, "--id", "fixture", "--file", source]);
|
|
80
|
-
complete(project, d.id, { runtime });
|
|
81
|
-
return { snapshot: snap(), dispatchId: d.id, runtime };
|
|
82
|
-
}
|
|
83
|
-
const work = (attempt, receipt) => record(project, "fixture", "implementation", { ...attempt, checks: [{ name: "runtime", status: "PASS", evidence: receipt.evidence }] });
|
|
84
|
-
const review = (attempt, verdict = "PASS") => record(project, "fixture", "review", { ...attempt, verdict, criteriaMet: verdict === "PASS", findings: [], evidence: write(`.nccgs/evidence/review-${++serial}.md`, "Synthetic independent review; never native evidence") });
|
|
85
|
-
return { project, write, intake, task, snap, assess, verify, request, worker, work, review, dispose() {
|
|
86
|
-
assert.equal(path.dirname(fs.realpathSync(project)).toLowerCase(), temporaryRoot.toLowerCase());
|
|
87
|
-
assert.ok(path.basename(project).startsWith("nccgs-phase2-")); assert.equal(fs.lstatSync(project).isSymbolicLink(), false);
|
|
88
|
-
fs.rmSync(project, { recursive: true, force: true });
|
|
89
|
-
} };
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
test("technical exit 0 cannot turn failed or assisted gameplay into acceptance PASS", () => {
|
|
93
|
-
const f = fixture();
|
|
94
|
-
try {
|
|
95
|
-
const attempt = f.worker();
|
|
96
|
-
const receipt = f.verify({ ...passed, builds: { needle: { won: false }, orbit: { won: false } }, reportOnly: true });
|
|
97
|
-
assert.equal(receipt.status, "PASS"); assert.deepEqual(f.assess().criteria.map(c => c.status), ["FAIL", "FAIL"]);
|
|
98
|
-
f.work(attempt, receipt);
|
|
99
|
-
const reviewer = f.worker("review");
|
|
100
|
-
assert.throws(() => f.review(reviewer), /Acceptance criteria not proven/);
|
|
101
|
-
f.review(reviewer, "CHANGES_REQUIRED");
|
|
102
|
-
assert.throws(() => record(f.project, "fixture", "report", { outcome: "success", summary: "Incorrect PASS" }), /Cannot report success/);
|
|
103
|
-
const partial = record(f.project, "fixture", "report", { outcome: "partial", summary: "Two unassisted builds remain unproven" });
|
|
104
|
-
assert.equal(partial.stages.report.acceptanceAssessment.status, "FAIL");
|
|
105
|
-
assert.throws(() => close(f.project, "fixture"), /review has not passed/);
|
|
106
|
-
assert.equal(f.verify({ ...passed, assisted: true }).status, "PASS");
|
|
107
|
-
assert.equal(f.assess().status, "FAIL");
|
|
108
|
-
} finally { f.dispose(); }
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
test("fresh measured criteria plus independent review permit DONE", () => {
|
|
112
|
-
const f = fixture();
|
|
113
|
-
try {
|
|
114
|
-
const attempt = f.worker(), receipt = f.verify(); f.work(attempt, receipt);
|
|
115
|
-
assert.equal(f.assess().status, "PASS"); f.review(f.worker("review"));
|
|
116
|
-
record(f.project, "fixture", "report", { outcome: "success", summary: "Synthetic acceptance passed" });
|
|
117
|
-
assert.equal(close(f.project, "fixture").phase, "DONE");
|
|
118
|
-
assert.equal(run(["acceptance-status", "--project", f.project, "--id", "fixture"]).automaticPassed, true);
|
|
119
|
-
} finally { f.dispose(); }
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test("missing, malformed, old, failed and source-stale measurements stay unverified", () => {
|
|
123
|
-
const f = fixture();
|
|
124
|
-
try {
|
|
125
|
-
assert.equal(f.assess().status, "UNVERIFIED");
|
|
126
|
-
f.write(measurement, passed); f.verify(passed, { skip: true });
|
|
127
|
-
assert.equal(f.assess().status, "UNVERIFIED");
|
|
128
|
-
f.verify(passed, { malformed: true }); assert.equal(f.assess().status, "UNVERIFIED");
|
|
129
|
-
f.verify({ ...passed, builds: {} }); assert.equal(f.assess().status, "UNVERIFIED");
|
|
130
|
-
f.verify(); assert.equal(f.assess().status, "PASS");
|
|
131
|
-
assert.equal(f.verify(passed, { exitCode: 2 }).status, "FAIL"); assert.equal(f.assess().status, "UNVERIFIED");
|
|
132
|
-
f.verify(); f.write("Assets/game.cs", "// changed after verification"); assert.equal(f.assess().status, "UNVERIFIED");
|
|
133
|
-
} finally { f.dispose(); }
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
test("frozen measurement survives output overwrite but detects captured-evidence tampering", () => {
|
|
137
|
-
const f = fixture();
|
|
138
|
-
try {
|
|
139
|
-
f.verify(); f.write(measurement, { ...passed, assisted: true }); assert.equal(f.assess().status, "PASS");
|
|
140
|
-
const assessment = f.assess(), receipt = json(path.join(f.project, assessment.criteria[0].receipt.path));
|
|
141
|
-
const captured = receipt.acceptance.criteria[0].checks[0].evidence.path;
|
|
142
|
-
f.write(captured, { ...passed, assisted: true }); assert.equal(f.assess().status, "UNVERIFIED");
|
|
143
|
-
assert.throws(() => guardTool(f.project, { tool_name: "Write", tool_input: { file_path: captured } }), /immutable/);
|
|
144
|
-
assert.equal(safeTaskCommand("node .claude/nccgs/tools/task.mjs acceptance-status --id fixture", f.project), "acceptance-status");
|
|
145
|
-
} finally { f.dispose(); }
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
test("pending latest attempt supersedes PASS and owns its measurement paths exclusively", () => {
|
|
149
|
-
const f = fixture(); let job;
|
|
150
|
-
try {
|
|
151
|
-
f.verify(); assert.equal(f.assess().status, "PASS");
|
|
152
|
-
const args = { name: "runtime", executable: process.execPath, arguments: ["Tools/probe.mjs", JSON.stringify({ data: passed, delayMs: 1000 })], requestKey: "pending" };
|
|
153
|
-
job = startVerification(f.project, "fixture", args);
|
|
154
|
-
assert.equal(f.assess().status, "UNVERIFIED");
|
|
155
|
-
assert.throws(() => startVerification(f.project, "fixture", { ...args, requestKey: "overlap" }), /owns these measurement/);
|
|
156
|
-
assert.equal(waitVerification(f.project, job.jobId, "fixture", 20000).status, "PASS");
|
|
157
|
-
assert.equal(f.assess().status, "PASS");
|
|
158
|
-
} finally { if (job) waitVerification(f.project, job.jobId, "fixture", 20000); f.dispose(); }
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
test("check-only criteria require mapping and manual criteria keep their owner gate", () => {
|
|
162
|
-
const f = fixture([{ id: "legacy", description: "Must not infer acceptance from exit 0", check: "runtime" }, { id: "feel", description: "Human playtest", manual: true }]);
|
|
163
|
-
try {
|
|
164
|
-
f.verify(); assert.deepEqual(f.assess().criteria.map(c => c.status), ["UNVERIFIED", "MANUAL_PENDING"]);
|
|
165
|
-
assert.deepEqual(f.task().contract.manualGates, ["feel"]);
|
|
166
|
-
f.write(".nccgs/bad.json", { ...f.intake, acceptance: [{ ...criteria[0], assertions: [{ artifact: "../outside.json", pointer: "/x", op: "eq", value: true }] }] });
|
|
167
|
-
assert.throws(() => initialize(f.project, "bad", null, null, null, { intake: ".nccgs/bad.json" }), /measurement/);
|
|
168
|
-
f.write(".nccgs/bad.json", { ...f.intake, acceptance: [{ ...criteria[0], assertions: [{ artifact: ".nccgs/evidence/measurements/../criteria/x.json", pointer: "/x", op: "eq", value: true }] }] });
|
|
169
|
-
assert.throws(() => initialize(f.project, "alias", null, null, null, { intake: ".nccgs/bad.json" }), /canonical/);
|
|
170
|
-
} finally { f.dispose(); }
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
test("typed thresholds and escaped JSON pointers are evaluated without coercion", () => {
|
|
174
|
-
const f = fixture([{ id: "load", description: "Measured load", check: "runtime", assertions: [
|
|
175
|
-
{ artifact: measurement, pointer: "/a~1b/~0load", op: "gte", value: 200 },
|
|
176
|
-
{ artifact: measurement, pointer: "/p95", op: "lte", value: 16.7 }] }]);
|
|
177
|
-
try {
|
|
178
|
-
f.verify({ "a/b": { "~load": 201 }, p95: 15 }); assert.equal(f.assess().status, "PASS");
|
|
179
|
-
f.verify({ "a/b": { "~load": "201" }, p95: 15 }); assert.equal(f.assess().status, "UNVERIFIED");
|
|
180
|
-
f.verify({ "a/b": { "~load": 201 }, p95: 20 }); assert.equal(f.assess().status, "FAIL");
|
|
181
|
-
} finally { f.dispose(); }
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
test("early viability blocks expansion while leaving repair, QA and review available", () => {
|
|
185
|
-
const f = fixture();
|
|
186
|
-
try {
|
|
187
|
-
f.worker();
|
|
188
|
-
assert.throws(() => reserve(f.project, "fixture", f.request()), /Early acceptance checkpoint/);
|
|
189
|
-
f.verify({ ...passed, assisted: true });
|
|
190
|
-
assert.throws(() => reserve(f.project, "fixture", f.request()), /Early acceptance checkpoint/);
|
|
191
|
-
assert.throws(() => reserve(f.project, "fixture", f.request("analysis", { purpose: "qa" })), /reserved for a completed repair/);
|
|
192
|
-
const repair = f.worker("implementation", { purpose: "repair" }, () => f.write("Assets/game.cs", "// repaired tuning"));
|
|
193
|
-
assert.equal(f.assess().status, "UNVERIFIED");
|
|
194
|
-
f.verify();
|
|
195
|
-
f.worker("analysis", { purpose: "qa" });
|
|
196
|
-
f.worker();
|
|
197
|
-
assert.equal(status(f.project, "fixture").deliveryBudget.remaining.repair, 0);
|
|
198
|
-
assert.ok(repair.dispatchId);
|
|
199
|
-
} finally { f.dispose(); }
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
test("STANDARD preserves a repair plus QA plus re-review after initial review at total cap", () => {
|
|
203
|
-
const f = fixture(criteria.map(c => ({ ...c, checkpoint: "final" })));
|
|
204
|
-
try {
|
|
205
|
-
let implementation;
|
|
206
|
-
for (let i = 0; i < 8; i++) implementation = f.worker();
|
|
207
|
-
const failed = f.verify({ ...passed, assisted: true }); f.work(implementation, failed);
|
|
208
|
-
assert.throws(() => reserve(f.project, "fixture", f.request()), /budget reserved/);
|
|
209
|
-
f.review(f.worker("review"), "CHANGES_REQUIRED");
|
|
210
|
-
assert.deepEqual(status(f.project, "fixture").deliveryBudget.remaining, { repair: 1, qa: 1, review: 1 });
|
|
211
|
-
const repair = f.worker("implementation", { purpose: "repair" }, () => f.write("Assets/game.cs", "// bounded correction"));
|
|
212
|
-
const fresh = f.verify(); f.work(repair, fresh);
|
|
213
|
-
f.worker("analysis", { purpose: "qa" }); f.review(f.worker("review"));
|
|
214
|
-
assert.equal(status(f.project, "fixture").usage.dispatches, 12);
|
|
215
|
-
record(f.project, "fixture", "report", { outcome: "success", summary: "Synthetic repaired acceptance passed" });
|
|
216
|
-
assert.equal(close(f.project, "fixture").phase, "DONE");
|
|
217
|
-
assert.throws(() => reserve(f.project, "fixture", f.request()), /budget exhausted/);
|
|
218
|
-
} finally { f.dispose(); }
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
test("protected purposes cannot be used without feedback or by an unrelated role", () => {
|
|
222
|
-
const f = fixture([]);
|
|
223
|
-
try {
|
|
224
|
-
assert.throws(() => reserve(f.project, "fixture", f.request("implementation", { purpose: "repair" })), /requires recorded negative/);
|
|
225
|
-
assert.throws(() => reserve(f.project, "fixture", f.request("implementation", { purpose: "qa" })), /read-only QA/);
|
|
226
|
-
assert.throws(() => reserve(f.project, "fixture", f.request("analysis", { purpose: "qa", repairFeedback: { fake: true } })), /reserved for a completed repair/);
|
|
227
|
-
assert.throws(() => reserve(f.project, "fixture", f.request("implementation", { limits: { maxDispatches: 4 } })), /cannot fit/);
|
|
228
|
-
} finally { f.dispose(); }
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
test("cancellation, child tasks, input refresh and reclassification cannot refund protected budget", () => {
|
|
232
|
-
const f = fixture([]);
|
|
233
|
-
try {
|
|
234
|
-
for (let i = 0; i < 7; i++) f.worker();
|
|
235
|
-
f.write(".nccgs/child.json", { ...f.intake, acceptance: [] });
|
|
236
|
-
initialize(f.project, "child", null, null, null, { intake: ".nccgs/child.json", parentId: "fixture" });
|
|
237
|
-
const req = f.request("analysis"), child = reserve(f.project, "child", req);
|
|
238
|
-
const saved = [process.env.CLAUDECODE, process.env.CLAUDE_CODE_ENTRYPOINT];
|
|
239
|
-
delete process.env.CLAUDECODE; delete process.env.CLAUDE_CODE_ENTRYPOINT;
|
|
240
|
-
try { reconcile(f.project, child.id, { status: "cancelled", reason: "Synthetic cancelled before launch", reference: "Synthetic fixture" }); }
|
|
241
|
-
finally { for (const [i, name] of ["CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT"].entries()) if (saved[i] === undefined) delete process.env[name]; else process.env[name] = saved[i]; }
|
|
242
|
-
assert.equal(status(f.project, "fixture").deliveryBudget.deliveryRemaining, 0);
|
|
243
|
-
refreshInputs(f.project, "fixture", "Synthetic explicit refresh");
|
|
244
|
-
reclassify(f.project, "fixture", { risk: "CONTROLLED", reason: "Synthetic escalation", evidence: ["Synthetic observed risk"] });
|
|
245
|
-
assert.throws(() => reserve(f.project, "fixture", f.request()), /budget reserved/);
|
|
246
|
-
assert.throws(() => reserve(f.project, "child", f.request("analysis")), /budget reserved/);
|
|
247
|
-
assert.equal(reserve(f.project, "child", req).id, child.id);
|
|
248
|
-
assert.equal(status(f.project, "fixture").usage.dispatches, 8);
|
|
249
|
-
} finally { f.dispose(); }
|
|
250
|
-
});
|
|
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 { fileURLToPath } from "node:url";
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import { artifact, json } from "../.claude/nccgs/tools/policy.mjs";
|
|
8
|
+
import { initialize, snapshot, record, close, taskPath, refreshInputs, reclassify } from "../.claude/nccgs/tools/gates.mjs";
|
|
9
|
+
import { reserve, claim, attachAgent, complete, status, reconcile } from "../.claude/nccgs/tools/orchestration.mjs";
|
|
10
|
+
import { startVerification, waitVerification } from "../.claude/nccgs/tools/verification-jobs.mjs";
|
|
11
|
+
import { assessCriteria } from "../.claude/nccgs/tools/criteria.mjs";
|
|
12
|
+
import { guardTool, safeTaskCommand } from "../.claude/nccgs/tools/tool-guard.mjs";
|
|
13
|
+
import { run } from "../.claude/nccgs/tools/task.mjs";
|
|
14
|
+
|
|
15
|
+
// Provider observations in these tests are SYNTHETIC fixtures, never Claude evidence.
|
|
16
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
17
|
+
const measurement = ".nccgs/evidence/measurements/runtime.json";
|
|
18
|
+
const criteria = ["needle", "orbit"].map(id => ({ id, description: `${id} wins unassisted`, check: "runtime", checkpoint: "early",
|
|
19
|
+
assertions: [{ artifact: measurement, pointer: `/builds/${id}/won`, op: "eq", value: true },
|
|
20
|
+
{ artifact: measurement, pointer: "/assisted", op: "eq", value: false },
|
|
21
|
+
{ artifact: measurement, pointer: "/reportOnly", op: "eq", value: false }] }));
|
|
22
|
+
const passed = { builds: { needle: { won: true }, orbit: { won: true } }, assisted: false, reportOnly: false };
|
|
23
|
+
|
|
24
|
+
function fixture(acceptance = criteria, contractOverrides = {}) {
|
|
25
|
+
const temporaryRoot = fs.realpathSync(os.tmpdir()), project = fs.mkdtempSync(path.join(temporaryRoot, "nccgs-phase2-"));
|
|
26
|
+
const write = (relative, value) => {
|
|
27
|
+
const file = path.join(project, relative); fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
28
|
+
fs.writeFileSync(file, typeof value === "string" ? value : JSON.stringify(value)); return artifact(project, relative);
|
|
29
|
+
};
|
|
30
|
+
fs.cpSync(path.join(root, ".claude/nccgs/tools"), path.join(project, ".claude/nccgs/tools"), { recursive: true });
|
|
31
|
+
for (const file of ["pipeline.json", "studio.json"]) write(`.claude/nccgs/${file}`, fs.readFileSync(path.join(root, ".claude/nccgs", file), "utf8"));
|
|
32
|
+
write("Assets/game.cs", "// fixture baseline");
|
|
33
|
+
write("Tools/probe.mjs", `import fs from 'node:fs';fs.mkdirSync('.nccgs/evidence/measurements',{recursive:true});const input=JSON.parse(process.argv[2]);if(input.delayMs)await new Promise(r=>setTimeout(r,input.delayMs));if(!input.skip)fs.writeFileSync('${measurement}',input.malformed?'bad JSON':JSON.stringify(input.data));console.log('Synthetic gameplay measurement fixture');process.exitCode=input.exitCode||0;`);
|
|
34
|
+
write(".nccgs/project.yaml", "schema_version: 2\nmodels:\n profile: claude-standalone\n");
|
|
35
|
+
write(".nccgs/execution.json", { schemaVersion: 1, name: "standalone", externalReview: { mode: "off" } });
|
|
36
|
+
write(".nccgs/inputs.json", { schemaVersion: 1, canonicalInputs: [], outputDirectories: [], checks: { runtime: { timeoutMs: 15000 } } });
|
|
37
|
+
const intake = { objective: "Synthetic test of acceptance mapping and repair budget", mode: "implement", readiness: "ready", requiredChecks: ["runtime"], acceptance, ...contractOverrides };
|
|
38
|
+
write(".nccgs/intake.json", intake);
|
|
39
|
+
initialize(project, "fixture", null, null, null, { intake: ".nccgs/intake.json" });
|
|
40
|
+
let serial = 0;
|
|
41
|
+
const task = () => json(taskPath(project, "fixture"));
|
|
42
|
+
const snap = () => snapshot(project, task()).sha256;
|
|
43
|
+
const assess = () => assessCriteria(project, task(), snap());
|
|
44
|
+
function verify(data = passed, options = {}) {
|
|
45
|
+
const job = startVerification(project, "fixture", { name: "runtime", executable: process.execPath, arguments: ["Tools/probe.mjs", JSON.stringify({ data, ...options })], requestKey: `probe-${++serial}` });
|
|
46
|
+
return waitVerification(project, job.jobId, "fixture", 20000);
|
|
47
|
+
}
|
|
48
|
+
function request(kind = "implementation", overrides = {}) {
|
|
49
|
+
return { kind, agent: kind === "review" ? "nccgs-adversarial-reviewer" : kind === "analysis" ? "nccgs-qa-engineer" : "nccgs-gameplay-programmer",
|
|
50
|
+
requestKey: `dispatch-${++serial}`, reason: "SYNTHETIC TEST FIXTURE", writePaths: kind === "implementation" ? ["Assets/game.cs"] : [], ...overrides };
|
|
51
|
+
}
|
|
52
|
+
function worker(kind = "implementation", overrides = {}, mutate = null) {
|
|
53
|
+
const req = request(kind, overrides);
|
|
54
|
+
if (req.purpose === "repair") {
|
|
55
|
+
req.workPacket = `.nccgs/evidence/work-packets/${req.requestKey}.json`;
|
|
56
|
+
req.handoffPaths = [`.nccgs/evidence/analysis/${req.requestKey}.md`];
|
|
57
|
+
write(req.workPacket, { schemaVersion: 1, taskId: "fixture", contractHash: task().contractHash, sourceSnapshot: snap(),
|
|
58
|
+
problem: "Synthetic acceptance failure", solution: "Apply the predetermined fixture correction", changes: [{ path: "Assets/game.cs", instruction: "Apply the synthetic bounded correction" }],
|
|
59
|
+
context: [], verification: ["runtime"] });
|
|
60
|
+
}
|
|
61
|
+
const d = reserve(project, "fixture", req), count = ++serial;
|
|
62
|
+
claim(project, d.id, { sessionId: "fixture-session", toolUseId: `tool-${count}`, agent: req.agent });
|
|
63
|
+
if (req.workPacket) {
|
|
64
|
+
attachAgent(project, d.id, `${kind}-${count}`);
|
|
65
|
+
guardTool(project, { session_id: "fixture-session", agent_id: `${kind}-${count}`, tool_use_id: `edit-${count}`, tool_name: "Edit", tool_input: { file_path: "Assets/game.cs", old_string: "fixture", new_string: "correction" } });
|
|
66
|
+
}
|
|
67
|
+
if (mutate) mutate();
|
|
68
|
+
if (req.workPacket) {
|
|
69
|
+
const content = "NCCGS-HANDOFF: COMPLETE\nSynthetic bounded correction; coordinator verification pending.";
|
|
70
|
+
guardTool(project, { session_id: "fixture-session", agent_id: `${kind}-${count}`, tool_use_id: `handoff-${count}`, tool_name: "Write", tool_input: { file_path: req.handoffPaths[0], content } });
|
|
71
|
+
write(req.handoffPaths[0], content);
|
|
72
|
+
}
|
|
73
|
+
const current = task(), route = kind === "review" ? current.execution.profile.models.review : current.implementationRoute;
|
|
74
|
+
const source = `.nccgs/evidence/raw-${count}.json`;
|
|
75
|
+
write(source, { taskId: "fixture", contractHash: current.contractHash, snapshot: snap(), dispatchId: d.id,
|
|
76
|
+
sessionId: "fixture-session", toolUseId: `tool-${count}`, agentId: `${kind}-${count}`, status: "completed", capturedAt: new Date().toISOString(),
|
|
77
|
+
observedProvider: "claude-code", observedModel: route.model === "sonnet" ? "claude-sonnet-4-6" : route.model === "fable" ? "claude-fable-5-1" : "claude-opus-5-5", observedEffort: null,
|
|
78
|
+
provenance: { kind: "provider-log", reference: "SYNTHETIC TEST FIXTURE, not real Claude" } });
|
|
79
|
+
const runtime = run(["observe", "--project", project, "--id", "fixture", "--file", source]);
|
|
80
|
+
complete(project, d.id, { runtime });
|
|
81
|
+
return { snapshot: snap(), dispatchId: d.id, runtime };
|
|
82
|
+
}
|
|
83
|
+
const work = (attempt, receipt) => record(project, "fixture", "implementation", { ...attempt, checks: [{ name: "runtime", status: "PASS", evidence: receipt.evidence }] });
|
|
84
|
+
const review = (attempt, verdict = "PASS") => record(project, "fixture", "review", { ...attempt, verdict, criteriaMet: verdict === "PASS", findings: [], evidence: write(`.nccgs/evidence/review-${++serial}.md`, "Synthetic independent review; never native evidence") });
|
|
85
|
+
return { project, write, intake, task, snap, assess, verify, request, worker, work, review, dispose() {
|
|
86
|
+
assert.equal(path.dirname(fs.realpathSync(project)).toLowerCase(), temporaryRoot.toLowerCase());
|
|
87
|
+
assert.ok(path.basename(project).startsWith("nccgs-phase2-")); assert.equal(fs.lstatSync(project).isSymbolicLink(), false);
|
|
88
|
+
fs.rmSync(project, { recursive: true, force: true });
|
|
89
|
+
} };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
test("technical exit 0 cannot turn failed or assisted gameplay into acceptance PASS", () => {
|
|
93
|
+
const f = fixture();
|
|
94
|
+
try {
|
|
95
|
+
const attempt = f.worker();
|
|
96
|
+
const receipt = f.verify({ ...passed, builds: { needle: { won: false }, orbit: { won: false } }, reportOnly: true });
|
|
97
|
+
assert.equal(receipt.status, "PASS"); assert.deepEqual(f.assess().criteria.map(c => c.status), ["FAIL", "FAIL"]);
|
|
98
|
+
f.work(attempt, receipt);
|
|
99
|
+
const reviewer = f.worker("review");
|
|
100
|
+
assert.throws(() => f.review(reviewer), /Acceptance criteria not proven/);
|
|
101
|
+
f.review(reviewer, "CHANGES_REQUIRED");
|
|
102
|
+
assert.throws(() => record(f.project, "fixture", "report", { outcome: "success", summary: "Incorrect PASS" }), /Cannot report success/);
|
|
103
|
+
const partial = record(f.project, "fixture", "report", { outcome: "partial", summary: "Two unassisted builds remain unproven" });
|
|
104
|
+
assert.equal(partial.stages.report.acceptanceAssessment.status, "FAIL");
|
|
105
|
+
assert.throws(() => close(f.project, "fixture"), /review has not passed/);
|
|
106
|
+
assert.equal(f.verify({ ...passed, assisted: true }).status, "PASS");
|
|
107
|
+
assert.equal(f.assess().status, "FAIL");
|
|
108
|
+
} finally { f.dispose(); }
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("fresh measured criteria plus independent review permit DONE", () => {
|
|
112
|
+
const f = fixture();
|
|
113
|
+
try {
|
|
114
|
+
const attempt = f.worker(), receipt = f.verify(); f.work(attempt, receipt);
|
|
115
|
+
assert.equal(f.assess().status, "PASS"); f.review(f.worker("review"));
|
|
116
|
+
record(f.project, "fixture", "report", { outcome: "success", summary: "Synthetic acceptance passed" });
|
|
117
|
+
assert.equal(close(f.project, "fixture").phase, "DONE");
|
|
118
|
+
assert.equal(run(["acceptance-status", "--project", f.project, "--id", "fixture"]).automaticPassed, true);
|
|
119
|
+
} finally { f.dispose(); }
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("missing, malformed, old, failed and source-stale measurements stay unverified", () => {
|
|
123
|
+
const f = fixture();
|
|
124
|
+
try {
|
|
125
|
+
assert.equal(f.assess().status, "UNVERIFIED");
|
|
126
|
+
f.write(measurement, passed); f.verify(passed, { skip: true });
|
|
127
|
+
assert.equal(f.assess().status, "UNVERIFIED");
|
|
128
|
+
f.verify(passed, { malformed: true }); assert.equal(f.assess().status, "UNVERIFIED");
|
|
129
|
+
f.verify({ ...passed, builds: {} }); assert.equal(f.assess().status, "UNVERIFIED");
|
|
130
|
+
f.verify(); assert.equal(f.assess().status, "PASS");
|
|
131
|
+
assert.equal(f.verify(passed, { exitCode: 2 }).status, "FAIL"); assert.equal(f.assess().status, "UNVERIFIED");
|
|
132
|
+
f.verify(); f.write("Assets/game.cs", "// changed after verification"); assert.equal(f.assess().status, "UNVERIFIED");
|
|
133
|
+
} finally { f.dispose(); }
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("frozen measurement survives output overwrite but detects captured-evidence tampering", () => {
|
|
137
|
+
const f = fixture();
|
|
138
|
+
try {
|
|
139
|
+
f.verify(); f.write(measurement, { ...passed, assisted: true }); assert.equal(f.assess().status, "PASS");
|
|
140
|
+
const assessment = f.assess(), receipt = json(path.join(f.project, assessment.criteria[0].receipt.path));
|
|
141
|
+
const captured = receipt.acceptance.criteria[0].checks[0].evidence.path;
|
|
142
|
+
f.write(captured, { ...passed, assisted: true }); assert.equal(f.assess().status, "UNVERIFIED");
|
|
143
|
+
assert.throws(() => guardTool(f.project, { tool_name: "Write", tool_input: { file_path: captured } }), /immutable/);
|
|
144
|
+
assert.equal(safeTaskCommand("node .claude/nccgs/tools/task.mjs acceptance-status --id fixture", f.project), "acceptance-status");
|
|
145
|
+
} finally { f.dispose(); }
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("pending latest attempt supersedes PASS and owns its measurement paths exclusively", () => {
|
|
149
|
+
const f = fixture(); let job;
|
|
150
|
+
try {
|
|
151
|
+
f.verify(); assert.equal(f.assess().status, "PASS");
|
|
152
|
+
const args = { name: "runtime", executable: process.execPath, arguments: ["Tools/probe.mjs", JSON.stringify({ data: passed, delayMs: 1000 })], requestKey: "pending" };
|
|
153
|
+
job = startVerification(f.project, "fixture", args);
|
|
154
|
+
assert.equal(f.assess().status, "UNVERIFIED");
|
|
155
|
+
assert.throws(() => startVerification(f.project, "fixture", { ...args, requestKey: "overlap" }), /owns these measurement/);
|
|
156
|
+
assert.equal(waitVerification(f.project, job.jobId, "fixture", 20000).status, "PASS");
|
|
157
|
+
assert.equal(f.assess().status, "PASS");
|
|
158
|
+
} finally { if (job) waitVerification(f.project, job.jobId, "fixture", 20000); f.dispose(); }
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("check-only criteria require mapping and manual criteria keep their owner gate", () => {
|
|
162
|
+
const f = fixture([{ id: "legacy", description: "Must not infer acceptance from exit 0", check: "runtime" }, { id: "feel", description: "Human playtest", manual: true }]);
|
|
163
|
+
try {
|
|
164
|
+
f.verify(); assert.deepEqual(f.assess().criteria.map(c => c.status), ["UNVERIFIED", "MANUAL_PENDING"]);
|
|
165
|
+
assert.deepEqual(f.task().contract.manualGates, ["feel"]);
|
|
166
|
+
f.write(".nccgs/bad.json", { ...f.intake, acceptance: [{ ...criteria[0], assertions: [{ artifact: "../outside.json", pointer: "/x", op: "eq", value: true }] }] });
|
|
167
|
+
assert.throws(() => initialize(f.project, "bad", null, null, null, { intake: ".nccgs/bad.json" }), /measurement/);
|
|
168
|
+
f.write(".nccgs/bad.json", { ...f.intake, acceptance: [{ ...criteria[0], assertions: [{ artifact: ".nccgs/evidence/measurements/../criteria/x.json", pointer: "/x", op: "eq", value: true }] }] });
|
|
169
|
+
assert.throws(() => initialize(f.project, "alias", null, null, null, { intake: ".nccgs/bad.json" }), /canonical/);
|
|
170
|
+
} finally { f.dispose(); }
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("typed thresholds and escaped JSON pointers are evaluated without coercion", () => {
|
|
174
|
+
const f = fixture([{ id: "load", description: "Measured load", check: "runtime", assertions: [
|
|
175
|
+
{ artifact: measurement, pointer: "/a~1b/~0load", op: "gte", value: 200 },
|
|
176
|
+
{ artifact: measurement, pointer: "/p95", op: "lte", value: 16.7 }] }]);
|
|
177
|
+
try {
|
|
178
|
+
f.verify({ "a/b": { "~load": 201 }, p95: 15 }); assert.equal(f.assess().status, "PASS");
|
|
179
|
+
f.verify({ "a/b": { "~load": "201" }, p95: 15 }); assert.equal(f.assess().status, "UNVERIFIED");
|
|
180
|
+
f.verify({ "a/b": { "~load": 201 }, p95: 20 }); assert.equal(f.assess().status, "FAIL");
|
|
181
|
+
} finally { f.dispose(); }
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("early viability blocks expansion while leaving repair, QA and review available", () => {
|
|
185
|
+
const f = fixture();
|
|
186
|
+
try {
|
|
187
|
+
f.worker();
|
|
188
|
+
assert.throws(() => reserve(f.project, "fixture", f.request()), /Early acceptance checkpoint/);
|
|
189
|
+
f.verify({ ...passed, assisted: true });
|
|
190
|
+
assert.throws(() => reserve(f.project, "fixture", f.request()), /Early acceptance checkpoint/);
|
|
191
|
+
assert.throws(() => reserve(f.project, "fixture", f.request("analysis", { purpose: "qa" })), /reserved for a completed repair/);
|
|
192
|
+
const repair = f.worker("implementation", { purpose: "repair" }, () => f.write("Assets/game.cs", "// repaired tuning"));
|
|
193
|
+
assert.equal(f.assess().status, "UNVERIFIED");
|
|
194
|
+
f.verify();
|
|
195
|
+
f.worker("analysis", { purpose: "qa" });
|
|
196
|
+
f.worker();
|
|
197
|
+
assert.equal(status(f.project, "fixture").deliveryBudget.remaining.repair, 0);
|
|
198
|
+
assert.ok(repair.dispatchId);
|
|
199
|
+
} finally { f.dispose(); }
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("STANDARD preserves a repair plus QA plus re-review after initial review at total cap", () => {
|
|
203
|
+
const f = fixture(criteria.map(c => ({ ...c, checkpoint: "final" })));
|
|
204
|
+
try {
|
|
205
|
+
let implementation;
|
|
206
|
+
for (let i = 0; i < 8; i++) implementation = f.worker();
|
|
207
|
+
const failed = f.verify({ ...passed, assisted: true }); f.work(implementation, failed);
|
|
208
|
+
assert.throws(() => reserve(f.project, "fixture", f.request()), /budget reserved/);
|
|
209
|
+
f.review(f.worker("review"), "CHANGES_REQUIRED");
|
|
210
|
+
assert.deepEqual(status(f.project, "fixture").deliveryBudget.remaining, { repair: 1, qa: 1, review: 1 });
|
|
211
|
+
const repair = f.worker("implementation", { purpose: "repair" }, () => f.write("Assets/game.cs", "// bounded correction"));
|
|
212
|
+
const fresh = f.verify(); f.work(repair, fresh);
|
|
213
|
+
f.worker("analysis", { purpose: "qa" }); f.review(f.worker("review"));
|
|
214
|
+
assert.equal(status(f.project, "fixture").usage.dispatches, 12);
|
|
215
|
+
record(f.project, "fixture", "report", { outcome: "success", summary: "Synthetic repaired acceptance passed" });
|
|
216
|
+
assert.equal(close(f.project, "fixture").phase, "DONE");
|
|
217
|
+
assert.throws(() => reserve(f.project, "fixture", f.request()), /budget exhausted/);
|
|
218
|
+
} finally { f.dispose(); }
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("protected purposes cannot be used without feedback or by an unrelated role", () => {
|
|
222
|
+
const f = fixture([]);
|
|
223
|
+
try {
|
|
224
|
+
assert.throws(() => reserve(f.project, "fixture", f.request("implementation", { purpose: "repair" })), /requires recorded negative/);
|
|
225
|
+
assert.throws(() => reserve(f.project, "fixture", f.request("implementation", { purpose: "qa" })), /read-only QA/);
|
|
226
|
+
assert.throws(() => reserve(f.project, "fixture", f.request("analysis", { purpose: "qa", repairFeedback: { fake: true } })), /reserved for a completed repair/);
|
|
227
|
+
assert.throws(() => reserve(f.project, "fixture", f.request("implementation", { limits: { maxDispatches: 4 } })), /cannot fit/);
|
|
228
|
+
} finally { f.dispose(); }
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("cancellation, child tasks, input refresh and reclassification cannot refund protected budget", () => {
|
|
232
|
+
const f = fixture([]);
|
|
233
|
+
try {
|
|
234
|
+
for (let i = 0; i < 7; i++) f.worker();
|
|
235
|
+
f.write(".nccgs/child.json", { ...f.intake, acceptance: [] });
|
|
236
|
+
initialize(f.project, "child", null, null, null, { intake: ".nccgs/child.json", parentId: "fixture" });
|
|
237
|
+
const req = f.request("analysis"), child = reserve(f.project, "child", req);
|
|
238
|
+
const saved = [process.env.CLAUDECODE, process.env.CLAUDE_CODE_ENTRYPOINT];
|
|
239
|
+
delete process.env.CLAUDECODE; delete process.env.CLAUDE_CODE_ENTRYPOINT;
|
|
240
|
+
try { reconcile(f.project, child.id, { status: "cancelled", reason: "Synthetic cancelled before launch", reference: "Synthetic fixture" }); }
|
|
241
|
+
finally { for (const [i, name] of ["CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT"].entries()) if (saved[i] === undefined) delete process.env[name]; else process.env[name] = saved[i]; }
|
|
242
|
+
assert.equal(status(f.project, "fixture").deliveryBudget.deliveryRemaining, 0);
|
|
243
|
+
refreshInputs(f.project, "fixture", "Synthetic explicit refresh");
|
|
244
|
+
reclassify(f.project, "fixture", { risk: "CONTROLLED", reason: "Synthetic escalation", evidence: ["Synthetic observed risk"] });
|
|
245
|
+
assert.throws(() => reserve(f.project, "fixture", f.request()), /budget reserved/);
|
|
246
|
+
assert.throws(() => reserve(f.project, "child", f.request("analysis")), /budget reserved/);
|
|
247
|
+
assert.equal(reserve(f.project, "child", req).id, child.id);
|
|
248
|
+
assert.equal(status(f.project, "fixture").usage.dispatches, 8);
|
|
249
|
+
} finally { f.dispose(); }
|
|
250
|
+
});
|