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
|
@@ -1,200 +1,200 @@
|
|
|
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 test from "node:test";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { artifact, json } from "../.claude/nccgs/tools/policy.mjs";
|
|
8
|
-
import { initialize, snapshot, taskPath, record, close, check, reclassify } from "../.claude/nccgs/tools/gates.mjs";
|
|
9
|
-
import { reserve, claim, attachAgent, complete } from "../.claude/nccgs/tools/orchestration.mjs";
|
|
10
|
-
import { validateDesign, validateDesignCommand, requireDesignReady } from "../.claude/nccgs/tools/design-readiness.mjs";
|
|
11
|
-
import { run } from "../.claude/nccgs/tools/task.mjs";
|
|
12
|
-
import { guardTool } from "../.claude/nccgs/tools/tool-guard.mjs";
|
|
13
|
-
|
|
14
|
-
// All provider records here are synthetic. Native acceptance uses separate evidence.
|
|
15
|
-
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
16
|
-
function fixture() {
|
|
17
|
-
const parent = fs.realpathSync(os.tmpdir()), project = fs.mkdtempSync(path.join(parent, "nccgs-design-"));
|
|
18
|
-
const write = (relative, value) => {
|
|
19
|
-
const file = path.join(project, relative); fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
20
|
-
fs.writeFileSync(file, typeof value === "string" ? value : JSON.stringify(value)); return artifact(project, relative);
|
|
21
|
-
};
|
|
22
|
-
fs.cpSync(path.join(root, ".claude/nccgs"), path.join(project, ".claude/nccgs"), { recursive: true });
|
|
23
|
-
write(".nccgs/project.yaml", "schema_version: 2\nmodels:\n profile: claude-standalone\n");
|
|
24
|
-
write(".nccgs/execution.json", { schemaVersion: 1, name: "standalone", designReviewRequired: true });
|
|
25
|
-
write(".nccgs/inputs.json", { schemaVersion: 1, canonicalInputs: ["Design/brief.md"], outputDirectories: [], checks: {} });
|
|
26
|
-
write("Assets/game.cs", "// synthetic unchanged source");
|
|
27
|
-
const source = write("Design/brief.md", "Owner asks for two viable builds; human acceptance is required, bot diagnostics are not acceptance.");
|
|
28
|
-
const cite = { ...source, anchor: "Owner asks" };
|
|
29
|
-
const docPath = ".nccgs/evidence/analysis/design.json";
|
|
30
|
-
const document = { schemaVersion: 1, objective: "Two viable builds", scope: "feature", target: "production-change", readiness: "READY",
|
|
31
|
-
sources: [source], requirements: [{ id: "REQ-1", source: cite, statement: "Two viable builds", rules: "Keep both natural build paths and random upgrades; human judgement defines experience acceptance", kind: "fixed", criteria: ["AC-1"] }],
|
|
32
|
-
acceptance: [{ id: "AC-1", description: "Owner accepts both builds", manual: true, procedure: "Play both builds unassisted", environment: "Target Unity player", passCondition: "Owner explicitly accepts each build", rationale: "Experience requires human judgement, not all-seed bot wins", authority: cite }],
|
|
33
|
-
requiredChecks: [], manualGates: ["AC-1"], decisions: [], diagnostics: [{ id: "BOT", procedure: "Run seeded bot", limitations: "Bot policy is not human skill" }], outOfScope: [], risks: [] };
|
|
34
|
-
let count = 0;
|
|
35
|
-
const task = id => json(taskPath(project, id));
|
|
36
|
-
const init = (id, value) => {
|
|
37
|
-
write(`.nccgs/evidence/intake/${id}.json`, value);
|
|
38
|
-
return initialize(project, id, null, null, null, { intake: `.nccgs/evidence/intake/${id}.json` });
|
|
39
|
-
};
|
|
40
|
-
function worker(id, kind = "analysis") {
|
|
41
|
-
const n = ++count, t = task(id), agent = kind === "review" ? "nccgs-adversarial-reviewer" : "nccgs-game-designer";
|
|
42
|
-
const d = reserve(project, id, { requestKey: `r-${n}`, kind, agent, reason: "SYNTHETIC fixture", writePaths: [] });
|
|
43
|
-
claim(project, d.id, { sessionId: "fixture", toolUseId: `call-${n}`, agent });
|
|
44
|
-
attachAgent(project, d.id, `agent-${n}`);
|
|
45
|
-
const snap = snapshot(project, t).sha256;
|
|
46
|
-
const raw = `.nccgs/evidence/raw-${n}.json`;
|
|
47
|
-
write(raw, { taskId: id, contractHash: t.contractHash, snapshot: snap, dispatchId: d.id, sessionId: "fixture", toolUseId: `call-${n}`, agentId: `agent-${n}`, status: "completed", capturedAt: new Date().toISOString(), observedProvider: "claude-code", observedModel: "claude-opus-5-5", observedEffort: null, provenance: { kind: "provider-log", reference: "SYNTHETIC TEST; not actual Claude" } });
|
|
48
|
-
const runtime = run(["observe", "--project", project, "--id", id, "--file", raw]);
|
|
49
|
-
complete(project, d.id, { runtime }); return { snapshot: snap, dispatchId: d.id, runtime };
|
|
50
|
-
}
|
|
51
|
-
function design(value = document, verdict = "PASS", recordReview = true) {
|
|
52
|
-
init("design", { objective: "Produce and review the scoped design", mode: "plan", workflow: "design", readiness: "ready", requiredChecks: ["design-contract"], acceptance: [], sources: [source.path], analysisArtifacts: [docPath, ".nccgs/evidence/analysis/review.md"] });
|
|
53
|
-
write(docPath, value);
|
|
54
|
-
const author = worker("design");
|
|
55
|
-
const validation = validateDesignCommand(project, "design", docPath);
|
|
56
|
-
record(project, "design", "analysis", { ...author, evidence: artifact(project, docPath), checks: [{ name: "design-contract", status: "PASS", evidence: validation.evidence }] });
|
|
57
|
-
const reviewer = worker("design", "review");
|
|
58
|
-
const reviewInput = { ...reviewer, verdict, criteriaMet: verdict === "PASS", findings: [], evidence: write(".nccgs/evidence/analysis/review.md", "Synthetic independent document review") };
|
|
59
|
-
if (recordReview) record(project, "design", "review", reviewInput);
|
|
60
|
-
if (recordReview && verdict === "PASS") { record(project, "design", "report", { outcome: "success", summary: "Synthetic design accepted; game acceptance remains pending" }); close(project, "design"); }
|
|
61
|
-
return { author, validation, reviewer, reviewInput };
|
|
62
|
-
}
|
|
63
|
-
return { project, write, task, init, document, docPath, design, worker, dispose() {
|
|
64
|
-
assert.equal(path.dirname(fs.realpathSync(project)), parent); assert.ok(path.basename(project).startsWith("nccgs-design-"));
|
|
65
|
-
fs.rmSync(project, { recursive: true, force: true });
|
|
66
|
-
} };
|
|
67
|
-
}
|
|
68
|
-
const implRequest = { requestKey: "implementation", kind: "implementation", agent: "nccgs-gameplay-programmer", reason: "Synthetic dependent work", writePaths: ["Assets/game.cs"] };
|
|
69
|
-
|
|
70
|
-
test("design false-pass and coverage findings block PASS even when accepted as medium risks", () => {
|
|
71
|
-
const f = fixture(); try {
|
|
72
|
-
const { reviewInput } = f.design(undefined, "CHANGES_REQUIRED", false);
|
|
73
|
-
const finding = { severity: "MEDIUM", status: "ACCEPTED", description: "A shared contaminated retry config can make both digests match while upgrades leak" };
|
|
74
|
-
assert.throws(() => record(f.project, "design", "review", { ...reviewInput, verdict: "PASS", criteriaMet: true, findings: [finding] }), /classify acceptanceImpact/);
|
|
75
|
-
for (const acceptanceImpact of ["false-pass", "missing-coverage", "authority-gap"]) {
|
|
76
|
-
assert.throws(() => record(f.project, "design", "review", { ...reviewInput, verdict: "PASS", criteriaMet: true, findings: [{ ...finding, acceptanceImpact }] }), /unresolved coverage\/authority gaps/);
|
|
77
|
-
}
|
|
78
|
-
assert.doesNotThrow(() => record(f.project, "design", "review", { ...reviewInput, findings: [{ ...finding, status: "OPEN", acceptanceImpact: "false-pass" }] }));
|
|
79
|
-
assert.throws(() => f.init("blocked-work", { mode: "implement", design: { taskId: "design" } }), /completed \/design plan/);
|
|
80
|
-
const nextReview = f.worker("design", "review");
|
|
81
|
-
assert.doesNotThrow(() => record(f.project, "design", "review", { ...reviewInput, ...nextReview, verdict: "PASS", criteriaMet: true, findings: [{ ...finding, acceptanceImpact: "none", description: "An explicitly nonblocking presentation preference" }] }));
|
|
82
|
-
} finally { f.dispose(); }
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test("self-declared readiness cannot reserve implementation; partial reporting and analysis remain possible", () => {
|
|
86
|
-
const f = fixture(); try {
|
|
87
|
-
f.init("work", { objective: "New game feature", mode: "implement", readiness: "ready" });
|
|
88
|
-
assert.throws(() => reserve(f.project, "work", implRequest), /reviewed design missing/);
|
|
89
|
-
assert.equal(fs.existsSync(path.join(f.project, ".nccgs/orchestration.json")), false);
|
|
90
|
-
assert.throws(() => check(f.project, f.task("work"), "implementation"), /reviewed design missing/);
|
|
91
|
-
assert.equal(record(f.project, "work", "report", { outcome: "blocked", summary: "Needs independently reviewed design" }).phase, "BLOCKED");
|
|
92
|
-
assert.doesNotThrow(() => reserve(f.project, "work", { ...implRequest, kind: "analysis", writePaths: [] }));
|
|
93
|
-
} finally { f.dispose(); }
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test("ready contract validates coverage, threshold authority, diagnostics and delegated tuning", () => {
|
|
97
|
-
const f = fixture(); try {
|
|
98
|
-
assert.doesNotThrow(() => validateDesign(f.project, f.document));
|
|
99
|
-
const bad = change => { const d = structuredClone(f.document); change(d); return d; };
|
|
100
|
-
for (const value of [bad(d => d.requirements[0].criteria = []), bad(d => d.acceptance[0].authority.anchor = ""),
|
|
101
|
-
bad(d => d.acceptance[0] = { ...d.acceptance[0], manual: false, check: "bot" }),
|
|
102
|
-
bad(d => d.decisions.push({ id: "open", status: "open", question: "What is viable?" })),
|
|
103
|
-
bad(d => d.diagnostics[0].id = "AC-1"), bad(d => d.requirements[0].kind = "delegated-tuning")]) {
|
|
104
|
-
assert.throws(() => validateDesign(f.project, value), /Design readiness/);
|
|
105
|
-
}
|
|
106
|
-
} finally { f.dispose(); }
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test("independently reviewed design transfers exact acceptance and survives implementation source edits", () => {
|
|
110
|
-
const f = fixture(); try {
|
|
111
|
-
f.design(); const work = f.init("work", { mode: "implement", design: { taskId: "design" } });
|
|
112
|
-
assert.deepEqual(work.contract.acceptance, f.document.acceptance);
|
|
113
|
-
assert.deepEqual(work.contract.manualGates, ["AC-1"]);
|
|
114
|
-
const d = reserve(f.project, "work", implRequest);
|
|
115
|
-
claim(f.project, d.id, { sessionId: "implementation", toolUseId: "edit", agent: implRequest.agent });
|
|
116
|
-
attachAgent(f.project, d.id, "programmer");
|
|
117
|
-
assert.doesNotThrow(() => guardTool(f.project, { session_id: "implementation", agent_id: "programmer", tool_name: "Edit", tool_input: { file_path: "Assets/game.cs" } }));
|
|
118
|
-
f.write("Assets/game.cs", "// implementation progressed after design review");
|
|
119
|
-
assert.doesNotThrow(() => requireDesignReady(f.project, f.task("work")));
|
|
120
|
-
assert.ok(f.task("design").closed);
|
|
121
|
-
} finally { f.dispose(); }
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test("conflicting implementation criteria and stale design source are refused without spending dispatches", () => {
|
|
125
|
-
const f = fixture(); try {
|
|
126
|
-
f.design();
|
|
127
|
-
assert.throws(() => f.init("weakened", { mode: "implement", design: { taskId: "design" }, acceptance: [] }), /differs from reviewed design/);
|
|
128
|
-
f.init("work", { mode: "implement", design: { taskId: "design" } });
|
|
129
|
-
f.write("Design/brief.md", "Changed owner instructions after review");
|
|
130
|
-
assert.throws(() => reserve(f.project, "work", implRequest), /stale artifact/);
|
|
131
|
-
assert.throws(() => check(f.project, f.task("work"), "implementation"), /stale artifact/);
|
|
132
|
-
} finally { f.dispose(); }
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
test("BLOCKED design can be validated and negatively reviewed but cannot receive PASS or unlock work", () => {
|
|
136
|
-
const f = fixture(); try {
|
|
137
|
-
const blocked = structuredClone(f.document); blocked.readiness = "BLOCKED"; blocked.decisions = [{ id: "D", question: "Resolve viable definition", status: "open" }];
|
|
138
|
-
const result = f.design(blocked, "BLOCKED");
|
|
139
|
-
assert.throws(() => record(f.project, "design", "review", { ...result.reviewInput, ...f.worker("design", "review"), verdict: "PASS", criteriaMet: true }), /BLOCKED design/);
|
|
140
|
-
assert.throws(() => f.init("work", { mode: "implement", design: { taskId: "design" } }), /completed \/design/);
|
|
141
|
-
} finally { f.dispose(); }
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
test("document receipts cannot be edited by tools or reused for a different analysis artifact", () => {
|
|
145
|
-
const f = fixture(); try {
|
|
146
|
-
const result = f.design();
|
|
147
|
-
assert.throws(() => guardTool(f.project, { tool_name: "Write", tool_input: { file_path: result.validation.evidence.path } }), /immutable/);
|
|
148
|
-
f.write(f.docPath, { ...f.document, objective: "Swapped after independent review" });
|
|
149
|
-
assert.throws(() => f.init("work", { mode: "implement", design: { taskId: "design" } }), /stale artifact/);
|
|
150
|
-
} finally { f.dispose(); }
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
test("ready local fixes keep a narrow exemption while feature reclassification requires design", () => {
|
|
154
|
-
const f = fixture(); try {
|
|
155
|
-
f.init("fix", { objective: "Repair a documented local formatting defect", mode: "implement", scope: "local", target: "fix", readiness: "ready", acceptance: [{ id: "A", description: "Existing formatting is restored", manual: true }] });
|
|
156
|
-
assert.doesNotThrow(() => requireDesignReady(f.project, f.task("fix")));
|
|
157
|
-
reclassify(f.project, "fix", { scope: "feature", risk: "STANDARD", reason: "Discovered broader feature semantics", evidence: [{ observation: "Synthetic discovery" }] });
|
|
158
|
-
assert.throws(() => reserve(f.project, "fix", implRequest), /reviewed design missing/);
|
|
159
|
-
} finally { f.dispose(); }
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
test("fixed design comparison captures exact changes and preserves the declared baseline", () => {
|
|
163
|
-
const f = fixture(); try {
|
|
164
|
-
const baselinePath = ".nccgs/evidence/context/baseline.json";
|
|
165
|
-
const baseline = f.write(baselinePath, f.document);
|
|
166
|
-
f.init("diff", { objective: "Review a narrow document correction", mode: "plan", workflow: "design", readiness: "ready",
|
|
167
|
-
sources: [baselinePath], requiredChecks: ["design-contract"], analysisArtifacts: [f.docPath], acceptance: [] });
|
|
168
|
-
const candidate = structuredClone(f.document);
|
|
169
|
-
candidate.requirements[0].rules += "; clarified wording";
|
|
170
|
-
candidate.acceptance[0].procedure += "; record the owner decision";
|
|
171
|
-
f.write(f.docPath, candidate);
|
|
172
|
-
const result = run(["design-compare", "--project", f.project, "--id", "diff", "--baseline", baselinePath, "--file", f.docPath]);
|
|
173
|
-
assert.deepEqual(result.requirements, { added: [], removed: [], changed: ["REQ-1"], orderChanged: false });
|
|
174
|
-
assert.deepEqual(result.acceptance.changed, ["AC-1"]);
|
|
175
|
-
assert.deepEqual(result.otherTopLevelChanges, []); assert.deepEqual(result.baseline, baseline);
|
|
176
|
-
assert.equal(result.origin, "design-contract-comparison");
|
|
177
|
-
assert.throws(() => guardTool(f.project, { tool_name: "Write", tool_input: { file_path: result.evidence.path } }), /immutable/);
|
|
178
|
-
assert.throws(() => run(["design-compare", "--project", f.project, "--id", "diff", "--baseline", f.docPath, "--file", f.docPath]), /distinct pinned/);
|
|
179
|
-
f.write(baselinePath, { ...f.document, objective: "Tampered baseline" });
|
|
180
|
-
assert.throws(() => run(["design-compare", "--project", f.project, "--id", "diff", "--baseline", baselinePath, "--file", f.docPath]), /stale artifact|changed|hash/i);
|
|
181
|
-
} finally { f.dispose(); }
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
test("design comparison reports additions removals order and outside-scope changes without calling them PASS", () => {
|
|
185
|
-
const f = fixture(); try {
|
|
186
|
-
const baselinePath = ".nccgs/evidence/context/baseline.json";
|
|
187
|
-
f.write(baselinePath, f.document);
|
|
188
|
-
f.init("diff", { objective: "Compare candidate", mode: "plan", workflow: "design", readiness: "ready", sources: [baselinePath],
|
|
189
|
-
requiredChecks: ["design-contract"], analysisArtifacts: [f.docPath], acceptance: [] });
|
|
190
|
-
const candidate = structuredClone(f.document);
|
|
191
|
-
candidate.requirements[0].id = "REQ-2"; candidate.manualGates = [];
|
|
192
|
-
f.write(f.docPath, candidate);
|
|
193
|
-
const result = run(["design-compare", "--project", f.project, "--id", "diff", "--baseline", baselinePath, "--file", f.docPath]);
|
|
194
|
-
assert.deepEqual(result.requirements.added, ["REQ-2"]); assert.deepEqual(result.requirements.removed, ["REQ-1"]);
|
|
195
|
-
assert.equal(result.requirements.orderChanged, true); assert.deepEqual(result.otherTopLevelChanges, ["manualGates"]);
|
|
196
|
-
assert.equal(result.status, undefined); // Computed diff, never scope or semantic acceptance.
|
|
197
|
-
candidate.acceptance.push(structuredClone(candidate.acceptance[0])); f.write(f.docPath, candidate);
|
|
198
|
-
assert.throws(() => run(["design-compare", "--project", f.project, "--id", "diff", "--baseline", baselinePath, "--file", f.docPath]), /unique ids/);
|
|
199
|
-
} finally { f.dispose(); }
|
|
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 test from "node:test";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { artifact, json } from "../.claude/nccgs/tools/policy.mjs";
|
|
8
|
+
import { initialize, snapshot, taskPath, record, close, check, reclassify } from "../.claude/nccgs/tools/gates.mjs";
|
|
9
|
+
import { reserve, claim, attachAgent, complete } from "../.claude/nccgs/tools/orchestration.mjs";
|
|
10
|
+
import { validateDesign, validateDesignCommand, requireDesignReady } from "../.claude/nccgs/tools/design-readiness.mjs";
|
|
11
|
+
import { run } from "../.claude/nccgs/tools/task.mjs";
|
|
12
|
+
import { guardTool } from "../.claude/nccgs/tools/tool-guard.mjs";
|
|
13
|
+
|
|
14
|
+
// All provider records here are synthetic. Native acceptance uses separate evidence.
|
|
15
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
16
|
+
function fixture() {
|
|
17
|
+
const parent = fs.realpathSync(os.tmpdir()), project = fs.mkdtempSync(path.join(parent, "nccgs-design-"));
|
|
18
|
+
const write = (relative, value) => {
|
|
19
|
+
const file = path.join(project, relative); fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
20
|
+
fs.writeFileSync(file, typeof value === "string" ? value : JSON.stringify(value)); return artifact(project, relative);
|
|
21
|
+
};
|
|
22
|
+
fs.cpSync(path.join(root, ".claude/nccgs"), path.join(project, ".claude/nccgs"), { recursive: true });
|
|
23
|
+
write(".nccgs/project.yaml", "schema_version: 2\nmodels:\n profile: claude-standalone\n");
|
|
24
|
+
write(".nccgs/execution.json", { schemaVersion: 1, name: "standalone", designReviewRequired: true });
|
|
25
|
+
write(".nccgs/inputs.json", { schemaVersion: 1, canonicalInputs: ["Design/brief.md"], outputDirectories: [], checks: {} });
|
|
26
|
+
write("Assets/game.cs", "// synthetic unchanged source");
|
|
27
|
+
const source = write("Design/brief.md", "Owner asks for two viable builds; human acceptance is required, bot diagnostics are not acceptance.");
|
|
28
|
+
const cite = { ...source, anchor: "Owner asks" };
|
|
29
|
+
const docPath = ".nccgs/evidence/analysis/design.json";
|
|
30
|
+
const document = { schemaVersion: 1, objective: "Two viable builds", scope: "feature", target: "production-change", readiness: "READY",
|
|
31
|
+
sources: [source], requirements: [{ id: "REQ-1", source: cite, statement: "Two viable builds", rules: "Keep both natural build paths and random upgrades; human judgement defines experience acceptance", kind: "fixed", criteria: ["AC-1"] }],
|
|
32
|
+
acceptance: [{ id: "AC-1", description: "Owner accepts both builds", manual: true, procedure: "Play both builds unassisted", environment: "Target Unity player", passCondition: "Owner explicitly accepts each build", rationale: "Experience requires human judgement, not all-seed bot wins", authority: cite }],
|
|
33
|
+
requiredChecks: [], manualGates: ["AC-1"], decisions: [], diagnostics: [{ id: "BOT", procedure: "Run seeded bot", limitations: "Bot policy is not human skill" }], outOfScope: [], risks: [] };
|
|
34
|
+
let count = 0;
|
|
35
|
+
const task = id => json(taskPath(project, id));
|
|
36
|
+
const init = (id, value) => {
|
|
37
|
+
write(`.nccgs/evidence/intake/${id}.json`, value);
|
|
38
|
+
return initialize(project, id, null, null, null, { intake: `.nccgs/evidence/intake/${id}.json` });
|
|
39
|
+
};
|
|
40
|
+
function worker(id, kind = "analysis") {
|
|
41
|
+
const n = ++count, t = task(id), agent = kind === "review" ? "nccgs-adversarial-reviewer" : "nccgs-game-designer";
|
|
42
|
+
const d = reserve(project, id, { requestKey: `r-${n}`, kind, agent, reason: "SYNTHETIC fixture", writePaths: [] });
|
|
43
|
+
claim(project, d.id, { sessionId: "fixture", toolUseId: `call-${n}`, agent });
|
|
44
|
+
attachAgent(project, d.id, `agent-${n}`);
|
|
45
|
+
const snap = snapshot(project, t).sha256;
|
|
46
|
+
const raw = `.nccgs/evidence/raw-${n}.json`;
|
|
47
|
+
write(raw, { taskId: id, contractHash: t.contractHash, snapshot: snap, dispatchId: d.id, sessionId: "fixture", toolUseId: `call-${n}`, agentId: `agent-${n}`, status: "completed", capturedAt: new Date().toISOString(), observedProvider: "claude-code", observedModel: kind === "review" ? "claude-fable-5-1" : "claude-opus-5-5", observedEffort: null, provenance: { kind: "provider-log", reference: "SYNTHETIC TEST; not actual Claude" } });
|
|
48
|
+
const runtime = run(["observe", "--project", project, "--id", id, "--file", raw]);
|
|
49
|
+
complete(project, d.id, { runtime }); return { snapshot: snap, dispatchId: d.id, runtime };
|
|
50
|
+
}
|
|
51
|
+
function design(value = document, verdict = "PASS", recordReview = true) {
|
|
52
|
+
init("design", { objective: "Produce and review the scoped design", mode: "plan", workflow: "design", readiness: "ready", requiredChecks: ["design-contract"], acceptance: [], sources: [source.path], analysisArtifacts: [docPath, ".nccgs/evidence/analysis/review.md"] });
|
|
53
|
+
write(docPath, value);
|
|
54
|
+
const author = worker("design");
|
|
55
|
+
const validation = validateDesignCommand(project, "design", docPath);
|
|
56
|
+
record(project, "design", "analysis", { ...author, evidence: artifact(project, docPath), checks: [{ name: "design-contract", status: "PASS", evidence: validation.evidence }] });
|
|
57
|
+
const reviewer = worker("design", "review");
|
|
58
|
+
const reviewInput = { ...reviewer, verdict, criteriaMet: verdict === "PASS", findings: [], evidence: write(".nccgs/evidence/analysis/review.md", "Synthetic independent document review") };
|
|
59
|
+
if (recordReview) record(project, "design", "review", reviewInput);
|
|
60
|
+
if (recordReview && verdict === "PASS") { record(project, "design", "report", { outcome: "success", summary: "Synthetic design accepted; game acceptance remains pending" }); close(project, "design"); }
|
|
61
|
+
return { author, validation, reviewer, reviewInput };
|
|
62
|
+
}
|
|
63
|
+
return { project, write, task, init, document, docPath, design, worker, dispose() {
|
|
64
|
+
assert.equal(path.dirname(fs.realpathSync(project)), parent); assert.ok(path.basename(project).startsWith("nccgs-design-"));
|
|
65
|
+
fs.rmSync(project, { recursive: true, force: true });
|
|
66
|
+
} };
|
|
67
|
+
}
|
|
68
|
+
const implRequest = { requestKey: "implementation", kind: "implementation", agent: "nccgs-gameplay-programmer", reason: "Synthetic dependent work", writePaths: ["Assets/game.cs"] };
|
|
69
|
+
|
|
70
|
+
test("design false-pass and coverage findings block PASS even when accepted as medium risks", () => {
|
|
71
|
+
const f = fixture(); try {
|
|
72
|
+
const { reviewInput } = f.design(undefined, "CHANGES_REQUIRED", false);
|
|
73
|
+
const finding = { severity: "MEDIUM", status: "ACCEPTED", description: "A shared contaminated retry config can make both digests match while upgrades leak" };
|
|
74
|
+
assert.throws(() => record(f.project, "design", "review", { ...reviewInput, verdict: "PASS", criteriaMet: true, findings: [finding] }), /classify acceptanceImpact/);
|
|
75
|
+
for (const acceptanceImpact of ["false-pass", "missing-coverage", "authority-gap"]) {
|
|
76
|
+
assert.throws(() => record(f.project, "design", "review", { ...reviewInput, verdict: "PASS", criteriaMet: true, findings: [{ ...finding, acceptanceImpact }] }), /unresolved coverage\/authority gaps/);
|
|
77
|
+
}
|
|
78
|
+
assert.doesNotThrow(() => record(f.project, "design", "review", { ...reviewInput, findings: [{ ...finding, status: "OPEN", acceptanceImpact: "false-pass" }] }));
|
|
79
|
+
assert.throws(() => f.init("blocked-work", { mode: "implement", design: { taskId: "design" } }), /completed \/design plan/);
|
|
80
|
+
const nextReview = f.worker("design", "review");
|
|
81
|
+
assert.doesNotThrow(() => record(f.project, "design", "review", { ...reviewInput, ...nextReview, verdict: "PASS", criteriaMet: true, findings: [{ ...finding, acceptanceImpact: "none", description: "An explicitly nonblocking presentation preference" }] }));
|
|
82
|
+
} finally { f.dispose(); }
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("self-declared readiness cannot reserve implementation; partial reporting and analysis remain possible", () => {
|
|
86
|
+
const f = fixture(); try {
|
|
87
|
+
f.init("work", { objective: "New game feature", mode: "implement", readiness: "ready" });
|
|
88
|
+
assert.throws(() => reserve(f.project, "work", implRequest), /reviewed design missing/);
|
|
89
|
+
assert.equal(fs.existsSync(path.join(f.project, ".nccgs/orchestration.json")), false);
|
|
90
|
+
assert.throws(() => check(f.project, f.task("work"), "implementation"), /reviewed design missing/);
|
|
91
|
+
assert.equal(record(f.project, "work", "report", { outcome: "blocked", summary: "Needs independently reviewed design" }).phase, "BLOCKED");
|
|
92
|
+
assert.doesNotThrow(() => reserve(f.project, "work", { ...implRequest, kind: "analysis", writePaths: [] }));
|
|
93
|
+
} finally { f.dispose(); }
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("ready contract validates coverage, threshold authority, diagnostics and delegated tuning", () => {
|
|
97
|
+
const f = fixture(); try {
|
|
98
|
+
assert.doesNotThrow(() => validateDesign(f.project, f.document));
|
|
99
|
+
const bad = change => { const d = structuredClone(f.document); change(d); return d; };
|
|
100
|
+
for (const value of [bad(d => d.requirements[0].criteria = []), bad(d => d.acceptance[0].authority.anchor = ""),
|
|
101
|
+
bad(d => d.acceptance[0] = { ...d.acceptance[0], manual: false, check: "bot" }),
|
|
102
|
+
bad(d => d.decisions.push({ id: "open", status: "open", question: "What is viable?" })),
|
|
103
|
+
bad(d => d.diagnostics[0].id = "AC-1"), bad(d => d.requirements[0].kind = "delegated-tuning")]) {
|
|
104
|
+
assert.throws(() => validateDesign(f.project, value), /Design readiness/);
|
|
105
|
+
}
|
|
106
|
+
} finally { f.dispose(); }
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("independently reviewed design transfers exact acceptance and survives implementation source edits", () => {
|
|
110
|
+
const f = fixture(); try {
|
|
111
|
+
f.design(); const work = f.init("work", { mode: "implement", design: { taskId: "design" } });
|
|
112
|
+
assert.deepEqual(work.contract.acceptance, f.document.acceptance);
|
|
113
|
+
assert.deepEqual(work.contract.manualGates, ["AC-1"]);
|
|
114
|
+
const d = reserve(f.project, "work", implRequest);
|
|
115
|
+
claim(f.project, d.id, { sessionId: "implementation", toolUseId: "edit", agent: implRequest.agent });
|
|
116
|
+
attachAgent(f.project, d.id, "programmer");
|
|
117
|
+
assert.doesNotThrow(() => guardTool(f.project, { session_id: "implementation", agent_id: "programmer", tool_name: "Edit", tool_input: { file_path: "Assets/game.cs" } }));
|
|
118
|
+
f.write("Assets/game.cs", "// implementation progressed after design review");
|
|
119
|
+
assert.doesNotThrow(() => requireDesignReady(f.project, f.task("work")));
|
|
120
|
+
assert.ok(f.task("design").closed);
|
|
121
|
+
} finally { f.dispose(); }
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("conflicting implementation criteria and stale design source are refused without spending dispatches", () => {
|
|
125
|
+
const f = fixture(); try {
|
|
126
|
+
f.design();
|
|
127
|
+
assert.throws(() => f.init("weakened", { mode: "implement", design: { taskId: "design" }, acceptance: [] }), /differs from reviewed design/);
|
|
128
|
+
f.init("work", { mode: "implement", design: { taskId: "design" } });
|
|
129
|
+
f.write("Design/brief.md", "Changed owner instructions after review");
|
|
130
|
+
assert.throws(() => reserve(f.project, "work", implRequest), /stale artifact/);
|
|
131
|
+
assert.throws(() => check(f.project, f.task("work"), "implementation"), /stale artifact/);
|
|
132
|
+
} finally { f.dispose(); }
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("BLOCKED design can be validated and negatively reviewed but cannot receive PASS or unlock work", () => {
|
|
136
|
+
const f = fixture(); try {
|
|
137
|
+
const blocked = structuredClone(f.document); blocked.readiness = "BLOCKED"; blocked.decisions = [{ id: "D", question: "Resolve viable definition", status: "open" }];
|
|
138
|
+
const result = f.design(blocked, "BLOCKED");
|
|
139
|
+
assert.throws(() => record(f.project, "design", "review", { ...result.reviewInput, ...f.worker("design", "review"), verdict: "PASS", criteriaMet: true }), /BLOCKED design/);
|
|
140
|
+
assert.throws(() => f.init("work", { mode: "implement", design: { taskId: "design" } }), /completed \/design/);
|
|
141
|
+
} finally { f.dispose(); }
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("document receipts cannot be edited by tools or reused for a different analysis artifact", () => {
|
|
145
|
+
const f = fixture(); try {
|
|
146
|
+
const result = f.design();
|
|
147
|
+
assert.throws(() => guardTool(f.project, { tool_name: "Write", tool_input: { file_path: result.validation.evidence.path } }), /immutable/);
|
|
148
|
+
f.write(f.docPath, { ...f.document, objective: "Swapped after independent review" });
|
|
149
|
+
assert.throws(() => f.init("work", { mode: "implement", design: { taskId: "design" } }), /stale artifact/);
|
|
150
|
+
} finally { f.dispose(); }
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("ready local fixes keep a narrow exemption while feature reclassification requires design", () => {
|
|
154
|
+
const f = fixture(); try {
|
|
155
|
+
f.init("fix", { objective: "Repair a documented local formatting defect", mode: "implement", scope: "local", target: "fix", readiness: "ready", acceptance: [{ id: "A", description: "Existing formatting is restored", manual: true }] });
|
|
156
|
+
assert.doesNotThrow(() => requireDesignReady(f.project, f.task("fix")));
|
|
157
|
+
reclassify(f.project, "fix", { scope: "feature", risk: "STANDARD", reason: "Discovered broader feature semantics", evidence: [{ observation: "Synthetic discovery" }] });
|
|
158
|
+
assert.throws(() => reserve(f.project, "fix", implRequest), /reviewed design missing/);
|
|
159
|
+
} finally { f.dispose(); }
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("fixed design comparison captures exact changes and preserves the declared baseline", () => {
|
|
163
|
+
const f = fixture(); try {
|
|
164
|
+
const baselinePath = ".nccgs/evidence/context/baseline.json";
|
|
165
|
+
const baseline = f.write(baselinePath, f.document);
|
|
166
|
+
f.init("diff", { objective: "Review a narrow document correction", mode: "plan", workflow: "design", readiness: "ready",
|
|
167
|
+
sources: [baselinePath], requiredChecks: ["design-contract"], analysisArtifacts: [f.docPath], acceptance: [] });
|
|
168
|
+
const candidate = structuredClone(f.document);
|
|
169
|
+
candidate.requirements[0].rules += "; clarified wording";
|
|
170
|
+
candidate.acceptance[0].procedure += "; record the owner decision";
|
|
171
|
+
f.write(f.docPath, candidate);
|
|
172
|
+
const result = run(["design-compare", "--project", f.project, "--id", "diff", "--baseline", baselinePath, "--file", f.docPath]);
|
|
173
|
+
assert.deepEqual(result.requirements, { added: [], removed: [], changed: ["REQ-1"], orderChanged: false });
|
|
174
|
+
assert.deepEqual(result.acceptance.changed, ["AC-1"]);
|
|
175
|
+
assert.deepEqual(result.otherTopLevelChanges, []); assert.deepEqual(result.baseline, baseline);
|
|
176
|
+
assert.equal(result.origin, "design-contract-comparison");
|
|
177
|
+
assert.throws(() => guardTool(f.project, { tool_name: "Write", tool_input: { file_path: result.evidence.path } }), /immutable/);
|
|
178
|
+
assert.throws(() => run(["design-compare", "--project", f.project, "--id", "diff", "--baseline", f.docPath, "--file", f.docPath]), /distinct pinned/);
|
|
179
|
+
f.write(baselinePath, { ...f.document, objective: "Tampered baseline" });
|
|
180
|
+
assert.throws(() => run(["design-compare", "--project", f.project, "--id", "diff", "--baseline", baselinePath, "--file", f.docPath]), /stale artifact|changed|hash/i);
|
|
181
|
+
} finally { f.dispose(); }
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("design comparison reports additions removals order and outside-scope changes without calling them PASS", () => {
|
|
185
|
+
const f = fixture(); try {
|
|
186
|
+
const baselinePath = ".nccgs/evidence/context/baseline.json";
|
|
187
|
+
f.write(baselinePath, f.document);
|
|
188
|
+
f.init("diff", { objective: "Compare candidate", mode: "plan", workflow: "design", readiness: "ready", sources: [baselinePath],
|
|
189
|
+
requiredChecks: ["design-contract"], analysisArtifacts: [f.docPath], acceptance: [] });
|
|
190
|
+
const candidate = structuredClone(f.document);
|
|
191
|
+
candidate.requirements[0].id = "REQ-2"; candidate.manualGates = [];
|
|
192
|
+
f.write(f.docPath, candidate);
|
|
193
|
+
const result = run(["design-compare", "--project", f.project, "--id", "diff", "--baseline", baselinePath, "--file", f.docPath]);
|
|
194
|
+
assert.deepEqual(result.requirements.added, ["REQ-2"]); assert.deepEqual(result.requirements.removed, ["REQ-1"]);
|
|
195
|
+
assert.equal(result.requirements.orderChanged, true); assert.deepEqual(result.otherTopLevelChanges, ["manualGates"]);
|
|
196
|
+
assert.equal(result.status, undefined); // Computed diff, never scope or semantic acceptance.
|
|
197
|
+
candidate.acceptance.push(structuredClone(candidate.acceptance[0])); f.write(f.docPath, candidate);
|
|
198
|
+
assert.throws(() => run(["design-compare", "--project", f.project, "--id", "diff", "--baseline", baselinePath, "--file", f.docPath]), /unique ids/);
|
|
199
|
+
} finally { f.dispose(); }
|
|
200
200
|
});
|
|
@@ -1,64 +1,67 @@
|
|
|
1
|
-
using System;
|
|
2
|
-
using System.Diagnostics;
|
|
3
|
-
using System.IO;
|
|
4
|
-
using System.IO.Pipes;
|
|
5
|
-
using System.Runtime.InteropServices;
|
|
6
|
-
using System.Threading;
|
|
7
|
-
using System.Threading.Tasks;
|
|
8
|
-
|
|
9
|
-
// Synthetic IPC guard fixture, not Unity execution evidence.
|
|
10
|
-
public static class CompilerShutdownFixture {
|
|
11
|
-
[DllImport("kernel32.dll")] static extern IntPtr CreateJobObject(IntPtr attributes,string name);
|
|
12
|
-
[DllImport("kernel32.dll")] static extern bool AssignProcessToJobObject(IntPtr job,IntPtr process);
|
|
13
|
-
[DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr handle);
|
|
14
|
-
static void Need(bool ok,string message) { if(!ok) throw new Exception(message); }
|
|
15
|
-
static void Exchange(IntPtr job,IntPtr process,int pid,bool expected,bool answer) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
Exchange(job,process.Handle,process.Id,
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
1
|
+
using System;
|
|
2
|
+
using System.Diagnostics;
|
|
3
|
+
using System.IO;
|
|
4
|
+
using System.IO.Pipes;
|
|
5
|
+
using System.Runtime.InteropServices;
|
|
6
|
+
using System.Threading;
|
|
7
|
+
using System.Threading.Tasks;
|
|
8
|
+
|
|
9
|
+
// Synthetic IPC guard fixture, not Unity execution evidence.
|
|
10
|
+
public static class CompilerShutdownFixture {
|
|
11
|
+
[DllImport("kernel32.dll")] static extern IntPtr CreateJobObject(IntPtr attributes,string name);
|
|
12
|
+
[DllImport("kernel32.dll")] static extern bool AssignProcessToJobObject(IntPtr job,IntPtr process);
|
|
13
|
+
[DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr handle);
|
|
14
|
+
static void Need(bool ok,string message) { if(!ok) throw new Exception(message); }
|
|
15
|
+
static void Exchange(IntPtr job,IntPtr process,int pid,bool expected,bool answer) {
|
|
16
|
+
// The synthetic LOCAL namespace works in the existing Windows AppContainer.
|
|
17
|
+
// Production cleanup, peer PID/Job checks and the golden frame are unchanged.
|
|
18
|
+
// https://learn.microsoft.com/windows/apps/develop/communication/interprocess-communication
|
|
19
|
+
string name=@"LOCAL\nccgs-compiler-guard-"+Guid.NewGuid().ToString("N");
|
|
20
|
+
using(var server=new NamedPipeServerStream(name,PipeDirection.InOut,1,PipeTransmissionMode.Byte,PipeOptions.Asynchronous)) {
|
|
21
|
+
var connected=server.WaitForConnectionAsync();
|
|
22
|
+
var receive=Task.Run(()=>{
|
|
23
|
+
connected.GetAwaiter().GetResult();
|
|
24
|
+
var length=new byte[4]; int count=server.Read(length,0,4);
|
|
25
|
+
if(count==0) return 0; // Refused peers must receive no request bytes.
|
|
26
|
+
Need(count==4 && BitConverter.ToInt32(length,0)==117,"bad request frame");
|
|
27
|
+
int remaining=117; var buffer=new byte[117];
|
|
28
|
+
while(remaining>0) { int n=server.Read(buffer,117-remaining,remaining); Need(n>0,"truncated request"); remaining-=n; }
|
|
29
|
+
if(answer) {
|
|
30
|
+
using(var writer=new BinaryWriter(server,System.Text.Encoding.Unicode,true)) {
|
|
31
|
+
writer.Write(8); writer.Write(3); writer.Write(Process.GetCurrentProcess().Id); writer.Flush();
|
|
32
|
+
}
|
|
33
|
+
} else Thread.Sleep(1500);
|
|
34
|
+
return 121;
|
|
35
|
+
});
|
|
36
|
+
bool accepted=NccgsUnityCompilerShutdown.Request(job,process,pid,name);
|
|
37
|
+
Need(accepted==expected,"incorrect shutdown acknowledgement");
|
|
38
|
+
Need(receive.Wait(3000),"pipe fixture failed to settle");
|
|
39
|
+
if(!expected && answer) Need(receive.Result==0,"foreign/unowned peer received shutdown bytes");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
public static int Main(string[] args) {
|
|
43
|
+
IntPtr job=CreateJobObject(IntPtr.Zero,null);
|
|
44
|
+
try {
|
|
45
|
+
Need(job!=IntPtr.Zero,"job creation failed");
|
|
46
|
+
var process=Process.GetCurrentProcess();
|
|
47
|
+
// Golden frame captured from the installed Roslyn 4.3.1 serializer, not a
|
|
48
|
+
// second implementation of our own encoder.
|
|
49
|
+
const string golden="75000000000000000000000000000000000000002125534450340031006100350061006600390064003200630034003500390061003000360063003000370039003500620066003200310061003100630030003400360032003000300066003300370035006200660001000000257214510000000000000000";
|
|
50
|
+
Need(BitConverter.ToString(NccgsUnityCompilerShutdown.ShutdownFrame()).Replace("-","").ToLowerInvariant()==golden,"Roslyn protocol changed");
|
|
51
|
+
Exchange(job,process.Handle,process.Id,false,true); // Same PID, foreign job.
|
|
52
|
+
Need(AssignProcessToJobObject(job,process.Handle),"fixture job assignment failed");
|
|
53
|
+
Exchange(job,process.Handle,process.Id+1000000,false,true); // Wrong pipe server PID.
|
|
54
|
+
Exchange(job,process.Handle,process.Id,true,true); // Exact owned pipe.
|
|
55
|
+
Exchange(job,process.Handle,process.Id,false,false); // Missing acknowledgement stays unconfirmed.
|
|
56
|
+
string data=Path.Combine(args[0],"Editor","Data");
|
|
57
|
+
string dll=Path.Combine(data,"DotNetSdkRoslyn","VBCSCompiler.dll"),host=Path.Combine(data,"NetCoreRuntime","dotnet.exe");
|
|
58
|
+
Directory.CreateDirectory(Path.GetDirectoryName(dll)); Directory.CreateDirectory(Path.GetDirectoryName(host));
|
|
59
|
+
File.WriteAllText(dll,"unqualified binary"); File.WriteAllText(host,"unqualified host");
|
|
60
|
+
Need(NccgsUnityCompilerShutdown.QualifiedPipe(host,new[]{host,"exec",dll,"-pipename:safe"})==null,"unqualified binaries accepted");
|
|
61
|
+
Need(NccgsUnityCompilerShutdown.QualifiedPipe(host,new[]{host,"exec",dll,"-pipename:../../foreign"})==null,"invalid pipe accepted");
|
|
62
|
+
Need(NccgsUnityCompilerShutdown.QualifiedPipe(host,new[]{host,"exec",dll,"-pipename:safe","-shutdown"})==null,"unexpected arguments accepted");
|
|
63
|
+
Console.WriteLine("compiler shutdown guards PASS"); return 0;
|
|
64
|
+
} catch(Exception error) { Console.Error.WriteLine(error); return 1; }
|
|
65
|
+
finally { if(job!=IntPtr.Zero) CloseHandle(job); }
|
|
66
|
+
}
|
|
67
|
+
}
|