Files
copilotkit--copilotkit/showcase/scripts/__tests__/sync-promote-service-options.test.ts
2026-07-13 12:58:18 +08:00

688 lines
29 KiB
TypeScript

import { execFileSync, spawnSync } from "node:child_process";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { parse as parseYaml } from "yaml";
import { SERVICES } from "../railway-envs";
// Importing the module MUST NOT trigger main() (the robust import.meta.url
// guard ensures the file only runs main when invoked directly, never on
// import). If this import wrote the workflow, the suite would have side
// effects on the real file — see the dedicated guard test below.
import { computeOptionTokens, SENTINEL } from "../sync-promote-service-options";
import type { ServiceEntry } from "../railway-envs";
const SCRIPT = resolve(__dirname, "..", "sync-promote-service-options.ts");
// The REAL, committed workflow file that GitHub validates the `service`
// workflow_dispatch choice against server-side. `gh workflow run` is rejected
// with HTTP 422 ("Provided value '<svc>' ... not in the list of allowed
// values") whenever a chosen value is absent from THIS file on the default
// branch — so the durable regression test below asserts against the committed
// file, not just the in-memory generator output.
const COMMITTED_WORKFLOW = resolve(
__dirname,
"..",
"..",
"..",
".github",
"workflows",
"showcase_promote.yml",
);
/**
* The full set of services that MUST appear in the promote dropdown. This is
* the regression guard's allowlist: if a future generator change (or a botched
* SSOT edit) drops ANY of these, both the generator-output and committed-file
* assertions below fail LOUDLY in CI.
*
* `shell-docs` is called out by name on purpose: it is promoted regularly by a
* teammate, and the explicit worry is that a future pinning/generator
* regression must NEVER silently strip it from the dropdown. The 12 `starter-*`
* services are listed because they were historically OMITTED (the committed
* dropdown was never regenerated after they joined the SSOT), which caused the
* HTTP 422 rejection this regression test exists to prevent recurring.
*/
const REQUIRED_PROMOTE_TARGETS = [
// The teammate-critical target — guarded by name, never just by count.
"shell-docs",
// The 12 starter-* container fleet (the previously-omitted set).
"starter-adk",
"starter-agno",
"starter-crewai-crews",
"starter-langgraph-fastapi",
"starter-langgraph-js",
"starter-langgraph-python",
"starter-llamaindex",
"starter-mastra",
"starter-ms-agent-framework-dotnet",
"starter-ms-agent-framework-python",
"starter-pydantic-ai",
"starter-strands-python",
] as const;
/**
* Build a synthetic env-map `ServiceEntry` for computeOptionTokens tests.
* Only the fields the generator reads matter (`environments.prod.probe` +
* `dispatchName`); the rest are filler to satisfy the type. `slug` seeds the
* placeholder IDs/domains so each synthetic entry is internally distinct.
*/
function mkEntry(
slug: string,
opts: { dispatchName?: string; prodProbe?: boolean } = {},
): ServiceEntry & { dispatchName?: string } {
const { dispatchName, prodProbe = true } = opts;
return {
serviceId: `s-${slug}`,
ciBuilt: true,
gateValidated: true,
probeDriver: "harness",
...(dispatchName !== undefined ? { dispatchName } : {}),
environments: {
prod: {
instanceId: `p-${slug}`,
domain: `${slug}.prod`,
probe: prodProbe,
},
staging: {
instanceId: `st-${slug}`,
domain: `${slug}.staging`,
probe: true,
},
},
};
}
// The exact diagnostic fragments the generator emits for each marker-error
// case. Tests assert against these specific strings (not a generic /marker/i)
// so a regression that swaps one diagnostic for another is caught.
const DIAG_NOT_FOUND = "generated marker block not found";
const DIAG_DUPLICATE = "duplicate generated markers";
const DIAG_OUT_OF_ORDER = "malformed marker block";
// A minimal but representative workflow fixture: the real showcase_promote.yml
// has many more jobs, but for the generator's purposes all that matters is
// the `service:` input block carrying the generated marker region. We keep
// unrelated content around the markers to prove it is preserved verbatim.
const BEGIN =
"# >>> BEGIN GENERATED service options (showcase/scripts/sync-promote-service-options.ts) — DO NOT EDIT";
const END = "# <<< END GENERATED service options";
function fixture(generatedBody: string): string {
return [
'name: "Showcase: Promote (staging → prod)"',
"",
"on:",
" workflow_dispatch:",
" inputs:",
" service:",
' description: "Service to promote"',
" required: true",
" type: choice",
` ${BEGIN}`,
generatedBody,
` ${END}`,
" digest:",
' description: "Optional digest override"',
" required: false",
" type: string",
"",
"jobs:",
" resolve-targets:",
" runs-on: ubuntu-latest",
" steps:",
" - run: echo hi",
"",
].join("\n");
}
// A synthetic services map shaped like SERVICES (Record<string, ServiceEntry
// & { dispatchName?: string }>). Only the fields computeOptionTokens reads
// (`probe.prod` and `dispatchName`) carry meaningful values here; the rest are
// filler to satisfy the type. Keys are deliberately ordered so that sorting by
// SSOT KEY would produce a DIFFERENT order than sorting by the RENDERED token,
// proving the sort is by rendered token.
const SYNTHETIC: typeof SERVICES = {
// SSOT key "zeta" but renders as "alpha" via dispatchName → must sort FIRST.
zeta: mkEntry("zeta", { dispatchName: "alpha" }),
// SSOT key "beta", no dispatchName → token falls back to the bare key
// "beta", which sorts AFTER the rendered token "alpha" (from key "zeta").
beta: mkEntry("beta"),
// prod:false → MUST be excluded from the dropdown entirely.
excluded: mkEntry("excl", {
dispatchName: "aaa-would-sort-first",
prodProbe: false,
}),
};
describe("computeOptionTokens (unit)", () => {
it("excludes probe.prod:false, sorts by rendered token, prefers dispatchName", () => {
const tokens = computeOptionTokens(SYNTHETIC);
// (a) The prod:false entry is excluded even though its dispatchName
// ("aaa-would-sort-first") would otherwise sort to the very front.
expect(tokens).not.toContain("aaa-would-sort-first");
expect(tokens).not.toContain("excluded");
// (c) dispatchName wins over the SSOT key: "zeta" surfaces as "alpha",
// and its bare key never appears.
expect(tokens).toContain("alpha");
expect(tokens).not.toContain("zeta");
// "beta" has no dispatchName → its key is used verbatim.
expect(tokens).toContain("beta");
// (b) Ordering: SENTINEL, then "all", then promotable tokens sorted
// alphabetically by the RENDERED token. Sorting by SSOT key would have
// put "beta" before "alpha" (because key "beta" < key "zeta"); the
// rendered-token sort puts "alpha" first.
expect(tokens).toEqual([SENTINEL, "all", "alpha", "beta"]);
});
it("throws (fail loud) on a YAML-unsafe rendered token, naming token + SSOT key", () => {
// A future SSOT entry whose dispatchName carries a YAML-special char (here
// a colon + space) would, if interpolated raw, emit a malformed workflow
// the pre-commit hook silently `git add`s. The generator must fail loud.
const badMap: typeof SERVICES = {
// colon + space → NOT YAML-safe
gamma: mkEntry("gamma", { dispatchName: "bad: token" }),
};
expect(() => computeOptionTokens(badMap)).toThrow(/not YAML-safe/);
// The diagnostic names the offending token and its SSOT key.
expect(() => computeOptionTokens(badMap)).toThrow(/bad: token/);
expect(() => computeOptionTokens(badMap)).toThrow(/gamma/);
});
it("throws (fail loud) on a duplicate assembled token, naming the offender", () => {
// Two distinct SSOT keys whose RENDERED tokens collide: key "dup-a" has
// dispatchName "dup", and key "dup" has no dispatchName → both render as
// "dup". assertDispatchNamesUnique would NOT catch this (it only compares
// dispatchName-vs-dispatchName), so the generator must guard the
// assembled list itself or it would emit a duplicate `- dup` option that
// the pre-commit hook silently `git add`s. The exact-match resolve guard
// catches it because token "dup" matches BOTH services (key "dup" by
// name, key "dup-a" by dispatchName) → resolves to 2, not 1.
const dupMap: typeof SERVICES = {
"dup-a": mkEntry("dupa", { dispatchName: "dup" }),
// no dispatchName → renders as the bare key "dup", colliding with the
// dispatchName above.
dup: mkEntry("dup"),
};
expect(() => computeOptionTokens(dupMap)).toThrow(/resolves to 2 services/);
expect(() => computeOptionTokens(dupMap)).toThrow(/"dup"/);
});
it("throws (fail loud) on a CROSS-collision the dedupe guard misses: distinct rendered tokens that resolve ambiguously", () => {
// The bug the exact-match resolve guard exists to catch, which the OLD
// rendered-token dedupe guard MISSED:
// - Service A ("svc-a") has dispatchName "foo" → renders token "foo".
// - Service B ("foo") has dispatchName "bar" → renders token "bar".
// The two RENDERED tokens ("foo" and "bar") are DISTINCT, so a dedupe
// guard sees no duplicate and passes. But under the workflow resolve
// predicate (s.name === T || s.dispatchName === T), token "foo" matches
// BOTH A (by dispatchName "foo") AND B (by name "foo") → ambiguous and
// un-promotable. The generator must fail loud, naming the ambiguous
// token and both colliding service keys.
const crossMap: typeof SERVICES = {
"svc-a": mkEntry("a", { dispatchName: "foo" }), // renders token "foo"
// renders token "bar" (DISTINCT from "foo")
foo: mkEntry("foo", { dispatchName: "bar" }),
};
// Token "foo" resolves to 2 services (svc-a by dispatchName, foo by name).
expect(() => computeOptionTokens(crossMap)).toThrow(
/resolves to 2 services/,
);
// Diagnostic names the ambiguous token and BOTH colliding service keys.
expect(() => computeOptionTokens(crossMap)).toThrow(/"foo"/);
expect(() => computeOptionTokens(crossMap)).toThrow(/svc-a/);
});
it("does NOT throw when a prod:false service shares a token with a prod-eligible service (resolve step also filters probe.prod)", () => {
// The generator's uniqueness guard mirrors the workflow resolve step's
// predicate EXACTLY — including its `probe.prod === true` filter. So a
// non-prod service sharing a name/dispatchName token with a prod-eligible
// service is NOT a collision: the resolve step would filter the non-prod
// candidate out and resolve the token unambiguously to the single
// prod-eligible service.
// - Service X ("svc-x", probe.prod:true) has dispatchName "shared" →
// renders + emits token "shared".
// - Service Y ("shared", probe.prod:false) has name "shared" → would
// match token "shared" by name, but is filtered out by the prod guard.
// Under an all-services match filter, token "shared" would resolve to 2
// services (X by dispatchName, Y by name) and THROW (false positive). With
// the probe.prod restriction it resolves to exactly 1 (X) and must pass.
const prodFilteredMap: typeof SERVICES = {
// renders + emits token "shared"
"svc-x": mkEntry("x", { dispatchName: "shared" }),
// name "shared" collides with X's emitted token, but probe.prod:false
// means the resolve step (and now the guard) filters it out.
shared: mkEntry("shared", { prodProbe: false }),
};
// The guard must NOT throw: X resolves uniquely once Y is excluded.
expect(() => computeOptionTokens(prodFilteredMap)).not.toThrow();
// X's token IS emitted; the prod:false service Y never appears.
const tokens = computeOptionTokens(prodFilteredMap);
expect(tokens).toContain("shared");
expect(tokens).toEqual([SENTINEL, "all", "shared"]);
});
it("throws (fail loud) when a real-service token collides with a reserved literal", () => {
// A service whose rendered token is exactly "all" (the explicit
// whole-fleet literal). If allowed through it would emit a SECOND `- all`
// option and masquerade as the reserved value the resolve step
// special-cases. The generator must fail loud, naming the offender.
const reservedCollisionMap: typeof SERVICES = {
// collides with the reserved "all" literal
delta: mkEntry("delta", { dispatchName: "all" }),
};
expect(() => computeOptionTokens(reservedCollisionMap)).toThrow(
/reserved literal/,
);
expect(() => computeOptionTokens(reservedCollisionMap)).toThrow(/"all"/);
});
});
describe("sync-promote-service-options", () => {
let workDir: string;
let wfPath: string;
beforeEach(() => {
workDir = mkdtempSync(join(tmpdir(), "sync-promote-"));
wfPath = join(workDir, "showcase_promote.yml");
});
afterEach(() => {
rmSync(workDir, { recursive: true, force: true });
});
it("rewrites a drifted options block to the exact expected ordered list", () => {
// Seed with deliberately wrong/stale content inside the markers.
writeFileSync(
wfPath,
fixture(
[
" default: all",
" options:",
" - all",
" - bogus-stale-entry",
].join("\n"),
),
);
execFileSync("npx", ["tsx", SCRIPT, `--workflow=${wfPath}`], {
stdio: "pipe",
});
const after = readFileSync(wfPath, "utf8");
const doc = parseYaml(after);
const input = doc.on.workflow_dispatch.inputs.service;
// The rendered dropdown is the FULL, ordered token list: SENTINEL, then
// "all", then every probe.prod service rendered as (dispatchName ?? name)
// and sorted ALPHABETICALLY BY RENDERED TOKEN (not by SSOT key). Deep-equal
// against the generator's own output so the test tracks the SSOT and the
// token-sort order exactly.
expect(input.options).toEqual(computeOptionTokens(SERVICES));
expect(input.options[0]).toBe("__select_a_service__");
expect(input.options[1]).toBe("all");
expect(input.default).toBe("__select_a_service__");
expect(input.options).toContain("ag2");
// pocketbase now defines dispatchName "showcase-pocketbase", so the
// rendered token is the dispatchName, not the bare SSOT key.
expect(input.options).toContain("showcase-pocketbase");
expect(input.options).not.toContain("bogus-stale-entry");
// dispatchName tokens, not SSOT keys, for services that define one.
expect(input.options).not.toContain("showcase-ag2");
});
it("is idempotent: a second run is byte-identical and --check exits 0", () => {
writeFileSync(wfPath, fixture(" default: all\n options:"));
execFileSync("npx", ["tsx", SCRIPT, `--workflow=${wfPath}`], {
stdio: "pipe",
});
const first = readFileSync(wfPath, "utf8");
execFileSync("npx", ["tsx", SCRIPT, `--workflow=${wfPath}`], {
stdio: "pipe",
});
const second = readFileSync(wfPath, "utf8");
expect(second).toBe(first);
const check = spawnSync(
"npx",
["tsx", SCRIPT, "--check", `--workflow=${wfPath}`],
{ encoding: "utf8" },
);
expect(check.status).toBe(0);
});
it("--check exits 1 on drift with the re-run diagnostic", () => {
writeFileSync(
wfPath,
fixture(
[" default: all", " options:", " - all"].join(
"\n",
),
),
);
const result = spawnSync(
"npx",
["tsx", SCRIPT, "--check", `--workflow=${wfPath}`],
{ encoding: "utf8" },
);
expect(result.status).toBe(1);
expect(result.stderr).toMatch(/sync-promote-service-options/);
expect(result.stderr).toMatch(/re-run/i);
});
it("places the sentinel as option[0] and sets default to the sentinel", () => {
writeFileSync(wfPath, fixture(" options:"));
execFileSync("npx", ["tsx", SCRIPT, `--workflow=${wfPath}`], {
stdio: "pipe",
});
const doc = parseYaml(readFileSync(wfPath, "utf8"));
const input = doc.on.workflow_dispatch.inputs.service;
expect(input.options[0]).toBe("__select_a_service__");
expect(input.default).toBe("__select_a_service__");
});
it("fails loud (non-zero) when markers are missing — no silent rewrite", () => {
const noMarkers = [
"on:",
" workflow_dispatch:",
" inputs:",
" service:",
' description: "Service to promote"',
" type: choice",
"",
].join("\n");
writeFileSync(wfPath, noMarkers);
const result = spawnSync("npx", ["tsx", SCRIPT, `--workflow=${wfPath}`], {
encoding: "utf8",
});
// Render error → exit 3, with the SPECIFIC "not found" diagnostic.
expect(result.status).toBe(3);
expect(result.stderr).toContain(DIAG_NOT_FOUND);
expect(result.stderr).not.toContain(DIAG_DUPLICATE);
expect(result.stderr).not.toContain(DIAG_OUT_OF_ORDER);
// The file must be untouched (no silent rewrite of the wrong region).
expect(readFileSync(wfPath, "utf8")).toBe(noMarkers);
});
it("fails loud when markers are duplicated", () => {
const dup = [
" service:",
" type: choice",
` ${BEGIN}`,
" options:",
` ${END}`,
" other:",
" type: choice",
` ${BEGIN}`,
" options:",
` ${END}`,
"",
].join("\n");
writeFileSync(wfPath, dup);
const result = spawnSync("npx", ["tsx", SCRIPT, `--workflow=${wfPath}`], {
encoding: "utf8",
});
// Render error → exit 3, with the SPECIFIC "duplicate" diagnostic.
expect(result.status).toBe(3);
expect(result.stderr).toContain(DIAG_DUPLICATE);
expect(result.stderr).not.toContain(DIAG_NOT_FOUND);
expect(result.stderr).not.toContain(DIAG_OUT_OF_ORDER);
expect(readFileSync(wfPath, "utf8")).toBe(dup);
});
it("fails loud when markers are out of order (END before BEGIN)", () => {
// Exactly one BEGIN and one END, but END appears FIRST. This must be
// distinguished from "not found" and "duplicate" with its own diagnostic.
const outOfOrder = [
" service:",
" type: choice",
` ${END}`,
" options:",
` ${BEGIN}`,
"",
].join("\n");
writeFileSync(wfPath, outOfOrder);
const result = spawnSync("npx", ["tsx", SCRIPT, `--workflow=${wfPath}`], {
encoding: "utf8",
});
// Render error → nonzero exit 3, with the SPECIFIC "malformed" diagnostic.
expect(result.status).toBe(3);
expect(result.stderr).toContain(DIAG_OUT_OF_ORDER);
expect(result.stderr).not.toContain(DIAG_NOT_FOUND);
expect(result.stderr).not.toContain(DIAG_DUPLICATE);
// The file must be left untouched.
expect(readFileSync(wfPath, "utf8")).toBe(outOfOrder);
});
it("exits 2 on a read error (nonexistent --workflow path)", () => {
const missingPath = join(workDir, "does-not-exist.yml");
const result = spawnSync(
"npx",
["tsx", SCRIPT, `--workflow=${missingPath}`],
{ encoding: "utf8" },
);
// ALL read failures, including a missing file, exit 2 (not 3).
expect(result.status).toBe(2);
expect(result.stderr).toContain("failed to read");
});
it("rejects an unknown flag (fail loud, exit 2) instead of silently writing", () => {
// Seed a valid workflow so the ONLY reason to fail is the bad arg — a
// typo like `--chek` must NOT silently fall through to a destructive
// write of the real workflow.
writeFileSync(wfPath, fixture(" options:"));
const before = readFileSync(wfPath, "utf8");
const result = spawnSync(
"npx",
["tsx", SCRIPT, "--chek", `--workflow=${wfPath}`],
{ encoding: "utf8" },
);
expect(result.status).toBe(2);
expect(result.stderr).toContain("Unknown argument: --chek");
// The file is untouched — no silent write happened.
expect(readFileSync(wfPath, "utf8")).toBe(before);
});
it("rejects an empty --workflow= value (fail loud, exit 2)", () => {
const result = spawnSync("npx", ["tsx", SCRIPT, "--workflow="], {
encoding: "utf8",
});
expect(result.status).toBe(2);
expect(result.stderr).toContain("--workflow= requires a path value");
});
it("rejects a bare --workflow with no value (fail loud, exit 2)", () => {
const result = spawnSync("npx", ["tsx", SCRIPT, "--workflow", wfPath], {
encoding: "utf8",
});
expect(result.status).toBe(2);
expect(result.stderr).toContain("--workflow requires a value");
});
it("rejects a duplicate --workflow= (fail loud, exit 2)", () => {
const other = join(workDir, "other.yml");
const result = spawnSync(
"npx",
["tsx", SCRIPT, `--workflow=${wfPath}`, `--workflow=${other}`],
{ encoding: "utf8" },
);
expect(result.status).toBe(2);
expect(result.stderr).toContain("--workflow may only be supplied once");
});
it("preserves content outside the markers and keeps valid YAML", () => {
writeFileSync(wfPath, fixture(" options:"));
execFileSync("npx", ["tsx", SCRIPT, `--workflow=${wfPath}`], {
stdio: "pipe",
});
const after = readFileSync(wfPath, "utf8");
// Surrounding structure is intact.
expect(after).toContain('name: "Showcase: Promote (staging → prod)"');
expect(after).toContain(" digest:");
expect(after).toContain(" resolve-targets:");
// Still parses, and the digest input is untouched.
const doc = parseYaml(after);
expect(doc.on.workflow_dispatch.inputs.digest.type).toBe("string");
expect(doc.on.workflow_dispatch.inputs.service.type).toBe("choice");
});
it("importing the module does NOT trigger main() (import.meta.url guard)", () => {
// Spawn a tsx process whose entrypoint is an ESM eval that dynamically
// IMPORTS the module — so process.argv[1] is the eval shim, NOT the module
// path. The `import.meta.url` guard must therefore see argv[1] !== its own
// URL and skip main(). If the guard were broken, main() would run on
// import, read the DEFAULT workflow path, and print "wrote ..."/"already
// up to date." We assert no such generator output appears, and that the
// import itself completes (the sentinel prints AFTER the await).
const importer = join(workDir, "importer.mjs");
// Point at the .ts source via a file:// URL so tsx transpiles it on import.
const scriptUrl = `file://${SCRIPT}`;
writeFileSync(
importer,
[
`await import(${JSON.stringify(scriptUrl)});`,
`process.stdout.write("IMPORT_OK\\n");`,
].join("\n"),
);
const result = spawnSync("npx", ["tsx", importer], {
encoding: "utf8",
cwd: workDir,
});
// The import completed cleanly...
expect(result.status).toBe(0);
expect(result.stdout).toContain("IMPORT_OK");
// ...and main() never ran: none of its stdout/stderr signatures appear.
expect(result.stdout).not.toMatch(/^wrote /m);
expect(result.stdout).not.toContain("already up to date");
expect(result.stdout).not.toContain("up to date.");
expect(result.stderr).not.toContain("failed to read");
});
it("every generated token round-trips to exactly ONE SSOT service (resolve-step contract)", () => {
const tokens = computeOptionTokens(SERVICES);
// The literals the resolve step special-cases; they must never collide
// with a real service token.
const reserved = new Set([SENTINEL, "all"]);
for (const token of tokens) {
if (reserved.has(token)) continue;
// The SAME predicate the workflow's resolve step uses to map a chosen
// dropdown token back to an SSOT service, applied EXACTLY:
// (.name === token || .dispatchName === token) && .probe.prod === true
// The resolve step is `select(.name == $s or .dispatchName == $s) |
// select(.probe.prod == true)`, so the `probe.prod === true` filter is a
// load-bearing term of the predicate, NOT a redundant one we can omit.
// Including it here keeps this round-trip check identical to both the
// resolve step and the generator's own uniqueness guard: a future
// non-prod service sharing a name/dispatchName token with a prod-eligible
// service would resolve unambiguously to the single prod-eligible service
// (the non-prod candidate is filtered out), so it must NOT be counted as
// an ambiguous match here either.
const matches = Object.entries(SERVICES).filter(
([name, entry]) =>
// Mirror the generator's isProdPromotable: declares a prod env
// whose probe is enabled (probe defaults to true when omitted).
entry.environments.prod !== undefined &&
(entry.environments.prod.probe ?? true) === true &&
(name === token || entry.dispatchName === token),
);
// Exactly ONE service resolves from each generated token — no ambiguity,
// no orphans. This pins the generator↔resolve contract.
expect(
matches.length,
`token "${token}" should resolve to exactly one service, got ${matches.length}`,
).toBe(1);
}
// No real-service token may masquerade as a reserved literal. The loop
// above `continue`s on reserved tokens, so it can never observe this —
// assert it DIRECTLY against the SSOT instead: iterate every real
// service, render its token exactly as computeOptionTokens does
// (dispatchName ?? key), and require none equals "all" or SENTINEL.
for (const [name, entry] of Object.entries(SERVICES)) {
const rendered = entry.dispatchName ?? name;
expect(
rendered,
`SSOT service "${name}" renders token "${rendered}", which collides with reserved literal SENTINEL`,
).not.toBe(SENTINEL);
expect(
rendered,
`SSOT service "${name}" renders token "${rendered}", which collides with reserved literal "all"`,
).not.toBe("all");
}
// Sanity: the reserved literals appear exactly once each, in order.
expect(tokens[0]).toBe(SENTINEL);
expect(tokens[1]).toBe("all");
expect(tokens.filter((t) => t === SENTINEL)).toHaveLength(1);
expect(tokens.filter((t) => t === "all")).toHaveLength(1);
});
});
describe("promote dropdown regression guard (shell-docs + starters must stay listed)", () => {
// The whole set the dropdown must currently expose, computed from the SSOT.
// Used as the "no previously-listed target is dropped" oracle: every token
// the generator currently emits (minus the two reserved literals) MUST
// survive in both the generator output and the committed workflow file.
const expectedTokens = computeOptionTokens(SERVICES);
const expectedRealTargets = expectedTokens.filter(
(t) => t !== SENTINEL && t !== "all",
);
it("generator output contains shell-docs AND all 12 starters by name", () => {
const tokens = computeOptionTokens(SERVICES);
// Assert MEMBERSHIP by name (not just a count) so a regression that swaps
// one target for another is still caught.
for (const required of REQUIRED_PROMOTE_TARGETS) {
expect(
tokens,
`promote dropdown (generator output) must contain "${required}" — ` +
`it is a required promote target and must never be dropped`,
).toContain(required);
}
});
it("the COMMITTED showcase_promote.yml choice list contains shell-docs AND all 12 starters", () => {
// This is the file GitHub validates the choice enum against server-side.
// If shell-docs or any starter is absent here, `gh workflow run
// showcase_promote.yml -f service=<svc>` is rejected with HTTP 422 even
// though the SSOT lists the service. Asserting the COMMITTED file (not
// just the in-memory generator output) is what makes this guard real:
// it fails if the dropdown is ever left un-regenerated after an SSOT change.
const doc = parseYaml(readFileSync(COMMITTED_WORKFLOW, "utf8"));
const options: string[] = doc.on.workflow_dispatch.inputs.service.options;
for (const required of REQUIRED_PROMOTE_TARGETS) {
expect(
options,
`committed showcase_promote.yml service dropdown must contain ` +
`"${required}" (GitHub validates the choice enum against this file; ` +
`a missing value yields HTTP 422 on dispatch)`,
).toContain(required);
}
});
it("the committed dropdown is the EXACT union — drops no previously-listed target", () => {
// The committed file must equal the generator's full output, guaranteeing
// the fix is purely additive: every service the generator emits (the union
// of shell-docs, the starters, and every pre-existing target) is present,
// in order, with nothing removed.
const doc = parseYaml(readFileSync(COMMITTED_WORKFLOW, "utf8"));
const options: string[] = doc.on.workflow_dispatch.inputs.service.options;
// Nothing the generator currently emits may be missing from the committed
// file — this is the "never drop an existing target" invariant.
for (const target of expectedRealTargets) {
expect(
options,
`committed dropdown dropped previously-listed promote target ` +
`"${target}"`,
).toContain(target);
}
// And the committed list is byte-for-byte the generator's output, so the
// dropdown can never silently drift from the SSOT in either direction.
expect(options).toEqual(expectedTokens);
});
});