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>
231 lines
8.6 KiB
TypeScript
231 lines
8.6 KiB
TypeScript
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);
|
|
}
|
|
});
|
|
});
|