Merge branch 'main' into feature/techlog-ui-migration

Integrates the frontend template sync (a0fbafb → 5434760) into the TechLog UI
migration. Merged in this direction so every conflict is resolved and proved in
the worktree; main is only fast-forwarded afterwards and never holds a state
that was not verified here.

15 conflicts. The rule throughout: keep the template's mechanism, keep the
product's content, and never invent a third state neither branch would accept.

The template's product manifest and its runtime feature kill switch are adopted.
The route registry is deliberately not composed from contract.routes: the
reference feature still declares screens this product deleted, and reducing over
them would register paths with no component behind them. ROUTE_FEATURE_OWNER is
narrowed to registered routes for the same reason. The first resolution did
compose from contract.routes and was rejected by product-features.test.ts.

Three files pinned counts and a digest describing the gate contract. Neither
side's numbers describe the merged config/ci/gates.json, so they were recomputed
from it rather than chosen: 27 gates, 82 commands, 94 command references, 107
evidence references, 128 artifacts, shape sha256 5063586d.

README.md and docs/accessibility/manual-checklist.md now enumerate this
product's 27 routes, which the template's own verify:documentation requires.

product-feature-switch.test.tsx was rewritten around the invariant that still
applies here — no registered route without a component — rather than deleted
with the screens it used to exercise.

docs/operations/template-merge-2026-08-17.md records every decision, the gate
results, and the three follow-ups this merge deliberately did not decide.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-08-17 18:38:08 +09:00
co-authored by Claude Opus 5
100 changed files with 3310 additions and 443 deletions
@@ -129,6 +129,9 @@ jobs:
outputs:
dist_sha256: \${{ steps.candidate.outputs.dist_sha256 }}
archive_sha256: \${{ steps.candidate.outputs.archive_sha256 }}
env:
APP_PROFILE: "\${{ vars.APP_PROFILE }}"
RELEASE_TARGET: "\${{ vars.RELEASE_TARGET }}"
steps:
- uses: https://github.com/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
+2 -1
View File
@@ -5,7 +5,7 @@ import {
type ApplicationOutputPorts,
} from "../../src/application/create-application.ts";
import { createTestApplication } from "../helpers/create-test-application.ts";
import { createRuntimeCapabilitiesStub } from "../helpers/runtime-capabilities-stub.ts";
import { createProductFeaturesStub, createRuntimeCapabilitiesStub } from "../helpers/runtime-capabilities-stub.ts";
declare module "../../src/application/ports/in/application-api.ts" {
interface ApplicationFeatureInputs {
@@ -99,6 +99,7 @@ describe("application input/output boundary", () => {
}),
},
runtimeCapabilities: createRuntimeCapabilitiesStub(),
productFeatures: createProductFeaturesStub(),
navigation: { reload: () => {} },
} satisfies ApplicationOutputPorts;
const application = createApplication(ports);
+2 -1
View File
@@ -5,7 +5,7 @@ import { createAnonymousSessionAdapter } from "../../src/adapters/auth/external-
import type { StoragePort } from "../../src/application/ports/storage-port.ts";
import type { DiagnosticsPort } from "../../src/application/ports/diagnostics-port.ts";
import type { TelemetryPort } from "../../src/application/ports/telemetry-port.ts";
import { createRuntimeCapabilitiesStub } from "../helpers/runtime-capabilities-stub.ts";
import { createProductFeaturesStub, createRuntimeCapabilitiesStub } from "../helpers/runtime-capabilities-stub.ts";
type ReleaseFixture = {
buildId: string;
@@ -53,6 +53,7 @@ function applicationWith(options: {
diagnostics: options.diagnostics ?? { record: () => {} },
telemetry: options.telemetry ?? { emit: () => {} },
runtimeCapabilities: createRuntimeCapabilitiesStub(),
productFeatures: createProductFeaturesStub(),
releaseInfo: {
getCurrent: async () => current,
refresh:
+181 -38
View File
@@ -1,6 +1,6 @@
import { spawn, spawnSync, type ChildProcess } from "node:child_process";
import { createHash, generateKeyPairSync, sign } from "node:crypto";
import { constants } from "node:fs";
import { constants, readFileSync } from "node:fs";
import { cp, link, lstat, mkdir, mkdtemp, open, readFile, readdir, rename, rm, stat, symlink, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
@@ -20,6 +20,7 @@ import {
} from "../../scripts/lib/ci-artifact-validator.ts";
import { writeCiGateLogAtomic } from "../../scripts/lib/ci-gate-log.ts";
import { linkFixtureNodeModules } from "../../scripts/lib/fixture-node-modules.ts";
import { copyReleaseEvidenceTree } from "../../scripts/lib/removal-fixture.ts";
import { captureCiCandidateArchive, verifyCiCandidateArchive } from "../../scripts/lib/ci-candidate-archive.ts";
import {
CANDIDATE_ARCHIVE_USAGE,
@@ -46,6 +47,14 @@ import {
RELEASE_CANDIDATE_MANIFEST_PATH,
} from "../../scripts/lib/release-candidate.ts";
/**
* Budget for the provider suites specifically. They spawn a systemd scope, a
* bubblewrap sandbox and a signing provider, and build a release candidate to
* do it; the 10s default is sized for pure-JS unit tests. Raising the global
* default instead would hide a genuinely hung test.
*/
const PROCESS_HEAVY_TIMEOUT_MS = 30_000;
const temporaryRoots: string[] = [];
let providerBaseRoot: string | undefined;
const sha256 = (value: Buffer | string) =>
@@ -756,6 +765,10 @@ describe("candidate archive and provider upload boundaries", () => {
},
});
const completionStarted = Date.now();
// Start recording before anything is asserted: the provider's whole life is
// shorter than one `systemctl show`, so the tree has to be sampled, not
// sampled once at whatever moment the assertions happen to arrive.
const tree = recordProviderProcessTree(execution.child.pid!);
const unit = await waitForProviderUnit("vulnerability", execution.child.pid);
const properties = showProviderUnit(unit);
expect(properties).toMatchObject({
@@ -775,10 +788,21 @@ describe("candidate archive and provider upload boundaries", () => {
await expect(readFile(path.join(cgroupRoot, "cpu.max"), "utf8")).resolves.toBe("100000 100000\n");
expect(readProviderUnitMetadata(unit)).not.toMatch(new RegExp(`${command}|${credential}`, "u"));
expect(showProcessArguments(execution.child.pid)).not.toMatch(new RegExp(`${command}|${credential}`, "u"));
const cgroupPids = await readCgroupPids(cgroupRoot);
const processArguments = cgroupPids.map((pid) => showProcessArguments(pid));
const directChildPids = await waitForDirectProviderChildren(execution.child.pid!);
const directChildArguments = directChildPids.map((pid) => showProcessArguments(pid));
const reportIdentity = await lstat(fixture.reportPath);
const result = await execution.completion;
tree.stop();
expect(result.code, result.stderr).toBe(0);
expect(result.stdout).not.toContain(credential);
expect(result.stderr).not.toContain(credential);
// Everything below reads the recording of the whole run rather than a live
// snapshot. The sandbox exists for a few hundred milliseconds; asserting
// while it runs meant racing it, and the assertions are about what the run
// contained, not about what a particular instant looked like.
const cgroupPids = tree.cgroupPids();
const processArguments = tree.cgroupArguments();
const directChildPids = tree.directChildPids();
const directChildArguments = tree.directChildArguments();
const observedArguments = [
showProcessArguments(execution.child.pid),
...directChildArguments,
@@ -788,18 +812,15 @@ describe("candidate archive and provider upload boundaries", () => {
expect(directChildArguments.filter((arguments_) => arguments_.includes("/usr/bin/systemd-run"))).toHaveLength(1);
expect(directChildArguments.filter((arguments_) => arguments_.includes("provider-raw-guardian.ts"))).toHaveLength(1);
expect(processArguments.filter((arguments_) => arguments_.includes("provider-scope-wrapper.ts"))).toHaveLength(1);
expect(processArguments.filter((arguments_) => /\/usr\/bin\/(?:bwrap|prlimit)/u.test(arguments_)).length)
.toBeGreaterThan(0);
expect(
processArguments.filter((arguments_) => /\/usr\/bin\/(?:bwrap|prlimit)/u.test(arguments_)).length,
`sandbox never observed in the scope; recorded: ${processArguments.join(" | ")}`,
).toBeGreaterThan(0);
const infrastructureArguments = [...directChildArguments, ...processArguments].filter((arguments_) =>
/provider-(?:scope-wrapper|raw-guardian)|systemd-run|bwrap|prlimit/u.test(arguments_),
);
expect(infrastructureArguments.join("\n")).not.toContain(command);
expect(processArguments.filter((arguments_) => arguments_.includes(providerScript))).toHaveLength(1);
const reportIdentity = await lstat(fixture.reportPath);
const result = await execution.completion;
expect(result.code, result.stderr).toBe(0);
expect(result.stdout).not.toContain(credential);
expect(result.stderr).not.toContain(credential);
expect(Date.now() - completionStarted).toBeLessThan(4_000);
await expectProviderUnitGone(unit);
await expect(lstat(cgroupRoot)).rejects.toMatchObject({ code: "ENOENT" });
@@ -807,7 +828,7 @@ describe("candidate archive and provider upload boundaries", () => {
expect(directChildPids.every((pid) => !processExists(pid))).toBe(true);
await expect(lstat(fixture.reportPath)).rejects.toMatchObject({ code: "ENOENT" });
await expect(findProviderRawReferences(fixture.reportPath, reportIdentity)).resolves.toEqual([]);
}, 10_000);
}, PROCESS_HEAVY_TIMEOUT_MS);
it("kills and collects an active provider when its guardian dies", async () => {
const fixture = await createProviderFixture();
@@ -842,12 +863,15 @@ describe("candidate archive and provider upload boundaries", () => {
expect((await readdir(path.dirname(sealedPath))).filter((leaf) => leaf.includes(".guardian-")))
.toEqual([]);
// `providerWriter` is only a path; the retry has to materialise the script
// it names or the clean-retry claim is proven by a module-not-found error.
await writeFile(fixture.providerWriter, providerV2WriterSource());
const retried = runProviderSupervisor(fixture, {
command: `node ${JSON.stringify(fixture.providerWriter)}`,
sealedPath,
});
expect(retried.status, retried.stderr).toBe(0);
}, 15_000);
}, 20_000);
it("collects the whole provider scope when its supervisor dies", async () => {
const fixture = await createProviderFixture();
@@ -1272,9 +1296,17 @@ describe("verified promotion finalizer", () => {
"provider-verification.json": valid["promotion-verification.json"],
"promotion-verification.json": valid["provider-verification.json"],
};
// `{}` never reaches the digest comparison: it fails the report schema
// first, so this case asserted a decode error while claiming to cover the
// digest branch. The substitution has to be a structurally valid report
// that simply is not the one the verification records committed to.
const divergentReport = JSON.parse(
valid["vulnerability-report.json"].toString("utf8"),
) as Record<string, any>;
divergentReport.provider = "divergent-provider";
const reportMismatch = {
...valid,
"vulnerability-report.json": Buffer.from("{}\n"),
"vulnerability-report.json": jsonBytes(divergentReport),
};
const absent = { ...valid } as Partial<typeof valid>;
delete absent["provider-verification.json"];
@@ -1513,7 +1545,10 @@ describe("verified promotion finalizer", () => {
}),
).rejects.toThrow(/leaf.*identity|staging leaf/u);
await expect(readFile(canary, "utf8")).resolves.toBe("unchanged\n");
await expect(readdir(saved)).resolves.toEqual([]);
// A substituted leaf aborts the cleanup before anything is removed, so the
// promotion this call owned is still intact and a retry sees a coherent
// directory rather than a half-emptied one.
expect((await readdir(saved)).sort()).toEqual([...PROMOTED_FILE_NAMES].sort());
}, 30_000);
it("never deletes an unrelated leaf substituted after cleanup validation", async () => {
@@ -1678,28 +1713,31 @@ async function readCgroupPids(cgroupRoot: string): Promise<number[]> {
.trim().split("\n").filter(Boolean).map(Number);
}
async function waitForDirectProviderChildren(supervisorPid: number): Promise<number[]> {
for (let attempt = 0; attempt < 120; attempt += 1) {
const childrenPath = `/proc/${supervisorPid}/task/${supervisorPid}/children`;
const children = (await readFile(childrenPath, "utf8"))
.trim().split(/\s+/u).filter(Boolean).map(Number);
const arguments_ = children.flatMap((pid) => {
try {
return [showProcessArguments(pid)];
} catch (error) {
if (!processExists(pid)) return [];
throw error;
}
});
async function waitForRecordedProviderTree(
tree: ReturnType<typeof recordProviderProcessTree>,
providerScript: string,
): Promise<void> {
for (let attempt = 0; attempt < 400; attempt += 1) {
const children = tree.directChildArguments();
const members = tree.cgroupArguments();
// Every process the assertions below reason about. Returning as soon as
// some of them are present is what left bubblewrap out of the recording:
// it enters the scope a few milliseconds after the wrapper does.
if (
arguments_.some((value) => value.includes("/usr/bin/systemd-run")) &&
arguments_.some((value) => value.includes("provider-raw-guardian.ts"))
children.some((value) => value.includes("/usr/bin/systemd-run")) &&
children.some((value) => value.includes("provider-raw-guardian.ts")) &&
members.some((value) => value.includes("provider-scope-wrapper.ts")) &&
members.some((value) => /\/usr\/bin\/(?:bwrap|prlimit)/u.test(value)) &&
members.some((value) => value.includes(providerScript))
) {
return children;
return;
}
await delay(25);
await delay(10);
}
throw new Error("provider supervisor children were not simultaneously observable");
throw new Error(
"the provider run never contained a systemd-run child, a guardian child, " +
"a scope wrapper, a sandbox and the provider itself in its cgroup",
);
}
async function waitForDirectChildMatching(supervisorPid: number, pattern: string): Promise<number> {
@@ -1844,6 +1882,8 @@ function readProviderUnitMetadata(unitName: string): string {
function showProcessArguments(pid: number | undefined): string {
if (!pid) throw new Error("provider supervisor did not expose its PID");
const argv = readProcessArguments(pid);
if (argv !== null) return argv;
const result = spawnSync(
"/usr/bin/ps",
["-o", "args=", "-p", String(pid)],
@@ -1854,6 +1894,102 @@ function showProcessArguments(pid: number | undefined): string {
return result.stdout.trim();
}
/**
* Reads a process's argv straight from `/proc`.
*
* Spawning `ps` per pid costs milliseconds each, and the provider sandbox now
* completes a whole run in well under a second — the observation was losing a
* race against the thing it was observing. Returns null for a process that is
* already gone, which the sampler treats as "nothing more to record".
*/
function readProcessArguments(pid: number): string | null {
try {
return readFileSync(`/proc/${pid}/cmdline`, "utf8")
.split("\0")
.filter(Boolean)
.join(" ")
.trim();
} catch {
return null;
}
}
/**
* Records the provider's process tree for the whole life of the run.
*
* The assertions below are about what the sandbox looked like while it was
* running, and a single snapshot taken afterwards can only ever be a guess at
* that. Sampling from the moment the supervisor starts turns "did we look at
* the right instant?" into "what did this run actually contain?".
*/
function recordProviderProcessTree(supervisorPid: number) {
const directChildren = new Map<number, string>();
const cgroupMembers = new Map<number, string>();
let stopped = false;
const remember = (into: Map<number, string>, pid: number): void => {
if (into.has(pid)) return;
const argv = readProcessArguments(pid);
if (argv !== null && argv.length > 0) into.set(pid, argv);
};
const childrenOf = (pid: number): number[] => {
try {
return readFileSync(`/proc/${pid}/task/${pid}/children`, "utf8")
.trim()
.split(/\s+/u)
.filter(Boolean)
.map(Number);
} catch {
return [];
}
};
/**
* Scope membership is read from the process itself rather than from
* `systemctl show`. Waiting for the unit to be described before watching its
* cgroup meant bubblewrap had usually already exited by the time the first
* sample was taken — the recording missed exactly the process the assertions
* are about.
*/
const inProviderScope = (pid: number): boolean => {
try {
return readFileSync(`/proc/${pid}/cgroup`, "utf8").includes("ca-provider-");
} catch {
return false;
}
};
const sample = (): void => {
if (stopped) return;
const direct = childrenOf(supervisorPid);
for (const pid of direct) remember(directChildren, pid);
// Walk the whole subtree: the scope wrapper, bubblewrap and the provider
// itself sit below systemd-run, not beside it.
const pending = [...direct];
const seen = new Set(direct);
while (pending.length > 0 && seen.size < 512) {
const pid = pending.pop()!;
if (inProviderScope(pid)) remember(cgroupMembers, pid);
for (const child of childrenOf(pid)) {
if (seen.has(child)) continue;
seen.add(child);
pending.push(child);
}
}
};
const timer = setInterval(sample, 5);
timer.unref();
sample();
return Object.freeze({
stop() {
stopped = true;
clearInterval(timer);
},
directChildPids: () => [...directChildren.keys()],
directChildArguments: () => [...directChildren.values()],
cgroupPids: () => [...cgroupMembers.keys()],
cgroupArguments: () => [...cgroupMembers.values()],
});
}
async function startLoopbackCanary(root: string): Promise<Readonly<{
child: ChildProcess;
marker: string;
@@ -2128,10 +2264,17 @@ async function ensureProviderBaseFixture(): Promise<string> {
return ![".release", "artifacts", "dist", "node_modules"].includes(first ?? "");
},
});
await cp(path.join(sourceRoot, "artifacts"), path.join(root, "artifacts"), {
recursive: true,
});
await rm(path.join(root, "artifacts/release"), { recursive: true, force: true });
// The release evidence, minus the trace and Storybook trees. Copying the
// whole `artifacts/` directory pulled ~28MB of test output into every
// provider fixture; sharing the copier with the removal fixture is also what
// makes both fixtures contain the same evidence.
// `artifacts/release` travels with the fixture rather than being deleted and
// rebuilt. Supply-chain generation runs before the candidate is created and
// validates the release evidence paths, so deleting them made that step
// depend on a previous local run having left them behind — which is why this
// fixture only worked in a workspace that had already built a candidate. The
// build chain overwrites them anyway.
await copyReleaseEvidenceTree(sourceRoot, root);
await linkFixtureNodeModules(root, sourceRoot);
const git = spawnSync("git", ["show", "-s", "--format=%H%n%ct", "HEAD"], {
cwd: sourceRoot,
+7 -5
View File
@@ -184,16 +184,18 @@ describe("CI gate contract", () => {
const index = indexCiGateContract(contract);
expect(contract.schemaVersion).toBe(2);
expect(contract.gates.map(({ id }) => id)).toEqual(
Array.from({ length: 26 }, (_, index) =>
Array.from({ length: 27 }, (_, index) =>
`FE-GATE-${String(index + 1).padStart(3, "0")}`,
),
);
expect(contract.jobs).toHaveLength(9);
expect(contract.commands).toHaveLength(81);
expect(contract.gates.reduce((total, gate) => total + gate.commandIds.length, 0)).toBe(93);
expect(contract.commands).toHaveLength(82);
expect(contract.gates.reduce((total, gate) => total + gate.commandIds.length, 0)).toBe(94);
expect(contract.commands.filter(({ expect }) => expect === "fail")).toHaveLength(23);
expect(contract.gates.reduce((total, gate) => total + gate.evidenceArtifactIds.length, 0)).toBe(106);
expect(contract.artifacts).toHaveLength(126);
// Template merge. Recounted from the merged config/ci/gates.json rather
// than taking either side's number.
expect(contract.gates.reduce((total, gate) => total + gate.evidenceArtifactIds.length, 0)).toBe(107);
expect(contract.artifacts).toHaveLength(128);
expect(contract.stages).toHaveLength(5);
expect(contract.retention.classes).toHaveLength(5);
expect(index.gates.get("FE-GATE-015")?.commandIds).toHaveLength(2);
+166
View File
@@ -0,0 +1,166 @@
import { readFile, readdir } from "node:fs/promises";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
DEPLOYMENT_TARGETS,
findAdmissionViolations,
isDeploymentTarget,
type AdmissionInput,
} from "../../src/contracts/deployment-admission.ts";
import { runtimeConfigV2ArtifactSchema } from "../../src/contracts/release-artifacts.ts";
import { generateRuntimeConfig } from "../../scripts/generate-runtime-config.ts";
const PRODUCTION_ARTIFACT: AdmissionInput = Object.freeze({
APP_ENV: "production",
API_BASE_URL: "https://api.example.com/",
REQUEST_TIMEOUT_MS: 10_000,
MAX_RETRY_ATTEMPTS: 2,
TELEMETRY_ENABLED: false,
AUTH_MODE: "external",
CONFIG_SCHEMA_VERSION: "2.0",
RELEASE_MANIFEST_URL: "/release-manifest.json",
BUILD_ID: "20260815.42",
RELEASE_ID: "r-2026.08.15-1",
CAPABILITY_OVERRIDES: Object.freeze({
REALTIME: "DEFAULT",
WEB_WORKER: "DEFAULT",
SERVICE_WORKER: "DEFAULT",
OFFLINE_COMMANDS: "DEFAULT",
}),
}) as AdmissionInput;
const LOCAL_ARTIFACT: AdmissionInput = Object.freeze({
...PRODUCTION_ARTIFACT,
APP_ENV: "local",
API_BASE_URL: "http://localhost:8080/",
AUTH_MODE: "demo",
BUILD_ID: "local-build",
RELEASE_ID: "local-release",
}) as AdmissionInput;
describe("deployment admission", () => {
it("admits an artifact only to the environment it declares", () => {
expect(findAdmissionViolations("production", PRODUCTION_ARTIFACT)).toEqual([]);
expect(findAdmissionViolations("local", LOCAL_ARTIFACT)).toEqual([]);
});
it("refuses the exact local build that release coherence used to approve", () => {
// The review's strongest reproduction: FE-GATE-015 passed on a build whose
// runtime document was APP_ENV=local / AUTH_MODE=demo / loopback API. Each
// of those is now an independent refusal, so fixing one does not admit it.
const violations = findAdmissionViolations("production", LOCAL_ARTIFACT);
const fields = violations.map((violation) => violation.field);
expect(fields).toContain("APP_ENV");
expect(fields).toContain("AUTH_MODE");
expect(fields).toContain("API_BASE_URL");
expect(fields).toContain("BUILD_ID");
expect(fields).toContain("RELEASE_ID");
});
it("refuses endpoints a browser on the public internet cannot reach", () => {
for (const host of [
"http://api.example.com/",
"https://localhost/",
"https://127.0.0.1/",
"https://10.0.0.5/",
"https://192.168.1.10/",
"https://172.16.4.4/",
"https://169.254.169.254/",
"https://[::1]/",
]) {
const violations = findAdmissionViolations("production", {
...PRODUCTION_ARTIFACT,
API_BASE_URL: host,
} as AdmissionInput);
expect(violations.map((violation) => violation.field), host).toContain(
"API_BASE_URL",
);
}
});
it("permits a routable public host", () => {
expect(
findAdmissionViolations("production", {
...PRODUCTION_ARTIFACT,
API_BASE_URL: "https://api.172.16.example.com/",
} as AdmissionInput),
).toEqual([]);
});
it("refuses a placeholder identity on a public target", () => {
for (const buildId of ["local-build", "local", "dev", "unknown", ""]) {
const violations = findAdmissionViolations("production", {
...PRODUCTION_ARTIFACT,
BUILD_ID: buildId,
} as AdmissionInput);
expect(violations.map((violation) => violation.field), buildId).toContain(
"BUILD_ID",
);
}
});
it("treats an unknown target as not a target at all", () => {
for (const value of ["prod", "PRODUCTION", "", undefined, null, 1]) {
expect(isDeploymentTarget(value), String(value)).toBe(false);
}
for (const target of DEPLOYMENT_TARGETS) {
expect(isDeploymentTarget(target)).toBe(true);
}
});
});
describe("runtime config profiles", () => {
it("ships one valid profile per deployment target", async () => {
const files = (await readdir("config/runtime")).sort();
expect(files).toEqual(
[...DEPLOYMENT_TARGETS].map((target) => `${target}.json`).sort(),
);
for (const target of DEPLOYMENT_TARGETS) {
const source: unknown = JSON.parse(
await readFile(path.join("config/runtime", `${target}.json`), "utf8"),
);
const parsed = runtimeConfigV2ArtifactSchema.safeParse({
...(source as Record<string, unknown>),
BUILD_ID: "20260815.42",
RELEASE_ID: "r-1",
});
expect(parsed.success, `${target}: ${JSON.stringify(parsed.error?.issues)}`).toBe(
true,
);
expect((source as Record<string, unknown>)["APP_ENV"]).toBe(target);
}
});
it("produces an admissible document for every public target", async () => {
for (const target of ["staging", "production"] as const) {
const config = await generateRuntimeConfig(target, {
VITE_BUILD_ID: "20260815.42",
RELEASE_ID: "r-2026.08.15-1",
});
expect(
findAdmissionViolations(target, config as unknown as AdmissionInput),
).toEqual([]);
}
});
it("refuses a deployment override that would make the document unservable", async () => {
await expect(
generateRuntimeConfig("production", {
VITE_BUILD_ID: "20260815.42",
RELEASE_ID: "r-1",
RUNTIME_API_BASE_URL: "http://api.example.com/",
}),
).rejects.toThrow(/runtime config is invalid/u);
});
it("keeps a developer build from carrying a released identity by default", async () => {
const config = await generateRuntimeConfig("local", {});
expect(config["BUILD_ID"]).toBe("local-build");
expect(
findAdmissionViolations("production", config as unknown as AdmissionInput)
.length,
).toBeGreaterThan(0);
});
});
+70
View File
@@ -66,6 +66,76 @@ async function flushMicrotasks(): Promise<void> {
await Promise.resolve();
}
describe("runtime request deadline ceiling", () => {
/**
* `REQUEST_TIMEOUT_MS` was validated by the runtime config schema and then
* never handed to the V3 executor, so the deployment dial did nothing and
* every operation ran on its contract's own deadline. It is a ceiling: it may
* tighten an operation, never loosen one.
*/
async function settlesWithin(
contractDeadlineMs: number,
ceilingMs: number | undefined,
advanceMs: number,
): Promise<boolean> {
vi.useFakeTimers();
try {
const executor = createContractHttpExecutor({
baseUrl: "https://api.example/",
maxRetryAttempts: 0,
...(ceilingMs === undefined ? {} : { requestDeadlineCeilingMs: ceilingMs }),
attachCredentials: () => ({ kind: "READY", headers: {} }),
// A request that only ever ends by being cut off, so what settles it is
// exactly the deadline under test.
fetcher: (_input, init) =>
new Promise((_resolve, reject) => {
const signal = (init as RequestInit | undefined)?.signal;
signal?.addEventListener(
"abort",
() => reject(new DOMException("aborted", "AbortError")),
{ once: true },
);
}),
});
let settled = false;
const pending = executor
.execute(operation({ deadlineMs: contractDeadlineMs }), {}, {
routeId: ROUTE_ID,
scope,
})
.then(
() => { settled = true; },
() => { settled = true; },
);
await vi.advanceTimersByTimeAsync(advanceMs);
await flushMicrotasks();
const observed = settled;
if (!observed) await vi.advanceTimersByTimeAsync(contractDeadlineMs + 1_000);
await pending;
return observed;
} finally {
vi.useRealTimers();
}
}
it("applies the tighter of the contract and deployment bounds", async () => {
await expect(settlesWithin(5_000, 500, 800)).resolves.toBe(true);
await expect(settlesWithin(5_000, undefined, 800)).resolves.toBe(false);
});
it("never extends a contract deadline", async () => {
await expect(settlesWithin(500, 60_000, 800)).resolves.toBe(true);
});
it("ignores a ceiling that is not a usable duration", async () => {
for (const ceiling of [0, -1, Number.NaN]) {
await expect(settlesWithin(5_000, ceiling, 800), String(ceiling)).resolves.toBe(
false,
);
}
});
});
describe("descriptor-driven HTTP execution lifetime", () => {
it("normalizes a read-side 429 to the non-applicable effect vocabulary", async () => {
const executor = createContractHttpExecutor({
+36
View File
@@ -242,6 +242,42 @@ describe("presigned transfer", () => {
);
});
it("answers a refused capability envelope with a re-issuable recovery", async () => {
// BT-PRE-04. A capability document the adapter will not accept is closed as
// `POLICY_REJECTED`, and the caller's only way forward is a new capability.
// `NONE` said there was nothing to be done, which contradicted both the
// design record for an unsupported protocol and the vault, which already
// answers `REISSUE_CAPABILITY` for the same class of refusal.
for (const [label, overrides] of [
["unknown protocol", { protocol: "PRESIGNED_TRANSFER_V2" }],
["missing protocol", { protocol: undefined }],
] as const) {
const bytes = new Uint8Array([1, 2, 3]);
const payload: Record<string, unknown> = {
...downloadCapabilityPayload(bytes),
...overrides,
};
if (overrides.protocol === undefined) delete payload["protocol"];
const fetcher = vi.fn(async () => jsonResponse(payload)) as unknown as typeof fetch;
const { provider } = createHarness({ fetcher });
expect(
await provider.issueDownload({
resourceId: "resource-1",
signal: new AbortController().signal,
}),
label,
).toMatchObject({
ok: false,
error: {
code: "POLICY_REJECTED",
retryable: false,
recovery: "REISSUE_CAPABILITY",
},
});
}
});
it("keeps URL and headers adapter-private and streams bounded verified chunks", async () => {
const bytes = new Uint8Array([1, 2, 3, 4, 5]);
const payload = downloadCapabilityPayload(bytes);
+230
View File
@@ -0,0 +1,230 @@
import { describe, expect, it } from "vitest";
import {
activeProductFeatureIds,
resolveProductFeatures,
selectCompiledProductFeatures,
} from "../../src/contracts/product-features.ts";
import { runtimeConfigV2ArtifactSchema } from "../../src/contracts/release-artifacts.ts";
import {
COMPILED_PRODUCT_FEATURE_IDS,
INSTALLED_PRODUCT_FEATURE_IDS,
} from "../../src/features/installed-product-manifest.ts";
import { PLATFORM_ROUTE_REGISTRY } from "../../src/contracts/routes.ts";
import {
ROUTE_FEATURE_OWNER,
ROUTE_REGISTRY,
} from "../../src/features/installed-feature-contracts.ts";
const COMPILED = Object.freeze([
Object.freeze({ featureId: "reference-feature" }),
Object.freeze({ featureId: "billing" }),
]);
describe("build-time product selection", () => {
it("keeps everything when nothing is declared", () => {
for (const declared of [undefined, "", " "]) {
expect(
selectCompiledProductFeatures(COMPILED, declared).map((f) => f.featureId),
String(declared),
).toEqual(["reference-feature", "billing"]);
}
});
it("narrows to the declared subset", () => {
expect(
selectCompiledProductFeatures(COMPILED, "billing").map((f) => f.featureId),
).toEqual(["billing"]);
expect(
selectCompiledProductFeatures(COMPILED, " billing , reference-feature ").map(
(f) => f.featureId,
),
).toEqual(["reference-feature", "billing"]);
});
it("selects nothing only when asked explicitly", () => {
// A blank value keeps everything on purpose: an unset CI variable expands
// to a blank string, and that must not be how a build ships no features.
expect(selectCompiledProductFeatures(COMPILED, "none")).toEqual([]);
expect(selectCompiledProductFeatures(COMPILED, " none ")).toEqual([]);
expect(selectCompiledProductFeatures(COMPILED, "").length).toBe(2);
// A value that parses to no names at all is a typo, not an instruction.
expect(() => selectCompiledProductFeatures(COMPILED, ",")).toThrow(
/names no feature/u,
);
});
it("refuses to name a feature this build does not contain", () => {
// The whole point of the direction rule: an environment value can subtract
// from the source tree and must never be able to add to it. Accepting an
// unknown id silently would let a deployment believe it had switched on
// something that is not in the bundle.
expect(() => selectCompiledProductFeatures(COMPILED, "analytics")).toThrow(
/does not contain: analytics/u,
);
expect(() =>
selectCompiledProductFeatures(COMPILED, "billing,analytics"),
).toThrow(/analytics/u);
});
it("refuses a duplicated feature id in the manifest", () => {
expect(() =>
selectCompiledProductFeatures(
[{ featureId: "a" }, { featureId: "a" }],
undefined,
),
).toThrow(/duplicate product feature id/u);
});
});
describe("runtime product feature resolution", () => {
it("reports active, disabled and not-installed distinctly", () => {
const statuses = resolveProductFeatures(
["reference-feature", "billing"],
["reference-feature"],
{ "reference-feature": "DISABLED" },
);
expect(statuses).toEqual([
{ featureId: "billing", state: "NOT_INSTALLED" },
{ featureId: "reference-feature", state: "DISABLED_BY_CONFIG" },
]);
expect(activeProductFeatureIds(statuses)).toEqual([]);
});
it("cannot switch on a feature the build left out", () => {
// `DEFAULT` on an uninstalled feature is not an instruction to install it.
const statuses = resolveProductFeatures(["billing"], [], {
billing: "DEFAULT",
});
expect(statuses).toEqual([{ featureId: "billing", state: "NOT_INSTALLED" }]);
expect(activeProductFeatureIds(statuses)).toEqual([]);
});
it("ignores an override naming a feature this build never declared", () => {
// A shared runtime document may cover several builds, so a stale key is
// inert rather than fatal.
const statuses = resolveProductFeatures(
["reference-feature"],
["reference-feature"],
{ analytics: "DISABLED" },
);
expect(activeProductFeatureIds(statuses)).toEqual(["reference-feature"]);
});
it("leaves an installed feature active without an override", () => {
const statuses = resolveProductFeatures(
COMPILED_PRODUCT_FEATURE_IDS,
INSTALLED_PRODUCT_FEATURE_IDS,
);
expect(activeProductFeatureIds(statuses)).toEqual([
...INSTALLED_PRODUCT_FEATURE_IDS,
]);
});
});
describe("runtime config carries the switch", () => {
const base = {
APP_ENV: "local" as const,
API_BASE_URL: "http://localhost:8080/",
TELEMETRY_ENABLED: false,
AUTH_MODE: "demo" as const,
CONFIG_SCHEMA_VERSION: "2.0" as const,
RELEASE_MANIFEST_URL: "/release-manifest.json",
};
it("defaults to disabling nothing", () => {
const parsed = runtimeConfigV2ArtifactSchema.parse(base);
expect(parsed.FEATURE_OVERRIDES).toEqual({});
});
it("accepts only DEFAULT or DISABLED", () => {
expect(
runtimeConfigV2ArtifactSchema.parse({
...base,
FEATURE_OVERRIDES: { "reference-feature": "DISABLED" },
}).FEATURE_OVERRIDES,
).toEqual({ "reference-feature": "DISABLED" });
// There is no "ENABLED": the vocabulary itself is what makes the rule
// unbreakable, not a check somewhere downstream.
expect(
runtimeConfigV2ArtifactSchema.safeParse({
...base,
FEATURE_OVERRIDES: { "reference-feature": "ENABLED" },
}).success,
).toBe(false);
});
it("refuses a malformed feature id", () => {
for (const featureId of ["Reference", "reference_feature", "", "-x"]) {
expect(
runtimeConfigV2ArtifactSchema.safeParse({
...base,
FEATURE_OVERRIDES: { [featureId]: "DISABLED" },
}).success,
featureId,
).toBe(false);
}
});
});
describe("every installed registry consults the manifest", () => {
/**
* The manifest only means something if each registry actually asks it. A new
* registry that spreads a feature in directly would reintroduce exactly the
* coupling this file exists to remove, and nothing else would notice.
*/
it("gates every feature contribution on the selection", async () => {
const { readdir, readFile } = await import("node:fs/promises");
const nodePath = (await import("node:path")).default;
const root = "src/features";
const registries = (await readdir(root)).filter((entry) =>
/^installed-.*\.tsx?$/u.test(entry),
);
expect(registries.length).toBeGreaterThan(3);
const exempt = new Set([
// The manifest is the selection.
"installed-product-manifest.ts",
// Capabilities have their own §3.5 selection file and override vocabulary.
"installed-runtime-capabilities.ts",
// Template merge. This product removed the reference feature's screens,
// so no installed feature contributes a route component and this file has
// nothing to gate. `tests/component/product-feature-switch.test.tsx`
// holds the mountability invariant meanwhile; restore this entry to the
// guarded set the day a feature contributes a route runtime again.
"installed-feature-runtimes.tsx",
// Message keys stay total on purpose; see the file for why.
"installed-feature-messages.ts",
]);
for (const registry of registries) {
if (exempt.has(registry)) continue;
const source = await readFile(nodePath.join(root, registry), "utf8");
expect(
/INSTALLED_PRODUCT_FEATURE(S|_IDS)/u.test(source),
`${registry} must compose from the product manifest`,
).toBe(true);
}
});
});
describe("route ownership", () => {
it("attributes no platform route to a feature", () => {
// Template merge. The template named its own example routes here. This
// product deleted them, so the invariant is asserted against the platform
// registry itself: whatever the shell owns, disabling a feature can never
// withdraw it.
for (const routeId of Object.keys(PLATFORM_ROUTE_REGISTRY)) {
expect(ROUTE_FEATURE_OWNER[routeId], routeId).toBeUndefined();
expect(Object.keys(ROUTE_REGISTRY)).toContain(routeId);
}
expect(ROUTE_FEATURE_OWNER["NOT_FOUND"]).toBeUndefined();
});
it("owns exactly the routes the registry received from features", () => {
// Template merge. `owned` is empty in this product: the only installed
// feature's screens were removed by the UI migration. The invariant still
// holds and starts asserting again the moment a feature route returns.
for (const routeId of Object.keys(ROUTE_FEATURE_OWNER)) {
expect(Object.keys(ROUTE_REGISTRY), routeId).toContain(routeId);
}
});
});
@@ -18,6 +18,14 @@ import { pathToFileURL } from "node:url";
import { afterEach, describe, expect, it } from "vitest";
/**
* This suite's budget, not the file's. The 10s default is sized for pure-JS
* unit tests; these spawn processes, build archives and sign evidence, and on a
* machine running the rest of the suite in parallel they legitimately need
* longer. Raising the global default instead would hide a genuinely hung test.
*/
const PROCESS_HEAVY_TIMEOUT_MS = 30_000;
const roots: string[] = [];
afterEach(async () => {
@@ -699,7 +707,7 @@ describe("provider guardian transaction protocol", () => {
await expect(lstat(rawPath)).rejects.toMatchObject({ code: "ENOENT" });
await expect(lstat(sealedTempPath)).rejects.toMatchObject({ code: "ENOENT" });
await expect(readdir(evidenceRoot)).resolves.toEqual(["untrusted"]);
});
}, PROCESS_HEAVY_TIMEOUT_MS);
it("still publishes near the lease deadline when post-processing completes in time", async () => {
const { startProviderGuardian } = await import(
+1
View File
@@ -74,6 +74,7 @@ const runtimeV2 = {
SERVICE_WORKER: "DEFAULT",
OFFLINE_COMMANDS: "DEFAULT",
},
FEATURE_OVERRIDES: {},
} as const satisfies RuntimeConfigArtifact;
async function releaseV2With(
+45 -1
View File
@@ -2,7 +2,7 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, expect, it } from "vitest";
import { afterEach, describe, expect, it } from "vitest";
import { loadCiGateContract } from "../../scripts/contracts/ci-gates.ts";
import {
@@ -88,3 +88,47 @@ it("rejects pruning that leaves a reduced gate without commands", async () => {
removedEvidencePathFragments: ["runtime-schema.xml"],
})).rejects.toThrow(/commandIds|too small|at least 1/i);
});
describe("release evidence fixture copy", () => {
it("keeps the release evidence and leaves the regenerated trees behind", async () => {
const { copyReleaseEvidenceTree, RELEASE_EVIDENCE_REGENERATED_TREES } =
await import("../../scripts/lib/removal-fixture.ts");
const { RELEASE_CANDIDATE_EVIDENCE_PATHS } = await import(
"../../scripts/lib/release-candidate.ts"
);
const { mkdtemp, access, readdir } = await import("node:fs/promises");
const { tmpdir } = await import("node:os");
const nodePath = (await import("node:path")).default;
const root = await mkdtemp(nodePath.join(tmpdir(), "release-evidence-"));
await copyReleaseEvidenceTree(process.cwd(), root);
// Every release artifact that exists here has to survive the copy; a
// fixture missing one cannot build a candidate at all, and every provider
// suite then fails while constructing its own fixture.
//
// Which ones exist depends on what this checkout has generated — a product
// repository that has not run the release chain has fewer than the template
// does — so the subject is preservation, not the presence of a full chain.
// Requiring at least one keeps that from quietly asserting nothing.
let preserved = 0;
for (const evidence of RELEASE_CANDIDATE_EVIDENCE_PATHS) {
if (!evidence.startsWith("artifacts/")) continue;
try {
await access(evidence);
} catch {
continue;
}
await expect(access(nodePath.join(root, evidence)), evidence).resolves.toBeUndefined();
preserved += 1;
}
expect(preserved).toBeGreaterThan(0);
// The regenerated trees are why this is a filter and not a plain copy: they
// are tens of megabytes of traces and coverage HTML. They still exist,
// because the repository inventory expects the directories.
for (const tree of RELEASE_EVIDENCE_REGENERATED_TREES) {
await expect(access(nodePath.join(root, tree)), tree).resolves.toBeUndefined();
await expect(readdir(nodePath.join(root, tree)), tree).resolves.toEqual([]);
}
});
});
+9 -1
View File
@@ -24,6 +24,14 @@ import {
type ProductionModuleInventory,
} from "../../scripts/lib/risk-coverage.ts";
/**
* This suite's budget, not the file's. The 10s default is sized for pure-JS
* unit tests; these spawn processes, build archives and sign evidence, and on a
* machine running the rest of the suite in parallel they legitimately need
* longer. Raising the global default instead would hide a genuinely hung test.
*/
const PROCESS_HEAVY_TIMEOUT_MS = 30_000;
const roots: string[] = [];
const now = Date.parse("2026-08-02T00:00:00.000Z");
const execFileAsync = promisify(execFile);
@@ -141,7 +149,7 @@ describe("repository-aware risk coverage", () => {
result.repositoryTotal - result.selectedTotal,
);
expect(result.status).toBe("FAIL");
});
}, PROCESS_HEAVY_TIMEOUT_MS);
it("reports exact inventory and generated-exclusion provenance", async () => {
const repositoryRoot = await repositoryFixture();
+2
View File
@@ -26,6 +26,7 @@ const runtime: Runtime = {
SERVICE_WORKER: "DEFAULT",
OFFLINE_COMMANDS: "DEFAULT",
},
FEATURE_OVERRIDES: {},
},
configSchema: "V2",
build: {
@@ -262,6 +263,7 @@ describe("runtime adapter composition", () => {
...runtime.config.CAPABILITY_OVERRIDES,
SERVICE_WORKER: "DISABLED",
},
FEATURE_OVERRIDES: {},
},
},
release,
+9 -1
View File
@@ -47,6 +47,14 @@ import {
} from "../../scripts/lib/release-candidate.ts";
import { supplyChainDigest } from "../../scripts/lib/supply-chain.ts";
/**
* This suite's budget, not the file's. The 10s default is sized for pure-JS
* unit tests; these spawn processes, build archives and sign evidence, and on a
* machine running the rest of the suite in parallel they legitimately need
* longer. Raising the global default instead would hide a genuinely hung test.
*/
const PROCESS_HEAVY_TIMEOUT_MS = 30_000;
const digest = (value: string): string =>
createHash("sha256").update(value).digest("hex");
const digestBytes = (value: Buffer): string =>
@@ -1383,7 +1391,7 @@ describe("security follow-up contracts", () => {
} finally {
await rm(root, { recursive: true, force: true });
}
});
}, PROCESS_HEAVY_TIMEOUT_MS);
});
function providerExpectedContext() {
+6 -1
View File
@@ -14,6 +14,7 @@ import {
validateLicensePolicy,
} from "../../scripts/lib/supply-chain.ts";
import { digestReleaseInputFiles } from "../../scripts/lib/release-input-evidence.ts";
import { isReducedCiContractRun } from "../../scripts/contracts/ci-gates.ts";
import { findSecretMatches } from "../../scripts/lib/secret-scan.ts";
import {
parseSecretScanIncludedPaths,
@@ -535,7 +536,11 @@ describe("supply-chain policy", () => {
]);
});
it("covers every mandatory release input in the secret scan policy", async () => {
// A removal fixture deletes some of these inputs on purpose — removing the
// browser file/storage capability takes the whole browser-capability harness
// with it — and prunes them from its own policy. This is a claim about the
// full repository, so it does not describe a deliberately reduced one.
it.skipIf(isReducedCiContractRun())("covers every mandatory release input in the secret scan policy", async () => {
const policy = JSON.parse(
await readFile("config/security/secret-scan-policy.json", "utf8"),
) as { trackedRoots: string[] };
+45 -6
View File
@@ -6,6 +6,7 @@ import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
isReducedCiContractRun,
loadCiGateContract,
parseCiGateContract,
} from "../../scripts/contracts/ci-gates.ts";
@@ -70,8 +71,19 @@ describe("selective Task 3 contract closure", () => {
"--signal=SIGKILL",
unit,
]);
// `bwrap --args FD` stops parsing at the first non-option and never hands
// the remainder back, so a command placed in the args file is dropped and
// bubblewrap exits with its usage text. Refusing `--` in the option stream
// is what keeps that silent no-sandbox launch from returning.
expect(() =>
encodeProviderBwrapInput(
["--unshare-net", "--", "/usr/bin/prlimit"],
{ PROVIDER_COMMAND: command },
),
).toThrow(/terminate the option stream/u);
const frame = encodeProviderScopeFrame({
bwrapInput: Buffer.from("private-bwrap-vector\0"),
bwrapCommand: ["/usr/bin/prlimit", "--nofile=64:64", "--", "/bin/sh", "-eu", "-c", 'exec /bin/sh -eu -c "$PROVIDER_COMMAND"'],
reportPath: "/exact/report.json",
reportDev: 12,
reportIno: 34,
@@ -81,6 +93,27 @@ describe("selective Task 3 contract closure", () => {
Buffer.from("private-bwrap-vector\0").toString("base64"),
);
expect(launch.join("\0")).not.toContain("private-bwrap-vector");
// The command vector rides on real argv, so it must never be able to carry
// the secret that the args file exists to hide.
expect(frame.subarray(4).toString("utf8")).not.toContain(credential);
expect(() =>
encodeProviderScopeFrame({
bwrapInput: Buffer.from("x\0"),
bwrapCommand: [],
reportPath: "/exact/report.json",
reportDev: 12,
reportIno: 34,
}),
).toThrow(/bwrap command is invalid/u);
expect(() =>
encodeProviderScopeFrame({
bwrapInput: Buffer.from("x\0"),
bwrapCommand: ["prlimit"],
reportPath: "/exact/report.json",
reportDev: 12,
reportIno: 34,
}),
).toThrow(/bwrap command is invalid/u);
});
it("removes only the pinned raw inode during parent-loss cleanup", async () => {
@@ -143,12 +176,18 @@ describe("selective Task 3 contract closure", () => {
.toContain("package script missing: root -> missing");
});
it("accepts only the canonical exact-count authority and rejects orphan retention", async () => {
// A removal fixture runs against a pruned contract on purpose, so the
// canonical counts do not describe it. Asserting them there failed the
// fixture for the reduction it exists to demonstrate.
it.skipIf(isReducedCiContractRun())("accepts only the canonical exact-count authority and rejects orphan retention", async () => {
const canonical = await loadCiGateContract(process.cwd());
expect(canonical.gates).toHaveLength(26);
expect(canonical.commands).toHaveLength(81);
expect(canonical.gates.reduce((sum, gate) => sum + gate.commandIds.length, 0)).toBe(93);
expect(canonical.artifacts).toHaveLength(126);
// Template merge. The template added one gate and one command to the
// product's 26/81; the artifact set is the product's 126 plus the
// template's 2.
expect(canonical.gates).toHaveLength(27);
expect(canonical.commands).toHaveLength(82);
expect(canonical.gates.reduce((sum, gate) => sum + gate.commandIds.length, 0)).toBe(94);
expect(canonical.artifacts).toHaveLength(128);
expect(canonical.stages).toHaveLength(5);
expect(canonical.retention.classes).toHaveLength(5);
@@ -157,7 +196,7 @@ describe("selective Task 3 contract closure", () => {
expect(() => parseCiGateContract(orphan)).toThrow(/five canonical retention|orphan retention/u);
});
it("rejects the retired validate-candidate-archive grammar", async () => {
it.skipIf(isReducedCiContractRun())("rejects the retired validate-candidate-archive grammar", async () => {
const canonical = JSON.parse(
JSON.stringify(await loadCiGateContract(process.cwd())),
) as Record<string, any>;