nccgs 1.0.0 → 1.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.
@@ -1,4 +1,5 @@
1
1
  import assert from "node:assert/strict";
2
+ import crypto from "node:crypto";
2
3
  import fs from "node:fs";
3
4
  import os from "node:os";
4
5
  import path from "node:path";
@@ -9,6 +10,8 @@ import { fileURLToPath } from "node:url";
9
10
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
10
11
  const studio = JSON.parse(fs.readFileSync(path.join(root, ".claude/nccgs/studio.json"), "utf8"));
11
12
  const workflows = JSON.parse(fs.readFileSync(path.join(root, ".claude/nccgs/workflow-catalog.json"), "utf8"));
13
+ const frameworkVersion = fs.readFileSync(path.join(root, "VERSION"), "utf8").trim();
14
+ const sha256 = (value) => crypto.createHash("sha256").update(value).digest("hex");
12
15
 
13
16
  test("studio roles are unique and workflow references resolve", () => {
14
17
  const roles = new Set(studio.agents.map((agent) => agent.name));
@@ -24,6 +27,12 @@ test("quality profile assigns all model classes", () => {
24
27
  }
25
28
  });
26
29
 
30
+ test("migrate-project defaults to a single full run", () => {
31
+ const skill = fs.readFileSync(path.join(root, ".claude/skills/migrate-project/SKILL.md"), "utf8");
32
+ assert.match(skill, /Default to `full`/);
33
+ assert.match(skill, /review, plan, apply, and verify in one invocation/i);
34
+ });
35
+
27
36
  test("model configurator can remove and restore model pins", () => {
28
37
  const temp = fs.mkdtempSync(path.join(os.tmpdir(), "nccgs-model-test-"));
29
38
  try {
@@ -104,7 +113,7 @@ test("installer preserves project truth, merges settings, and applies inherit pr
104
113
  assert.ok(fs.existsSync(path.join(temp, ".nccgs/backups")));
105
114
  const installStatePath = path.join(temp, ".claude/nccgs/install-state.json");
106
115
  const installState = JSON.parse(fs.readFileSync(installStatePath, "utf8"));
107
- assert.equal(installState.frameworkVersion, "1.0.0");
116
+ assert.equal(installState.frameworkVersion, frameworkVersion);
108
117
  assert.equal(installState.modelProfile, "inherit");
109
118
 
110
119
  const obsoleteRelative = ".claude/agents/nccgs-obsolete.md";
@@ -119,3 +128,117 @@ test("installer preserves project truth, merges settings, and applies inherit pr
119
128
  fs.rmSync(temp, { recursive: true, force: true });
120
129
  }
121
130
  });
131
+
132
+ test("update replaces untouched files, preserves local overrides, and doctor gates review", () => {
133
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), "nccgs-idempotent-update-test-"));
134
+ try {
135
+ for (const marker of ["Assets", "Packages", "ProjectSettings"]) fs.mkdirSync(path.join(temp, marker));
136
+ const install = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "install", "--model-profile", "quality"], { cwd: temp, encoding: "utf8" });
137
+ assert.equal(install.status, 0, install.stderr || install.stdout);
138
+
139
+ const managedRelative = ".claude/nccgs/constitution.md";
140
+ const managedPath = path.join(temp, ...managedRelative.split("/"));
141
+ const statePath = path.join(temp, ".claude/nccgs/install-state.json");
142
+ const state = JSON.parse(fs.readFileSync(statePath, "utf8"));
143
+ const oldManagedContent = "managed by NCCGS 1.0.0\n";
144
+ fs.writeFileSync(managedPath, oldManagedContent, "utf8");
145
+ state.frameworkVersion = "1.0.0";
146
+ state.files.find((item) => item.relative === managedRelative).sha256 = sha256(oldManagedContent);
147
+ fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
148
+
149
+ const update = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "update"], { cwd: temp, encoding: "utf8" });
150
+ assert.equal(update.status, 0, update.stderr || update.stdout);
151
+ assert.match(update.stdout, new RegExp(`1\\.0\\.0 -> ${frameworkVersion.replace(/\./g, "\\.")}`));
152
+ assert.match(update.stdout, /automatic update/);
153
+ assert.match(update.stdout, /No project migration required/);
154
+ assert.equal(fs.readFileSync(managedPath, "utf8"), fs.readFileSync(path.join(root, managedRelative), "utf8"));
155
+
156
+ let doctor = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "doctor"], { cwd: temp, encoding: "utf8" });
157
+ assert.equal(doctor.status, 0, doctor.stderr || doctor.stdout);
158
+ assert.match(doctor.stdout, /NCCGS doctor passed/);
159
+
160
+ fs.writeFileSync(managedPath, "project-local override\n", "utf8");
161
+ const preserved = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "update"], { cwd: temp, encoding: "utf8" });
162
+ assert.equal(preserved.status, 0, preserved.stderr || preserved.stdout);
163
+ assert.match(preserved.stdout, /preserved override/);
164
+ assert.match(preserved.stdout, /Project migration required/);
165
+ assert.equal(fs.readFileSync(managedPath, "utf8"), "project-local override\n");
166
+
167
+ const overridePath = path.join(temp, ".nccgs/framework-overrides.json");
168
+ const overrides = JSON.parse(fs.readFileSync(overridePath, "utf8"));
169
+ const override = overrides.overrides.find((item) => item.path === managedRelative);
170
+ assert.equal(override.status, "pending_review");
171
+ assert.equal(override.disposition, "preserve");
172
+
173
+ doctor = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "doctor"], { cwd: temp, encoding: "utf8" });
174
+ assert.equal(doctor.status, 1);
175
+ assert.match(doctor.stderr, /awaits \/migrate-project review/);
176
+
177
+ const approve = spawnSync(process.execPath, [path.join(temp, ".claude/nccgs/tools/approve-overrides.mjs"), "--project", temp, "--approve-pending"], { cwd: temp, encoding: "utf8" });
178
+ assert.equal(approve.status, 0, approve.stderr || approve.stdout);
179
+ doctor = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "doctor"], { cwd: temp, encoding: "utf8" });
180
+ assert.equal(doctor.status, 0, doctor.stderr || doctor.stdout);
181
+ } finally {
182
+ fs.rmSync(temp, { recursive: true, force: true });
183
+ }
184
+ });
185
+
186
+ test("doctor accepts a legacy CRLF raw hash after Git normalizes the file to LF", () => {
187
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), "nccgs-eol-test-"));
188
+ try {
189
+ for (const marker of ["Assets", "Packages", "ProjectSettings"]) fs.mkdirSync(path.join(temp, marker));
190
+ const install = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "install"], { cwd: temp, encoding: "utf8" });
191
+ assert.equal(install.status, 0, install.stderr || install.stdout);
192
+
193
+ const relative = ".claude/nccgs/constitution.md";
194
+ const file = path.join(temp, ...relative.split("/"));
195
+ const lf = fs.readFileSync(file, "utf8").replace(/\r\n?/g, "\n");
196
+ fs.writeFileSync(file, lf, "utf8");
197
+ const statePath = path.join(temp, ".claude/nccgs/install-state.json");
198
+ const state = JSON.parse(fs.readFileSync(statePath, "utf8"));
199
+ const record = state.files.find((item) => item.relative === relative);
200
+ record.sha256 = sha256(lf.replace(/\n/g, "\r\n"));
201
+ delete record.canonicalSha256;
202
+ state.schemaVersion = 2;
203
+ fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
204
+
205
+ const doctor = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "doctor"], { cwd: temp, encoding: "utf8" });
206
+ assert.equal(doctor.status, 0, doctor.stderr || doctor.stdout);
207
+ } finally {
208
+ fs.rmSync(temp, { recursive: true, force: true });
209
+ }
210
+ });
211
+
212
+ test("update preserves an intentionally missing managed file until migration review", () => {
213
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), "nccgs-absent-override-test-"));
214
+ try {
215
+ for (const marker of ["Assets", "Packages", "ProjectSettings"]) fs.mkdirSync(path.join(temp, marker));
216
+ const install = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "install"], { cwd: temp, encoding: "utf8" });
217
+ assert.equal(install.status, 0, install.stderr || install.stdout);
218
+
219
+ const relative = ".claude/skills/status/SKILL.md";
220
+ const file = path.join(temp, ...relative.split("/"));
221
+ fs.unlinkSync(file);
222
+ const update = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "update"], { cwd: temp, encoding: "utf8" });
223
+ assert.equal(update.status, 0, update.stderr || update.stdout);
224
+ assert.ok(!fs.existsSync(file));
225
+ const manifest = JSON.parse(fs.readFileSync(path.join(temp, ".nccgs/framework-overrides.json"), "utf8"));
226
+ const override = manifest.overrides.find((item) => item.path === relative);
227
+ assert.equal(override.disposition, "absent");
228
+ assert.equal(override.status, "pending_review");
229
+ } finally {
230
+ fs.rmSync(temp, { recursive: true, force: true });
231
+ }
232
+ });
233
+
234
+ test("update refuses a project without an NCCGS install state", () => {
235
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), "nccgs-update-missing-test-"));
236
+ try {
237
+ for (const marker of ["Assets", "Packages", "ProjectSettings"]) fs.mkdirSync(path.join(temp, marker));
238
+ const result = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "update"], { cwd: temp, encoding: "utf8" });
239
+ assert.equal(result.status, 2);
240
+ assert.match(result.stderr, /not installed/);
241
+ } finally {
242
+ fs.rmSync(temp, { recursive: true, force: true });
243
+ }
244
+ });