chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:58:18 +08:00
commit 6d5d58c1a9
18293 changed files with 3502153 additions and 0 deletions
@@ -0,0 +1,206 @@
import { describe, expect, it } from "vitest";
import {
buildResultArtifactName,
mergeBuildResultFiles,
parseBuildOutputs,
shouldRedeployStaging,
successSet,
} from "../build-outputs";
import type { BuildOutcome, ServiceBuildResult } from "../build-outputs";
const sample: ServiceBuildResult[] = [
{ service: "showcase-mastra", status: "success" },
{ service: "showcase-ag2", status: "failure" },
{ service: "shell-docs", status: "skipped" },
{ service: "showcase-aimock", status: "success" },
];
describe("build-outputs", () => {
it("parses a JSON array of {service,status} entries", () => {
const json = JSON.stringify(sample);
expect(parseBuildOutputs(json)).toEqual(sample);
});
it("throws on malformed JSON", () => {
expect(() => parseBuildOutputs("not json")).toThrow(/parse/i);
});
it("throws on entries missing fields", () => {
expect(() => parseBuildOutputs(JSON.stringify([{ service: "x" }]))).toThrow(
/status/i,
);
});
it("throws on an unknown status value", () => {
const bad = JSON.stringify([{ service: "x", status: "weird" }]);
expect(() => parseBuildOutputs(bad)).toThrow(/status/i);
});
it("rejects an empty service string", () => {
const bad = JSON.stringify([{ service: "", status: "success" }]);
expect(() => parseBuildOutputs(bad)).toThrow(/service/i);
});
it("rejects a whitespace-only service string", () => {
const bad = JSON.stringify([{ service: " ", status: "success" }]);
expect(() => parseBuildOutputs(bad)).toThrow(/service/i);
});
it("error message for parseBuildOutputs includes the entry index", () => {
const bad = JSON.stringify([
{ service: "ok", status: "success" },
{ service: "x", status: "weird" },
]);
expect(() => parseBuildOutputs(bad)).toThrow(/\[1\]/);
});
it("successSet returns only services with status === 'success'", () => {
expect(successSet(sample).sort()).toEqual(
["showcase-aimock", "showcase-mastra"].sort(),
);
});
it("successSet returns empty when no services succeeded", () => {
const allFailed: ServiceBuildResult[] = [
{ service: "a", status: "failure" },
{ service: "b", status: "failure" },
];
expect(successSet(allFailed)).toEqual([]);
});
it("type BuildOutcome enumerates success|failure|skipped", () => {
const outcomes: BuildOutcome[] = ["success", "failure", "skipped"];
expect(outcomes).toHaveLength(3);
});
});
describe("buildResultArtifactName", () => {
it("returns the canonical per-slot artifact name", () => {
expect(buildResultArtifactName("showcase-aimock")).toBe(
"build-result-showcase-aimock",
);
});
it("rejects empty service names (would collide with aggregate artifact)", () => {
expect(() => buildResultArtifactName("")).toThrow(/service/i);
});
it("rejects whitespace-only service names", () => {
expect(() => buildResultArtifactName(" ")).toThrow(/service/i);
});
});
describe("mergeBuildResultFiles", () => {
it("merges per-slot {service,status} JSON payloads into a single array", () => {
const slotPayloads = [
'{"service":"showcase-mastra","status":"success"}',
'{"service":"showcase-ag2","status":"failure"}',
'{"service":"showcase-aimock","status":"success"}',
];
expect(mergeBuildResultFiles(slotPayloads)).toEqual([
{ service: "showcase-mastra", status: "success" },
{ service: "showcase-ag2", status: "failure" },
{ service: "showcase-aimock", status: "success" },
]);
});
it("throws when a slot payload is missing service or status", () => {
expect(() => mergeBuildResultFiles(['{"service":"x"}'])).toThrow(/status/i);
});
it("throws on an invalid status value", () => {
expect(() =>
mergeBuildResultFiles(['{"service":"x","status":"weird"}']),
).toThrow(/status/i);
});
it("returns an empty array when no slot payloads are provided", () => {
expect(mergeBuildResultFiles([])).toEqual([]);
});
it("rejects an empty service in a slot payload", () => {
expect(() =>
mergeBuildResultFiles(['{"service":"","status":"success"}']),
).toThrow(/service/i);
});
it("rejects a whitespace-only service in a slot payload", () => {
expect(() =>
mergeBuildResultFiles(['{"service":" ","status":"success"}']),
).toThrow(/service/i);
});
it("throws when duplicate service names appear across slot payloads", () => {
const slotPayloads = [
'{"service":"dup","status":"failure"}',
'{"service":"other","status":"success"}',
'{"service":"dup","status":"success"}',
];
expect(() => mergeBuildResultFiles(slotPayloads)).toThrow(/duplicate/i);
expect(() => mergeBuildResultFiles(slotPayloads)).toThrow(/dup/);
});
it("normalizes trailing whitespace in service to its trimmed canonical form", () => {
const result = mergeBuildResultFiles([
'{"service":"foo ","status":"success"}',
]);
expect(result).toEqual([{ service: "foo", status: "success" }]);
});
it("normalizes leading whitespace in service to its trimmed canonical form", () => {
const result = mergeBuildResultFiles([
'{"service":" foo","status":"success"}',
]);
expect(result).toEqual([{ service: "foo", status: "success" }]);
});
it("parseBuildOutputs also returns the trimmed canonical service", () => {
const json = JSON.stringify([{ service: " bar ", status: "success" }]);
expect(parseBuildOutputs(json)).toEqual([
{ service: "bar", status: "success" },
]);
});
it("detects duplicates that differ only by surrounding whitespace", () => {
const slotPayloads = [
'{"service":"foo","status":"failure"}',
'{"service":"foo ","status":"success"}',
];
expect(() => mergeBuildResultFiles(slotPayloads)).toThrow(/duplicate/i);
expect(() => mergeBuildResultFiles(slotPayloads)).toThrow(/foo/);
});
it("rejects an array payload with a clear 'expected object' error (not 'missing service')", () => {
expect(() => mergeBuildResultFiles(["[]"])).toThrow(/expected object/i);
expect(() => mergeBuildResultFiles(["[]"])).not.toThrow(/missing/i);
});
it("parseBuildOutputs rejects an array nested as an entry with 'expected object'", () => {
const bad = JSON.stringify([[]]);
expect(() => parseBuildOutputs(bad)).toThrow(/expected object/i);
});
});
describe("shouldRedeployStaging", () => {
it("returns true when at least one service succeeded", () => {
expect(
shouldRedeployStaging([
{ service: "a", status: "success" },
{ service: "b", status: "failure" },
]),
).toBe(true);
});
it("returns false when every service failed or was skipped", () => {
expect(
shouldRedeployStaging([
{ service: "a", status: "failure" },
{ service: "b", status: "skipped" },
]),
).toBe(false);
});
it("returns false on empty input", () => {
expect(shouldRedeployStaging([])).toBe(false);
});
});
@@ -0,0 +1,719 @@
/**
* Tests for showcase/scripts/lib/manifest.ts.
*
* parseManifest is the single source of truth for reading and shape-validating
* manifest.yaml. Tests pin the tagged-union return shape that the consumers
* (audit.ts / validate-parity.ts / capture-previews.ts) rely on.
*/
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import fs from "fs";
import os from "os";
import path from "path";
import { parseManifest, createDemoId, type DemoId } from "../manifest.js";
// chmod-enforcement probe so tests that rely on EACCES can be cleanly
// skipped on CI runners that ignore chmod (running as root, certain
// network mounts). Mirrors the probe in validate-pins.test.ts.
const isRoot = process.getuid?.() === 0;
function probeChmodEnforced(): boolean {
if (isRoot) return false;
let probe: string | undefined;
try {
probe = fs.mkdtempSync(path.join(os.tmpdir(), "lib-manifest-chmod-probe-"));
fs.chmodSync(probe, 0o000);
try {
fs.readdirSync(probe);
return false;
} catch (e) {
const err = e as NodeJS.ErrnoException;
return err.code === "EACCES";
}
} catch {
return false;
} finally {
if (probe) {
try {
fs.chmodSync(probe, 0o755);
} catch {
// best-effort restore
}
try {
fs.rmSync(probe, { recursive: true, force: true });
} catch {
// best-effort
}
}
}
}
const chmodEnforced = probeChmodEnforced();
const cannotEnforceEacces = isRoot || !chmodEnforced;
function tmpdir(prefix = "lib-manifest-"): string {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
function write(file: string, body: string): void {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, body, "utf-8");
}
describe("parseManifest", () => {
let root: string;
beforeEach(() => {
root = tmpdir();
});
afterEach(() => {
fs.rmSync(root, { recursive: true, force: true });
});
it("returns {kind:'missing'} when the file does not exist", () => {
const r = parseManifest(path.join(root, "does-not-exist.yaml"));
expect(r.kind).toBe("missing");
});
it("returns {kind:'malformed', subkind:'shape'} for an empty file (yaml.parse → null)", () => {
// Empty YAML parses to null, which is not a valid manifest mapping. The
// guard must reject it before callers try to read .demos / .deployed.
// This is a SHAPE error (YAML parsed, result was null), not a syntax
// error.
const f = path.join(root, "manifest.yaml");
write(f, "");
const r = parseManifest(f);
expect(r.kind).toBe("malformed");
if (r.kind === "malformed") {
expect(r.subkind).toBe("shape");
expect(r.error.length).toBeGreaterThan(0);
}
});
it("returns {kind:'malformed', subkind:'shape'} for a non-object YAML (bare scalar)", () => {
const f = path.join(root, "manifest.yaml");
write(f, "42\n");
const r = parseManifest(f);
expect(r.kind).toBe("malformed");
if (r.kind === "malformed") {
expect(r.subkind).toBe("shape");
}
});
it("returns {kind:'malformed', subkind:'shape'} for an array at top level", () => {
const f = path.join(root, "manifest.yaml");
write(f, "- a\n- b\n");
const r = parseManifest(f);
expect(r.kind).toBe("malformed");
if (r.kind === "malformed") {
expect(r.subkind).toBe("shape");
}
});
it("returns {kind:'malformed', subkind:'syntax'} for a syntactically broken YAML", () => {
// Subkind discriminator separates YAML parser failures (syntax) from
// post-parse shape-mismatch failures (shape). CI can route these
// differently — a syntax error is almost always a typo; a shape error
// points at a schema-drift issue.
const f = path.join(root, "manifest.yaml");
write(f, "demos: [[[\nunterminated\n");
const r = parseManifest(f);
expect(r.kind).toBe("malformed");
if (r.kind === "malformed") {
expect(r.subkind).toBe("syntax");
}
});
it("returns {kind:'malformed', subkind:'shape'} when slug is missing", () => {
// slug is required — every consumer (audit.ts / validate-parity.ts /
// capture-previews.ts) relies on it. Missing slug = unconditional bug.
const f = path.join(root, "manifest.yaml");
write(f, "name: My Pkg\n");
const r = parseManifest(f);
expect(r.kind).toBe("malformed");
if (r.kind === "malformed") {
expect(r.subkind).toBe("shape");
expect(r.error).toMatch(/slug/i);
}
});
it("returns {kind:'malformed', subkind:'shape'} when slug is not a string", () => {
// YAML `slug: 42` parses to a number; the `as Manifest` cast would
// previously have let the number propagate. parseManifest must reject.
const f = path.join(root, "manifest.yaml");
write(f, "slug: 42\n");
const r = parseManifest(f);
expect(r.kind).toBe("malformed");
if (r.kind === "malformed") {
expect(r.subkind).toBe("shape");
expect(r.error).toMatch(/slug/i);
}
});
it("returns {kind:'malformed', subkind:'shape'} when slug is the empty string", () => {
const f = path.join(root, "manifest.yaml");
write(f, 'slug: ""\n');
const r = parseManifest(f);
expect(r.kind).toBe("malformed");
if (r.kind === "malformed") {
expect(r.subkind).toBe("shape");
}
});
it("returns {kind:'malformed', subkind:'shape'} when name is present but not a string", () => {
// name is optional but, if present, must be a string. Previously the
// `as Manifest` cast would have let a number through silently.
const f = path.join(root, "manifest.yaml");
write(f, "slug: x\nname: 42\n");
const r = parseManifest(f);
expect(r.kind).toBe("malformed");
if (r.kind === "malformed") {
expect(r.subkind).toBe("shape");
expect(r.error).toMatch(/name/i);
}
});
it("returns {kind:'malformed', subkind:'shape'} when demos is not an array", () => {
const f = path.join(root, "manifest.yaml");
write(f, "slug: x\ndemos: not-array\n");
const r = parseManifest(f);
expect(r.kind).toBe("malformed");
if (r.kind === "malformed") {
expect(r.subkind).toBe("shape");
expect(r.error).toMatch(/demos/i);
}
});
it("accepts {kind:'ok'} when demos is explicitly null (treated as omitted)", () => {
// `demos: ~` (YAML explicit null) is semantically equivalent to
// "demos omitted" — the current implementation short-circuits on
// `obj.demos != null` so both null and undefined are allowed. This
// test locks in that behavior. Prior to the simplification, the code
// path guarded on `obj.demos !== undefined` which meant YAML's null
// hit the non-array branch and reported "expected array, got object"
// (because `typeof null === "object"`) — a confusing message.
const f = path.join(root, "manifest.yaml");
write(f, "slug: x\ndemos: ~\n");
const r = parseManifest(f);
expect(r.kind).toBe("ok");
});
it("reports non-nullish non-array demos with precise type (number, not 'object')", () => {
// describeType special-cases null/array so the error message is
// correct for the JS footgun where `typeof null === "object"` and
// `typeof [] === "object"`. Here a number should say "got number".
const f = path.join(root, "manifest.yaml");
write(f, "slug: x\ndemos: 42\n");
const r = parseManifest(f);
expect(r.kind).toBe("malformed");
if (r.kind === "malformed") {
expect(r.error).toMatch(/got number/);
}
});
it("returns {kind:'malformed', subkind:'shape'} when a demo entry lacks a string id", () => {
const f = path.join(root, "manifest.yaml");
write(f, "slug: x\ndemos:\n - noid: true\n");
const r = parseManifest(f);
expect(r.kind).toBe("malformed");
if (r.kind === "malformed") {
expect(r.subkind).toBe("shape");
expect(r.error).toMatch(/id/i);
}
});
it("returns {kind:'malformed', subkind:'shape'} when deployed is present but not a boolean", () => {
const f = path.join(root, "manifest.yaml");
write(f, 'slug: x\ndeployed: "yes"\n');
const r = parseManifest(f);
expect(r.kind).toBe("malformed");
if (r.kind === "malformed") {
expect(r.subkind).toBe("shape");
expect(r.error).toMatch(/deployed/i);
}
});
it("returns {kind:'ok', manifest} for a valid manifest", () => {
const f = path.join(root, "manifest.yaml");
write(
f,
"slug: mypkg\nname: My Pkg\ndeployed: true\ndemos:\n - id: foo\n name: Foo\n - id: bar\n",
);
const r = parseManifest(f);
expect(r.kind).toBe("ok");
if (r.kind === "ok") {
expect(r.manifest.slug).toBe("mypkg");
expect(r.manifest.name).toBe("My Pkg");
expect(r.manifest.deployed).toBe(true);
expect(r.manifest.demos?.length).toBe(2);
expect(r.manifest.demos?.[0].id).toBe("foo");
}
});
it("returns {kind:'ok', manifest} with deployed:false preserved", () => {
// Symmetric to the deployed:true positive test above. `deployed: false`
// is a legal boolean and must parse as {kind:"ok"} with the flag
// preserved, not collapsed into undefined. The strict boolean guard
// rejects stringly-typed "no", "false", etc., but a real boolean false
// must round-trip cleanly.
const f = path.join(root, "manifest.yaml");
write(f, "slug: mypkg\ndeployed: false\n");
const r = parseManifest(f);
expect(r.kind).toBe("ok");
if (r.kind === "ok") {
expect(r.manifest.slug).toBe("mypkg");
expect(r.manifest.deployed).toBe(false);
}
});
it("returns {kind:'ok'} with an empty demos array when demos is omitted", () => {
// `demos` is always set by parseManifest: empty readonly
// array when the manifest omits the field, so callers can iterate
// without `?.` chaining. The old optional-undefined shape is gone.
const f = path.join(root, "manifest.yaml");
write(f, "slug: mypkg\n");
const r = parseManifest(f);
expect(r.kind).toBe("ok");
if (r.kind === "ok") {
expect(r.manifest.demos).toEqual([]);
}
});
it("returns {kind:'malformed', subkind:'shape'} when demos[i] is null", () => {
// YAML `demos: [~]` parses to `[null]`. The per-entry object guard must
// reject this as shape-malformed rather than crashing on `d.id` later.
const f = path.join(root, "manifest.yaml");
write(f, "slug: x\ndemos:\n - ~\n");
const r = parseManifest(f);
expect(r.kind).toBe("malformed");
if (r.kind === "malformed") {
expect(r.subkind).toBe("shape");
expect(r.error).toMatch(/demos\[0\].*null/i);
}
});
it("returns {kind:'malformed', subkind:'shape'} when demos[i] is a scalar", () => {
// YAML `demos: [42]` parses to `[42]`. The per-entry object guard must
// describe the concrete scalar type in the error (not "object").
const f = path.join(root, "manifest.yaml");
write(f, "slug: x\ndemos:\n - 42\n");
const r = parseManifest(f);
expect(r.kind).toBe("malformed");
if (r.kind === "malformed") {
expect(r.subkind).toBe("shape");
expect(r.error).toMatch(/demos\[0\].*number/i);
}
});
it("returns {kind:'malformed', subkind:'shape'} when demos[i].id is an empty string", () => {
// Empty ids would round-trip as valid strings but make downstream
// demo-path construction (`integrations/<slug>/src/app/demos/<id>`) collapse
// onto the demos dir itself. Reject at validation time.
const f = path.join(root, "manifest.yaml");
write(f, 'slug: x\ndemos:\n - id: ""\n');
const r = parseManifest(f);
expect(r.kind).toBe("malformed");
if (r.kind === "malformed") {
expect(r.subkind).toBe("shape");
expect(r.error).toMatch(/demos\[0\]\.id.*non-empty/i);
}
});
it("returns {kind:'malformed', subkind:'shape'} on duplicate demo ids", () => {
// Two demos with the same id used to silently propagate — audit.ts
// would build two missing-demo-dir anomalies for the same path and
// validate-parity.ts would double-count coverage. Reject up-front.
const f = path.join(root, "manifest.yaml");
write(
f,
"slug: x\ndemos:\n - id: agentic-chat\n - id: human-in-the-loop\n - id: agentic-chat\n",
);
const r = parseManifest(f);
expect(r.kind).toBe("malformed");
if (r.kind === "malformed") {
expect(r.subkind).toBe("shape");
expect(r.error).toMatch(/duplicate demo id.*agentic-chat/i);
}
});
it("verifies manifest.slug matches the expected dir slug", () => {
// parseManifest accepts an optional `dirSlug` parameter so callers
// that derive filePath from a slug can detect drift between the
// manifest's declared slug and the directory that holds it (copy/
// paste error, rename-without-updating). Catch at the parser so
// downstream tools don't silently apply the wrong slug.
const f = path.join(root, "manifest.yaml");
write(f, "slug: bar-pkg\n");
const r = parseManifest(f, "foo-pkg");
expect(r.kind).toBe("malformed");
if (r.kind === "malformed") {
expect(r.subkind).toBe("shape");
expect(r.error).toMatch(/slug.*mismatch|mismatch.*slug/i);
expect(r.error).toContain("foo-pkg");
expect(r.error).toContain("bar-pkg");
}
});
it("accepts a manifest where the declared slug matches dirSlug", () => {
// Positive case for the slug-mismatch check: matching slug and
// dirSlug should still return {kind:'ok'}.
const f = path.join(root, "manifest.yaml");
write(f, "slug: mypkg\n");
const r = parseManifest(f, "mypkg");
expect(r.kind).toBe("ok");
});
it("skips the slug-mismatch check when dirSlug is omitted (backwards-compatible)", () => {
// Callers that don't operate against the packages tree (test
// fixtures, programmatic invocations with synthetic paths) should
// continue to work unchanged when they don't pass dirSlug.
const f = path.join(root, "manifest.yaml");
write(f, "slug: whatever\n");
const r = parseManifest(f);
expect(r.kind).toBe("ok");
});
it("returns {kind:'malformed', subkind:'shape'} when a demo's name field is not a string", () => {
// Prior permissive behavior silently coerced non-string `name` to
// undefined. Match the strictness applied to top-level `name`:
// present-but-wrong-type is a shape malformed.
const f = path.join(root, "manifest.yaml");
write(f, "slug: x\ndemos:\n - id: foo\n name: 42\n");
const r = parseManifest(f);
expect(r.kind).toBe("malformed");
if (r.kind === "malformed") {
expect(r.subkind).toBe("shape");
expect(r.error).toMatch(/demos\[0\]\.name/i);
}
});
it("freezes the returned manifest and its demos array", () => {
// parseManifest must return a frozen Manifest: downstream tools
// share the value across buckets and a mutation by one would poison
// the rest. Both the outer object and the nested demos array must
// be frozen.
const f = path.join(root, "manifest.yaml");
write(f, "slug: x\ndemos:\n - id: foo\n - id: bar\n");
const r = parseManifest(f);
expect(r.kind).toBe("ok");
if (r.kind === "ok") {
expect(Object.isFrozen(r.manifest)).toBe(true);
expect(Object.isFrozen(r.manifest.demos)).toBe(true);
expect(() => {
(r.manifest as unknown as Record<string, unknown>)["new"] = "bogus";
}).toThrow();
expect(() => {
(r.manifest.demos as unknown as unknown[])[0] = { id: "x" };
}).toThrow();
}
});
// --- demo.route validation (added by R2 fix cycle) ---------------------
//
// `route` is optional on each demo. When present, it must be a non-empty
// string that begins with "/demos/". Downstream validators (validate-parity
// routeToDirName, bundle-demo-content) rely on the "/demos/" prefix to
// strip it uniformly; accepting a bare "/hitl" or a number silently
// produces the wrong on-disk directory lookup.
it("returns {kind:'malformed', subkind:'shape'} when demos[i].route is a number", () => {
const f = path.join(root, "manifest.yaml");
write(f, "slug: x\ndemos:\n - id: foo\n route: 42\n");
const r = parseManifest(f);
expect(r.kind).toBe("malformed");
if (r.kind === "malformed") {
expect(r.subkind).toBe("shape");
expect(r.error).toMatch(/route/i);
}
});
it("returns {kind:'malformed', subkind:'shape'} when demos[i].route is null", () => {
// YAML `route: ~` parses to null. hasOwnProp is true but the value is
// not a string, so the non-empty-string guard rejects it.
const f = path.join(root, "manifest.yaml");
write(f, "slug: x\ndemos:\n - id: foo\n route: ~\n");
const r = parseManifest(f);
expect(r.kind).toBe("malformed");
if (r.kind === "malformed") {
expect(r.subkind).toBe("shape");
expect(r.error).toMatch(/route/i);
}
});
it("returns {kind:'malformed', subkind:'shape'} when demos[i].route is an object", () => {
const f = path.join(root, "manifest.yaml");
write(f, "slug: x\ndemos:\n - id: foo\n route:\n nested: true\n");
const r = parseManifest(f);
expect(r.kind).toBe("malformed");
if (r.kind === "malformed") {
expect(r.subkind).toBe("shape");
expect(r.error).toMatch(/route/i);
}
});
it("returns {kind:'malformed', subkind:'shape'} when demos[i].route is the empty string", () => {
const f = path.join(root, "manifest.yaml");
write(f, 'slug: x\ndemos:\n - id: foo\n route: ""\n');
const r = parseManifest(f);
expect(r.kind).toBe("malformed");
if (r.kind === "malformed") {
expect(r.subkind).toBe("shape");
expect(r.error).toMatch(/route/i);
}
});
it("returns {kind:'malformed', subkind:'shape'} when demos[i].route does not start with /demos/", () => {
// Catches the exact anti-pattern the prefix guard exists for: a bare
// "/hitl" looks route-shaped but routeToDirName's prefix strip would
// return the whole string unchanged, then miss a real directory match.
const f = path.join(root, "manifest.yaml");
write(f, "slug: x\ndemos:\n - id: foo\n route: /hitl\n");
const r = parseManifest(f);
expect(r.kind).toBe("malformed");
if (r.kind === "malformed") {
expect(r.subkind).toBe("shape");
expect(r.error).toMatch(/\/demos\//);
}
});
it("returns {kind:'malformed', subkind:'shape'} when demos[i].route is exactly '/demos/'", () => {
// The `/demos/` prefix guard accepts any string that starts with the
// prefix, including the bare prefix with an empty tail. Downstream
// consumers (routeToDirName, bundle-demo-content) strip the prefix and
// expect a non-empty segment to follow; an empty tail would silently
// point at the parent demos/ directory rather than a specific demo.
// Reject at the parser boundary so validate-parity / bundle-demo-content
// can treat a successful parse as a non-empty-segment invariant.
const f = path.join(root, "manifest.yaml");
write(f, "slug: x\ndemos:\n - id: foo\n route: /demos/\n");
const r = parseManifest(f);
expect(r.kind).toBe("malformed");
if (r.kind === "malformed") {
expect(r.subkind).toBe("shape");
expect(r.error).toMatch(/non-empty segment/i);
}
});
it("returns {kind:'ok'} and persists demo.route on the frozen entry when well-formed", () => {
const f = path.join(root, "manifest.yaml");
write(f, "slug: x\ndemos:\n - id: foo\n route: /demos/hitl-in-chat\n");
const r = parseManifest(f);
expect(r.kind).toBe("ok");
if (r.kind === "ok") {
const first = r.manifest.demos?.[0];
expect(first?.id).toBe("foo");
expect(first?.route).toBe("/demos/hitl-in-chat");
expect(Object.isFrozen(first)).toBe(true);
}
});
it("returns {kind:'ok'} with demo.route undefined when route is omitted (backward compat)", () => {
// Absence of `route` must not be treated as shape-malformed. Existing
// manifests predate this field; they must still parse cleanly and the
// frozen demo entry's .route must be undefined (not null, not a
// placeholder).
const f = path.join(root, "manifest.yaml");
write(f, "slug: x\ndemos:\n - id: foo\n");
const r = parseManifest(f);
expect(r.kind).toBe("ok");
if (r.kind === "ok") {
const first = r.manifest.demos?.[0];
expect(first?.id).toBe("foo");
expect(first?.route).toBeUndefined();
expect(Object.isFrozen(first)).toBe(true);
}
});
it("sets demos to a frozen empty readonly array when demos is omitted", () => {
// `demos` is non-optional in the public type: when absent, return an
// empty readonly array so consumers can iterate without `?.` chains.
const f = path.join(root, "manifest.yaml");
write(f, "slug: x\n");
const r = parseManifest(f);
expect(r.kind).toBe("ok");
if (r.kind === "ok") {
expect(r.manifest.demos).toEqual([]);
expect(Object.isFrozen(r.manifest.demos)).toBe(true);
}
});
it.skipIf(cannotEnforceEacces)(
"returns {kind:'unreadable'} when the parent dir is unreadable (EACCES on stat)",
() => {
// Regression for FX30-C Fix 2 (R29-2 H3): parseManifest used
// fs.existsSync which CONFLATES ENOENT with EACCES — a manifest
// whose parent dir is 0700 owned by another user collapses to
// "missing" instead of surfacing as "unreadable". That's the
// exact anti-pattern probeDir in validate-parity.ts was written
// to avoid. parseManifest must use statSync + errno inspection
// so EACCES/ENOTDIR/etc route to `{kind:"unreadable"}`.
const root = fs.mkdtempSync(
path.join(os.tmpdir(), "lib-manifest-parent-eacces-"),
);
const parentDir = path.join(root, "locked");
fs.mkdirSync(parentDir);
const manifestFile = path.join(parentDir, "manifest.yaml");
fs.writeFileSync(manifestFile, "slug: ok\n");
fs.chmodSync(parentDir, 0o000);
try {
const r = parseManifest(manifestFile);
expect(r.kind).toBe("unreadable");
} finally {
try {
fs.chmodSync(parentDir, 0o755);
} catch {
// best-effort
}
fs.rmSync(root, { recursive: true, force: true });
}
},
);
it("returns {kind:'unreadable'} when statSync throws a non-ENOENT errno", () => {
// Deterministic variant of the chmod-based test above: stub statSync
// to throw EACCES so the behavior is verified even on FS layers that
// ignore chmod. Pre-ENOENT semantics (via fs.existsSync) would have
// collapsed this to {kind:"missing"}.
const tmp = fs.mkdtempSync(
path.join(os.tmpdir(), "lib-manifest-stat-spy-"),
);
const f = path.join(tmp, "manifest.yaml");
fs.writeFileSync(f, "slug: ok\n");
const orig = fs.statSync;
const spy = vi.spyOn(fs, "statSync").mockImplementation(((
p: fs.PathLike,
options?: unknown,
) => {
if (typeof p === "string" && p === f) {
const e: NodeJS.ErrnoException = new Error("EACCES: injected");
e.code = "EACCES";
throw e;
}
return (orig as unknown as (p: fs.PathLike, o?: unknown) => unknown)(
p,
options,
);
}) as typeof fs.statSync);
try {
const r = parseManifest(f);
expect(r.kind).toBe("unreadable");
if (r.kind === "unreadable") {
expect(r.error).toContain("EACCES");
}
} finally {
spy.mockRestore();
fs.rmSync(tmp, { recursive: true, force: true });
}
});
it("returns {kind:'missing'} (not 'unreadable') when statSync throws ENOENT", () => {
// ENOENT is the legitimate "file absent" signal and must continue to
// route to {kind:"missing"}. Only non-ENOENT errno values escalate
// to "unreadable".
const r = parseManifest(
path.join(os.tmpdir(), "definitely-absent-xyz", "manifest.yaml"),
);
expect(r.kind).toBe("missing");
});
it("returns {kind:'malformed'} when dirSlug is the empty string (caller bug)", () => {
// Regression for FX30-C Fix 3 (R29-2 H2): `""` is NOT a valid opt-out
// sentinel — `undefined` means "caller opted out of slug-check".
// An empty string usually comes from path.basename on a weird path
// (trailing slash, etc.) in the caller; silently collapsing it to
// undefined hides a caller bug. The parser must surface it.
const tmp = fs.mkdtempSync(
path.join(os.tmpdir(), "lib-manifest-empty-slug-"),
);
const f = path.join(tmp, "manifest.yaml");
fs.writeFileSync(f, "slug: mypkg\n");
try {
const r = parseManifest(f, "");
expect(r.kind).toBe("malformed");
if (r.kind === "malformed") {
expect(r.subkind).toBe("shape");
expect(r.error).toMatch(/empty.*dirSlug|dirSlug.*empty/i);
}
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
it("returns {kind:'unreadable'} when readFileSync throws (e.g. EACCES)", () => {
// Simulate a permission error via spy. existsSync returns true but
// readFileSync throws — the parser must surface this as 'unreadable',
// distinct from 'missing' (file absent) and 'malformed' (file present,
// contents bad).
//
// The spy falls through to the original implementation for any path
// other than the target manifest. Throwing "unexpected readFileSync
// call" from within a spy masked the real failure (e.g. vitest's own
// readFileSync for transform cache) with a confusing error — fall-
// through is the correct behavior for a mock of this shape.
const f = path.join(root, "manifest.yaml");
write(f, "slug: ok\n");
const orig = fs.readFileSync;
const spy = vi.spyOn(fs, "readFileSync").mockImplementation(((
p: fs.PathOrFileDescriptor,
options?: unknown,
) => {
if (typeof p === "string" && p === f) {
const e: NodeJS.ErrnoException = new Error("EACCES: permission denied");
e.code = "EACCES";
throw e;
}
return (
orig as unknown as (p: fs.PathOrFileDescriptor, o?: unknown) => unknown
)(p, options);
}) as typeof fs.readFileSync);
try {
const r = parseManifest(f);
expect(r.kind).toBe("unreadable");
if (r.kind === "unreadable") {
expect(r.error).toContain("EACCES");
}
} finally {
spy.mockRestore();
}
});
});
describe("createDemoId", () => {
it("accepts a non-empty string and returns a branded DemoId", () => {
const id = createDemoId("foo");
expect(id).toBe("foo");
});
it("returns null for the empty string", () => {
expect(createDemoId("")).toBeNull();
});
it("accepts unknown input and returns null for non-string values", () => {
// Signature widened from (s: string) to (s: unknown) so the dead
// typeof check becomes a live guard. Non-string inputs at API
// boundaries (yaml.parse results, untyped JSON) return null rather
// than silently producing a fake branded id.
expect(createDemoId(null)).toBeNull();
expect(createDemoId(undefined)).toBeNull();
expect(createDemoId(42)).toBeNull();
expect(createDemoId({})).toBeNull();
expect(createDemoId([])).toBeNull();
expect(createDemoId(true)).toBeNull();
});
it("narrows its input via a type predicate (compile-time shape)", () => {
// The returned value, when non-null, is both a DemoId AND carries a
// TypeScript narrowing that lets callers read it as a string without
// further casts. This test is mostly structural — the assertion is
// that the code compiles; we still run a basic runtime check.
const candidate: unknown = "abc";
const id = createDemoId(candidate);
if (id !== null) {
// Compiler should accept DemoId as assignable to a string-slot.
const asString: string = id;
expect(asString).toBe("abc");
// Also exercise the DemoId type import to ensure the export exists.
const branded: DemoId = id;
expect(branded).toBe("abc");
}
});
});
@@ -0,0 +1,53 @@
import { describe, expect, it } from "vitest";
import { execFileSync } from "node:child_process";
import path from "node:path";
const REPO_ROOT = path.resolve(__dirname, "../../../..");
describe("Railway GraphQL host unification", () => {
it("no source file references backboard.railway.com (must be .app)", () => {
// grep across showcase/ and .github/workflows/. Use -F (literal) so
// the dot in the pattern is not regex-interpreted. The test file
// itself contains the forbidden literal; exclude it via git pathspec
// `:(exclude)` magic. Also exclude two files that legitimately
// reference the deprecated host in documentation/negative-assertion
// form (no functional use):
// - railway-graphql.ts JSDoc explains the deprecated .com endpoint
// - railway-graphql.test.ts asserts the endpoint is NOT .com
// Use --untracked so a newly-added (not-yet-committed) file that
// reintroduces the forbidden host still trips the guard. Use
// execFileSync with an args array so REPO_ROOT is not shell-parsed.
let out = "";
try {
out = execFileSync(
"git",
[
"-C",
REPO_ROOT,
"grep",
"--untracked",
"-nF",
"backboard.railway.com",
"--",
"showcase",
":(exclude)showcase/scripts/lib/__tests__/railway-graphql.scan.test.ts",
":(exclude)showcase/scripts/lib/railway-graphql.ts",
":(exclude)showcase/scripts/lib/__tests__/railway-graphql.test.ts",
".github/workflows",
],
{ encoding: "utf-8" },
);
} catch (err) {
// git grep exits 1 when no matches — that's the clean-miss success
// case. Any other exit status (git missing, bad cwd, pathspec
// syntax error, etc.) must fail loud rather than silently pass.
const status = (err as { status?: number }).status;
if (status === 1) {
out = "";
} else {
throw err;
}
}
expect(out).toBe("");
});
});
@@ -0,0 +1,14 @@
import { describe, expect, it } from "vitest";
import { RAILWAY_GRAPHQL_ENDPOINT } from "../railway-graphql";
describe("railway-graphql", () => {
it("uses the canonical backboard.railway.app host", () => {
expect(RAILWAY_GRAPHQL_ENDPOINT).toBe(
"https://backboard.railway.app/graphql/v2",
);
});
it("never resolves to the .com host (historical drift target)", () => {
expect(RAILWAY_GRAPHQL_ENDPOINT).not.toContain("backboard.railway.com");
});
});
@@ -0,0 +1,152 @@
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { resolveRailwayToken, RailwayTokenError } from "../railway-token";
/**
* Envelope-level tests for resolveRailwayToken — the unified resolver
* used by redeploy-env.ts and verify-railway-image-refs.ts.
*
* Each failure mode must throw a DISTINCT, actionable error so the
* silent-token-fallthrough diagnostic gap can never re-open.
*/
describe("resolveRailwayToken (envelope)", () => {
let dir: string;
const originalEnv = process.env.RAILWAY_TOKEN;
const originalHome = process.env.HOME;
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), "rwy-token-env-"));
delete process.env.RAILWAY_TOKEN;
process.env.HOME = dir;
});
afterEach(() => {
rmSync(dir, { recursive: true, force: true });
if (originalEnv === undefined) delete process.env.RAILWAY_TOKEN;
else process.env.RAILWAY_TOKEN = originalEnv;
if (originalHome === undefined) delete process.env.HOME;
else process.env.HOME = originalHome;
});
it("returns RAILWAY_TOKEN env var when set, source='env'", () => {
process.env.RAILWAY_TOKEN = "env-token-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
const result = resolveRailwayToken();
expect(result.token).toBe("env-token-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
expect(result.source).toBe("env");
});
it("returns token from ~/.railway/config.json when env unset, source='config'", () => {
mkdirSync(join(dir, ".railway"));
writeFileSync(
join(dir, ".railway", "config.json"),
JSON.stringify({
user: { accessToken: "from-config-aaaaaaaaaaaaaaaaaaaaaaaa" },
}),
);
const result = resolveRailwayToken();
expect(result.token).toBe("from-config-aaaaaaaaaaaaaaaaaaaaaaaa");
expect(result.source).toBe("config");
});
it("throws DISTINCT error when $HOME is unset and env-var also unset", () => {
delete process.env.HOME;
expect(() => resolveRailwayToken()).toThrow(RailwayTokenError);
try {
resolveRailwayToken();
throw new Error("should have thrown");
} catch (e) {
expect(e).toBeInstanceOf(RailwayTokenError);
const err = e as RailwayTokenError;
expect(err.code).toBe("NO_HOME");
expect(err.message).toMatch(/\$HOME/);
}
});
it("trims RAILWAY_TOKEN env var (padded with whitespace/newline)", () => {
process.env.RAILWAY_TOKEN = " padded-token-xxxxxxxxxxxxxxxxxxxxxxxx\n";
const result = resolveRailwayToken();
expect(result.token).toBe("padded-token-xxxxxxxxxxxxxxxxxxxxxxxx");
expect(result.source).toBe("env");
});
it("treats whitespace-only RAILWAY_TOKEN as UNSET and falls through to config", () => {
process.env.RAILWAY_TOKEN = " ";
mkdirSync(join(dir, ".railway"));
writeFileSync(
join(dir, ".railway", "config.json"),
JSON.stringify({
user: { accessToken: "from-config-bbbbbbbbbbbbbbbbbbbbbbbb" },
}),
);
const result = resolveRailwayToken();
expect(result.token).toBe("from-config-bbbbbbbbbbbbbbbbbbbbbbbb");
expect(result.source).toBe("config");
});
it("treats whitespace-only RAILWAY_TOKEN as UNSET and surfaces NO_FILE when no config exists", () => {
process.env.RAILWAY_TOKEN = " \n";
try {
resolveRailwayToken();
throw new Error("should have thrown");
} catch (e) {
expect(e).toBeInstanceOf(RailwayTokenError);
const err = e as RailwayTokenError;
// Must NOT be returned as source="env" with a whitespace token —
// that produces invalid Bearer headers and silent 401s.
expect(err.code).toBe("NO_FILE");
}
});
it("throws DISTINCT error when ~/.railway/config.json does not exist", () => {
try {
resolveRailwayToken();
throw new Error("should have thrown");
} catch (e) {
expect(e).toBeInstanceOf(RailwayTokenError);
const err = e as RailwayTokenError;
expect(err.code).toBe("NO_FILE");
expect(err.message).toMatch(/RAILWAY_TOKEN/);
expect(err.message).toMatch(/railway login/);
}
});
it("throws DISTINCT error when ~/.railway/config.json is malformed JSON", () => {
mkdirSync(join(dir, ".railway"));
writeFileSync(join(dir, ".railway", "config.json"), "{ not json");
try {
resolveRailwayToken();
throw new Error("should have thrown");
} catch (e) {
expect(e).toBeInstanceOf(RailwayTokenError);
const err = e as RailwayTokenError;
expect(err.code).toBe("MALFORMED");
expect(err.message).toMatch(/Malformed ~\/\.railway\/config\.json/);
}
});
it("throws DISTINCT error when config parses but yields no usable token (the silent-fallthrough gap)", () => {
mkdirSync(join(dir, ".railway"));
// Parses fine — just no token field at any layer.
writeFileSync(
join(dir, ".railway", "config.json"),
JSON.stringify({ projects: { something: "else" } }),
);
try {
resolveRailwayToken();
throw new Error("should have thrown");
} catch (e) {
expect(e).toBeInstanceOf(RailwayTokenError);
const err = e as RailwayTokenError;
// CRITICAL: must NOT be the generic NO_FILE message — the
// user needs to know the file WAS found but had no token.
expect(err.code).toBe("NO_TOKEN_IN_CONFIG");
expect(err.message).toMatch(
/~\/\.railway\/config\.json.*no.*token|found.*no.*token/i,
);
// Must also hint at the remedy.
expect(err.message).toMatch(/railway login|RAILWAY_TOKEN/);
}
});
});
@@ -0,0 +1,193 @@
import { describe, expect, it, vi } from "vitest";
import { resolveRailwayTokenFromConfig } from "../railway-token";
import type { RailwayConfigShape } from "../railway-token";
describe("resolveRailwayTokenFromConfig", () => {
it("prefers user.accessToken when present", () => {
const cfg: RailwayConfigShape = {
user: {
accessToken: "new-access-token-43-chars-or-more-aaaaaaaa",
token: "legacy-short-token",
},
};
const warn = vi.fn();
const result = resolveRailwayTokenFromConfig(cfg, { warn });
expect(result).toBe("new-access-token-43-chars-or-more-aaaaaaaa");
expect(warn).not.toHaveBeenCalled();
});
it("falls back to top-level accessToken", () => {
const cfg: RailwayConfigShape = {
accessToken: "top-level-access-token-aaaaaaaaaaaaaaaaaaaa",
};
const warn = vi.fn();
const result = resolveRailwayTokenFromConfig(cfg, { warn });
expect(result).toBe("top-level-access-token-aaaaaaaaaaaaaaaaaaaa");
expect(warn).not.toHaveBeenCalled();
});
it("falls back to legacy user.token AND emits a deprecation warning", () => {
const cfg: RailwayConfigShape = {
user: { token: "legacy-short-token-aaaaaaaaaa" },
};
const warn = vi.fn();
const result = resolveRailwayTokenFromConfig(cfg, { warn });
expect(result).toBe("legacy-short-token-aaaaaaaaaa");
expect(warn).toHaveBeenCalledTimes(1);
expect(warn.mock.calls[0][0]).toMatch(
/deprecated.*user\.token.*accessToken/i,
);
});
it("falls back to top-level token with deprecation warning", () => {
const cfg: RailwayConfigShape = { token: "legacy-top-aaaaaaaaa" };
const warn = vi.fn();
const result = resolveRailwayTokenFromConfig(cfg, { warn });
expect(result).toBe("legacy-top-aaaaaaaaa");
expect(warn).toHaveBeenCalledTimes(1);
});
it("returns undefined and does not warn on an empty config", () => {
const warn = vi.fn();
const result = resolveRailwayTokenFromConfig({}, { warn });
expect(result).toBeUndefined();
expect(warn).not.toHaveBeenCalled();
});
it("ignores empty-string tokens at every layer", () => {
const cfg: RailwayConfigShape = {
user: { accessToken: "", token: "" },
accessToken: "",
token: "",
};
const warn = vi.fn();
const result = resolveRailwayTokenFromConfig(cfg, { warn });
expect(result).toBeUndefined();
});
it("treats whitespace-only user.accessToken as empty and falls through", () => {
const cfg: RailwayConfigShape = {
user: { accessToken: " " },
};
const warn = vi.fn();
const result = resolveRailwayTokenFromConfig(cfg, { warn });
expect(result).toBeUndefined();
expect(warn).not.toHaveBeenCalled();
});
it("falls through whitespace-only user.accessToken to legacy user.token with warn", () => {
const cfg: RailwayConfigShape = {
user: {
accessToken: "\n\t ",
token: "legacy-short-token-aaaaaaaaaa",
},
};
const warn = vi.fn();
const result = resolveRailwayTokenFromConfig(cfg, { warn });
expect(result).toBe("legacy-short-token-aaaaaaaaaa");
expect(warn).toHaveBeenCalledTimes(1);
});
it("treats whitespace-only tokens at every layer as empty", () => {
const cfg: RailwayConfigShape = {
user: { accessToken: " ", token: "\t" },
accessToken: "\n",
token: " ",
};
const warn = vi.fn();
const result = resolveRailwayTokenFromConfig(cfg, { warn });
expect(result).toBeUndefined();
expect(warn).not.toHaveBeenCalled();
});
it("returns undefined when config is null", () => {
const warn = vi.fn();
const result = resolveRailwayTokenFromConfig(null, { warn });
expect(result).toBeUndefined();
expect(warn).not.toHaveBeenCalled();
});
it("returns undefined when config is undefined", () => {
const warn = vi.fn();
const result = resolveRailwayTokenFromConfig(undefined, { warn });
expect(result).toBeUndefined();
expect(warn).not.toHaveBeenCalled();
});
it("returns undefined when config is a string (non-object JSON)", () => {
const warn = vi.fn();
const result = resolveRailwayTokenFromConfig(
"not-an-object" as unknown as RailwayConfigShape,
{ warn },
);
expect(result).toBeUndefined();
expect(warn).not.toHaveBeenCalled();
});
it("returns undefined when config is a number", () => {
const warn = vi.fn();
const result = resolveRailwayTokenFromConfig(
42 as unknown as RailwayConfigShape,
{ warn },
);
expect(result).toBeUndefined();
expect(warn).not.toHaveBeenCalled();
});
it("returns the trimmed token when user.accessToken has surrounding whitespace", () => {
const cfg: RailwayConfigShape = {
user: { accessToken: " padded-token-aaaaaaaaaaaaaaaaaaaa " },
};
const warn = vi.fn();
const result = resolveRailwayTokenFromConfig(cfg, { warn });
expect(result).toBe("padded-token-aaaaaaaaaaaaaaaaaaaa");
expect(warn).not.toHaveBeenCalled();
});
it("returns the trimmed token when user.accessToken has a trailing newline", () => {
const cfg: RailwayConfigShape = {
user: { accessToken: "abc-access-token-aaaaaaaaaaaaaaaaaaaa\n" },
};
const warn = vi.fn();
const result = resolveRailwayTokenFromConfig(cfg, { warn });
expect(result).toBe("abc-access-token-aaaaaaaaaaaaaaaaaaaa");
});
it("returns the trimmed token when top-level accessToken has surrounding whitespace", () => {
const cfg: RailwayConfigShape = {
accessToken: "\ttop-padded-aaaaaaaaaaaaaaaaaaaaaaaa\n",
};
const warn = vi.fn();
const result = resolveRailwayTokenFromConfig(cfg, { warn });
expect(result).toBe("top-padded-aaaaaaaaaaaaaaaaaaaaaaaa");
expect(warn).not.toHaveBeenCalled();
});
it("returns the trimmed token for legacy user.token (with warn)", () => {
const cfg: RailwayConfigShape = {
user: { token: " legacy-user-token-aaaaaaaaaa\n" },
};
const warn = vi.fn();
const result = resolveRailwayTokenFromConfig(cfg, { warn });
expect(result).toBe("legacy-user-token-aaaaaaaaaa");
expect(warn).toHaveBeenCalledTimes(1);
});
it("returns the trimmed token for legacy top-level token (with warn)", () => {
const cfg: RailwayConfigShape = { token: "\nlegacy-top-aaaaaaaaa " };
const warn = vi.fn();
const result = resolveRailwayTokenFromConfig(cfg, { warn });
expect(result).toBe("legacy-top-aaaaaaaaa");
expect(warn).toHaveBeenCalledTimes(1);
});
it("returns undefined when config is an array", () => {
const warn = vi.fn();
const result = resolveRailwayTokenFromConfig(
[] as unknown as RailwayConfigShape,
{ warn },
);
expect(result).toBeUndefined();
expect(warn).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,431 @@
/**
* Tests for showcase/scripts/lib/slug-map.ts.
*
* Pins the shared slug/examples mapping tables and the
* born-in-showcase set so all three validators agree.
*/
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import fs from "fs";
import os from "os";
import path from "path";
import { fileURLToPath } from "url";
import {
BORN_IN_SHOWCASE,
SLUG_MAP,
SLUG_TO_EXAMPLES,
FALLBACK_MAP,
isShowcaseSlug,
} from "../slug-map.js";
import type { ShowcaseSlug } from "../slug-map.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const PACKAGES_DIR = path.resolve(__dirname, "..", "..", "..", "integrations");
describe("BORN_IN_SHOWCASE", () => {
it("contains the 7 known born-in-showcase slugs", () => {
expect(BORN_IN_SHOWCASE.has("ag2")).toBe(true);
expect(BORN_IN_SHOWCASE.has("claude-sdk-python")).toBe(true);
expect(BORN_IN_SHOWCASE.has("claude-sdk-typescript")).toBe(true);
expect(BORN_IN_SHOWCASE.has("langroid")).toBe(true);
expect(BORN_IN_SHOWCASE.has("ms-agent-harness-dotnet")).toBe(true);
expect(BORN_IN_SHOWCASE.has("spring-ai")).toBe(true);
expect(BORN_IN_SHOWCASE.has("strands-typescript")).toBe(true);
});
it("has exactly 7 entries — guards against accidental additions", () => {
// Size assertion: if someone adds a new born-in-showcase slug without
// updating the test above, this pins the cardinality so the addition
// is caught rather than silently accepted.
expect(BORN_IN_SHOWCASE.size).toBe(7);
});
it("is a frozen / immutable ReadonlySet (add throws)", () => {
// Callers must not mutate the shared set at runtime. A ReadonlySet type
// is compile-time only; we back it with a frozen Set so a runtime
// `.add()` attempt throws in strict mode rather than silently diverging
// from the other validator copies.
const s = BORN_IN_SHOWCASE as unknown as Set<string>;
expect(() => s.add("sneaky-mutation")).toThrow();
});
it("rejects .delete and .clear on the frozen set", () => {
const s = BORN_IN_SHOWCASE as unknown as Set<string>;
expect(() => s.delete("ag2")).toThrow();
expect(() => s.clear()).toThrow();
});
});
describe("SLUG_TO_EXAMPLES (showcase slug → examples dir names)", () => {
// This test reads the live showcase/integrations/ tree. In sparse
// checkouts (CI shards, partial clones) the directory may be absent;
// skip rather than false-fail when that happens.
it.skipIf(!fs.existsSync(PACKAGES_DIR))(
"has no dead entries — every target dir exists under showcase/integrations/",
() => {
// Regression guard: the old audit.ts map contained crewai-flows,
// agent-spec-langgraph, and mcp-apps which produced phantom "no
// examples source" anomalies. Removing them here is the whole point of
// the extraction.
for (const slug of Object.keys(SLUG_TO_EXAMPLES)) {
const pkgPath = path.join(PACKAGES_DIR, slug);
expect(
fs.existsSync(pkgPath),
`SLUG_TO_EXAMPLES slug '${slug}' has no matching showcase/integrations/${slug}/`,
).toBe(true);
}
},
);
// Companion to the skipIf test above — runs UNCONDITIONALLY against a
// fixture tmpdir so the invariant ("every SLUG_TO_EXAMPLES key has a
// matching integrations/<slug>/ dir") is still exercised on sparse
// checkouts / CI shards / Docker build contexts where the real
// showcase/integrations/ tree is absent. A skipIf without a contra-positive
// assertion is indistinguishable from "test was deleted."
describe("fixture-based invariant (runs regardless of checkout layout)", () => {
let fixtureDir: string;
let fixturePackagesDir: string;
beforeAll(() => {
fixtureDir = fs.mkdtempSync(path.join(os.tmpdir(), "slug-map-fixture-"));
fixturePackagesDir = path.join(fixtureDir, "integrations");
fs.mkdirSync(fixturePackagesDir, { recursive: true });
// Seed a directory for every SLUG_TO_EXAMPLES key. This mirrors the
// expected layout of showcase/integrations/ — the invariant check below
// is identical in spirit to the skipIf test, just pointed at a
// fixture whose contents we fully control.
for (const slug of Object.keys(SLUG_TO_EXAMPLES)) {
fs.mkdirSync(path.join(fixturePackagesDir, slug), { recursive: true });
}
});
afterAll(() => {
fs.rmSync(fixtureDir, { recursive: true, force: true });
});
it("every SLUG_TO_EXAMPLES key resolves to a real dir in the fixture", () => {
const seen: string[] = [];
for (const slug of Object.keys(SLUG_TO_EXAMPLES)) {
const pkgPath = path.join(fixturePackagesDir, slug);
expect(
fs.existsSync(pkgPath),
`fixture missing integrations/${slug}/ — invariant seeding is broken`,
).toBe(true);
seen.push(slug);
}
// Sentinel: assert we actually iterated at least one slug. A silent
// empty SLUG_TO_EXAMPLES would otherwise pass vacuously.
expect(seen.length).toBeGreaterThan(0);
});
});
it("does not include the three known dead entries", () => {
expect(
(SLUG_TO_EXAMPLES as Record<string, unknown>)["crewai-flows"],
).toBeUndefined();
expect(
(SLUG_TO_EXAMPLES as Record<string, unknown>)["agent-spec-langgraph"],
).toBeUndefined();
expect(
(SLUG_TO_EXAMPLES as Record<string, unknown>)["mcp-apps"],
).toBeUndefined();
});
it("rejects adding a new top-level key at runtime", () => {
// Object.isFrozen is the weak form of this assertion: it only checks
// a flag. An actual mutation attempt is the real invariant — strict
// mode is active in ESM, so assignment on a frozen object throws.
expect(() => {
(SLUG_TO_EXAMPLES as unknown as Record<string, readonly string[]>)[
"bogus-new-slug"
] = ["nothing"];
}).toThrow();
});
it("rejects reassigning an existing top-level entry at runtime", () => {
expect(() => {
(SLUG_TO_EXAMPLES as unknown as Record<string, readonly string[]>)[
"mastra"
] = ["replaced"];
}).toThrow();
});
it("rejects mutating an inner array (element assignment throws)", () => {
// freezeMap2D must freeze BOTH the outer record AND each inner array.
// Without the inner freeze, `SLUG_TO_EXAMPLES.mastra[0] = "x"` would
// silently succeed even though the outer Object.isFrozen reports true.
expect(() => {
(SLUG_TO_EXAMPLES.mastra as unknown as string[])[0] = "mutated";
}).toThrow();
});
it("rejects .push on an inner array (all mutation methods fail)", () => {
expect(() => {
(SLUG_TO_EXAMPLES.mastra as unknown as string[]).push("extra");
}).toThrow();
});
});
describe("SLUG_MAP (examples dir → showcase slug)", () => {
it("contains the known mapping for langgraph-js → langgraph-typescript", () => {
// Sample entry inversely matched with SLUG_TO_EXAMPLES.
expect(SLUG_MAP.get("langgraph-js")).toBe("langgraph-typescript");
});
it("inverse of SLUG_MAP covers a sample SLUG_TO_EXAMPLES entry", () => {
// For a slug with a unique examples dir (not a fan-out like crewai-*),
// the entries should be bidirectionally consistent.
const exampleDirs = SLUG_TO_EXAMPLES["langgraph-typescript"];
expect(exampleDirs).toBeDefined();
for (const dir of exampleDirs!) {
expect(SLUG_MAP.get(dir)).toBe("langgraph-typescript");
}
});
// Reads the live showcase/integrations/ tree — skip in sparse checkouts
// where that directory is not materialized.
it.skipIf(!fs.existsSync(PACKAGES_DIR))(
"every VALUE in SLUG_MAP names a real showcase/integrations/<slug>/ dir",
() => {
// Dead-entry guard: the old SLUG_MAP carried values like `crewai`,
// `maf-dotnet`, `maf-python`, `aws-strands`, `agent-spec-langgraph`,
// `a2a`, `mcp-apps`, `pydanticai` that did NOT exist under
// showcase/integrations/. Those broke validate-pins.ts's reverse lookup
// and forced FALLBACK_MAP to re-express the corrections.
for (const [, slug] of SLUG_MAP) {
const pkgPath = path.join(PACKAGES_DIR, slug);
expect(
fs.existsSync(pkgPath),
`SLUG_MAP value '${slug}' has no matching showcase/integrations/${slug}/`,
).toBe(true);
}
},
);
// Companion to the skipIf test above — runs UNCONDITIONALLY against a
// fixture tmpdir so the SLUG_MAP value invariant is exercised even
// when showcase/integrations/ is absent. Without this, a sparse CI
// checkout would silently skip the invariant and a regression
// (reintroducing a dead value like `crewai` / `mcp-apps`) would pass.
describe("fixture-based SLUG_MAP invariant (runs regardless of checkout layout)", () => {
let fixtureDir: string;
let fixturePackagesDir: string;
beforeAll(() => {
fixtureDir = fs.mkdtempSync(path.join(os.tmpdir(), "slug-map-fixture-"));
fixturePackagesDir = path.join(fixtureDir, "integrations");
fs.mkdirSync(fixturePackagesDir, { recursive: true });
// Seed the fixture with every unique VALUE in SLUG_MAP plus every
// FALLBACK_MAP target — those are the invariants we enforce.
const targets = new Set<string>();
for (const [, slug] of SLUG_MAP) targets.add(slug);
for (const target of Object.values(FALLBACK_MAP)) targets.add(target);
for (const slug of targets) {
fs.mkdirSync(path.join(fixturePackagesDir, slug), { recursive: true });
}
});
afterAll(() => {
fs.rmSync(fixtureDir, { recursive: true, force: true });
});
it("every SLUG_MAP value resolves to a real dir in the fixture", () => {
let iter = 0;
for (const [, slug] of SLUG_MAP) {
const pkgPath = path.join(fixturePackagesDir, slug);
expect(
fs.existsSync(pkgPath),
`fixture missing integrations/${slug}/ — seeding is broken`,
).toBe(true);
iter++;
}
expect(iter).toBeGreaterThan(0);
});
it("every FALLBACK_MAP target resolves to a real dir in the fixture", () => {
let iter = 0;
for (const [slug, target] of Object.entries(FALLBACK_MAP)) {
const pkgPath = path.join(fixturePackagesDir, target);
expect(
fs.existsSync(pkgPath),
`FALLBACK_MAP['${slug}'] = '${target}' has no matching dir`,
).toBe(true);
iter++;
}
expect(iter).toBeGreaterThan(0);
});
});
it("is frozen — .set throws", () => {
const m = SLUG_MAP as unknown as Map<string, string>;
expect(() => m.set("bad", "mutation")).toThrow();
});
it("is frozen — .delete and .clear throw", () => {
const m = SLUG_MAP as unknown as Map<string, string>;
expect(() => m.delete("langgraph-js")).toThrow();
expect(() => m.clear()).toThrow();
});
});
describe("isShowcaseSlug runtime validator", () => {
it("accepts a non-empty, kebab-cased-or-plain slug string", () => {
expect(isShowcaseSlug("ag2")).toBe(true);
expect(isShowcaseSlug("langgraph-typescript")).toBe(true);
expect(isShowcaseSlug("ms-agent-framework-python")).toBe(true);
});
it("rejects the empty string", () => {
expect(isShowcaseSlug("")).toBe(false);
});
it("rejects non-string inputs via defensive typeof check", () => {
// Signature widened from (s: string) to (s: unknown) so the guard
// is a live validator at any API boundary. Pass the values directly
// — no `as unknown as string` casts needed now that the parameter
// type accepts unknown.
expect(isShowcaseSlug(null)).toBe(false);
expect(isShowcaseSlug(undefined)).toBe(false);
expect(isShowcaseSlug(42)).toBe(false);
expect(isShowcaseSlug({})).toBe(false);
expect(isShowcaseSlug([])).toBe(false);
expect(isShowcaseSlug(true)).toBe(false);
});
it("narrows its argument via a user-defined type predicate", () => {
// isShowcaseSlug is declared `(s: unknown): s is ShowcaseSlug`, so
// when it returns true the compiler narrows the caller's variable to
// ShowcaseSlug. This test is primarily a compile-time assertion; a
// runtime check backs it up.
const candidate: unknown = "ag2";
if (isShowcaseSlug(candidate)) {
// Must be assignable to ShowcaseSlug without further casts.
const s: ShowcaseSlug = candidate;
expect(s).toBe("ag2");
} else {
throw new Error("expected 'ag2' to satisfy isShowcaseSlug");
}
});
it("rejects slugs containing whitespace or path separators", () => {
// These are the most likely garbage-input patterns at the boundary.
expect(isShowcaseSlug("foo bar")).toBe(false);
expect(isShowcaseSlug("foo/bar")).toBe(false);
expect(isShowcaseSlug("foo\\bar")).toBe(false);
});
it("is applied at construction — every BORN_IN_SHOWCASE and SLUG_MAP slug satisfies it", () => {
for (const s of BORN_IN_SHOWCASE) expect(isShowcaseSlug(s)).toBe(true);
for (const [, slug] of SLUG_MAP) expect(isShowcaseSlug(slug)).toBe(true);
for (const slug of Object.keys(SLUG_TO_EXAMPLES))
expect(isShowcaseSlug(slug)).toBe(true);
for (const slug of Object.keys(FALLBACK_MAP))
expect(isShowcaseSlug(slug)).toBe(true);
});
});
describe("freezeSet / freezeMap behavioral invariants", () => {
it("BORN_IN_SHOWCASE rejects re-defining its mutation methods", () => {
// Behavioral form of the old descriptor-bit check: the concrete
// invariant is that a later caller cannot restore a working `.add`
// by re-replacing the property. Assert that any re-defineProperty
// attempt throws, rather than inspecting descriptor bits directly —
// tests should pin observable behavior, not implementation shape.
const s = BORN_IN_SHOWCASE as unknown as Set<string>;
expect(() => {
Object.defineProperty(s, "add", { value: (_v: string) => s });
}).toThrow();
expect(() => {
Object.defineProperty(s, "delete", { value: (_v: string) => true });
}).toThrow();
expect(() => {
Object.defineProperty(s, "clear", { value: () => undefined });
}).toThrow();
});
it("SLUG_MAP rejects re-defining its mutation methods", () => {
const m = SLUG_MAP as unknown as Map<string, string>;
expect(() => {
Object.defineProperty(m, "set", {
value: (_k: string, _v: string) => m,
});
}).toThrow();
expect(() => {
Object.defineProperty(m, "delete", { value: (_k: string) => true });
}).toThrow();
expect(() => {
Object.defineProperty(m, "clear", { value: () => undefined });
}).toThrow();
});
});
describe("SLUG_TO_EXAMPLES / FALLBACK_MAP / BORN_IN_SHOWCASE derive from one entries source", () => {
it("every FALLBACK_MAP entry names a slug also present in SLUG_TO_EXAMPLES", () => {
// Derivation invariant: both maps come from the same per-slug entry
// (slug, examples dirs, optional fallback). A FALLBACK_MAP key with
// no SLUG_TO_EXAMPLES counterpart would mean the two maps were edited
// independently and fell out of sync.
for (const slug of Object.keys(FALLBACK_MAP)) {
expect(
(SLUG_TO_EXAMPLES as Record<string, unknown>)[slug],
`FALLBACK_MAP slug '${slug}' missing from SLUG_TO_EXAMPLES`,
).toBeDefined();
}
});
it("FALLBACK_MAP target equals the first SLUG_TO_EXAMPLES candidate for the same slug", () => {
// Both maps share the same underlying entry; the fallback is simply
// the chosen preferred dir out of SLUG_TO_EXAMPLES[slug]. If someone
// edits one side but not the other, the two maps will disagree.
for (const [slug, target] of Object.entries(FALLBACK_MAP)) {
const dirs = SLUG_TO_EXAMPLES[slug];
expect(dirs).toBeDefined();
expect(dirs![0]).toBe(target);
}
});
it("BORN_IN_SHOWCASE and SLUG_TO_EXAMPLES are disjoint (no slug is both)", () => {
// A born-in-showcase slug has no examples counterpart by definition;
// putting it in SLUG_TO_EXAMPLES would contradict that. The derivation
// pipeline enforces that an entry with `bornInShowcase: true` has NO
// examples dirs, so the two outputs can never overlap.
for (const slug of BORN_IN_SHOWCASE) {
expect(
(SLUG_TO_EXAMPLES as Record<string, unknown>)[slug],
`'${slug}' is in both BORN_IN_SHOWCASE and SLUG_TO_EXAMPLES`,
).toBeUndefined();
}
});
it("BORN_IN_SHOWCASE and FALLBACK_MAP are disjoint", () => {
// Same pairing: born-in-showcase slugs have no examples dir, so a
// FALLBACK_MAP target for one would be nonsensical.
for (const slug of BORN_IN_SHOWCASE) {
expect(
(FALLBACK_MAP as Record<string, unknown>)[slug],
`'${slug}' is in both BORN_IN_SHOWCASE and FALLBACK_MAP`,
).toBeUndefined();
}
});
});
describe("FALLBACK_MAP (documents SLUG_MAP staleness)", () => {
it("contains the stale-mapping entries validate-pins.ts relied on", () => {
expect(FALLBACK_MAP["crewai-crews"]).toBe("crewai-crews");
expect(FALLBACK_MAP["ms-agent-dotnet"]).toBe("ms-agent-framework-dotnet");
expect(FALLBACK_MAP["ms-agent-python"]).toBe("ms-agent-framework-python");
expect(FALLBACK_MAP["pydantic-ai"]).toBe("pydantic-ai");
expect(FALLBACK_MAP["strands"]).toBe("strands-python");
});
it("rejects runtime mutation", () => {
expect(() => {
(FALLBACK_MAP as unknown as Record<string, string>)["new-key"] = "bogus";
}).toThrow();
expect(() => {
(FALLBACK_MAP as unknown as Record<string, string>)["strands"] = "other";
}).toThrow();
});
});
+186
View File
@@ -0,0 +1,186 @@
/**
* build-outputs.ts — Parse + merge the structured per-slot build results
* emitted by `showcase_build.yml`. Each matrix slot in the build job
* uploads a per-slot artifact named `build-result-<dispatch_name>`
* containing a single `result.json` payload of the shape
* `{service: "<dispatch_name>", status: "success"|"failure"|"skipped"}`.
* The aggregate-build-results job downloads every `build-result-*`
* artifact and merges the payloads via `mergeBuildResultFiles` below;
* the resulting array is uploaded as the canonical `build-results`
* artifact for cross-workflow consumption. The deploy workflow (and the
* redeploy guard) read this list instead of parsing job names.
*
* NOTE: the "single result.json per slot" invariant is enforced
* workflow-side (each matrix slot writes exactly one file before
* uploading its artifact); this module assumes that contract and
* validates only the parsed payload shape, not the filesystem layout.
*/
// Single source of truth for the set of valid build outcomes. The
// `as const` tuple drives BOTH the runtime `VALID_STATUSES` set AND
// the compile-time `BuildOutcome` union (derived via indexed access
// below), so the tuple is the only place a status needs to be added.
// Since `BuildOutcome` is derived from this tuple there is no separate
// union that could drift out of sync — no redundant exhaustiveness
// assertion is needed.
const BUILD_OUTCOMES = ["success", "failure", "skipped"] as const;
export type BuildOutcome = (typeof BUILD_OUTCOMES)[number];
const VALID_STATUSES: ReadonlySet<BuildOutcome> = new Set(BUILD_OUTCOMES);
export interface ServiceBuildResult {
service: string;
status: BuildOutcome;
}
function isNonBlankString(value: unknown): value is string {
return typeof value === "string" && value.trim().length > 0;
}
/**
* Shared validator for a single `{service, status}` payload. Used by
* both `parseBuildOutputs` (per array entry) and `mergeBuildResultFiles`
* (per slot payload) so validation rules + error wording live in one
* place. `contextLabel` is prefixed to every error message — callers
* pass something like `"parseBuildOutputs entry[3]"` or
* `"mergeBuildResultFiles slot[2]"` so the failure points at the
* offending row.
*/
function validateServiceBuildResult(
raw: unknown,
contextLabel: string,
): ServiceBuildResult {
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
throw new Error(
`${contextLabel}: expected object with {service, status}, got ${JSON.stringify(raw)}`,
);
}
const service = (raw as { service?: unknown }).service;
if (typeof service !== "string") {
throw new Error(
`${contextLabel}: missing required string field "service": ${JSON.stringify(raw)}`,
);
}
const trimmedService = service.trim();
if (trimmedService.length === 0) {
throw new Error(
`${contextLabel}: field "service" must be a non-empty, non-whitespace string: ${JSON.stringify(raw)}`,
);
}
const status = (raw as { status?: unknown }).status;
if (
typeof status !== "string" ||
!VALID_STATUSES.has(status as BuildOutcome)
) {
throw new Error(
`${contextLabel}: invalid "status" (must be success|failure|skipped): ${JSON.stringify(raw)}`,
);
}
return { service: trimmedService, status: status as BuildOutcome };
}
export function parseBuildOutputs(raw: string): ServiceBuildResult[] {
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch (e) {
throw new Error(
`Failed to parse build outputs JSON: ${
e instanceof Error ? e.message : String(e)
}`,
{ cause: e },
);
}
if (!Array.isArray(parsed)) {
throw new Error("Build outputs must be a JSON array");
}
return parsed.map((entry, idx) =>
validateServiceBuildResult(entry, `parseBuildOutputs entry[${idx}]`),
);
}
export function successSet(results: ServiceBuildResult[]): string[] {
return results.filter((r) => r.status === "success").map((r) => r.service);
}
/**
* Canonical artifact-name convention for the per-slot build-result
* handoff. Each matrix slot in showcase_build.yml uploads exactly one
* artifact named `build-result-<dispatch_name>` containing a single
* `result.json` file. The aggregator job downloads every artifact
* matching the `build-result-*` pattern and merges them via
* mergeBuildResultFiles below. We refuse empty/whitespace service
* names so the per-slot artifact cannot collide with the aggregated
* `build-results` artifact published downstream.
*/
export function buildResultArtifactName(service: string): string {
if (!isNonBlankString(service)) {
throw new Error(
"buildResultArtifactName: `service` must be a non-empty, non-whitespace string",
);
}
return `build-result-${service}`;
}
/**
* Merge a list of per-slot result.json payloads (raw strings, one per
* matrix slot's uploaded artifact) into a single ServiceBuildResult[].
* Each payload MUST be a JSON object with `service: string` and
* `status: success|failure|skipped`. The merge is order-preserving so
* downstream consumers can rely on stable iteration.
*
* Fails loud on duplicate `service` names across slots: a duplicate
* means an upstream dispatch-name collision (two slots claiming the
* same service), which would let a `failure` + `success` pair for the
* same service spuriously look like a success in `successSet`. We
* surface the collision instead of silently deduping.
*/
export function mergeBuildResultFiles(
slotPayloads: readonly string[],
): ServiceBuildResult[] {
const merged = slotPayloads.map((raw, idx) => {
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch (e) {
throw new Error(
`mergeBuildResultFiles slot[${idx}]: not valid JSON: ${
e instanceof Error ? e.message : String(e)
}`,
{ cause: e },
);
}
return validateServiceBuildResult(
parsed,
`mergeBuildResultFiles slot[${idx}]`,
);
});
const seen = new Set<string>();
const duplicates = new Set<string>();
for (const { service } of merged) {
if (seen.has(service)) {
duplicates.add(service);
} else {
seen.add(service);
}
}
if (duplicates.size > 0) {
const names = Array.from(duplicates).sort().join(", ");
throw new Error(
`mergeBuildResultFiles: duplicate service name(s) across slots: ${names}`,
);
}
return merged;
}
/**
* Returns true iff at least one service in the build set finished as
* `success`. Gates redeploy: when no service succeeded, redeploy MUST
* be skipped so we do not re-pull the stale `:latest` and silently
* look healthy.
*/
export function shouldRedeployStaging(results: ServiceBuildResult[]): boolean {
return results.some((r) => r.status === "success");
}
+63
View File
@@ -0,0 +1,63 @@
export interface MultiFileRegionSource {
demoKey: string;
regionName: string;
files: string[];
}
export const ALLOWED_MULTI_FILE_REGION_KEYS = new Set([
"ag2::headless-complete::custom-bubbles",
"ag2::open-gen-ui-advanced::sandbox-function-registration",
"agno::headless-complete::custom-bubbles",
"agno::open-gen-ui-advanced::sandbox-function-registration",
"built-in-agent::headless-complete::custom-bubbles",
"claude-sdk-python::headless-complete::custom-bubbles",
"claude-sdk-python::open-gen-ui-advanced::sandbox-function-registration",
"claude-sdk-typescript::headless-complete::custom-bubbles",
"claude-sdk-typescript::open-gen-ui-advanced::sandbox-function-registration",
"crewai-crews::headless-complete::custom-bubbles",
"crewai-crews::open-gen-ui-advanced::sandbox-function-registration",
"google-adk::headless-complete::custom-bubbles",
"google-adk::open-gen-ui-advanced::sandbox-function-registration",
"langgraph-fastapi::headless-complete::custom-bubbles",
"langgraph-fastapi::open-gen-ui-advanced::sandbox-function-registration",
"langgraph-python::headless-complete::custom-bubbles",
"langgraph-python::open-gen-ui-advanced::sandbox-function-registration",
"langgraph-typescript::headless-complete::custom-bubbles",
"langgraph-typescript::open-gen-ui-advanced::sandbox-function-registration",
"langroid::headless-complete::custom-bubbles",
"langroid::open-gen-ui-advanced::sandbox-function-registration",
"llamaindex::headless-complete::custom-bubbles",
"llamaindex::open-gen-ui-advanced::sandbox-function-registration",
"mastra::headless-complete::custom-bubbles",
"mastra::open-gen-ui-advanced::sandbox-function-registration",
"ms-agent-dotnet::open-gen-ui-advanced::sandbox-function-registration",
"ms-agent-harness-dotnet::open-gen-ui-advanced::sandbox-function-registration",
"ms-agent-python::open-gen-ui-advanced::sandbox-function-registration",
"pydantic-ai::headless-complete::custom-bubbles",
"pydantic-ai::open-gen-ui-advanced::sandbox-function-registration",
"spring-ai::headless-complete::custom-bubbles",
"spring-ai::open-gen-ui-advanced::sandbox-function-registration",
"strands::headless-complete::custom-bubbles",
"strands::open-gen-ui-advanced::sandbox-function-registration",
"strands-typescript::headless-complete::custom-bubbles",
"strands-typescript::open-gen-ui-advanced::sandbox-function-registration",
]);
export function multiFileRegionKey(
demoKey: string,
regionName: string,
): string {
return `${demoKey}::${regionName}`;
}
export function findUnexpectedMultiFileRegions(
sources: MultiFileRegionSource[],
): MultiFileRegionSource[] {
return sources.filter(
(source) =>
source.files.length > 1 &&
!ALLOWED_MULTI_FILE_REGION_KEYS.has(
multiFileRegionKey(source.demoKey, source.regionName),
),
);
}
@@ -0,0 +1,40 @@
import { describe, it, expect } from "vitest";
import { checkEssentialContent } from "./essential-content.js";
describe("checkEssentialContent", () => {
it("flags a quickstart page missing the 'run agent' section", () => {
const result = checkEssentialContent({
path: "integrations/mastra/quickstart.mdx",
body: "# QS\n\nInstall the CLI.\n",
});
expect(result.status).toBe("fail");
expect(result.messages.join(" ").toLowerCase()).toContain("run");
});
it("passes a quickstart page with all required elements", () => {
const body = `# QS
## Install
\`\`\`bash
npm install
\`\`\`
## Run your agent
## Wire CopilotKit provider
## Try it
`;
const result = checkEssentialContent({
path: "integrations/mastra/quickstart.mdx",
body,
});
expect(result.status).toBe("pass");
});
it("uses the feature-page checklist for non-quickstart pages", () => {
const body = "# Frontend Tools\n\nWhat is this? Something.\n";
const result = checkEssentialContent({
path: "integrations/mastra/frontend-tools.mdx",
body,
});
expect(result.status).toBe("fail");
expect(result.messages.join(" ").toLowerCase()).toContain("code sample");
});
});
+72
View File
@@ -0,0 +1,72 @@
export interface PageInput {
path: string;
body: string;
}
export type Status = "pass" | "fail";
export interface ContentResult {
status: Status;
messages: string[];
}
interface Rule {
description: string;
test: (body: string) => boolean;
}
const QUICKSTART_RULES: Rule[] = [
{
description: "install step (bash/npm/uv install)",
test: (b) => /install|npm i\b|uv add|npx /i.test(b),
},
{
description: "run agent step",
test: (b) => /run.*(agent|server|dev)/i.test(b),
},
{
description: "wire CopilotKit provider",
test: (b) => /CopilotKit\s*(?:Provider)?|<CopilotKit\b/i.test(b),
},
{
description: "try-it / first interaction",
test: (b) => /try it|chat|ask the agent|start chatting/i.test(b),
},
];
const FEATURE_RULES: Rule[] = [
{
description: "what-is intro",
test: (b) => /what is this|what is\b|introduction/i.test(b),
},
{
description: "at least one fenced code sample",
test: (b) => /```[a-z]/i.test(b),
},
{
description: "next-steps or further-reading link",
test: (b) =>
/next steps|what's next|further reading|see also/i.test(b) ||
/<Card\b/i.test(b),
},
];
function pickRules(pathRel: string): Rule[] {
if (/quickstart/i.test(pathRel)) return QUICKSTART_RULES;
if (/troubleshooting/i.test(pathRel)) return [];
return FEATURE_RULES;
}
export function checkEssentialContent(input: PageInput): ContentResult {
const rules = pickRules(input.path);
const messages: string[] = [];
for (const rule of rules) {
if (!rule.test(input.body)) {
messages.push(`${input.path}: missing ${rule.description}`);
}
}
return {
status: messages.length === 0 ? "pass" : "fail",
messages,
};
}
+528
View File
@@ -0,0 +1,528 @@
/**
* Shared manifest schema + parser.
*
* Used by audit.ts, validate-parity.ts, and capture-previews.ts so the
* three tools agree on:
* 1. the Manifest / ManifestDemo TypeScript shape
* 2. runtime shape validation of `manifest.yaml`
* 3. the tagged-union return type distinguishing missing /
* malformed / unreadable / ok
*/
import fs from "fs";
import yaml from "yaml";
/**
* Branded, non-empty demo id. Structurally a string (so downstream
* callers can still use `demo.id` in template strings, `Set<string>`
* membership, equality comparisons, etc.) but the branding prevents
* arbitrary strings from flowing into a `DemoId` slot without going
* through the `createDemoId` smart constructor. parseManifest is the
* sole production caller of that constructor; it validates non-empty
* at runtime and re-reports shape-malformed on failure.
*
* The `__brand` property is phantom-only — it does not exist at
* runtime. That keeps the branded type zero-cost while still giving
* the compiler a distinct nominal type for id values.
*/
export type DemoId = string & { readonly __brand: "DemoId" };
/**
* Smart constructor for `DemoId`. Returns the branded value on success
* or `null` if validation fails (non-string or empty string). Kept as
* `null`-returning rather than throwing so `parseManifest` can turn a
* failure into its usual `{kind:"malformed", subkind:"shape"}` result
* without crossing an exception boundary.
*
* The parameter type is `unknown` — this function sits at an API
* boundary (yaml.parse results, JSON-roundtripped demos, caller-supplied
* strings) where the compile-time type does not hold. A widened param
* keeps the runtime typeof guard alive rather than reducing to a dead
* check.
*/
export function createDemoId(s: unknown): DemoId | null {
if (typeof s !== "string" || s.length === 0) return null;
return s as DemoId;
}
/**
* One entry under `manifest.yaml :: demos[]`. Name is optional; id is
* required AND non-empty (checked at runtime by parseManifest, typed
* via the `DemoId` brand).
*
* Fields are `readonly`: parseManifest freezes the returned object so
* downstream consumers cannot mutate a demo after validation. The type
* matches the runtime freeze.
*/
export interface ManifestDemo {
readonly id: DemoId;
readonly name?: string;
/**
* Informational demos (e.g. cli-start): when set, the row surfaces this
* copy-pasteable command in the dashboard instead of a live preview.
* Such demos have no on-disk folder — parity / bundling skip them.
*/
readonly command?: string;
/**
* Relative URL path for the demo. `id` is the CATALOG identifier
* (stable; matched against spec/qa filenames and shell-side registry
* entries), whereas `route` is the deliberate URL / filesystem path
* (`/demos/<dir>` — resolved to `src/app/demos/<dir>/` by consumers
* that need the on-disk location, e.g. bundle-demo-content and
* validate-parity). The two are intentionally separate so renaming a
* catalog id does not mass-rewrite URLs (and vice versa).
*
* Optional at the type boundary for backward compatibility with test
* fixtures and historical manifests. When present, parseManifest
* validates it is a non-empty string starting with "/demos/".
* Consumers that need an on-disk demo directory should prefer
* `route` when present and fall back to `id`.
*/
readonly route?: string;
}
/**
* Union of the fields used by audit.ts / validate-parity.ts / capture-previews.ts.
*
* `slug` is REQUIRED: every manifest in showcase/integrations/ carries a
* slug, and none of the three consumers ever constructs or accepts a
* Manifest without one. Marking it required here lets downstream code
* drop `manifest.slug ?? "(unknown)"` fallbacks without TypeScript
* complaining. parseManifest enforces `slug` is a non-empty string at
* runtime, so the type matches reality.
*
* `name` and `deployed` remain optional: in practice not every manifest
* sets `name`, and `deployed` only appears when meaningful.
*
* `demos` is NON-optional but always set by parseManifest — an empty
* readonly array when the manifest omits the field. Callers iterate
* unconditionally instead of `?.` chaining.
*
* Fields are `readonly`: parseManifest deep-freezes the returned object
* (including the nested `demos` array), so the public type matches the
* runtime invariant.
*/
export interface Manifest {
readonly slug: string;
readonly name?: string;
readonly deployed?: boolean;
/**
* Deployed backend URL for this integration (e.g. the Railway public
* domain). Used by capture-previews.ts to navigate to each demo. Not
* required by audit.ts / validate-parity.ts, so it remains optional;
* callers that need it should check before use.
* parseManifest validates that, when present, it is a non-empty string.
*/
readonly backend_url?: string;
readonly demos: readonly ManifestDemo[];
}
/**
* Tagged union of manifest parse outcomes. Callers discriminate on
* `kind`:
*
* - "missing" — manifest.yaml does not exist on disk
* - "malformed" — file exists but its contents do not round-trip
* to a valid Manifest. Further split on `subkind`:
* "syntax" = YAML parser rejected the text outright
* (unterminated arrays, bad indentation);
* "shape" = YAML parsed but the resulting value
* does not match the Manifest shape
* (null/scalar top-level, non-array demos,
* demo missing id, duplicate demo id, etc.)
* - "unreadable" — file exists but readFileSync threw
* (permissions, I/O race, etc.)
* - "ok" — parse succeeded and shape validated
*
* The `subkind` discriminator on "malformed" lets callers route each
* failure mode distinctly: a "syntax" subkind flags a likely typo in
* the YAML source, whereas "shape" flags a schema-drift / validation
* problem (missing required field, wrong type, duplicate id, etc.).
*/
export type ParsedManifest =
| { kind: "ok"; manifest: Manifest }
| { kind: "missing" }
| { kind: "malformed"; subkind: "syntax" | "shape"; error: string }
| { kind: "unreadable"; error: string };
function errMsg(e: unknown): string {
return e instanceof Error ? e.message : String(e);
}
/**
* Describe a value for error messages. Distinguishes null/array from
* plain `typeof` because `typeof null === "object"` and
* `typeof [] === "object"` both hide the real shape from users.
*/
function describeType(v: unknown): string {
if (v === null) return "null";
if (Array.isArray(v)) return "array";
return typeof v;
}
/**
* `Object.hasOwn`-backed predicate that narrows `obj` to a type where
* `key` is known to exist. `Object.hasOwn` avoids inherited-property
* pitfalls of the raw `in` operator for any plain object, and pairs
* with the TS predicate so callers read `obj[key]` without further
* casts.
*/
function hasOwnProp<K extends string>(
obj: object,
key: K,
): obj is object & Record<K, unknown> {
return Object.hasOwn(obj, key);
}
/**
* Shared frozen empty-array sentinel reused across the "no demos
* declared" path. Every ok-result's `.demos` is this same frozen
* readonly value when the manifest omits demos, so callers can iterate
* unconditionally and the public `readonly` type matches the runtime
* freeze.
*/
const EMPTY_DEMOS: readonly ManifestDemo[] = Object.freeze([]);
/**
* Read + parse + validate a manifest.yaml at `filePath`. Returns a
* tagged-union `ParsedManifest`; never throws for content errors.
*
* The shape checks are intentionally strict:
* - top-level must be a plain object mapping (not null, scalar, or
* array) — otherwise downstream `.demos` / `.deployed` reads would
* TypeError at runtime;
* - `slug` must be a non-empty string (every consumer relies
* on it — missing slug is always a bug);
* - `name`, if present, must be a string;
* - `demos`, if present, must be an array of objects each with a
* string `id`;
* - `deployed`, if present, must be a boolean (YAML `"yes"` parses to
* a string, which would be silently treated as truthy without this
* check — classic footgun).
*
* Any shape failure produces
* `{ kind: "malformed", subkind: "shape", error }` with a
* human-readable reason. YAML parser failures produce
* `{ kind: "malformed", subkind: "syntax", error }` (distinct so CI can
* route syntax errors differently from schema-drift errors). Missing
* files produce `{ kind: "missing" }` (distinct from malformed so
* callers can emit different anomalies). Read errors (EACCES etc.)
* produce `{ kind: "unreadable" }` — we do NOT collapse them into
* malformed because the file's contents are not actually known to be
* invalid.
*/
export function parseManifest(
filePath: string,
dirSlug?: string,
): ParsedManifest {
// Empty-string dirSlug is a caller bug: `undefined` is the opt-out
// sentinel ("caller did not supply a dir slug"). If a caller's
// path.basename or similar produced `""` (trailing-slash path,
// typo), silently collapsing to undefined would hide the bug and
// skip the slug-mismatch guard. Surface as malformed-shape so the
// caller sees the error at the parser boundary.
if (dirSlug === "") {
return {
kind: "malformed",
subkind: "shape",
error: `caller passed empty dirSlug (use undefined to opt out of the slug-check)`,
};
}
// Use statSync instead of fs.existsSync so ENOENT and non-ENOENT
// errno values (EACCES, ENOTDIR, etc.) are distinguished. existsSync
// CONFLATES these: a manifest whose parent dir is 0700 owned by
// another user returns false from existsSync, which would collapse
// an infrastructure failure into a benign "missing" signal. The
// long docstring on probeDir in validate-parity.ts explains the
// same anti-pattern — the fix is to stat + inspect errno.
try {
fs.statSync(filePath);
} catch (e) {
const code = (e as NodeJS.ErrnoException)?.code;
if (code === "ENOENT") return { kind: "missing" };
return { kind: "unreadable", error: errMsg(e) };
}
let raw: string;
try {
raw = fs.readFileSync(filePath, "utf-8");
} catch (e) {
return { kind: "unreadable", error: errMsg(e) };
}
let parsed: unknown;
try {
parsed = yaml.parse(raw);
} catch (e) {
return { kind: "malformed", subkind: "syntax", error: errMsg(e) };
}
// Top-level type guard. yaml.parse("") is null and yaml.parse("42") is
// 42 — neither is a manifest. Arrays also aren't valid (manifest.yaml
// is a mapping).
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
return {
kind: "malformed",
subkind: "shape",
error: `expected YAML object at top level, got ${
parsed === null ? "null (empty file?)" : describeType(parsed)
}`,
};
}
// At this point `parsed` is known to be a plain object mapping. We
// narrow each field access through `hasOwnProp` so the compiler does
// not need a blanket `as Record<string, unknown>` cast — every read
// below is narrowed by the predicate it just passed.
const obj: object = parsed;
// slug is required — every consumer (audit.ts / validate-parity.ts /
// capture-previews.ts) assumes a slug exists. A manifest without one is
// always a bug, not a tolerable edge case.
if (!hasOwnProp(obj, "slug")) {
return {
kind: "malformed",
subkind: "shape",
error: `missing required "slug" (non-empty string)`,
};
}
const slug = obj.slug;
if (typeof slug !== "string" || slug.length === 0) {
// `hasOwnProp(obj, "slug")` already passed above, so the key is
// present; the value can still be the empty string, null, or a
// non-string. `describeType` covers all of those uniformly.
return {
kind: "malformed",
subkind: "shape",
error: `expected "slug" to be a non-empty string, got ${describeType(slug)}`,
};
}
// Slug-mismatch guard. Every consumer derives the target
// package by computing `packagesDir/<slug>/manifest.yaml`, so if the
// manifest's declared slug disagrees with the directory that holds
// it, downstream tools would silently key a copy-paste or rename
// mistake into the wrong package. Catch at the parser so both tools
// report the drift the same way.
//
// Opt-in: callers pass the expected dir slug via the `dirSlug`
// parameter. When omitted (undefined), the check is skipped so
// existing tests and programmatic callers that don't operate against
// the packages tree continue to work. An explicit empty string is
// rejected at the top of the function as a caller bug — `undefined`
// is the opt-out sentinel; `""` is never a valid dir slug.
// The two production callers (audit.ts, validate-parity.ts) pass the
// slug they used to build the filePath.
const expectedDirSlug = typeof dirSlug === "string" ? dirSlug : undefined;
if (expectedDirSlug !== undefined && expectedDirSlug !== slug) {
return {
kind: "malformed",
subkind: "shape",
error: `slug mismatch: manifest declares "${slug}" but lives under dir "${expectedDirSlug}"`,
};
}
// name (optional) must be a string if present.
let name: string | undefined;
if (hasOwnProp(obj, "name") && obj.name !== undefined) {
if (typeof obj.name !== "string") {
return {
kind: "malformed",
subkind: "shape",
error: `expected "name" to be a string, got ${describeType(obj.name)}`,
};
}
name = obj.name;
}
// deployed (optional) must be a real boolean if present.
let deployed: boolean | undefined;
if (hasOwnProp(obj, "deployed")) {
if (typeof obj.deployed !== "boolean") {
return {
kind: "malformed",
subkind: "shape",
error: `expected "deployed" to be boolean, got ${describeType(obj.deployed)}`,
};
}
deployed = obj.deployed;
}
// backend_url (optional) must be a non-empty string if present. Not
// required by the audit/parity tools, so an absent field is fine.
let backendUrl: string | undefined;
if (hasOwnProp(obj, "backend_url") && obj.backend_url !== undefined) {
if (typeof obj.backend_url !== "string" || obj.backend_url.length === 0) {
return {
kind: "malformed",
subkind: "shape",
error: `expected "backend_url" to be a non-empty string, got ${describeType(obj.backend_url)}`,
};
}
backendUrl = obj.backend_url;
}
// demos must be an array of objects with non-empty string id. Both
// "key absent" and "explicit null" (YAML `demos:`) are treated as
// "no demos declared" — parseManifest normalizes to an empty frozen
// array so `Manifest.demos` is always set. If the key is present with
// a non-nullish value that is not an array, fall through and report.
let demos: readonly ManifestDemo[] = EMPTY_DEMOS;
if (hasOwnProp(obj, "demos") && obj.demos != null) {
const rawDemos = obj.demos;
if (!Array.isArray(rawDemos)) {
return {
kind: "malformed",
subkind: "shape",
error: `expected "demos" to be an array, got ${describeType(rawDemos)}`,
};
}
const validated: ManifestDemo[] = [];
// Duplicate-id detection: two demos with the same id cascade into
// double-counted coverage and double missing-demo-dir anomalies
// downstream. Reject at validation time so the error surfaces at
// the manifest, not at the consuming tool.
const seen = new Set<string>();
for (let i = 0; i < rawDemos.length; i++) {
const d: unknown = rawDemos[i];
if (d === null || typeof d !== "object" || Array.isArray(d)) {
return {
kind: "malformed",
subkind: "shape",
error: `expected demos[${i}] to be an object, got ${describeType(d)}`,
};
}
if (!hasOwnProp(d, "id") || typeof d.id !== "string") {
// Missing key or non-string value: describe the actual type so
// debuggers can distinguish this from the empty-string branch
// below. `describeType(undefined)` covers the missing-key case
// (the `hasOwnProp` narrowing means `d.id` is typed as unknown
// only inside the branch, but at runtime an absent key reads as
// undefined).
const actual = hasOwnProp(d, "id") ? d.id : undefined;
return {
kind: "malformed",
subkind: "shape",
error: `expected demos[${i}].id to be a string, got ${describeType(actual)}`,
};
}
const brandedId = createDemoId(d.id);
if (brandedId === null) {
// `d.id` is a string here (checked above); createDemoId only
// returns null for the empty-string case. Keep the "non-empty
// string" wording so this branch is distinct from the
// missing/non-string branch above.
return {
kind: "malformed",
subkind: "shape",
error: `expected demos[${i}].id to be a non-empty string`,
};
}
if (seen.has(brandedId)) {
return {
kind: "malformed",
subkind: "shape",
error: `duplicate demo id "${brandedId}" at demos[${i}]`,
};
}
seen.add(brandedId);
// demo-level `name` strictness: match the strictness applied to
// other fields so schema drift (present-but-wrong-type name)
// surfaces at the parser, not downstream. Absent `name` remains
// valid.
let demoName: string | undefined;
if (hasOwnProp(d, "name") && d.name !== undefined) {
if (typeof d.name !== "string") {
return {
kind: "malformed",
subkind: "shape",
error: `expected demos[${i}].name to be a string, got ${describeType(d.name)}`,
};
}
demoName = d.name;
}
// demo-level `command`: optional informational demos (e.g. cli-start).
// When set, the row surfaces this copy-pasteable command in the
// dashboard instead of a live preview. Such demos have no on-disk
// folder — parity / bundling skip them.
let demoCommand: string | undefined;
if (hasOwnProp(d, "command") && d.command !== undefined) {
if (typeof d.command !== "string" || d.command.length === 0) {
return {
kind: "malformed",
subkind: "shape",
error: `expected demos[${i}].command to be a non-empty string, got ${describeType(d.command)}`,
};
}
demoCommand = d.command;
}
// demo-level `route`: optional. When present, must be a non-empty
// string beginning with "/demos/" so downstream consumers
// (bundle-demo-content, validate-parity) can uniformly strip that
// prefix to derive the on-disk demo directory. The `/demos/` guard
// catches accidental absolute URLs or bare segments that would
// silently point to the wrong directory at runtime.
let demoRoute: string | undefined;
if (hasOwnProp(d, "route") && d.route !== undefined) {
if (typeof d.route !== "string" || d.route.length === 0) {
return {
kind: "malformed",
subkind: "shape",
error: `expected demos[${i}].route to be a non-empty string, got ${describeType(d.route)}`,
};
}
if (!d.route.startsWith("/demos/")) {
return {
kind: "malformed",
subkind: "shape",
error: `expected demos[${i}].route to start with "/demos/", got "${d.route}"`,
};
}
// Reject exactly "/demos/" (empty tail segment). Downstream
// consumers strip the "/demos/" prefix to derive an on-disk
// directory name; an empty tail would point at the parent
// demos/ directory rather than a specific demo. Guard at the
// parser boundary so validate-parity / bundle-demo-content can
// treat a successful parse as a non-empty segment invariant.
if (d.route.length <= "/demos/".length) {
return {
kind: "malformed",
subkind: "shape",
error: `expected demos[${i}].route to have a non-empty segment after "/demos/", got "${d.route}"`,
};
}
demoRoute = d.route;
}
const demoEntry: {
id: DemoId;
name?: string;
command?: string;
route?: string;
} = {
id: brandedId,
};
if (demoName !== undefined) demoEntry.name = demoName;
if (demoCommand !== undefined) demoEntry.command = demoCommand;
if (demoRoute !== undefined) demoEntry.route = demoRoute;
validated.push(Object.freeze(demoEntry));
}
demos = Object.freeze(validated);
}
// Construct the result field-by-field from the narrowed locals so
// there is no `as unknown as Manifest` double-cast crossing the
// validation boundary. Each field below was checked individually
// above; the compiler tracks the narrowed type through the local
// bindings, so this object literal typechecks against Manifest
// without any casts. The final object is frozen so the `readonly`
// fields on Manifest match the runtime behavior.
const manifest: Manifest = Object.freeze({
slug,
...(name !== undefined ? { name } : {}),
...(deployed !== undefined ? { deployed } : {}),
...(backendUrl !== undefined ? { backend_url: backendUrl } : {}),
demos,
});
return { kind: "ok", manifest };
}
+51
View File
@@ -0,0 +1,51 @@
/**
* railway-graphql.ts — Single source of truth for the Railway GraphQL
* endpoint host for TypeScript importers in this repo. The historic
* `.com` host (`backboard.railway.com`) is unauthenticated for the
* public GraphQL API and silently returns 401/403; the canonical host
* is `backboard.railway.app`.
*
* Note: the Ruby `bin/railway` script and the inline `curl` commands
* embedded in `.github/workflows/*.yml` hold their OWN copies of this
* URL because they cannot import a TypeScript module. Those copies are
* kept in sync by hand and enforced by the regression guard in
* `./__tests__/railway-graphql.scan.test.ts`, which fails the build if
* any source file under `showcase/` or `.github/workflows/` reintroduces
* the `.com` host.
*/
export const RAILWAY_GRAPHQL_ENDPOINT =
"https://backboard.railway.app/graphql/v2" as const;
// Fail-fast at module load if a hand-edit ever breaks the URL literal.
new URL(RAILWAY_GRAPHQL_ENDPOINT);
/** Default cap for sanitizeErrorBody. Multi-KB Cloudflare WAF HTML
* pages would otherwise spam stderr / $GITHUB_STEP_SUMMARY. */
export const RAILWAY_ERROR_BODY_MAX_DEFAULT = 200;
/**
* Sanitize a Railway API error body for inclusion in logs / the
* markdown summary. Railway/Cloudflare error responses can be
* multi-KB HTML pages:
*
* - strip `<` and `>` (would break markdown tables)
* - strip control chars `\n`, `\r`, `\t` (would break single-line
* log records AND newline-bearing markdown rows in redeploy-env)
* - cap at `max` chars (default 200) with an ellipsis on overflow
*
* Shared between redeploy-env.ts and verify-railway-image-refs.ts so
* both consumers strip control chars at the source.
*/
export function sanitizeErrorBody(
body: string,
max: number = RAILWAY_ERROR_BODY_MAX_DEFAULT,
): string {
// Strip angle brackets (markdown-breaking) and control chars
// (newline/carriage-return/tab — break single-line log records
// and the markdown row redeploy-env produces). Original behavior
// removed `<>` without substitution; preserve that for `<>` and
// additionally remove `\n\r\t`.
const stripped = body.replace(/[<>\n\r\t]/g, "");
if (stripped.length <= max) return stripped;
return stripped.slice(0, max) + "…";
}
+211
View File
@@ -0,0 +1,211 @@
import fs from "node:fs";
import path from "node:path";
/**
* railway-token.ts — Shared resolver for the Railway GraphQL bearer.
*
* The Railway CLI stores the public-GraphQL bearer in `user.accessToken`.
* The shorter `user.token` is a legacy CLI session token that does NOT
* authenticate to the public GraphQL API. Older configs still on disk
* have `user.token` set and `user.accessToken` empty; those callers get
* a one-cycle deprecation warning and still work.
*
* Resolution order matches the first four candidates of
* `showcase/bin/railway` `Auth.token`; the per-project
* `projects.<id>.token` fallback is intentionally not honored (no
* project-scoped tokens here):
* 1. user.accessToken
* 2. accessToken (top-level)
* 3. user.token (legacy → warn)
* 4. token (top-level) (legacy → warn)
*
* Returns undefined when no usable token is present; callers print the
* "set RAILWAY_TOKEN or run `railway login`" error.
*
* This resolver reads ONLY the parsed config object passed in and does
* NOT consult `process.env.RAILWAY_TOKEN` — the caller is responsible
* for the environment-variable lane. Any returned value is trimmed so
* stray whitespace/newlines from `~/.railway/config.json` never reach
* an `Authorization: Bearer <token>` header.
*/
export interface RailwayConfigShape {
user?: {
accessToken?: string;
token?: string;
};
accessToken?: string;
token?: string;
}
export interface ResolverDeps {
warn?: (message: string) => void;
}
const DEPRECATION_MESSAGE =
"[railway-token] WARNING: legacy Railway config field is deprecated: " +
"`user.token` / top-level `token` no longer authenticates the public " +
"GraphQL API. The Railway CLI now writes `user.accessToken`; re-run " +
"`railway login` to refresh ~/.railway/config.json. Support for the " +
"legacy field will be removed in a future release.";
function nonEmpty(v: unknown): v is string {
return typeof v === "string" && v.trim().length > 0;
}
export function resolveRailwayTokenFromConfig(
config: RailwayConfigShape | null | undefined,
deps: ResolverDeps = {},
): string | undefined {
// Defensive guard: config originates from JSON.parse of
// ~/.railway/config.json (untrusted). Reject anything that isn't a
// plain object before property access.
if (config === null || config === undefined) return undefined;
if (typeof config !== "object") return undefined;
if (Array.isArray(config)) return undefined;
const warn = deps.warn ?? ((m: string) => console.warn(m));
const userAccess = config.user?.accessToken;
if (nonEmpty(userAccess)) return userAccess.trim();
const topAccess = config.accessToken;
if (nonEmpty(topAccess)) return topAccess.trim();
const userLegacy = config.user?.token;
if (nonEmpty(userLegacy)) {
warn(DEPRECATION_MESSAGE);
return userLegacy.trim();
}
const topLegacy = config.token;
if (nonEmpty(topLegacy)) {
warn(DEPRECATION_MESSAGE);
return topLegacy.trim();
}
return undefined;
}
/**
* Failure-mode codes for resolveRailwayToken. Each is a distinct,
* actionable diagnostic so callers (and operators reading CI logs) can
* tell exactly WHY token resolution failed:
*
* NO_HOME : $HOME is unset (so ~/.railway/config.json can't
* be located) AND RAILWAY_TOKEN is also unset.
* NO_FILE : $HOME is set but ~/.railway/config.json does
* not exist (and env-var is unset).
* MALFORMED : ~/.railway/config.json exists but JSON.parse
* threw.
* NO_TOKEN_IN_CONFIG : ~/.railway/config.json exists and parses OK
* but contains no usable token at any of the
* four known layers. (Closes the silent-token-
* fallthrough diagnostic gap where the operator
* previously saw the generic "No Railway token
* found" with no hint the file was inspected.)
*/
export type RailwayTokenErrorCode =
| "NO_HOME"
| "NO_FILE"
| "MALFORMED"
| "NO_TOKEN_IN_CONFIG";
export class RailwayTokenError extends Error {
readonly code: RailwayTokenErrorCode;
constructor(code: RailwayTokenErrorCode, message: string) {
super(message);
this.name = "RailwayTokenError";
this.code = code;
}
}
export interface ResolveRailwayTokenOptions extends ResolverDeps {
/** Override $HOME lookup (testing only). */
home?: string;
/** Override env-var lookup (testing only). */
env?: NodeJS.ProcessEnv;
/** Filesystem injection (testing only). */
fs?: Pick<typeof fs, "existsSync" | "readFileSync">;
}
export interface RailwayTokenResolution {
token: string;
source: "env" | "config";
}
/**
* Unified entrypoint shared by redeploy-env.ts and
* verify-railway-image-refs.ts. Encapsulates the previously-duplicated
* getToken() envelope so the four failure modes can have distinct,
* actionable diagnostics in one place.
*
* Resolution order:
* 1. process.env.RAILWAY_TOKEN (returned with source="env")
* 2. ~/.railway/config.json via resolveRailwayTokenFromConfig
* (returned with source="config")
*
* Throws RailwayTokenError with a discriminator `.code` for each failure
* mode (NO_HOME / NO_FILE / MALFORMED / NO_TOKEN_IN_CONFIG). NEVER calls
* process.exit — the script entrypoint is responsible for mapping the
* error to a non-zero exit code so this function stays unit-testable.
*/
export function resolveRailwayToken(
opts: ResolveRailwayTokenOptions = {},
): RailwayTokenResolution {
const env = opts.env ?? process.env;
const fsImpl = opts.fs ?? fs;
// Trim the env-var lane to honor the module's no-whitespace-in-header
// invariant. A `RAILWAY_TOKEN` secret with a trailing newline (common
// from `op read`/heredoc/shell export) would otherwise be returned
// verbatim and produce an invalid `Authorization: Bearer <token>\n`
// header → silent Railway 401. A whitespace-only value is treated as
// UNSET (falls through to the config-file lane).
const envToken = env.RAILWAY_TOKEN;
if (typeof envToken === "string") {
const trimmed = envToken.trim();
if (trimmed.length > 0) {
return { token: trimmed, source: "env" };
}
}
const home = opts.home ?? env.HOME;
if (!home) {
throw new RailwayTokenError(
"NO_HOME",
"No Railway token found. RAILWAY_TOKEN is unset (or whitespace-only) and $HOME is unset so ~/.railway/config.json cannot be located.",
);
}
const configPath = path.join(home, ".railway", "config.json");
if (!fsImpl.existsSync(configPath)) {
throw new RailwayTokenError(
"NO_FILE",
"No Railway token found. Set RAILWAY_TOKEN or run `railway login`.",
);
}
let parsed: unknown;
try {
parsed = JSON.parse(fsImpl.readFileSync(configPath, "utf-8"));
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
throw new RailwayTokenError(
"MALFORMED",
`Malformed ~/.railway/config.json: ${msg}`,
);
}
const token = resolveRailwayTokenFromConfig(
parsed as RailwayConfigShape | null | undefined,
opts,
);
if (typeof token === "string" && token.length > 0) {
return { token, source: "config" };
}
throw new RailwayTokenError(
"NO_TOKEN_IN_CONFIG",
"No Railway token found: ~/.railway/config.json was found and parsed but contains no usable token (user.accessToken / accessToken / user.token / token). Set RAILWAY_TOKEN or re-run `railway login`.",
);
}
+424
View File
@@ -0,0 +1,424 @@
/**
* Shared slug / examples-directory mapping tables.
*
* Three tools consume these:
* - audit.ts (showcase slug → examples dir name[s])
* - validate-pins.ts (showcase slug → examples dir, via SLUG_MAP
* inverse + FALLBACK_MAP override)
* - validate-parity.ts (born-in-showcase membership)
*
* Everything here is immutable. We freeze the plain-object maps and
* install throwing replacement methods on the Set/Map via
* `Object.defineProperty` with `writable:false, configurable:false`
* (a frozen plain Object is not enough for Map/Set — their `.set`/`.add`
* methods don't respect Object.freeze). The consequence: any runtime
* mutation attempt throws, matching the TypeScript `Readonly*` types.
*/
/**
* Semantic alias: a slug that names a `showcase/integrations/<slug>/`
* directory. Structurally a plain string (so external callers can
* compare against arbitrary strings without ceremony) but named
* distinctly from `ExamplesDir` so the map signatures below document
* their direction of mapping. The runtime invariant — every
* ShowcaseSlug that appears in SLUG_MAP (as a value), SLUG_TO_EXAMPLES
* (as a key), or FALLBACK_MAP (as a key) is an actual directory under
* `showcase/integrations/` — is enforced by slug-map.test.ts, not by the
* type system.
*
* We deliberately keep this as a type alias (not a branded type) to
* avoid forcing `as` casts on every external caller that builds a
* slug from a `path.basename` or `fs.readdirSync` result. The safety
* this trades away is recovered by `isShowcaseSlug` — a runtime
* validator applied at API boundaries (see `ENTRIES` construction
* below and the BORN_IN_SHOWCASE / SLUG_MAP assertions).
*/
export type ShowcaseSlug = string;
/**
* Semantic alias: a directory name under `examples/integrations/` (or
* `integrations/` in older trees). Intentionally distinct at the type
* level from ShowcaseSlug so the SLUG_MAP / SLUG_TO_EXAMPLES /
* FALLBACK_MAP signatures read unambiguously. Structurally `string`
* for the same reason as ShowcaseSlug: external callers iterate these
* maps with plain strings and we don't want to force `as` casts on
* every caller.
*/
export type ExamplesDir = string;
/**
* Runtime validator for a ShowcaseSlug. Applied at API boundaries
* (SLUG entries construction, BORN_IN_SHOWCASE membership, callers
* that accept user-supplied slug strings) to catch obvious garbage —
* empty strings, whitespace, path separators — before it flows into
* a filesystem path or a slug-indexed Map.
*
* The parameter type is `unknown` — this function sits at an API
* boundary where TS guarantees are weakest (yaml.parse results, JSON
* roundtrips, user-supplied strings). A widened param keeps the typeof
* guard live rather than reducing to a dead check under `(s: string)`.
*
* The return signature is a user-defined type predicate (`s is
* ShowcaseSlug`), so callers can narrow an `unknown` or `string` local
* to a `ShowcaseSlug` without a cast after a successful check.
*/
export function isShowcaseSlug(s: unknown): s is ShowcaseSlug {
if (typeof s !== "string") return false;
if (s.length === 0) return false;
// Reject whitespace and path separators: these are the characters
// that would break `path.join(packages, slug)` most surprisingly
// (newlines, spaces, `/`, `\`). We intentionally don't enforce a
// strict kebab-case pattern — existing slugs include dots and
// uppercase in related repos, so a strict regex would over-constrain.
if (/[\s/\\]/.test(s)) return false;
return true;
}
/**
* Wrap a Set so mutation methods throw. Object.freeze on a Set does
* not prevent .add/.delete — the set itself is frozen but its internal
* slots are not. Casting to ReadonlySet is compile-time only.
*
* The returned type is ReadonlySet<T> (not Set<T>): callers that keep
* a Set handle would bypass the runtime guard, so we force the narrow
* type out of the helper.
*
* The replacement methods are installed with `writable: false` and
* `configurable: false` so they cannot themselves be re-replaced
* (`Object.defineProperty(set, "add", {value: realAdd})`) to restore
* mutation. Without those descriptors, a later caller could silently
* circumvent the freeze.
*/
function freezeSet<T>(s: Set<T>): ReadonlySet<T> {
// Replace mutation methods FIRST, THEN freeze — Object.freeze locks
// the object non-extensible, after which defineProperty throws.
// `add` is typed to return the set so we match that signature but
// throw before anything can observe the return value.
const fail = (method: string) => () => {
throw new TypeError(`Cannot ${method} frozen Set`);
};
const lock = { writable: false, configurable: false, enumerable: false };
Object.defineProperty(s, "add", { ...lock, value: fail("add") });
Object.defineProperty(s, "delete", { ...lock, value: fail("delete") });
Object.defineProperty(s, "clear", { ...lock, value: fail("clear") });
return Object.freeze(s);
}
function freezeMap<K, V>(m: Map<K, V>): ReadonlyMap<K, V> {
const fail = (method: string) => () => {
throw new TypeError(`Cannot ${method} frozen Map`);
};
const lock = { writable: false, configurable: false, enumerable: false };
Object.defineProperty(m, "set", { ...lock, value: fail("set") });
Object.defineProperty(m, "delete", { ...lock, value: fail("delete") });
Object.defineProperty(m, "clear", { ...lock, value: fail("clear") });
return Object.freeze(m);
}
/**
* Freeze a 2D record (outer object + inner arrays) in one call. The
* outer record is frozen so keys cannot be added/removed/reassigned;
* each inner array is also frozen so element assignment (`arr[0] = …`)
* throws in strict mode. Prevents the common "I froze the outer but
* forgot the inner" bug and ensures the Readonly<...> type matches the
* runtime behavior.
*/
function freezeMap2D<K extends string, V>(
obj: Record<K, readonly V[]>,
): Readonly<Record<K, readonly V[]>> {
for (const k of Object.keys(obj) as K[]) {
Object.freeze(obj[k]);
}
return Object.freeze(obj);
}
/**
* Single source of truth for the showcase/examples mapping tables.
* `SLUG_TO_EXAMPLES`, `FALLBACK_MAP`, and `BORN_IN_SHOWCASE` below are
* derived from this array via reducers. Adding / removing / renaming
* a slug happens in ONE place; before, three parallel maps had to be
* edited in lockstep and silently fell out of sync.
*
* Entry shape:
* - slug — the `showcase/integrations/<slug>/` directory name
* - bornInShowcase — true iff the package has no examples/integrations
* counterpart (skip instead of warn downstream).
* When true, `examples` MUST be empty.
* - examples — candidate dir names under `examples/integrations/`
* (or `integrations/` in older trees). The FIRST
* entry is treated as the preferred fallback.
* - fallback — if true, expose an entry in FALLBACK_MAP that
* points at `examples[0]`. FALLBACK_MAP documents
* known SLUG_MAP staleness where the slug under
* `showcase/integrations/` no longer matches SLUG_MAP's
* examples→slug direction.
*
* `SLUG_MAP` (examples → slug) is NOT derived — it reflects the
* historical migrate-integration-examples.ts intent and is kept as a
* standalone declaration so the "known stale" documentation at its
* call sites in validate-pins.ts continues to hold.
*/
/**
* Discriminated union form of SlugEntry.
*
* Three mutually-exclusive shapes:
* - born-in-showcase: no examples counterpart, no fallback. The
* `examples` tuple is statically empty and
* `fallback` is never set.
* - examples (no fallback): a real examples dir exists but the
* SLUG_MAP forward mapping is not stale.
* The `examples` tuple is non-empty and
* `fallback` is explicitly false.
* - examples + fallback: SLUG_MAP is stale for this slug;
* FALLBACK_MAP documents the correction.
* The `examples` tuple is non-empty and
* `fallback` is true; FALLBACK_MAP keys
* off this variant.
*
* Modeled as a discriminated union so consumers can read
* `e.examples[0]` on the fallback branch without a runtime length
* assertion — the tuple type guarantees at least one element, and the
* compiler narrows accordingly. Prior to this split, the flat
* `examples: readonly ExamplesDir[]` forced a runtime check at the
* FALLBACK_MAP reducer to rule out an empty tuple.
*/
type SlugEntry =
| {
readonly slug: ShowcaseSlug;
readonly bornInShowcase: true;
readonly examples: readonly [];
readonly fallback: false;
}
| {
readonly slug: ShowcaseSlug;
readonly bornInShowcase: false;
readonly examples: readonly [ExamplesDir, ...ExamplesDir[]];
readonly fallback: boolean;
};
const ENTRIES: readonly SlugEntry[] = [
// Born-in-showcase packages have no examples/integrations counterpart.
{ slug: "ag2", bornInShowcase: true, examples: [], fallback: false },
{
slug: "claude-sdk-python",
bornInShowcase: true,
examples: [],
fallback: false,
},
{
slug: "claude-sdk-typescript",
bornInShowcase: true,
examples: [],
fallback: false,
},
{ slug: "langroid", bornInShowcase: true, examples: [], fallback: false },
{
slug: "ms-agent-harness-dotnet",
bornInShowcase: true,
examples: [],
fallback: false,
},
{ slug: "spring-ai", bornInShowcase: true, examples: [], fallback: false },
{
slug: "strands-typescript",
bornInShowcase: true,
examples: [],
fallback: false,
},
// Packages with a straightforward examples/integrations counterpart
// whose dir name matches SLUG_MAP's examples→slug direction.
{
slug: "langgraph-python",
bornInShowcase: false,
examples: ["langgraph-python"],
fallback: false,
},
{
slug: "langgraph-typescript",
bornInShowcase: false,
examples: ["langgraph-js"],
fallback: false,
},
{
slug: "langgraph-fastapi",
bornInShowcase: false,
examples: ["langgraph-fastapi"],
fallback: false,
},
{
slug: "mastra",
bornInShowcase: false,
examples: ["mastra"],
fallback: false,
},
{
slug: "agno",
bornInShowcase: false,
examples: ["agno"],
fallback: false,
},
{
slug: "llamaindex",
bornInShowcase: false,
examples: ["llamaindex"],
fallback: false,
},
{
slug: "google-adk",
bornInShowcase: false,
examples: ["adk"],
fallback: false,
},
// Packages whose showcase slug no longer matches SLUG_MAP's
// examples→slug direction — these need FALLBACK_MAP entries so
// validate-pins.ts can still resolve them.
{
slug: "crewai-crews",
bornInShowcase: false,
examples: ["crewai-crews"],
fallback: true,
},
{
slug: "pydantic-ai",
bornInShowcase: false,
examples: ["pydantic-ai"],
fallback: true,
},
{
slug: "ms-agent-dotnet",
bornInShowcase: false,
examples: ["ms-agent-framework-dotnet"],
fallback: true,
},
{
slug: "ms-agent-python",
bornInShowcase: false,
examples: ["ms-agent-framework-python"],
fallback: true,
},
{
slug: "strands",
bornInShowcase: false,
examples: ["strands-python"],
fallback: true,
},
];
// API-boundary validation: every slug that enters the derived maps
// must pass `isShowcaseSlug`. The loop runs at module load so a bad
// entry trips construction immediately rather than on first use.
// The "bornInShowcase implies empty examples" and "fallback implies
// non-empty examples" invariants are enforced statically by the
// SlugEntry discriminated union above and no longer need a runtime
// check here.
for (const e of ENTRIES) {
if (!isShowcaseSlug(e.slug)) {
throw new Error(
`lib/slug-map: invalid ShowcaseSlug in ENTRIES: ${JSON.stringify(e.slug)}`,
);
}
}
/**
* Packages intentionally without a Dojo (examples/integrations)
* counterpart. They are the single source of truth for:
* - audit.ts → skip the "missing examples/integrations counterpart"
* anomaly;
* - validate-pins.ts → emit [SKIP] instead of [WARN].
*
* Derived from ENTRIES by filtering `bornInShowcase === true`.
*/
export const BORN_IN_SHOWCASE: ReadonlySet<ShowcaseSlug> = freezeSet(
new Set<ShowcaseSlug>(
ENTRIES.filter((e) => e.bornInShowcase).map((e) => e.slug),
),
);
/**
* Forward map: examples/integrations directory name → showcase slug.
* Mirrors migrate-integration-examples.ts (which does not export its
* SLUG_MAP). Kept as a Map for O(1) lookup and so `.get()` returns
* `ShowcaseSlug | undefined` unambiguously.
*
* This map is NOT derived from ENTRIES: it represents the historical
* migration intent at the time showcase was split from
* examples/integrations, and validate-pins.ts's comments explicitly
* call out that it is "known stale". FALLBACK_MAP (derived from
* ENTRIES) documents the corrections where SLUG_MAP no longer matches
* the current slug under `showcase/integrations/`.
*
* Only entries whose VALUES correspond to real `showcase/integrations/<slug>/`
* directories are included. Dead entries (crewai-flows → crewai,
* pydantic-ai → pydanticai, ms-agent-framework-dotnet → maf-dotnet,
* etc.) were removed because they broke validate-pins.ts's reverse
* lookup and forced FALLBACK_MAP to re-express the corrections. The
* slug-map.test.ts pins this invariant against the real integrations/ tree
* so future edits cannot reintroduce the drift.
*/
export const SLUG_MAP: ReadonlyMap<ExamplesDir, ShowcaseSlug> = freezeMap(
new Map<ExamplesDir, ShowcaseSlug>([
["langgraph-python", "langgraph-python"],
["langgraph-js", "langgraph-typescript"],
["langgraph-fastapi", "langgraph-fastapi"],
["mastra", "mastra"],
["agno", "agno"],
["llamaindex", "llamaindex"],
["adk", "google-adk"],
]),
);
// Construction-time assertion: every SLUG_MAP value is a valid slug.
for (const [, slug] of SLUG_MAP) {
if (!isShowcaseSlug(slug)) {
throw new Error(
`lib/slug-map: invalid SLUG_MAP value: ${JSON.stringify(slug)}`,
);
}
}
/**
* Reverse / corrected map used by audit.ts: showcase slug → candidate
* examples/integrations dir name(s). Dead entries that pointed at
* non-existent showcase packages (crewai-flows, agent-spec-langgraph,
* mcp-apps) are intentionally excluded so audit.ts no longer emits
* phantom "no examples source" anomalies for them.
*
* Derived from ENTRIES: each entry with non-empty `examples` becomes
* `SLUG_TO_EXAMPLES[slug] = examples`. freezeMap2D freezes BOTH the
* outer record (no adding/removing keys, no reassigning arrays) AND
* each inner array (no element assignment). The compile-time
* `Readonly<Record<..., readonly ExamplesDir[]>>` matches.
*/
export const SLUG_TO_EXAMPLES: Readonly<
Record<ShowcaseSlug, readonly ExamplesDir[]>
> = freezeMap2D<ShowcaseSlug, ExamplesDir>(
ENTRIES.reduce<Record<ShowcaseSlug, readonly ExamplesDir[]>>((acc, e) => {
if (e.examples.length > 0) {
acc[e.slug] = e.examples;
}
return acc;
}, {}),
);
/**
* Fallback map used by validate-pins.ts: showcase slug → examples dir
* name. These entries document known SLUG_MAP staleness — the slug
* under `showcase/integrations/` no longer matches the value in SLUG_MAP,
* so we override here. If SLUG_MAP is refreshed, clean up the
* `fallback: true` flag on the corresponding ENTRIES row and this
* map rebuilds to match.
*
* Derived from ENTRIES: each entry with `fallback: true` exposes
* `FALLBACK_MAP[slug] = examples[0]`.
*/
export const FALLBACK_MAP: Readonly<Record<ShowcaseSlug, ExamplesDir>> =
Object.freeze(
ENTRIES.reduce<Record<ShowcaseSlug, ExamplesDir>>((acc, e) => {
if (e.fallback) {
acc[e.slug] = e.examples[0];
}
return acc;
}, {}),
);