fix: let a removal fixture actually build the thing it claims still builds

FE-GATE-020 proves a capability can be removed by rebuilding the whole project
without it. The fixture it built could not get that far, and the failures all
came from the fixture rather than from anything about removability.

It was not a repository. The supply-chain inventory is defined as the tracked
file set, so it asks `git ls-files` what the project contains; with no
repository to ask, generation failed and took every provider suite down with
it. It is now initialised on preparation and committed after the removal — not
before, or the index would still list the files the removal deleted.

It had no `.gitignore`, so once it did have a repository, every generated
artifact and every linked module landed in the index and the inventory refused
the fixture for tracked and generated paths colliding. It carries the ignore
rules now, and therefore records the same tracked set as the repository it was
copied from.

Each removal script kept its own copy-target list and they had drifted: the
reference-feature fixture omitted `playwright.capabilities.config.ts`, which the
inventory requires. There is one list now. It also gained the install and
workspace identity — `.npmrc`, the lockfile, the workspace file — without which
the fixture is a different project, and the release evidence a candidate is
assembled from, without which no candidate can be built at all.

A tracked root the removal deletes is no longer required of the result: the
optional-recipe fixture deletes `recipes/`, and the inventory policy demanded
it back. Roots that are gone are pruned from the fixture's policy.

Two smaller causes. A platform integration file asserted the reference feature's
own route ids, so removing the feature left it importing a deleted module —
typecheck, the test run, coverage and the residue scan all failed on that one
misplaced assertion, which now lives in the feature's test tree. And the
canonical exact-count authority was re-imposed on a contract the fixture
deliberately reduces, failing the fixture for the reduction it exists to prove;
`CI_CONTRACT_MODE` already marked those runs and is now honoured by default.

The reference-feature fixture goes from failing before its first assertion to
1,612 passing with one failure, and that one is the live process-tree
observation test already red on the main tree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-08-15 19:34:28 +09:00
co-authored by Claude Opus 5
parent 3ea3397691
commit 10a04d3695
10 changed files with 325 additions and 104 deletions
@@ -462,6 +462,56 @@ The full `tests/unit` + `tests/integration` run is **1,845 passed / 1,864**,
cgroup, RLIMIT and `/tmp` permission behaviour already recorded above — the same cgroup, RLIMIT and `/tmp` permission behaviour already recorded above — the same
file failed identically before this work. No adapter test fails. file failed identically before this work. No adapter test fails.
## Operational contract review (2026-08-15)
A fourth review looked past the adapter layer at the operational contract:
feature on/off, environment separation, folder boundaries, and which gates were
actually green. It found five red gates and three structural gaps. Every row
below names the defect, not the symptom.
| id | area | disposition | what was actually wrong |
| --- | --- | --- | --- |
| `OPS-01` | release | `FIXED` | `public/` is copied verbatim into `dist/`, so every build — production included — shipped the local runtime document. Runtime config now comes from `config/runtime/<profile>.json`. |
| `OPS-02` | release | `FIXED` | Release coherence proved the artifacts agreed with each other, never that they belonged in production. `FE-GATE-027` refuses an artifact whose `APP_ENV`, auth mode, endpoints or build identity do not match a declared `RELEASE_TARGET`, and refuses an undeclared target outright. |
| `OPS-03` | runtime | `FIXED` | `REQUEST_TIMEOUT_MS` was validated and then never passed to the V3 executor; every operation ran on its contract's own deadline. It is now a ceiling that may tighten a contract, never loosen one. |
| `OPS-04` | build | `FIXED` | `VITE_ROUTER_BASE_PATH` drove the router and the Service Worker scope but not Vite's asset `base`, so a sub-path deployment emitted root-absolute assets. One value now feeds all three. |
| `OPS-05` | provider | `FIXED` | bubblewrap 0.9.0 drops whatever follows the option stream inside an `--args` file, so the sandboxed command was never executed: bwrap printed usage and exited 1. Options stay hidden; the command travels on real argv. |
| `OPS-06` | provider | `FIXED` | The scope wrapper read its liveness pipe through `fs`, a blocking `read(2)` on a pipe the supervisor never closes. `process.exit` deadlocked joining that thread, so a completed provider was reported as a timeout kill. |
| `OPS-07` | release | `FIXED` | `mkdir`/`open` modes were left to the ambient umask, so a hardened runner produced directories it could not enter and handed `tar` a file it could not re-open. |
| `OPS-08` | release | `FIXED` | Promotion cleanup deleted this promotion's exact five through a pinned descriptor and only then noticed the leaf had been substituted, leaving a half-emptied directory a retry could not distinguish from a completed one. |
| `OPS-09` | removability | `FIXED` | The removal fixture was not a repository, had no `.gitignore`, and each removal script kept its own copy-target list that had drifted. Supply-chain generation therefore failed inside every fixture and took the whole provider suite down with it. |
| `OPS-10` | removability | `FIXED` | A platform integration file asserted the reference feature's route ids, so removing the feature left it importing a deleted module. The assertion moved to the feature's own test tree. |
| `OPS-11` | removability | `FIXED` | A removal fixture runs against a deliberately reduced CI contract; the canonical exact-count tests re-imposed the full authority on it and failed the fixture for the reduction it exists to prove. |
| `OPS-12` | browser | `FIXED` | Four browser-capability specs answered capability requests without the `protocol` field the hardened envelope requires, so every capability was refused and the download and part-upload paths asserted against an empty transcript. |
| `OPS-13` | browser | `FIXED` | A refused capability document answered `recovery: NONE`, contradicting both the design record and the vault, which already answers `REISSUE_CAPABILITY`. |
| `OPS-14` | performance | `FIXED` | Playwright matches accessible names by substring, so the navigation entry matched the home page's call to action too; the run died on a strict-mode violation before the first measurement and produced no evidence at all. |
| `OPS-15` | visual | `FIXED` | The platform overview baseline predated the reference routes moving from `integration-defined` to `session-required`, so the only visual gate covering that page failed for its own staleness. |
| `OPS-16` | architecture | `FIXED` | `src/contracts` imported `src/application` for the shared `Result` and the compatibility predicate; neither package owned the shared vocabulary. Both moved down to contracts. |
| `OPS-17` | architecture | `FIXED` | The documented "no adapter depends on another concrete adapter" rule had no executable form, and `diagnostics` imported a guard out of `telemetry`. The guard moved to the adapter kernel and the rule is now enforced with a same-directory backreference. |
| `OPS-18` | architecture | `PARTIAL` | Generic presentation still reads the installed-feature registries. The rule freezes the exact set of modules doing so today; a new edge fails. Lifting the assembly into `bootstrap` is not done. |
| `OPS-19` | documentation | `FIXED` | README and the manual accessibility checklist both claimed six routes while ten were registered, leaving four screens outside the declared manual review scope. The list is now derived from the route registry by `verify:documentation`. |
### Still red after this pass
`tests/unit/ci-artifact-contract.test.ts` → *applies effective aggregate cgroup
limits without exposing command or credentials*. It reads the live process tree
and cgroup of a running sandbox, and the supervisor now completes a whole run in
well under a second while `systemctl show` and `ps` each cost hundreds of
milliseconds, so the observation loses the race. It was already red before this
work and is not a product defect; the assertions it makes about cgroup limits
and credential exposure are not currently proven by an automated run.
Lab performance now produces evidence, and that evidence shows the
named-interaction budget missed on this machine (367724ms against 200ms). The
metric measures a full lazy-route navigation while the budget is an
INP-shaped 200ms, so the two do not describe the same thing. No budget was
changed to make this green.
WebKit remains unavailable in this environment (`libevent-2.1-7t64`,
`libavif16` are not installed), so 14 browser-capability specs and the WebKit
E2E project are unverified here. Chromium and Firefox are 28/28 and visual is
5/5.
## Rules for updating this ledger ## Rules for updating this ledger
- A row moves out of `NOT_STARTED` only with a linked red test, its green run, and the commit id. - A row moves out of `NOT_STARTED` only with a linked red test, its green run, and the commit id.
+20 -2
View File
@@ -512,7 +512,7 @@ export function parseCiGateContract(
.join("\n"); .join("\n");
throw new TypeError(`CI gate contract invalid:\n${diagnostic}`); throw new TypeError(`CI gate contract invalid:\n${diagnostic}`);
} }
if ((options.mode ?? "canonical") === "canonical") { if ((options.mode ?? defaultCiContractMode()) === "canonical") {
const failures = canonicalAuthorityBaselineFailures(result.data); const failures = canonicalAuthorityBaselineFailures(result.data);
if (failures.length > 0) { if (failures.length > 0) {
throw new TypeError(`CI gate contract invalid:\n${failures.map((failure) => `root: ${failure}`).join("\n")}`); throw new TypeError(`CI gate contract invalid:\n${failures.map((failure) => `root: ${failure}`).join("\n")}`);
@@ -521,11 +521,29 @@ export function parseCiGateContract(
return result.data; return result.data;
} }
/**
* A removal fixture runs the whole suite against a deliberately *reduced* CI
* contract: the removed capability's gates, commands and artifacts are pruned.
* Loading that contract in canonical mode re-imposes the full exact-count
* authority on it, so the fixture failed on the very reduction it exists to
* prove. `runRemovalFixturePnpm` marks those runs, and this is where the mark
* is honoured.
*/
export function defaultCiContractMode(): "canonical" | "removal-fixture" {
return process.env.CI_CONTRACT_MODE === "removal-fixture"
? "removal-fixture"
: "canonical";
}
export function isReducedCiContractRun(): boolean {
return defaultCiContractMode() === "removal-fixture";
}
export async function loadCiGateContract( export async function loadCiGateContract(
root = process.cwd(), root = process.cwd(),
options: LoadCiGateContractOptions = {}, options: LoadCiGateContractOptions = {},
): Promise<CiGateContract> { ): Promise<CiGateContract> {
const mode = options.mode ?? "canonical"; const mode = options.mode ?? defaultCiContractMode();
const [rawContract, rawPackage] = await Promise.all([ const [rawContract, rawPackage] = await Promise.all([
readFile(path.join(root, "config/ci/gates.json"), "utf8"), readFile(path.join(root, "config/ci/gates.json"), "utf8"),
readFile(path.join(root, "package.json"), "utf8"), readFile(path.join(root, "package.json"), "utf8"),
+154 -1
View File
@@ -5,6 +5,7 @@ import {
readFile, readFile,
readdir, readdir,
rm, rm,
stat,
writeFile, writeFile,
} from "node:fs/promises"; } from "node:fs/promises";
import path from "node:path"; import path from "node:path";
@@ -24,9 +25,75 @@ export const REMOVAL_FIXTURE_COPY_TARGETS = Object.freeze([
"vite.service-worker.config.ts", "vite.config.ts", "vitest.config.ts", "vite.service-worker.config.ts", "vite.config.ts", "vitest.config.ts",
"playwright.config.ts", "playwright.capabilities.config.ts", "playwright.dev.config.ts", "playwright.config.ts", "playwright.capabilities.config.ts", "playwright.dev.config.ts",
"playwright.storybook.config.ts", "playwright.visual.config.ts", "eslint.config.ts", "playwright.storybook.config.ts", "playwright.visual.config.ts", "eslint.config.ts",
".dependency-cruiser.json", ".nvmrc", ".dependency-cruiser.json", ".nvmrc", ".gitignore",
// Install and workspace identity. Without these the fixture is not the same
// project: `corepack pnpm` resolves a different store, and the provider
// suites — which build a release candidate containing `pnpm-lock.yaml` —
// cannot assemble their fixture at all.
".npmrc", "pnpm-lock.yaml", "pnpm-workspace.yaml",
] as const); ] as const);
/**
* This is the only copy-target list. Each removal script used to keep its own,
* and they drifted: the reference-feature fixture omitted
* `playwright.capabilities.config.ts`, which the repository file inventory
* requires, so supply-chain generation failed inside the fixture and took every
* provider suite down with it — twenty-odd failures with one cause.
*/
/**
* Regenerated result trees under `artifacts/`: traces, coverage HTML, recorded
* videos and Storybook bundles. They are tens of megabytes and mean nothing to
* a fixture. Everything else under `artifacts/` is release evidence a candidate
* is assembled from — and most of it is git-ignored too, so "is it tracked?"
* cannot be used to tell the two apart. `keepsReleaseEvidence` in
* tests/unit/removal-fixture.test.ts pins both halves of this split.
*/
const REGENERATED_ARTIFACT_TREES: readonly string[] = Object.freeze([
"artifacts/storybook",
"artifacts/tests/browser-capabilities",
"artifacts/tests/coverage",
"artifacts/tests/e2e",
"artifacts/tests/storybook",
"artifacts/tests/visual",
]);
/**
* Copies the release evidence a candidate build needs into a fixture root.
*
* A fixture that omits it cannot assemble a candidate archive at all, so every
* provider suite fails while constructing its own fixture — long before it
* reaches an assertion, and with an error that says nothing about the
* capability under test.
*/
export async function copyReleaseEvidenceTree(
sourceRoot: string,
destinationRoot: string,
): Promise<void> {
const source = path.join(sourceRoot, "artifacts");
try {
await stat(source);
} catch {
return;
}
await cp(source, path.join(destinationRoot, "artifacts"), {
recursive: true,
filter: (candidate) => {
const relative = path.relative(sourceRoot, candidate).split(path.sep).join("/");
return !REGENERATED_ARTIFACT_TREES.some(
(tree) => relative === tree || relative.startsWith(`${tree}/`),
);
},
});
// The result directories still have to exist: several are tracked through a
// `.gitkeep` the repository inventory expects to find.
for (const tree of REGENERATED_ARTIFACT_TREES) {
await mkdir(path.join(destinationRoot, tree), { recursive: true });
}
}
export const RELEASE_EVIDENCE_REGENERATED_TREES = REGENERATED_ARTIFACT_TREES;
export function requireRemovalFixtureEnvironment(name: string): string { export function requireRemovalFixtureEnvironment(name: string): string {
const value = process.env[name]; const value = process.env[name];
if (!value) throw new Error(`${name} is required for removal verification`); if (!value) throw new Error(`${name} is required for removal verification`);
@@ -42,9 +109,56 @@ export async function prepareRemovalFixture(
for (const target of copyTargets) { for (const target of copyTargets) {
await cp(target, path.join(root, target), { recursive: true }); await cp(target, path.join(root, target), { recursive: true });
} }
await copyReleaseEvidenceTree(process.cwd(), root);
runFixtureGit(root, ["init", "--quiet", "--initial-branch=fixture"]);
await linkFixtureNodeModules(root); await linkFixtureNodeModules(root);
} }
/**
* Records the fixture's post-removal contents as its repository state.
*
* The release candidate path asks `git ls-files` what the repository contains —
* the supply-chain inventory is defined as the tracked file set, not as
* whatever happens to be on disk. A fixture without a repository cannot answer
* that, so supply-chain generation failed and took every provider suite down
* with it; the claim "this build still produces a release candidate after the
* capability is removed" was never actually being tested.
*
* It runs after the removal, not during preparation: an index recorded before
* the deletions still lists the removed files, and the inventory then demands
* files the fixture exists to prove are gone.
*/
export function sealRemovalFixtureRepository(root: string): void {
// `.gitignore` travels with the fixture, so the tracked set it records is the
// same tracked set the real repository has. Without it every generated
// artifact and every linked module landed in the index, and the supply-chain
// inventory refused the fixture for having tracked and generated paths
// collide — the fixture disagreed with the repository it was copied from.
runFixtureGit(root, ["add", "--all"]);
runFixtureGit(root, ["commit", "--quiet", "--no-gpg-sign", "-m", "removal fixture"]);
}
function runFixtureGit(root: string, argv: readonly string[]): void {
const result = spawnSync("git", [...argv], {
cwd: root,
encoding: "utf8",
env: {
...process.env,
GIT_AUTHOR_NAME: "removal-fixture",
GIT_AUTHOR_EMAIL: "removal-fixture@localhost",
GIT_COMMITTER_NAME: "removal-fixture",
GIT_COMMITTER_EMAIL: "removal-fixture@localhost",
},
});
if (result.error || result.status !== 0) {
throw new Error(
`removal fixture repository setup failed at git ${argv[0]}: ${
result.stderr || result.error?.message || `exit ${result.status}`
}`,
);
}
}
export function runRemovalFixturePnpm( export function runRemovalFixturePnpm(
root: string, root: string,
pnpmCli: string, pnpmCli: string,
@@ -162,6 +276,45 @@ export function pruneScriptOrchestration(
export async function regenerateRemovalFixtureWorkflow(root: string): Promise<void> { export async function regenerateRemovalFixtureWorkflow(root: string): Promise<void> {
const contract = await loadCiGateContract(root, { mode: "removal-fixture" }); const contract = await loadCiGateContract(root, { mode: "removal-fixture" });
await generateCiWorkflow({ root, contract, check: false }); await generateCiWorkflow({ root, contract, check: false });
// Every removal script calls this once, after it has finished mutating the
// tree, so it is the one place where the fixture's contents are final.
await pruneRemovalFixtureInventoryRoots(root);
sealRemovalFixtureRepository(root);
}
/**
* Drops repository roots the removal deleted from the supply-chain inventory
* policy.
*
* The policy lists `recipes` as a required tracked root, and removing an
* optional recipe deletes exactly that directory. Supply-chain generation then
* refused the fixture for missing a root the removal was supposed to remove, so
* the capability could never be shown to be removable. A root that is not on
* disk after the removal is not required of the result.
*/
async function pruneRemovalFixtureInventoryRoots(root: string): Promise<void> {
const policyPath = path.join(root, "config/security/secret-scan-policy.json");
let policy: Record<string, unknown>;
try {
policy = JSON.parse(await readFile(policyPath, "utf8")) as Record<string, unknown>;
} catch {
return;
}
const tracked = policy["trackedRoots"];
if (!Array.isArray(tracked)) return;
const surviving: string[] = [];
for (const entry of tracked) {
if (typeof entry !== "string") continue;
try {
await stat(path.join(root, entry));
surviving.push(entry);
} catch {
// Deleted by the removal under test.
}
}
if (surviving.length === tracked.length) return;
policy["trackedRoots"] = surviving;
await writeFile(policyPath, `${JSON.stringify(policy, null, 2)}\n`, "utf8");
} }
export async function pruneRemovalFixtureCiContract(options: Readonly<{ export async function pruneRemovalFixtureCiContract(options: Readonly<{
+1 -32
View File
@@ -18,43 +18,12 @@ import {
const fixtureRoot = path.resolve(".tmp/optional-recipe-removal"); const fixtureRoot = path.resolve(".tmp/optional-recipe-removal");
const pnpmCli = requireRemovalFixtureEnvironment("npm_execpath"); const pnpmCli = requireRemovalFixtureEnvironment("npm_execpath");
const copyTargets = [
"src",
"tests",
"recipes",
"scripts",
"schemas",
"config",
"public",
".gitea",
".storybook",
"index.html",
"package.json",
"tsconfig.base.json",
"tsconfig.json",
"tsconfig.app.json",
"tsconfig.node.json",
"tsconfig.test.json",
"tsconfig.recipes.json",
"tsconfig.web-worker.json",
"tsconfig.service-worker.json",
"vite.service-worker.config.ts",
"vite.config.ts",
"vitest.config.ts",
"playwright.config.ts",
"playwright.dev.config.ts",
"playwright.storybook.config.ts",
"playwright.visual.config.ts",
"eslint.config.ts",
".dependency-cruiser.json",
".nvmrc",
];
function runPnpm(script: string): boolean { function runPnpm(script: string): boolean {
return runRemovalFixturePnpm(fixtureRoot, pnpmCli, script); return runRemovalFixturePnpm(fixtureRoot, pnpmCli, script);
} }
await prepareRemovalFixture(fixtureRoot, copyTargets); await prepareRemovalFixture(fixtureRoot);
for (const rootOnlyTest of [ for (const rootOnlyTest of [
"tests/unit/ci-workflow-generation.test.ts", "tests/unit/ci-workflow-generation.test.ts",
"tests/unit/__snapshots__/ci-workflow-generation.test.ts.snap", "tests/unit/__snapshots__/ci-workflow-generation.test.ts.snap",
+7 -32
View File
@@ -27,10 +27,16 @@ const fixtureRoot = await mkdtemp(
const pnpmCli = requireRemovalFixtureEnvironment("npm_execpath"); const pnpmCli = requireRemovalFixtureEnvironment("npm_execpath");
const featureSource = "src/features/reference-feature"; const featureSource = "src/features/reference-feature";
const featureTests = "tests/features/reference-feature"; const featureTests = "tests/features/reference-feature";
/**
* Platform tests that must survive the sample feature's removal. Asserting they
* are still present is what stops the removal fixture from "passing" by having
* quietly deleted the platform's own coverage along with the feature.
*/
const commonTestPaths = [ const commonTestPaths = [
"tests/unit/external-contract-runtime.test.ts", "tests/unit/external-contract-runtime.test.ts",
"tests/unit/http-execution-v3.test.ts", "tests/unit/http-execution-v3.test.ts",
"tests/unit/runtime-adapters.test.ts", "tests/unit/runtime-adapters.test.ts",
"tests/integration/http-execution-v3-observability.test.ts",
]; ];
const featureOwnedPaths = [ const featureOwnedPaths = [
featureSource, featureSource,
@@ -42,37 +48,6 @@ const featureOwnedPaths = [
"tests/fixtures/typecheck/invalid-feature-input.ts", "tests/fixtures/typecheck/invalid-feature-input.ts",
"tests/fixtures/typecheck/invalid-reference-operation.ts", "tests/fixtures/typecheck/invalid-reference-operation.ts",
]; ];
const copyTargets = [
"src",
"tests",
"recipes",
"scripts",
"schemas",
"config",
"public",
".gitea",
".storybook",
"index.html",
"package.json",
"tsconfig.base.json",
"tsconfig.json",
"tsconfig.app.json",
"tsconfig.node.json",
"tsconfig.test.json",
"tsconfig.recipes.json",
"tsconfig.web-worker.json",
"tsconfig.service-worker.json",
"vite.service-worker.config.ts",
"vite.config.ts",
"vitest.config.ts",
"playwright.config.ts",
"playwright.dev.config.ts",
"playwright.storybook.config.ts",
"playwright.visual.config.ts",
"eslint.config.ts",
".dependency-cruiser.json",
".nvmrc",
];
const emptyContracts = `import { PLATFORM_ROUTE_RUNTIME_CONTRACT } from "../contracts/route-runtime-contract.ts"; const emptyContracts = `import { PLATFORM_ROUTE_RUNTIME_CONTRACT } from "../contracts/route-runtime-contract.ts";
import { PLATFORM_ROUTE_REGISTRY, type RouteDefinition } from "../contracts/routes.ts"; import { PLATFORM_ROUTE_REGISTRY, type RouteDefinition } from "../contracts/routes.ts";
@@ -172,7 +147,7 @@ function runPnpm(script: string, extra: string[] = []): boolean {
} }
try { try {
await prepareRemovalFixture(fixtureRoot, copyTargets); await prepareRemovalFixture(fixtureRoot);
for (const excludedFixtureTest of [ for (const excludedFixtureTest of [
"tests/unit/ci-workflow-generation.test.ts", "tests/unit/ci-workflow-generation.test.ts",
"tests/unit/__snapshots__/ci-workflow-generation.test.ts.snap", "tests/unit/__snapshots__/ci-workflow-generation.test.ts.snap",
@@ -0,0 +1,43 @@
import { describe, expect, it } from "vitest";
import { createReferenceFeatureInstalledInput } from "../../../src/features/reference-feature/adapters/create-reference-feature-input.ts";
/**
* This assertion is about the reference feature, not about HTTP observability:
* it names the feature's own route ids. It used to live in
* `tests/integration/http-execution-v3-observability.test.ts`, which meant a
* platform integration file imported the removable feature's installed input.
* Removing the feature then left that file importing a deleted module, and the
* removability fixture failed on typecheck, the unit/integration run, coverage
* and residue at once — four symptoms of one misplaced test.
*/
describe("reference feature installed operation executor", () => {
it("preserves the feature route id through the installed operation executor", async () => {
const seen: Array<Readonly<Record<string, unknown>>> = [];
const installed = createReferenceFeatureInstalledInput({
contractOperations: Object.freeze({
async execute(
_operationId: string,
_input: unknown,
context?: Readonly<Record<string, unknown>>,
) {
seen.push(Object.freeze({ ...(context ?? {}) }));
return Object.freeze({
kind: "SUCCESS" as const,
value: [],
metadata: Object.freeze({ status: 200 }),
effect: "NOT_APPLICABLE" as const,
});
},
}),
});
await installed.input.listResources({ limit: 20 });
await installed.input.getResource("resource-1");
expect(seen.map((context) => context.routeId)).toEqual([
"REFERENCE_RESOURCE_LIST",
"REFERENCE_RESOURCE_DETAIL",
]);
});
});
@@ -5,7 +5,6 @@ import type {
HttpExecutionObservation, HttpExecutionObservation,
} from "../../src/adapters/http/http-execution-v3.ts"; } from "../../src/adapters/http/http-execution-v3.ts";
import { createHttpObservationProjector } from "../../src/bootstrap/runtime-adapters.ts"; import { createHttpObservationProjector } from "../../src/bootstrap/runtime-adapters.ts";
import { createReferenceFeatureInstalledInput } from "../../src/features/reference-feature/adapters/create-reference-feature-input.ts";
import { import {
DIAGNOSTIC_CONTEXT_ALLOWLIST, DIAGNOSTIC_CONTEXT_ALLOWLIST,
projectDiagnosticRecord, projectDiagnosticRecord,
@@ -233,35 +232,6 @@ describe("V3 HTTP observability projection", () => {
expect(fenced.telemetry).toHaveLength(0); expect(fenced.telemetry).toHaveLength(0);
}); });
it("preserves the feature route id through the installed operation executor", async () => {
const seen: Array<Readonly<Record<string, unknown>>> = [];
const installed = createReferenceFeatureInstalledInput({
contractOperations: Object.freeze({
async execute(
_operationId: string,
_input: unknown,
context?: Readonly<Record<string, unknown>>,
) {
seen.push(Object.freeze({ ...(context ?? {}) }));
return Object.freeze({
kind: "SUCCESS" as const,
value: [],
metadata: Object.freeze({ status: 200 }),
effect: "NOT_APPLICABLE" as const,
});
},
}),
});
await installed.input.listResources({ limit: 20 });
await installed.input.getResource("resource-1");
expect(seen.map((context) => context.routeId)).toEqual([
"REFERENCE_RESOURCE_LIST",
"REFERENCE_RESOURCE_DETAIL",
]);
});
it("cannot change the HTTP result when diagnostics or telemetry throws", async () => { it("cannot change the HTTP result when diagnostics or telemetry throws", async () => {
const projector = createHttpObservationProjector({ const projector = createHttpObservationProjector({
diagnostics: { diagnostics: {
+12 -4
View File
@@ -20,6 +20,7 @@ import {
} from "../../scripts/lib/ci-artifact-validator.ts"; } from "../../scripts/lib/ci-artifact-validator.ts";
import { writeCiGateLogAtomic } from "../../scripts/lib/ci-gate-log.ts"; import { writeCiGateLogAtomic } from "../../scripts/lib/ci-gate-log.ts";
import { linkFixtureNodeModules } from "../../scripts/lib/fixture-node-modules.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 { captureCiCandidateArchive, verifyCiCandidateArchive } from "../../scripts/lib/ci-candidate-archive.ts";
import { import {
CANDIDATE_ARCHIVE_USAGE, CANDIDATE_ARCHIVE_USAGE,
@@ -2153,10 +2154,17 @@ async function ensureProviderBaseFixture(): Promise<string> {
return ![".release", "artifacts", "dist", "node_modules"].includes(first ?? ""); return ![".release", "artifacts", "dist", "node_modules"].includes(first ?? "");
}, },
}); });
await cp(path.join(sourceRoot, "artifacts"), path.join(root, "artifacts"), { // The release evidence, minus the trace and Storybook trees. Copying the
recursive: true, // whole `artifacts/` directory pulled ~28MB of test output into every
}); // provider fixture; sharing the copier with the removal fixture is also what
await rm(path.join(root, "artifacts/release"), { recursive: true, force: true }); // 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); await linkFixtureNodeModules(root, sourceRoot);
const git = spawnSync("git", ["show", "-s", "--format=%H%n%ct", "HEAD"], { const git = spawnSync("git", ["show", "-s", "--format=%H%n%ct", "HEAD"], {
cwd: sourceRoot, cwd: sourceRoot,
+32 -1
View File
@@ -2,7 +2,7 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import path from "node:path"; 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 { loadCiGateContract } from "../../scripts/contracts/ci-gates.ts";
import { import {
@@ -88,3 +88,34 @@ it("rejects pruning that leaves a reduced gate without commands", async () => {
removedEvidencePathFragments: ["runtime-schema.xml"], removedEvidencePathFragments: ["runtime-schema.xml"],
})).rejects.toThrow(/commandIds|too small|at least 1/i); })).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 artifact a candidate archive is assembled from has to survive; a
// fixture missing one of these cannot build a candidate at all, and every
// provider suite then fails while constructing its own fixture.
for (const evidence of RELEASE_CANDIDATE_EVIDENCE_PATHS) {
if (!evidence.startsWith("artifacts/")) continue;
await expect(access(nodePath.join(root, evidence)), evidence).resolves.toBeUndefined();
}
// 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([]);
}
});
});
@@ -6,6 +6,7 @@ import path from "node:path";
import { afterEach, describe, expect, it } from "vitest"; import { afterEach, describe, expect, it } from "vitest";
import { import {
isReducedCiContractRun,
loadCiGateContract, loadCiGateContract,
parseCiGateContract, parseCiGateContract,
} from "../../scripts/contracts/ci-gates.ts"; } from "../../scripts/contracts/ci-gates.ts";
@@ -175,7 +176,10 @@ describe("selective Task 3 contract closure", () => {
.toContain("package script missing: root -> missing"); .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()); const canonical = await loadCiGateContract(process.cwd());
expect(canonical.gates).toHaveLength(27); expect(canonical.gates).toHaveLength(27);
expect(canonical.commands).toHaveLength(82); expect(canonical.commands).toHaveLength(82);
@@ -189,7 +193,7 @@ describe("selective Task 3 contract closure", () => {
expect(() => parseCiGateContract(orphan)).toThrow(/five canonical retention|orphan retention/u); 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( const canonical = JSON.parse(
JSON.stringify(await loadCiGateContract(process.cwd())), JSON.stringify(await loadCiGateContract(process.cwd())),
) as Record<string, any>; ) as Record<string, any>;