// @vitest-environment jsdom import { render, screen } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import { createAnonymousSessionAdapter } from "../../src/adapters/auth/external-session-adapter.ts"; import { createTechLogFeatureInstalledInput } from "../../src/features/tech-log/adapters/create-tech-log-feature-input.ts"; import { MOCK_STUDIO_INSTALL_CONTEXT } from "../helpers/studio-install-context.ts"; import { createTestApplication } from "../helpers/create-test-application.ts"; import { createProductFeaturesStub } from "../helpers/runtime-capabilities-stub.ts"; /** * §3.5. The runtime kill switch, exercised through the running app. * * Withdrawing a feature from navigation is not the same as taking it out of * service: a typed deep link would still mount it. Both halves are asserted on * the same render, so the switch cannot be half-wired. * * The template asserted this by rendering its own demonstration screens. Those * screens exist to explain the template, and a product replaces them with its * domain — so a test that needs them makes deleting them look like a * regression. It is not: the mechanism under test is the ownership lookup plus * `isFeatureActive`, and neither has anything to do with which screens ship. * * So the ownership map is the fixture. `ROUTE_FEATURE_OWNER` is empty in this * product because no installed feature contributes a route; one real registered * route is attributed to a real installed feature here, and everything else — * router, shell, components, codecs — is the product's own. */ const OWNED_ROUTE_ID = "TECH_LOG_EXPLORE"; const OWNER_FEATURE_ID = "reference-feature"; const OWNED_ROUTE_PATH = "/explore"; const OWNED_ROUTE_NAV_LABEL = "탐색"; vi.mock("../../src/features/installed-feature-contracts.ts", async (original) => { const actual = await original< typeof import("../../src/features/installed-feature-contracts.ts") >(); return { ...actual, ROUTE_FEATURE_OWNER: Object.freeze({ [OWNED_ROUTE_ID]: OWNER_FEATURE_ID, }), }; }); const { AppRouter } = await import("../../src/presentation/routes/app-router.tsx"); const { ApplicationProvider } = await import( "../../src/presentation/providers/application-provider.tsx" ); function renderAt(path: string, disabled: boolean) { window.history.pushState({}, "", path); return render( , ); } function outOfServiceSurface() { return screen.queryByText( (_, element) => element?.getAttribute("data-disabled-feature") === OWNER_FEATURE_ID, ); } describe("runtime product feature switch", () => { it("takes the owned route out of service for a direct deep link", async () => { renderAt(OWNED_ROUTE_PATH, true); const surface = await screen.findByText( (_, element) => element?.getAttribute("data-disabled-feature") === OWNER_FEATURE_ID, {}, { timeout: 5000 }, ); expect(surface).toBeTruthy(); }); it("serves the same deep link while the feature is active", async () => { renderAt(OWNED_ROUTE_PATH, false); expect(outOfServiceSurface()).toBeNull(); }); }); /** * Composition invariants that hold whichever screens a product ships. The first * one is what caught a bad merge resolution: composing the route registry from * every installed feature's `contract.routes` registered the reference * feature's paths after this product had deleted their components. */ describe("route composition", () => { it("registers no route that cannot be mounted", async () => { const { ROUTE_REGISTRY } = await vi.importActual< typeof import("../../src/features/installed-feature-contracts.ts") >("../../src/features/installed-feature-contracts.ts"); const { ROUTE_RUNTIME } = await import( "../../src/features/installed-feature-runtimes.tsx" ); const unmountable = Object.keys(ROUTE_REGISTRY).filter( (routeId) => !(routeId in ROUTE_RUNTIME), ); expect(unmountable).toEqual([]); }); /** * The other half of the switch — withdrawing a disabled feature's entry from * navigation — lives in the template's `PrimaryNavigation`, which this * product does not render: TechLog's public header is a hand-written list of * paths, and the studio shell has its own. So the withdrawal half is not * implemented here. * * That is harmless only while no feature-owned route is navigable, which is * what this asserts. If it fails, the header has to consult * `ROUTE_FEATURE_OWNER` (or navigation has to move back onto * `NAVIGATION_ROUTES`) before that route ships — otherwise a disabled * feature would keep advertising a link to a page that refuses to load. */ it("has no navigable feature-owned route while the shell is not feature-aware", async () => { const actual = await vi.importActual< typeof import("../../src/features/installed-feature-contracts.ts") >("../../src/features/installed-feature-contracts.ts"); const navigableOwned = actual.NAVIGATION_ROUTES.filter( (definition) => actual.ROUTE_FEATURE_OWNER[definition.routeId] !== undefined, ).map((definition) => definition.routeId); expect(navigableOwned).toEqual([]); }); it("attributes ownership only to routes the product registered", async () => { const actual = await vi.importActual< typeof import("../../src/features/installed-feature-contracts.ts") >("../../src/features/installed-feature-contracts.ts"); for (const routeId of Object.keys(actual.ROUTE_FEATURE_OWNER)) { expect(Object.keys(actual.ROUTE_REGISTRY)).toContain(routeId); } }); });