test: decouple the feature-switch gates from the template's demo screens
The template's demonstration screens exist to explain the template. A product replaces them with its domain, so deleting them is the expected end state — but two gates were coupled to them, and the merge accommodated that coupling instead of fixing it. product-features.test.ts derives its own scope now: a registry must gate on the manifest when it imports a module belonging to a manifest-declared feature. The previous fix put installed-feature-runtimes.tsx in the exempt list, which silenced the guard for that file permanently. Verified by removing the manifest reference from installed-feature-adapters.ts and watching the guard fail; a counter asserts the sweep still watches at least one file. product-feature-switch.test.tsx exercises the kill switch end to end again. The mechanism is the ownership lookup plus isFeatureActive, which has nothing to do with which screens ship, so the ownership map is the fixture: one real registered route attributed to a real installed feature, with the product's own router, components and codecs. That restoration exposed a real gap. The navigation-withdrawal half is not implemented here: it lives in the template's PrimaryNavigation and this product does not render the template's AppShell at all — the public header is a hand-written list of paths. A disabled feature's route is refused by the router but its link would still be advertised. Harmless only while no feature-owned route is navigable, which is now asserted so the gap cannot ship silently. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
cd1ef5cda2
commit
93ce86eef4
@@ -186,23 +186,37 @@ describe("every installed registry consults the manifest", () => {
|
||||
"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",
|
||||
]);
|
||||
// A registry has to gate on the manifest when it composes something a
|
||||
// manifest-declared feature owns. Deriving that from the file's own imports
|
||||
// is the difference between a rule and a list: a product that replaces the
|
||||
// template's demonstration feature with its own domain drops out of the
|
||||
// rule honestly, and the day it imports a declared feature again the guard
|
||||
// fires without anyone having to remember to re-add it.
|
||||
//
|
||||
// An earlier fix named `installed-feature-runtimes.tsx` in the exempt set
|
||||
// once this product deleted the reference feature's screens. That silenced
|
||||
// the guard for that file permanently — a worse outcome than the problem it
|
||||
// solved.
|
||||
let guarded = 0;
|
||||
for (const registry of registries) {
|
||||
if (exempt.has(registry)) continue;
|
||||
const source = await readFile(nodePath.join(root, registry), "utf8");
|
||||
const composesDeclaredFeature = COMPILED_PRODUCT_FEATURE_IDS.some(
|
||||
(featureId) => source.includes(`./${featureId}/`),
|
||||
);
|
||||
if (!composesDeclaredFeature) continue;
|
||||
guarded += 1;
|
||||
expect(
|
||||
/INSTALLED_PRODUCT_FEATURE(S|_IDS)/u.test(source),
|
||||
`${registry} must compose from the product manifest`,
|
||||
`${registry} composes a manifest-declared feature and must gate on the manifest`,
|
||||
).toBe(true);
|
||||
}
|
||||
// The rule has to still be watching something; an empty sweep would pass
|
||||
// no matter what happened to these files.
|
||||
expect(guarded).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user