Skip to content

Commit 137b014

Browse files
test(trigger-e2e): add deterministic gate/synth-PR trigger-condition E2E suite (#1504)
* test(trigger-e2e): add deterministic gate/synth-PR trigger-condition E2E suite Add an ADO-native, non-agentic E2E test framework for the runtime trigger conditions managed by ado-script (gate.js PR filters and exec-context-pr-synth.js synthetic-PR promotion), modeled on the executor-e2e native bundle. A parameterized victim pipeline runs only the two bundles and reports the decision via REST-observable build tags; an orchestrator harness creates real PRs and queues the victim many ways, asserting the gate decision. Test-only bundle: excluded from ado-script.zip exactly like executor-e2e. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f30758a5-81c8-4024-ab01-52ad22b4f877 * test(trigger-e2e): address PR review feedback - Derive the synthetic-PR spec from the PR's real target branch (promoteSynthSpec/excludeSynthSpec) instead of a hardcoded "main", so gate/synth scenarios evaluate correctly on any default branch. - Guard createPrContext against an explicitly-empty files map (avoid the entries[0]! undefined-destructuring crash; treat {} like omitted). - Cancel an orphaned victim build in the runner's finally block when the queue-phase poll throws (waitForBuild timeout), threading the queued build id via runVictim's onQueued callback; add unit tests for the cancel path. - setPullRequestLabels: name the specific failing label so a partial attach surfaces as a clear setup error, not a confusing gate mismatch. - filter_ir.rs: add a SYNC annotation pointing at gate-spec.ts::FACT_META so fact policy/dependency changes flag the hand-maintained mirror. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f30758a5-81c8-4024-ab01-52ad22b4f877 * test(trigger-e2e): address second-round PR review feedback - Fix latent multi-file push bug: pushAddFileBranch always uses new-branch semantics, so createPrContext's per-file loop would fail with a ref conflict on the second file. Add AdoRest.pushAddFilesBranch that batches all files into a single commit, and use it in createPrContext. - Decouple the shared AdoRest client from trigger-e2e env vars: waitForBuild now takes generic 15min/10s defaults; the TRIGGER_E2E_BUILD_TIMEOUT_MS / _POLL_MS knobs are read in trigger-e2e queue.ts and passed as opts, so an executor-e2e run can't be silently retimed. - Add change-count-pass scenario (two files, within range → runs), completing the numeric_range pass/skip coverage and exercising the batched push path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f30758a5-81c8-4024-ab01-52ad22b4f877 * test(trigger-e2e): machine-verify FACT_META against a generated fact catalog Close the gate-spec.ts / FACT_META drift hole that types.gen.ts cannot cover: policy and dependency VALUES were invisible to any type-checker, so a Rust-side change to Fact::failure_policy()/dependencies() (or a new/removed Fact) would only surface as a wrong spec at runtime. - Rust: add Fact::ALL (completeness enforced at compile time by a wildcard-free exhaustiveness match) and generate_fact_catalog(); expose it via a hidden export-fact-catalog CLI command. - codegen: emit committed fact-catalog.gen.json alongside types.gen.ts. - CI: extend the ado-script drift-check git-diff to cover the catalog, so a Rust change forces a regen + commit. - TS: factMetaCatalog() projects FACT_META into the catalog shape and a gate-spec.test.ts test deep-compares it to fact-catalog.gen.json. Verified: corrupting the catalog fails the unit test; adding a Fact variant fails compilation until Fact::ALL is updated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f30758a5-81c8-4024-ab01-52ad22b4f877 * test(trigger-e2e): polish per third-round PR review - main.rs: wrap the export-gate-schema / export-fact-catalog fs writes and create_dir_all in .with_context so a CLI write failure names the target path. - filter_ir.rs: document that Fact::ALL's [Fact; 14] length literal is a second compile-time guard that must track the variant count. - gate-spec.ts: replace predicateFacts' (p as { fact? }).fact duck-typing with a discriminant switch + never-exhaustiveness default, so a renamed/added PredicateSpec field or variant is a compile error instead of a silently under-specified fact list. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f30758a5-81c8-4024-ab01-52ad22b4f877 * docs(trigger-e2e): note multi-step setup leak risk and pool-name override Address remaining PR review doc suggestions: - createPrContext: document that a scenario doing further setup work after a successful createPrContext must teardown the PrContext itself before rethrowing, since the runner won't run cleanup until setup() returns. - README: call out that the hardcoded AZS-1ES pool name in both pipelines may need changing for the operator's ADO project. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f30758a5-81c8-4024-ab01-52ad22b4f877 --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent ac2201c commit 137b014

24 files changed

Lines changed: 2627 additions & 12 deletions

.github/workflows/ado-script.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,9 @@ jobs:
5454

5555
- name: Verify generated TypeScript is up to date
5656
run: |
57-
if ! git diff --exit-code -- scripts/ado-script/src/shared/types.gen.ts; then
57+
if ! git diff --exit-code -- scripts/ado-script/src/shared/types.gen.ts scripts/ado-script/src/trigger-e2e/fact-catalog.gen.json; then
5858
echo ""
59-
echo "::error::types.gen.ts is out of date with the Rust IR."
59+
echo "::error::Generated files are out of date with the Rust IR (types.gen.ts and/or fact-catalog.gen.json)."
6060
echo "Run 'cd scripts/ado-script && npm run codegen' and commit the result."
6161
exit 1
6262
fi

scripts/ado-script/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@
2525
"build:github-app-token": "ncc build src/github-app-token/index.ts -o .ado-build/github-app-token -m -t && node -e \"const fs=require('node:fs'); fs.copyFileSync('.ado-build/github-app-token/index.js','github-app-token.js'); fs.rmSync('.ado-build/github-app-token',{recursive:true,force:true});\"",
2626
"build:prepare-pr-base": "ncc build src/prepare-pr-base/index.ts -o .ado-build/prepare-pr-base -m -t && node -e \"const fs=require('node:fs'); fs.copyFileSync('.ado-build/prepare-pr-base/index.js','prepare-pr-base.js'); fs.rmSync('.ado-build/prepare-pr-base',{recursive:true,force:true});\"",
2727
"build:executor-e2e": "ncc build src/executor-e2e/index.ts -o .ado-build/executor-e2e -m -t && node -e \"const fs=require('node:fs'); fs.mkdirSync('test-bin',{recursive:true}); fs.copyFileSync('.ado-build/executor-e2e/index.js','test-bin/executor-e2e.js'); fs.rmSync('.ado-build/executor-e2e',{recursive:true,force:true});\"",
28+
"build:trigger-e2e": "ncc build src/trigger-e2e/index.ts -o .ado-build/trigger-e2e -m -t && node -e \"const fs=require('node:fs'); fs.mkdirSync('test-bin',{recursive:true}); fs.copyFileSync('.ado-build/trigger-e2e/index.js','test-bin/trigger-e2e.js'); fs.rmSync('.ado-build/trigger-e2e',{recursive:true,force:true});\"",
2829
"build:check": "ls -lh gate.js && wc -c gate.js",
29-
"codegen": "node -e \"require('node:fs').mkdirSync('schema', { recursive: true })\" && cargo run --quiet --manifest-path ../../Cargo.toml -- export-gate-schema --output schema/gate-spec.schema.json && npx json2ts schema/gate-spec.schema.json -o src/shared/types.gen.ts --bannerComment \"// AUTO-GENERATED from Rust IR via cargo run -- export-gate-schema. Do not edit; run npm run codegen.\"",
30+
"codegen": "node -e \"require('node:fs').mkdirSync('schema', { recursive: true })\" && cargo run --quiet --manifest-path ../../Cargo.toml -- export-gate-schema --output schema/gate-spec.schema.json && npx json2ts schema/gate-spec.schema.json -o src/shared/types.gen.ts --bannerComment \"// AUTO-GENERATED from Rust IR via cargo run -- export-gate-schema. Do not edit; run npm run codegen.\" && cargo run --quiet --manifest-path ../../Cargo.toml -- export-fact-catalog --output src/trigger-e2e/fact-catalog.gen.json",
3031
"test": "vitest run",
3132
"test:smoke": "npm run build:gate && npm run build:import && npm run build:exec-context-pr && npm run build:exec-context-pr-synth && npm run build:exec-context-manual && npm run build:exec-context-pipeline && npm run build:exec-context-ci-push && npm run build:exec-context-workitem && npm run build:exec-context-schedule && npm run build:exec-context-pr-checks && npm run build:exec-context-repo && npm run build:conclusion && npm run build:approval-summary && npm run build:github-app-token && npm run build:prepare-pr-base && vitest run -c vitest.config.smoke.ts",
3233
"lint": "echo TODO",

scripts/ado-script/src/__tests__/bundle-coverage.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,12 @@ const packageJsonPath = join(here, "..", "..", "package.json");
3030
* so the release glob (`ado-script/*.js`) never packages it — it is a test
3131
* harness run only by the executor-e2e pipeline, never downloaded by compiled
3232
* agentic pipelines at runtime.
33+
*
34+
* `trigger-e2e` is the analogous deterministic trigger-condition (gate /
35+
* synth-PR) E2E harness. Same treatment: its own `build:trigger-e2e` emits to
36+
* `test-bin/`, so it is never packaged in `ado-script.zip`.
3337
*/
34-
const NON_BUNDLE_DIRS = new Set(["shared", "__tests__", "executor-e2e"]);
38+
const NON_BUNDLE_DIRS = new Set(["shared", "__tests__", "executor-e2e", "trigger-e2e"]);
3539

3640
function listBundleDirs(): string[] {
3741
return readdirSync(srcDir)

scripts/ado-script/src/executor-e2e/ado-rest.ts

Lines changed: 140 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,55 @@ export class AdoRest {
256256
return commitId;
257257
}
258258

259+
/**
260+
* Create a NEW branch and a single commit adding one OR MORE files in one
261+
* push. Returns the new commit id.
262+
*
263+
* Prefer this over calling {@link pushAddFileBranch} in a loop: that helper
264+
* always uses new-branch semantics (`oldObjectId` = zeros), so a second call
265+
* against the now-existing branch would be rejected by ADO with a ref
266+
* conflict. Batching every file into a single commit sidesteps that entirely
267+
* and still produces a real diff vs. the base for PR scenarios.
268+
*/
269+
async pushAddFilesBranch(
270+
repo: string,
271+
branchName: string,
272+
baseCommitId: string,
273+
files: Record<string, string>,
274+
comment: string,
275+
): Promise<string> {
276+
const entries = Object.entries(files);
277+
if (entries.length === 0) throw new Error("pushAddFilesBranch requires at least one file");
278+
const path = this.projPath(
279+
`_apis/git/repositories/${AdoRest.seg(repo)}/pushes?api-version=7.1`,
280+
);
281+
const res = await this.request<{ commits?: { commitId: string }[] }>(path, {
282+
method: "POST",
283+
body: {
284+
refUpdates: [
285+
{
286+
name: branchName.startsWith("refs/") ? branchName : `refs/heads/${branchName}`,
287+
oldObjectId: "0000000000000000000000000000000000000000",
288+
},
289+
],
290+
commits: [
291+
{
292+
comment,
293+
parents: [baseCommitId],
294+
changes: entries.map(([filePath, content]) => ({
295+
changeType: "add",
296+
item: { path: filePath.startsWith("/") ? filePath : `/${filePath}` },
297+
newContent: { content, contentType: "rawtext" },
298+
})),
299+
},
300+
],
301+
},
302+
});
303+
const commitId = res?.commits?.[0]?.commitId;
304+
if (!commitId) throw new Error("pushAddFilesBranch returned no commit id");
305+
return commitId;
306+
}
307+
259308
// ---- Git: pull requests & threads ------------------------------------
260309

261310
async createPullRequest(
@@ -264,18 +313,21 @@ export class AdoRest {
264313
targetRef: string,
265314
title: string,
266315
description: string,
316+
isDraft?: boolean,
267317
): Promise<{ pullRequestId: number }> {
268318
const path = this.projPath(
269319
`_apis/git/repositories/${AdoRest.seg(repo)}/pullrequests?api-version=7.1`,
270320
);
321+
const body: Record<string, unknown> = {
322+
sourceRefName: sourceRef.startsWith("refs/") ? sourceRef : `refs/heads/${sourceRef}`,
323+
targetRefName: targetRef.startsWith("refs/") ? targetRef : `refs/heads/${targetRef}`,
324+
title,
325+
description,
326+
};
327+
if (isDraft !== undefined) body.isDraft = isDraft;
271328
const res = await this.request<{ pullRequestId: number }>(path, {
272329
method: "POST",
273-
body: {
274-
sourceRefName: sourceRef.startsWith("refs/") ? sourceRef : `refs/heads/${sourceRef}`,
275-
targetRefName: targetRef.startsWith("refs/") ? targetRef : `refs/heads/${targetRef}`,
276-
title,
277-
description,
278-
},
330+
body,
279331
});
280332
if (!res) throw new Error("createPullRequest returned no body");
281333
return res;
@@ -365,6 +417,27 @@ export class AdoRest {
365417
await this.request(path, { method: "PATCH", body: { status: "abandoned" }, allow404: true });
366418
}
367419

420+
/**
421+
* Attach one or more labels (tags) to a PR. ADO's label endpoint takes a
422+
* single `{ name }` per POST, so this loops. Used by the trigger E2E harness
423+
* to exercise the gate's `label_set_match` predicate against real PR labels.
424+
*/
425+
async setPullRequestLabels(repo: string, prId: number, labels: string[]): Promise<void> {
426+
for (const name of labels) {
427+
const path = this.projPath(
428+
`_apis/git/repositories/${AdoRest.seg(repo)}/pullRequests/${prId}/labels?api-version=7.1`,
429+
);
430+
try {
431+
await this.request(path, { method: "POST", body: { name } });
432+
} catch (err) {
433+
// Name the specific label that failed so a partial-attach surfaces as a
434+
// clear setup error rather than a confusing downstream gate mismatch.
435+
const message = err instanceof Error ? err.message : String(err);
436+
throw new Error(`setPullRequestLabels: failed to attach label '${name}' to PR ${prId}: ${message}`);
437+
}
438+
}
439+
}
440+
368441
// ---- Wiki -------------------------------------------------------------
369442

370443
async listWikis(): Promise<{ name: string; id: string; type?: string }[]> {
@@ -451,4 +524,65 @@ export class AdoRest {
451524
const path = this.projPath(`_apis/build/builds/${buildId}?api-version=7.1`);
452525
await this.request(path, { method: "PATCH", body: { status: "cancelling" }, allow404: true });
453526
}
527+
528+
/**
529+
* Queue a new build of a registered definition. `sourceBranch` selects the
530+
* branch to build (used by the trigger E2E harness to point a queued build
531+
* at a PR's source branch so `exec-context-pr-synth` can discover the open
532+
* PR). `templateParameters` supplies the victim pipeline's runtime
533+
* parameters (e.g. the base64 GATE_SPEC / PR_SYNTH_SPEC under test).
534+
*
535+
* Returns the new build id. Note: a build queued via this REST call has
536+
* `Build.Reason = Manual`; the victim relies on the synthetic-PR flag (set
537+
* by `exec-context-pr-synth` from a real open PR) — not the build reason —
538+
* to drive full PR-gate evaluation.
539+
*/
540+
async queueBuild(
541+
definitionId: number,
542+
opts: { sourceBranch?: string; templateParameters?: Record<string, string> } = {},
543+
): Promise<{ id: number }> {
544+
const path = this.projPath(`_apis/build/builds?api-version=7.1`);
545+
const body: Record<string, unknown> = { definition: { id: definitionId } };
546+
if (opts.sourceBranch) {
547+
body.sourceBranch = opts.sourceBranch.startsWith("refs/")
548+
? opts.sourceBranch
549+
: `refs/heads/${opts.sourceBranch}`;
550+
}
551+
if (opts.templateParameters && Object.keys(opts.templateParameters).length > 0) {
552+
body.templateParameters = opts.templateParameters;
553+
}
554+
const res = await this.request<{ id: number }>(path, { method: "POST", body });
555+
if (!res) throw new Error(`queueBuild(${definitionId}) returned no body`);
556+
return res;
557+
}
558+
559+
/**
560+
* Poll a build until it reaches `status === "completed"` (or the timeout
561+
* elapses). Returns the terminal `{ status, result }`. `result` is one of
562+
* `succeeded` | `partiallySucceeded` | `failed` | `canceled`.
563+
*
564+
* Timeout/poll defaults are generic (15 min / 10 s). Callers that need
565+
* suite-specific tuning pass explicit `opts` — env-var knobs belong in the
566+
* caller, not this shared client.
567+
*/
568+
async waitForBuild(
569+
buildId: number,
570+
opts: { timeoutMs?: number; pollMs?: number } = {},
571+
): Promise<{ status: string; result?: string }> {
572+
const timeoutMs = opts.timeoutMs ?? 900_000;
573+
const pollMs = opts.pollMs ?? 10_000;
574+
const deadline = Date.now() + timeoutMs;
575+
for (;;) {
576+
const build = await this.getBuild(buildId);
577+
if (build.status === "completed") {
578+
return { status: build.status, result: build.result };
579+
}
580+
if (Date.now() >= deadline) {
581+
throw new Error(
582+
`waitForBuild(${buildId}) timed out after ${timeoutMs}ms (last status='${build.status ?? "?"}')`,
583+
);
584+
}
585+
await new Promise((r) => setTimeout(r, pollMs));
586+
}
587+
}
454588
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { readFileSync } from "node:fs";
2+
3+
import { describe, expect, it } from "vitest";
4+
5+
import {
6+
buildGateSpec,
7+
changeCountCheck,
8+
changedFilesCheck,
9+
draftCheck,
10+
encodeGateSpec,
11+
encodePrSynthSpec,
12+
factMetaCatalog,
13+
labelsCheck,
14+
targetBranchCheck,
15+
titleCheck,
16+
} from "../gate-spec.js";
17+
18+
const factKinds = (checks: Parameters<typeof buildGateSpec>[1]) =>
19+
buildGateSpec("pull-request", checks).facts.map((f) => f.kind);
20+
21+
describe("buildGateSpec fact derivation", () => {
22+
it("emits no facts and no checks for an empty gate", () => {
23+
const spec = buildGateSpec("pull-request", []);
24+
expect(spec.facts).toEqual([]);
25+
expect(spec.checks).toEqual([]);
26+
expect(spec.context.tag_prefix).toBe("pr-gate");
27+
expect(spec.context.build_reason).toBe("PullRequest");
28+
expect(spec.context.bypass_label).toBe("PR");
29+
});
30+
31+
it("derives pr_metadata (skip_dependents) before pr_labels (fail_open) for a labels check", () => {
32+
const spec = buildGateSpec("pull-request", [labelsCheck({ anyOf: ["run-agent"] })]);
33+
const kinds = spec.facts.map((f) => f.kind);
34+
expect(kinds).toEqual(["pr_metadata", "pr_labels"]);
35+
const meta = Object.fromEntries(spec.facts.map((f) => [f.kind, f]));
36+
expect(meta.pr_metadata!.failure_policy).toBe("skip_dependents");
37+
expect(meta.pr_metadata!.dependencies).toEqual([]);
38+
expect(meta.pr_labels!.failure_policy).toBe("fail_open");
39+
expect(meta.pr_labels!.dependencies).toEqual(["pr_metadata"]);
40+
});
41+
42+
it("derives pr_metadata before pr_is_draft (fail_closed) for a draft check", () => {
43+
expect(factKinds([draftCheck(true)])).toEqual(["pr_metadata", "pr_is_draft"]);
44+
});
45+
46+
it("derives changed_files before changed_file_count for a change-count check", () => {
47+
expect(factKinds([changeCountCheck({ min: 5 })])).toEqual([
48+
"changed_files",
49+
"changed_file_count",
50+
]);
51+
});
52+
53+
it("derives only changed_files for a changed-files check", () => {
54+
expect(factKinds([changedFilesCheck({ include: ["src/**"] })])).toEqual(["changed_files"]);
55+
});
56+
57+
it("derives a single fail_closed pipeline-var fact for a title check", () => {
58+
const spec = buildGateSpec("pull-request", [titleCheck("release *")]);
59+
expect(spec.facts).toEqual([
60+
{ kind: "pr_title", failure_policy: "fail_closed", dependencies: [] },
61+
]);
62+
});
63+
64+
it("dedupes facts shared across multiple checks", () => {
65+
const kinds = factKinds([draftCheck(true), labelsCheck({ anyOf: ["x"] })]);
66+
// pr_metadata appears once, before both dependents, in enum order.
67+
expect(kinds).toEqual(["pr_metadata", "pr_is_draft", "pr_labels"]);
68+
});
69+
});
70+
71+
describe("check tag suffixes match the Rust FilterCheck::build_tag_suffix", () => {
72+
it("emits the expected suffixes", () => {
73+
const cases: [Parameters<typeof buildGateSpec>[1][number], string][] = [
74+
[titleCheck("x"), "title-mismatch"],
75+
[labelsCheck({ anyOf: ["x"] }), "labels-mismatch"],
76+
[changedFilesCheck({ include: ["src/**"] }), "changed-files-mismatch"],
77+
[draftCheck(true), "draft-mismatch"],
78+
[targetBranchCheck("main"), "target-branch-mismatch"],
79+
[changeCountCheck({ min: 1 }), "changes-mismatch"],
80+
];
81+
for (const [check, suffix] of cases) {
82+
const spec = buildGateSpec("pull-request", [check]);
83+
expect(spec.checks[0]!.tag_suffix).toBe(suffix);
84+
}
85+
});
86+
});
87+
88+
describe("encoding", () => {
89+
it("round-trips a gate spec through base64", () => {
90+
const spec = buildGateSpec("pull-request", [labelsCheck({ anyOf: ["run-agent"] })]);
91+
const decoded = JSON.parse(Buffer.from(encodeGateSpec(spec), "base64").toString("utf8"));
92+
expect(decoded).toEqual(spec);
93+
});
94+
95+
it("produces the canonical empty PR_SYNTH_SPEC base64", () => {
96+
expect(encodePrSynthSpec()).toBe(
97+
"eyJicmFuY2hlcyI6eyJpbmNsdWRlIjpbXSwiZXhjbHVkZSI6W119LCJwYXRocyI6eyJpbmNsdWRlIjpbXSwiZXhjbHVkZSI6W119fQ==",
98+
);
99+
});
100+
101+
it("encodes provided branch/path filters", () => {
102+
const decoded = JSON.parse(
103+
Buffer.from(
104+
encodePrSynthSpec({ branches: { include: ["main"] }, paths: { exclude: ["docs/**"] } }),
105+
"base64",
106+
).toString("utf8"),
107+
);
108+
expect(decoded).toEqual({
109+
branches: { include: ["main"], exclude: [] },
110+
paths: { include: [], exclude: ["docs/**"] },
111+
});
112+
});
113+
});
114+
115+
describe("FACT_META drift guard vs Rust-generated catalog", () => {
116+
// Deep-compares the hand-maintained FACT_META mirror against the committed
117+
// fact-catalog.gen.json, which `npm run codegen` regenerates from
118+
// filter_ir.rs::generate_fact_catalog and CI drift-checks (ado-script.yml).
119+
//
120+
// How this closes the drift hole the types.gen.ts import cannot:
121+
// - A Rust change to a fact's failure_policy/dependencies, or a new/removed
122+
// Fact variant, regenerates fact-catalog.gen.json → CI git-diff fails
123+
// until committed → committing makes this test fail until FACT_META is
124+
// updated to match. Neither the policy nor dependency VALUES are visible
125+
// to the types.gen.ts structural codegen, so this is the only guard.
126+
it("matches fact-catalog.gen.json exactly (kind, failure_policy, dependencies, order)", () => {
127+
const catalog = JSON.parse(
128+
readFileSync(new URL("../fact-catalog.gen.json", import.meta.url), "utf8"),
129+
);
130+
expect(factMetaCatalog()).toEqual(catalog);
131+
});
132+
});

0 commit comments

Comments
 (0)