@intentius/chant 0.124.14 → 0.124.15
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/dist/build.d.ts +6 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +1 -0
- package/dist/change-set.d.ts +24 -2
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +50 -1
- package/dist/ci-jobs.d.ts +227 -0
- package/dist/ci-jobs.d.ts.map +1 -0
- package/dist/ci-jobs.js +448 -0
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +3 -2
- package/dist/composite.d.ts +10 -2
- package/dist/composite.d.ts.map +1 -1
- package/dist/composite.js +8 -0
- package/dist/discovery/collect.d.ts +2 -1
- package/dist/discovery/collect.d.ts.map +1 -1
- package/dist/discovery/collect.js +16 -1
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/fold-import.js +2 -107
- package/dist/discovery/host-composite.d.ts +35 -0
- package/dist/discovery/host-composite.d.ts.map +1 -0
- package/dist/discovery/host-composite.js +93 -0
- package/dist/discovery/index.d.ts +9 -0
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/index.js +1 -1
- package/dist/gate-decision.d.ts +20 -9
- package/dist/gate-decision.d.ts.map +1 -1
- package/dist/gate-decision.js +72 -19
- package/dist/lifecycle/gate-audit.d.ts +69 -2
- package/dist/lifecycle/gate-audit.d.ts.map +1 -1
- package/dist/lifecycle/gate-audit.js +76 -2
- package/dist/lifecycle/gate-ledger.d.ts +24 -0
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.js +9 -1
- package/dist/op/activities/activity-contracts.d.ts +1 -1
- package/dist/op/activities/activity-contracts.js +1 -1
- package/dist/op/activities/change-set.d.ts +8 -3
- package/dist/op/activities/change-set.d.ts.map +1 -1
- package/dist/op/activities/change-set.js +5 -2
- package/dist/op/activities/reconcile.d.ts +28 -16
- package/dist/op/activities/reconcile.d.ts.map +1 -1
- package/dist/op/activities/reconcile.js +34 -20
- package/dist/op/builders.d.ts +1 -1
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/builders.js +1 -1
- package/dist/op/op-waves-run.d.ts +1 -1
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +3 -3
- package/dist/workspace/__fixtures__/contract-repo.d.ts +7 -0
- package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/contract-repo.js +17 -1
- package/dist/workspace/__fixtures__/sessions.d.ts +1 -1
- package/dist/workspace/__fixtures__/sessions.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/sessions.js +3 -3
- package/dist/workspace/change-set.schema.json +5 -3
- package/dist/workspace/ci-green-workflow.d.ts +3 -1
- package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
- package/dist/workspace/ci-green-workflow.js +5 -2
- package/dist/workspace/class-data.d.ts +84 -0
- package/dist/workspace/class-data.d.ts.map +1 -0
- package/dist/workspace/class-data.js +238 -0
- package/dist/workspace/kinds.d.ts +3 -7
- package/dist/workspace/kinds.d.ts.map +1 -1
- package/dist/workspace/kinds.js +6 -40
- package/dist/workspace/principal-classes.d.ts +4 -11
- package/dist/workspace/principal-classes.d.ts.map +1 -1
- package/dist/workspace/principal-classes.js +11 -94
- package/package.json +1 -1
- package/src/build.ts +8 -0
- package/src/change-set.test.ts +48 -0
- package/src/change-set.ts +52 -2
- package/src/ci-jobs.test.ts +49 -0
- package/src/ci-jobs.ts +515 -0
- package/src/components/pr-pipeline.ts +3 -2
- package/src/composite.ts +15 -3
- package/src/discovery/collect.ts +15 -1
- package/src/discovery/fold-build-parity.test.ts +71 -0
- package/src/discovery/fold-import.ts +2 -113
- package/src/discovery/fold-project-provenance.test.ts +21 -4
- package/src/discovery/host-composite.ts +101 -0
- package/src/discovery/index.ts +11 -1
- package/src/gate-decision-bundle.test.ts +5 -4
- package/src/gate-decision-identity.test.ts +59 -6
- package/src/gate-decision.ts +81 -21
- package/src/lifecycle/gate-audit.test.ts +55 -1
- package/src/lifecycle/gate-audit.ts +116 -3
- package/src/lifecycle/gate-ledger.test.ts +20 -1
- package/src/lifecycle/gate-ledger.ts +33 -1
- package/src/op/activities/activity-contracts.ts +1 -1
- package/src/op/activities/change-set.ts +11 -3
- package/src/op/activities/lexicon-upgrade.test.ts +2 -0
- package/src/op/activities/reconcile.test.ts +62 -2
- package/src/op/activities/reconcile.ts +46 -22
- package/src/op/builders.ts +1 -1
- package/src/op/op-waves-run.ts +4 -3
- package/src/op/op-waves.test.ts +2 -0
- package/src/op/work-lease-run.test.ts +12 -4
- package/src/workspace/__fixtures__/contract-repo.ts +17 -1
- package/src/workspace/__fixtures__/sessions.ts +3 -3
- package/src/workspace/change-set.schema.json +5 -3
- package/src/workspace/ci-green-workflow.test.ts +4 -4
- package/src/workspace/ci-green-workflow.ts +5 -2
- package/src/workspace/class-data.test.ts +64 -0
- package/src/workspace/class-data.ts +236 -0
- package/src/workspace/kinds.ts +6 -35
- package/src/workspace/principal-classes.ts +11 -94
- package/src/workspace/records-formats.test.ts +7 -2
- package/src/workspace/records-sessions-write.test.ts +3 -3
- package/src/workspace/status-contract.e2e.test.ts +37 -0
- package/src/workspace/status-contract.test.ts +23 -21
|
@@ -22,10 +22,9 @@
|
|
|
22
22
|
* installed package, which must be at the version the base pins.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
import { readFileSync } from "node:fs";
|
|
26
25
|
import { createRequire } from "node:module";
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
26
|
+
import { checkClassData, diskFiles, mergePinClasses, readClassFiles, type ClassData, type PackageFiles, type PinClassRead } from "./class-data";
|
|
27
|
+
import { pinnedPackageDir, type KindLoadProblem, type PinLike } from "./kinds";
|
|
29
28
|
import schema from "./workspace-principals.schema.json" with { type: "json" };
|
|
30
29
|
import type { TrustPolicy } from "./trust/policy";
|
|
31
30
|
import { joinPath, type WorkspaceTree } from "./tree";
|
|
@@ -33,8 +32,7 @@ import { CORE_CLASSES, CORE_PRINCIPAL_CLASSES, grantedClasses, type PrincipalCla
|
|
|
33
32
|
|
|
34
33
|
export const PRINCIPALS_SCHEMA_ID = schema.$id;
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
export const PRINCIPALS_SUBPATH = "./workspace-principals";
|
|
35
|
+
export { PRINCIPALS_SUBPATH } from "./class-data";
|
|
38
36
|
|
|
39
37
|
export { CORE_CLASSES, CORE_PRINCIPAL_CLASSES, type CorePrincipalClass, type PrincipalClass } from "./identity-gate-rule";
|
|
40
38
|
|
|
@@ -44,8 +42,6 @@ export type PrincipalClassName = string;
|
|
|
44
42
|
/** The grammar of a class name, as `workspace-principals.schema.json` and the declaration's writeScope keys say it. */
|
|
45
43
|
export const CLASS_NAME = /^(?!x-)[a-z][a-z0-9-]{0,39}$/;
|
|
46
44
|
|
|
47
|
-
const CORE_ROLES = new Set(CORE_CLASSES.map((c) => c.role).filter((r): r is string => r !== null));
|
|
48
|
-
|
|
49
45
|
export interface ClassRegistry {
|
|
50
46
|
get(name: string): PrincipalClass | undefined;
|
|
51
47
|
/** Every class name, core first, for "unknown class" messages. */
|
|
@@ -111,11 +107,8 @@ function validator(): Validate {
|
|
|
111
107
|
return compiled;
|
|
112
108
|
}
|
|
113
109
|
|
|
114
|
-
export
|
|
115
|
-
|
|
116
|
-
/** Why some of the data can't be used, one line each. Empty when all of it can. */
|
|
117
|
-
problems: string[];
|
|
118
|
-
}
|
|
110
|
+
export type { ClassData, PackageFiles } from "./class-data";
|
|
111
|
+
export { diskFiles } from "./class-data";
|
|
119
112
|
|
|
120
113
|
/**
|
|
121
114
|
* Check the text of a principals file against
|
|
@@ -135,46 +128,7 @@ export function parseClassData(text: string, source: string): ClassData {
|
|
|
135
128
|
const problems = (validate.errors ?? []).map((e) => `${source}: ${e.instancePath || "the principals file"} ${e.message ?? "is invalid"}`);
|
|
136
129
|
return { classes: [], problems: [...new Set(problems)] };
|
|
137
130
|
}
|
|
138
|
-
|
|
139
|
-
const classes: PrincipalClass[] = [];
|
|
140
|
-
const names = new Set<string>();
|
|
141
|
-
const roles = new Set<string>();
|
|
142
|
-
for (const c of (raw as { classes: { name: string; description: string; role: string }[] }).classes) {
|
|
143
|
-
if ((CORE_PRINCIPAL_CLASSES as readonly string[]).includes(c.name)) {
|
|
144
|
-
problems.push(`${source}: class ${c.name} is a core class and can't be supplied by a package`);
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
if (CORE_ROLES.has(c.role)) {
|
|
148
|
-
problems.push(`${source}: class ${c.name} names the role ${c.role}, which puts a principal in the core ${c.role} class`);
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
if (names.has(c.name)) {
|
|
152
|
-
problems.push(`${source}: class ${c.name} is listed twice`);
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
if (roles.has(c.role)) {
|
|
156
|
-
problems.push(`${source}: class ${c.name} names the role ${c.role}, which another class in the file names; a role puts a principal in one class`);
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
names.add(c.name);
|
|
160
|
-
roles.add(c.role);
|
|
161
|
-
classes.push({ name: c.name, description: c.description, role: c.role, source });
|
|
162
|
-
}
|
|
163
|
-
return { classes, problems };
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** A package's files, by a `/`-separated path inside it, or undefined when one is not there. */
|
|
167
|
-
export type PackageFiles = (path: string) => string | undefined;
|
|
168
|
-
|
|
169
|
-
/** A package directory on disk. */
|
|
170
|
-
export function diskFiles(dir: string): PackageFiles {
|
|
171
|
-
return (path) => {
|
|
172
|
-
try {
|
|
173
|
-
return readFileSync(join(dir, ...path.split("/")), "utf-8");
|
|
174
|
-
} catch {
|
|
175
|
-
return undefined;
|
|
176
|
-
}
|
|
177
|
-
};
|
|
131
|
+
return checkClassData(raw as Parameters<typeof checkClassData>[0], source);
|
|
178
132
|
}
|
|
179
133
|
|
|
180
134
|
/** A package directory at `dir` in `tree`, such as the workspace at the base revision. */
|
|
@@ -192,22 +146,7 @@ export function treeFiles(tree: WorkspaceTree, dir: string): PackageFiles {
|
|
|
192
146
|
|
|
193
147
|
/** Read the classes a package publishes at `./workspace-principals`. Reads files only; imports nothing. A package that publishes none supplies none. */
|
|
194
148
|
export function readPackageClasses(files: PackageFiles, source: string): ClassData {
|
|
195
|
-
|
|
196
|
-
if (manifest === undefined) return { classes: [], problems: [`${source}: no readable package.json`] };
|
|
197
|
-
let pkg: Record<string, unknown>;
|
|
198
|
-
try {
|
|
199
|
-
pkg = JSON.parse(manifest) as Record<string, unknown>;
|
|
200
|
-
} catch {
|
|
201
|
-
return { classes: [], problems: [`${source}: package.json is not JSON`] };
|
|
202
|
-
}
|
|
203
|
-
const target = kindsExportTarget(pkg, PRINCIPALS_SUBPATH);
|
|
204
|
-
if (target === undefined) return { classes: [], problems: [] };
|
|
205
|
-
if (typeof target !== "string") return { classes: [], problems: [`${source}: ${target.problem}`] };
|
|
206
|
-
const inside = posix.normalize(target.slice(2));
|
|
207
|
-
if (inside.startsWith("../") || inside === "..") return { classes: [], problems: [`${source}: exports["${PRINCIPALS_SUBPATH}"] points outside the package`] };
|
|
208
|
-
const text = files(inside);
|
|
209
|
-
if (text === undefined) return { classes: [], problems: [`${source}: exports["${PRINCIPALS_SUBPATH}"] names ${target}, which does not exist`] };
|
|
210
|
-
return parseClassData(text, source);
|
|
149
|
+
return readClassFiles(files, source, parseClassData);
|
|
211
150
|
}
|
|
212
151
|
|
|
213
152
|
export interface LoadedClasses {
|
|
@@ -227,10 +166,7 @@ export interface LoadedClasses {
|
|
|
227
166
|
*/
|
|
228
167
|
export function loadClassRegistry(pins: readonly PinLike[], workspaceRoot: string, opts: { tree?: WorkspaceTree } = {}): LoadedClasses {
|
|
229
168
|
const problems: KindLoadProblem[] = [];
|
|
230
|
-
const
|
|
231
|
-
const byName = new Map<string, string>();
|
|
232
|
-
const byRole = new Map<string, string>();
|
|
233
|
-
const clashing = new Set<string>();
|
|
169
|
+
const reads: PinClassRead[] = [];
|
|
234
170
|
pins.forEach((pin, i) => {
|
|
235
171
|
let files: PackageFiles;
|
|
236
172
|
let source: string;
|
|
@@ -247,29 +183,10 @@ export function loadClassRegistry(pins: readonly PinLike[], workspaceRoot: strin
|
|
|
247
183
|
files = diskFiles(found.dir);
|
|
248
184
|
source = found.source;
|
|
249
185
|
}
|
|
250
|
-
|
|
251
|
-
for (const p of read.problems) problems.push({ pin: i, message: p });
|
|
252
|
-
for (const c of read.classes) {
|
|
253
|
-
const named = byName.get(c.name);
|
|
254
|
-
const roled = byRole.get(c.role!);
|
|
255
|
-
if (named !== undefined || roled !== undefined) {
|
|
256
|
-
clashing.add(c.name);
|
|
257
|
-
if (named !== undefined) {
|
|
258
|
-
problems.push({ pin: i, message: `class ${c.name} is supplied by both ${named} and ${source}; a class name has one source` });
|
|
259
|
-
} else {
|
|
260
|
-
const other = extra.find((e) => e.role === c.role)!;
|
|
261
|
-
clashing.add(other.name);
|
|
262
|
-
problems.push({ pin: i, message: `class ${c.name} (${source}) names the role ${c.role}, which class ${other.name} (${roled}) names; a role puts a principal in one class` });
|
|
263
|
-
}
|
|
264
|
-
continue;
|
|
265
|
-
}
|
|
266
|
-
byName.set(c.name, source);
|
|
267
|
-
byRole.set(c.role!, source);
|
|
268
|
-
extra.push(c);
|
|
269
|
-
}
|
|
186
|
+
reads.push({ pin: i, source, read: readPackageClasses(files, source) });
|
|
270
187
|
});
|
|
271
|
-
|
|
272
|
-
return { registry: createClassRegistry(
|
|
188
|
+
const merged = mergePinClasses(reads);
|
|
189
|
+
return { registry: createClassRegistry(merged.classes), problems: [...problems, ...merged.problems].sort((a, b) => a.pin - b.pin) };
|
|
273
190
|
}
|
|
274
191
|
|
|
275
192
|
/**
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { execFileSync } from "node:child_process";
|
|
14
14
|
import { createHash } from "node:crypto";
|
|
15
|
-
import { mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { cpSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
16
16
|
import { tmpdir } from "node:os";
|
|
17
17
|
import { dirname, join } from "node:path";
|
|
18
18
|
import Ajv2020 from "ajv/dist/2020";
|
|
@@ -472,7 +472,12 @@ describe("the write commands through a declared kind (#2680)", () => {
|
|
|
472
472
|
|
|
473
473
|
describe("the decision kind", () => {
|
|
474
474
|
test("still reads as markdown front matter, and says so", async () => {
|
|
475
|
-
|
|
475
|
+
// The repository's own decision records, copied into a one-commit repository:
|
|
476
|
+
// a read in the real checkout asks git for the history of every pinned path
|
|
477
|
+
// across thousands of commits, which took 12 s alone and 20+ s under load.
|
|
478
|
+
cpSync(join(REPO, "docs", "design", "decisions"), join(root, "docs", "design", "decisions"), { recursive: true });
|
|
479
|
+
commit();
|
|
480
|
+
const doc = await queryRecords({ kind: "docs/design/decisions/decision.kind.mjs", cwd: root });
|
|
476
481
|
expect(validateOutput(doc)).toBe(true);
|
|
477
482
|
if ("error" in doc) throw new Error(doc.error.message);
|
|
478
483
|
expect(doc.kind.format).toBe("markdown-front-matter");
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { cpSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { afterAll, describe, expect, test } from "vitest";
|
|
15
|
-
import { cleanScratch, commitAll, contract, git, REPO, scratchDir } from "./__fixtures__/contract-repo";
|
|
15
|
+
import { cleanScratch, commitAll, contract, git, REPO, repoFixture, scratchDir } from "./__fixtures__/contract-repo";
|
|
16
16
|
import { declareSessions, DECISIONS_KIND, newSessionFields, sessionsRepo, SESSIONS_KIND } from "./__fixtures__/sessions";
|
|
17
17
|
import { sessionSeal } from "./record-sessions";
|
|
18
18
|
import { parseFrontMatter, recordTextDigest } from "./records";
|
|
@@ -36,12 +36,12 @@ const code = (doc: object) => ("error" in doc ? (doc as { error: { code: string
|
|
|
36
36
|
const HEX = /^[0-9a-f]{40}$/;
|
|
37
37
|
|
|
38
38
|
/** The reference fixture with a declaration, committed. */
|
|
39
|
-
|
|
39
|
+
const declared = repoFixture(() => {
|
|
40
40
|
const root = sessionsRepo();
|
|
41
41
|
declareSessions(root);
|
|
42
42
|
commitAll(root, "declare");
|
|
43
43
|
return root;
|
|
44
|
-
}
|
|
44
|
+
});
|
|
45
45
|
|
|
46
46
|
function front(root: string, path: string): Record<string, unknown> {
|
|
47
47
|
const fm = parseFrontMatter(readFileSync(join(root, path), "utf-8"));
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The chant repo's own workspace declaration reads through
|
|
3
|
+
* `chant workspace status` and validates against the output schema (#2557).
|
|
4
|
+
* It reads the real checkout, 28 members and a git call or more for each,
|
|
5
|
+
* which is 5 to 9 s: split from status-contract.test.ts so the unit budget
|
|
6
|
+
* keeps to fixtures built here (#3880).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import Ajv2020 from "ajv/dist/2020";
|
|
11
|
+
import { describe, expect, test } from "vitest";
|
|
12
|
+
import { listWorkspace } from "./ls";
|
|
13
|
+
import { workspaceStatus, type StatusDocument } from "./status";
|
|
14
|
+
import schema from "./status.schema.json";
|
|
15
|
+
|
|
16
|
+
const REPO = join(import.meta.dirname, "..", "..", "..", "..");
|
|
17
|
+
const validate = new Ajv2020({ strict: true, allErrors: true }).compile(schema);
|
|
18
|
+
|
|
19
|
+
function expectValid(doc: StatusDocument): void {
|
|
20
|
+
expect(validate(doc), JSON.stringify(validate.errors, null, 2)).toBe(true);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function result(doc: StatusDocument): Extract<StatusDocument, { members: unknown }> {
|
|
24
|
+
if ("error" in doc) throw new Error(`${doc.error.code}: ${doc.error.message}`);
|
|
25
|
+
return doc;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("the chant repo's own declaration", () => {
|
|
29
|
+
test("validates and lists every declared member", async () => {
|
|
30
|
+
const doc = result(await workspaceStatus({ cwd: join(REPO, "packages", "core"), env: "prod", compareTo: "staging" }));
|
|
31
|
+
expectValid(doc);
|
|
32
|
+
expect(doc.workspace).toMatchObject({ name: "chant", root: ".", file: "chant.workspace.json" });
|
|
33
|
+
const ls = listWorkspace({ cwd: REPO });
|
|
34
|
+
if ("error" in ls) throw new Error(ls.error.message);
|
|
35
|
+
expect(doc.members.map((m) => m.name)).toEqual(ls.members.map((m) => m.name));
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -7,13 +7,12 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { execFileSync } from "node:child_process";
|
|
10
|
-
import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { cpSync, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { tmpdir } from "node:os";
|
|
12
12
|
import { dirname, join } from "node:path";
|
|
13
13
|
import Ajv2020 from "ajv/dist/2020";
|
|
14
14
|
import { afterAll, describe, expect, test, vi } from "vitest";
|
|
15
15
|
import type { CommandContext } from "../cli/registry";
|
|
16
|
-
import { listWorkspace } from "./ls";
|
|
17
16
|
import {
|
|
18
17
|
formatStatus,
|
|
19
18
|
runWorkspaceStatus,
|
|
@@ -35,8 +34,6 @@ import { runOpLocally } from "../op/local-executor";
|
|
|
35
34
|
import { shellCmd } from "../op/activities/shell";
|
|
36
35
|
import type { OpConfig } from "../op/types";
|
|
37
36
|
|
|
38
|
-
const REPO = join(import.meta.dirname, "..", "..", "..", "..");
|
|
39
|
-
|
|
40
37
|
const ajv = new Ajv2020({ strict: true, allErrors: true });
|
|
41
38
|
const validate = ajv.compile(schema);
|
|
42
39
|
|
|
@@ -98,6 +95,22 @@ function lifecycle(root: string, files: Record<string, string>): string {
|
|
|
98
95
|
return commit;
|
|
99
96
|
}
|
|
100
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Build a fixture repository once and hand each caller its own copy. A repo
|
|
100
|
+
* costs a dozen git processes; a file copy costs none, and a test may write
|
|
101
|
+
* to its copy without touching the next test's.
|
|
102
|
+
*/
|
|
103
|
+
function fixture(build: () => string): () => string {
|
|
104
|
+
let template: string | undefined;
|
|
105
|
+
return () => {
|
|
106
|
+
template ??= build();
|
|
107
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "chant-status-")));
|
|
108
|
+
scratch.push(root);
|
|
109
|
+
cpSync(template, root, { recursive: true });
|
|
110
|
+
return root;
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
101
114
|
let n = 0;
|
|
102
115
|
/** One ledger line. */
|
|
103
116
|
function release(component: string, env: string, digest: string, gitSha: string, extra: Record<string, string> = {}): string {
|
|
@@ -121,7 +134,7 @@ const D = (c: string) => `sha256:${c.repeat(64)}`;
|
|
|
121
134
|
const declaration = (members: unknown[]) => JSON.stringify({ name: "acme", schema: 1, members }, null, 2);
|
|
122
135
|
|
|
123
136
|
/** web has moved to _members/ (#2538); api and the root member still read the flat ledger. */
|
|
124
|
-
|
|
137
|
+
const twoLayouts = fixture(() => {
|
|
125
138
|
const root = repo({
|
|
126
139
|
"chant.workspace.json": declaration([
|
|
127
140
|
{ name: "site", dir: ".", kind: "other", because: "the root project" },
|
|
@@ -142,7 +155,7 @@ function twoLayouts(): string {
|
|
|
142
155
|
"prod/releases.jsonl": jsonl(release("api", "prod", D("c"), "c".repeat(40))),
|
|
143
156
|
});
|
|
144
157
|
return root;
|
|
145
|
-
}
|
|
158
|
+
});
|
|
146
159
|
|
|
147
160
|
describe("status output schema", () => {
|
|
148
161
|
test("is a valid draft 2020-12 document with the published $id", () => {
|
|
@@ -412,7 +425,7 @@ const NOW = "2026-09-24T00:00:00.000Z";
|
|
|
412
425
|
* Each gate is in one state: approved, pending with part of a quorum,
|
|
413
426
|
* expired, superseded, and an Op gate that records no environment.
|
|
414
427
|
*/
|
|
415
|
-
|
|
428
|
+
const gated = fixture(() => {
|
|
416
429
|
const root = twoLayouts();
|
|
417
430
|
lifecycle(root, {
|
|
418
431
|
"_members/web/staging/releases.jsonl": jsonl(release("web", "staging", D("a"), "a".repeat(40))),
|
|
@@ -437,7 +450,7 @@ function gated(): string {
|
|
|
437
450
|
"_gates/nightly.jsonl": jsonl(pending("nightly", "confirm", "2026-09-23T00:00:00.000Z"), approval("nightly", "confirm", "dave", "2026-09-23T01:00:00.000Z")),
|
|
438
451
|
});
|
|
439
452
|
return root;
|
|
440
|
-
}
|
|
453
|
+
});
|
|
441
454
|
|
|
442
455
|
describe("gate state in chant workspace status --json (#2674)", () => {
|
|
443
456
|
test("each member lists its gates with state, approvals, quorum and the approve line", async () => {
|
|
@@ -570,7 +583,7 @@ describe("stewards in chant workspace status --json (#2731)", () => {
|
|
|
570
583
|
});
|
|
571
584
|
|
|
572
585
|
/** A box member declaring one steward, local by default and fountain on fountain-k3d. */
|
|
573
|
-
|
|
586
|
+
const stewarded = fixture(() => {
|
|
574
587
|
const steward = {
|
|
575
588
|
kind: "Chant::Steward",
|
|
576
589
|
name: "box-steward",
|
|
@@ -594,7 +607,7 @@ describe("stewards in chant workspace status --json (#2731)", () => {
|
|
|
594
607
|
});
|
|
595
608
|
lifecycle(root, { "README": "ledger\n" });
|
|
596
609
|
return root;
|
|
597
|
-
}
|
|
610
|
+
});
|
|
598
611
|
|
|
599
612
|
test("names the steward, its form in the environment asked for, its Ops, schedules, last runs and lease", async () => {
|
|
600
613
|
const root = stewarded();
|
|
@@ -754,14 +767,3 @@ describe("stewards in chant workspace status --json (#2731)", () => {
|
|
|
754
767
|
expect(doc.members[0].stewardReasons.map((r) => r.code)).toEqual(["stewards-unreadable"]);
|
|
755
768
|
});
|
|
756
769
|
});
|
|
757
|
-
|
|
758
|
-
describe("chant workspace status on the chant repo (#2557)", () => {
|
|
759
|
-
test("validates and lists every declared member", async () => {
|
|
760
|
-
const doc = result(await workspaceStatus({ cwd: join(REPO, "packages", "core"), env: "prod", compareTo: "staging" }));
|
|
761
|
-
expectValid(doc);
|
|
762
|
-
expect(doc.workspace).toMatchObject({ name: "chant", root: ".", file: "chant.workspace.json" });
|
|
763
|
-
const ls = listWorkspace({ cwd: REPO });
|
|
764
|
-
if ("error" in ls) throw new Error(ls.error.message);
|
|
765
|
-
expect(doc.members.map((m) => m.name)).toEqual(ls.members.map((m) => m.name));
|
|
766
|
-
});
|
|
767
|
-
});
|