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:
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user