diff --git a/config/runtime/development.json b/config/runtime/development.json index 63006b8..6c26b54 100644 --- a/config/runtime/development.json +++ b/config/runtime/development.json @@ -13,6 +13,7 @@ "SERVICE_WORKER": "DEFAULT", "OFFLINE_COMMANDS": "DEFAULT" }, + "TECH_LOG_STUDIO_SOURCE": "MOCK", "FEATURE_OVERRIDES": { "reference-feature": "DEFAULT" } diff --git a/config/runtime/local.json b/config/runtime/local.json index 952aa3c..8383085 100644 --- a/config/runtime/local.json +++ b/config/runtime/local.json @@ -13,6 +13,7 @@ "SERVICE_WORKER": "DEFAULT", "OFFLINE_COMMANDS": "DEFAULT" }, + "TECH_LOG_STUDIO_SOURCE": "MOCK", "FEATURE_OVERRIDES": { "reference-feature": "DEFAULT" } diff --git a/config/runtime/production.json b/config/runtime/production.json index 46ad778..3304e2e 100644 --- a/config/runtime/production.json +++ b/config/runtime/production.json @@ -14,6 +14,7 @@ "SERVICE_WORKER": "DEFAULT", "OFFLINE_COMMANDS": "DEFAULT" }, + "TECH_LOG_STUDIO_SOURCE": "HTTP", "FEATURE_OVERRIDES": { "reference-feature": "DEFAULT" } diff --git a/config/runtime/staging.json b/config/runtime/staging.json index 861f87b..fc376d8 100644 --- a/config/runtime/staging.json +++ b/config/runtime/staging.json @@ -14,6 +14,7 @@ "SERVICE_WORKER": "DEFAULT", "OFFLINE_COMMANDS": "DEFAULT" }, + "TECH_LOG_STUDIO_SOURCE": "HTTP", "FEATURE_OVERRIDES": { "reference-feature": "DEFAULT" } diff --git a/src/bootstrap/runtime-adapters.ts b/src/bootstrap/runtime-adapters.ts index baed010..57e71a5 100644 --- a/src/bootstrap/runtime-adapters.ts +++ b/src/bootstrap/runtime-adapters.ts @@ -505,6 +505,7 @@ export async function createRuntimeAdapters( }); const featureInputs = createInstalledFeatureInputs({ contractOperations, + studioSource: config.TECH_LOG_STUDIO_SOURCE, }); return Object.freeze({ diff --git a/src/bootstrap/runtime-config-schema.ts b/src/bootstrap/runtime-config-schema.ts index 7e87bfe..8539439 100644 --- a/src/bootstrap/runtime-config-schema.ts +++ b/src/bootstrap/runtime-config-schema.ts @@ -50,6 +50,12 @@ export type RuntimeConfig = Readonly<{ * feature the build did not install cannot be named into existence here. */ FEATURE_OVERRIDES: ProductFeatureOverrideMap; + /** + * §3.5-adjacent runtime switch: which Studio gateway adapter this build + * talks to. A V1 document predates the key and is always normalized to + * MOCK. + */ + TECH_LOG_STUDIO_SOURCE: "MOCK" | "HTTP"; /** Present only while a V1 document is still accepted. */ LEGACY_API_CONTRACT_VERSION?: string; }>; @@ -132,6 +138,11 @@ export function validateRuntimeConfig(value: unknown): RuntimeConfigValidation { FEATURE_OVERRIDES: Object.freeze({ ...(isV2 ? (parsed as RuntimeConfigV2).FEATURE_OVERRIDES : {}), }), + // A V1 document predates the Studio adapter switch; it always normalizes + // to the mock so the backend-absent default holds for legacy documents. + TECH_LOG_STUDIO_SOURCE: isV2 + ? (parsed as RuntimeConfigV2).TECH_LOG_STUDIO_SOURCE + : "MOCK", ...(isV2 ? {} : { diff --git a/src/contracts/env.ts b/src/contracts/env.ts index 5c00a61..5f7cb27 100644 --- a/src/contracts/env.ts +++ b/src/contracts/env.ts @@ -49,6 +49,9 @@ export const ENV_REGISTRY = Object.freeze({ CAPABILITY_OVERRIDES: runtime("public", false, null), // §3.5: likewise for features — subtractive, keyed by installed feature id. FEATURE_OVERRIDES: runtime("public", false, null), + // TechLog Studio gateway adapter selection. Defaults to MOCK while the + // backend does not exist yet. + TECH_LOG_STUDIO_SOURCE: runtime("public", false, "MOCK"), // §3.5: build-time narrowing of the product manifest. A feature left out // here is not imported by any registry and never reaches the bundle. VITE_PRODUCT_FEATURES: build("compile-time", false, null), diff --git a/src/contracts/release-artifacts.ts b/src/contracts/release-artifacts.ts index 7cd038c..d58e188 100644 --- a/src/contracts/release-artifacts.ts +++ b/src/contracts/release-artifacts.ts @@ -156,6 +156,9 @@ export const runtimeConfigV2ArtifactSchema = z CONFIG_SCHEMA_VERSION: z.literal("2.0"), CAPABILITY_OVERRIDES: capabilityOverrideArtifactSchema, FEATURE_OVERRIDES: featureOverrideArtifactSchema, + // TechLog Studio adapter selection. Backend is not live yet, so the + // default is the in-memory mock; a document may opt a build into HTTP. + TECH_LOG_STUDIO_SOURCE: z.enum(["MOCK", "HTTP"]).default("MOCK"), }) .strict() .superRefine(runtimeConfigArtifactInvariants); diff --git a/src/features/installed-feature-adapters.ts b/src/features/installed-feature-adapters.ts index 9f4b3ba..1d998e0 100644 --- a/src/features/installed-feature-adapters.ts +++ b/src/features/installed-feature-adapters.ts @@ -19,9 +19,10 @@ type InstalledFeatureInputs = Readonly< >; export function createInstalledFeatureInputs( - context: Parameters[0], + context: Parameters[0] & + Readonly<{ studioSource: "MOCK" | "HTTP" }>, ): InstalledFeatureInputs { - const techLogFeature = createTechLogFeatureInstalledInput(); + const techLogFeature = createTechLogFeatureInstalledInput(context); if (!INSTALLED_PRODUCT_FEATURE_IDS.includes(REFERENCE_FEATURE_ID)) { // A build that narrowed the reference feature out still ships TechLog. return Object.freeze({ diff --git a/src/features/tech-log/adapters/create-tech-log-feature-input.ts b/src/features/tech-log/adapters/create-tech-log-feature-input.ts index 93aef80..68f0ff7 100644 --- a/src/features/tech-log/adapters/create-tech-log-feature-input.ts +++ b/src/features/tech-log/adapters/create-tech-log-feature-input.ts @@ -2,13 +2,36 @@ import { TECH_LOG_FEATURE_ID, type TechLogFeatureInput, } from "../application/tech-log-feature-input.ts"; +import { + createHttpStudioGateway, + type StudioOperationExecutor, +} from "./http/http-studio-gateway.ts"; import { createMockStudioGateway } from "./mock/mock-studio-gateway.ts"; import { publicContentQueries } from "./static/public-query.ts"; -export function createTechLogFeatureInstalledInput() { +/** + * The HTTP gateway builds no headers of its own — `Idempotency-Key` comes + * from the execution intent and credentials (including CSRF) come from the + * platform's `attachCredentials` collaborator at the composition root. This + * context only has to say which adapter to construct and hand it the + * composed contract executor. + */ +export type TechLogInstallContext = Readonly<{ + studioSource: "MOCK" | "HTTP"; + contractOperations: StudioOperationExecutor; +}>; + +export function createTechLogFeatureInstalledInput( + context: TechLogInstallContext, +) { + const createStudioGateway = () => + context.studioSource === "MOCK" + ? createMockStudioGateway() + : createHttpStudioGateway({ operations: context.contractOperations }); + const input: TechLogFeatureInput = Object.freeze({ publicContent: publicContentQueries, - createStudioGateway: () => createMockStudioGateway(), + createStudioGateway, }); return Object.freeze({ diff --git a/tests/component/product-feature-switch.test.tsx b/tests/component/product-feature-switch.test.tsx index 6f61094..187594f 100644 --- a/tests/component/product-feature-switch.test.tsx +++ b/tests/component/product-feature-switch.test.tsx @@ -5,6 +5,7 @@ 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"; @@ -55,7 +56,7 @@ function renderAt(path: string, disabled: boolean) { ( ), { initialEntries: [initialEntry] }, ); - const techLog = createTechLogFeatureInstalledInput().input; + const techLog = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input; const view = render( { routeChunks, }; const record = vi.fn(); - const techLog = createTechLogFeatureInstalledInput().input; + const techLog = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input; const application = createTestApplication({ diagnostics: { record }, releaseInfo: { diff --git a/tests/features/tech-log/public-document-screens.test.tsx b/tests/features/tech-log/public-document-screens.test.tsx index c7d5add..eeebdeb 100644 --- a/tests/features/tech-log/public-document-screens.test.tsx +++ b/tests/features/tech-log/public-document-screens.test.tsx @@ -10,6 +10,7 @@ import { import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; 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 { TECH_LOG_ROUTE_REGISTRY, TECH_LOG_ROUTE_RUNTIME_CONTRACT, @@ -92,7 +93,7 @@ function renderDocumentRoute(routeId: DocumentRouteId, initialEntry: string) { ), { initialEntries: [initialEntry] }, ); - const techLog = createTechLogFeatureInstalledInput().input; + const techLog = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input; const view = render( { }); function renderShell(initialEntry = "/projects") { - const techLog = createTechLogFeatureInstalledInput().input; + const techLog = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input; const router = createMemoryRouter( [ { diff --git a/tests/features/tech-log/runtime-composition.test.ts b/tests/features/tech-log/runtime-composition.test.ts index 9069c61..39ce710 100644 --- a/tests/features/tech-log/runtime-composition.test.ts +++ b/tests/features/tech-log/runtime-composition.test.ts @@ -32,8 +32,11 @@ function inputOf(document: WorkingCopy) { return input; } -function installedInputs(): InstalledInputs { +function installedInputs( + studioSource: "MOCK" | "HTTP" = "MOCK", +): InstalledInputs { return createInstalledFeatureInputs({ + studioSource, contractOperations: { async execute() { throw new Error("reference executor is not used by composition tests"); @@ -59,6 +62,16 @@ test("installs TechLog beside the retained reference feature through application ); }); +test("selects the mock gateway by default and the HTTP gateway when switched", async () => { + const mockGateway = installedInputs("MOCK")["tech-log"].createStudioGateway(); + const httpGateway = installedInputs("HTTP")["tech-log"].createStudioGateway(); + + // The mock reads an in-memory fixture immediately. HTTP defers to the + // (here, throwing) contract executor, so it rejects instead. + await assert.doesNotReject(mockGateway.getDashboard()); + await assert.rejects(httpGateway.getDashboard()); +}); + test("each createStudioGateway call owns an isolated mutable Studio session", async () => { const installed = installedInputs(); const first = installed["tech-log"].createStudioGateway(); diff --git a/tests/features/tech-log/studio-decision-authoring.test.tsx b/tests/features/tech-log/studio-decision-authoring.test.tsx index 06eea32..cfe1364 100644 --- a/tests/features/tech-log/studio-decision-authoring.test.tsx +++ b/tests/features/tech-log/studio-decision-authoring.test.tsx @@ -7,6 +7,7 @@ import { MemoryRouter, useLocation, useNavigate } from "react-router-dom"; import { describe, expect, it } from "vitest"; 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 { FIXTURE_IDS } from "../../../src/features/tech-log/adapters/mock/fixtures.ts"; import type { StudioGateway } from "../../../src/features/tech-log/application/ports/studio-gateway.ts"; import type { WorkingCopyInput } from "../../../src/features/tech-log/contracts/studio/contract.ts"; @@ -68,7 +69,7 @@ function LocationProbe() { describe("TechLog Studio project decision authoring", () => { it("creates Decision as the fourth working-copy type", async () => { const user = userEvent.setup(); - const gateway = createTechLogFeatureInstalledInput().input.createStudioGateway(); + const gateway = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway(); render( @@ -91,7 +92,7 @@ describe("TechLog Studio project decision authoring", () => { }); it("validates, previews, and publishes a project-scoped Decision", async () => { - const gateway = createTechLogFeatureInstalledInput().input.createStudioGateway(); + const gateway = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway(); const document = await gateway.createDocument(decisionInput(), { idempotencyKey: "create-project-decision", }); @@ -133,7 +134,7 @@ describe("TechLog Studio project decision authoring", () => { it("edits Decision fields and renders the project decision card preview", async () => { const user = userEvent.setup(); - const gateway = createTechLogFeatureInstalledInput().input.createStudioGateway(); + const gateway = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway(); const document = await gateway.createDocument(decisionInput(), { idempotencyKey: "edit-project-decision", }); diff --git a/tests/features/tech-log/studio-editor-smoke.test.tsx b/tests/features/tech-log/studio-editor-smoke.test.tsx index 9d23517..6ac6bb5 100644 --- a/tests/features/tech-log/studio-editor-smoke.test.tsx +++ b/tests/features/tech-log/studio-editor-smoke.test.tsx @@ -7,6 +7,7 @@ import { MemoryRouter } from "react-router-dom"; import { afterEach, describe, expect, it, vi } from "vitest"; 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 { FIXTURE_IDS } from "../../../src/features/tech-log/adapters/mock/fixtures.ts"; import type { StudioGateway } from "../../../src/features/tech-log/application/ports/studio-gateway.ts"; import { DocumentEditorScreen } from "../../../src/features/tech-log/presentation/studio/components/document-editor-screen.tsx"; @@ -16,7 +17,7 @@ afterEach(() => vi.restoreAllMocks()); function renderEditor( documentId: string, - gateway: StudioGateway = createTechLogFeatureInstalledInput().input.createStudioGateway(), + gateway: StudioGateway = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway(), ) { return render( @@ -37,7 +38,7 @@ function labelsIn(container: HTMLElement, selector: string): string[] { describe("TechLog Studio document editor", () => { it("loads the Case controls in source order, owns dirty state, and renders an instant local preview", async () => { const user = userEvent.setup(); - const base = createTechLogFeatureInstalledInput().input.createStudioGateway(); + const base = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway(); const calls = { save: 0, validate: 0, preview: 0, publish: 0 }; const gateway = { ...base, @@ -237,7 +238,7 @@ describe("TechLog Studio document editor", () => { ); unknown.unmount(); - const base = createTechLogFeatureInstalledInput().input.createStudioGateway(); + const base = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway(); const getDocument = vi .fn() .mockRejectedValueOnce(new Error("offline")) diff --git a/tests/features/tech-log/studio-publication-flow.test.tsx b/tests/features/tech-log/studio-publication-flow.test.tsx index d76743c..630685e 100644 --- a/tests/features/tech-log/studio-publication-flow.test.tsx +++ b/tests/features/tech-log/studio-publication-flow.test.tsx @@ -15,6 +15,7 @@ import { } from "vitest"; 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 { FIXTURE_IDS } from "../../../src/features/tech-log/adapters/mock/fixtures.ts"; import { StudioGatewayError } from "../../../src/features/tech-log/application/ports/studio-gateway-error.ts"; import type { StudioGateway } from "../../../src/features/tech-log/application/ports/studio-gateway.ts"; @@ -79,7 +80,7 @@ afterEach(() => { function renderInStudio( node: React.ReactNode, - gateway: StudioGateway = createTechLogFeatureInstalledInput().input.createStudioGateway(), + gateway: StudioGateway = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway(), navigate: (href: string) => void = () => undefined, ) { return render( @@ -151,7 +152,7 @@ describe("TechLog Studio publication flow", () => { }); it("publishes a current warning preview only after every warning is acknowledged", async () => { - const gateway = createTechLogFeatureInstalledInput().input.createStudioGateway(); + const gateway = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway(); const document = await warningReadyDocument(gateway); const destinations: string[] = []; renderInStudio( @@ -180,7 +181,7 @@ describe("TechLog Studio publication flow", () => { it("keeps the publish pending state, preserves gateway command order, and retries with a new key", async () => { const user = userEvent.setup(); - const base = createTechLogFeatureInstalledInput().input.createStudioGateway(); + const base = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway(); const document = await warningReadyDocument(base); const first = deferred(); const calls: string[] = []; @@ -238,7 +239,7 @@ describe("TechLog Studio publication flow", () => { it("filters publication events and recovers a failed history read", async () => { const user = userEvent.setup(); - const base = createTechLogFeatureInstalledInput().input.createStudioGateway(); + const base = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway(); const listPublications = vi .fn() .mockRejectedValueOnce(new Error("offline")) @@ -260,7 +261,7 @@ describe("TechLog Studio publication flow", () => { it("unpublishes only the selected current row after the source confirmation", async () => { const user = userEvent.setup(); - const gateway = createTechLogFeatureInstalledInput().input.createStudioGateway(); + const gateway = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway(); renderInStudio(, gateway); await user.click( await screen.findByRole("button", { name: /Redis Adapter.*게시 취소/ }), diff --git a/tests/features/tech-log/studio-save-navigation.test.tsx b/tests/features/tech-log/studio-save-navigation.test.tsx index b3a449c..ba081e0 100644 --- a/tests/features/tech-log/studio-save-navigation.test.tsx +++ b/tests/features/tech-log/studio-save-navigation.test.tsx @@ -7,6 +7,7 @@ import { MemoryRouter } from "react-router-dom"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; 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 { FIXTURE_IDS } from "../../../src/features/tech-log/adapters/mock/fixtures.ts"; import { StudioGatewayError } from "../../../src/features/tech-log/application/ports/studio-gateway-error.ts"; import type { StudioGateway } from "../../../src/features/tech-log/application/ports/studio-gateway.ts"; @@ -104,7 +105,7 @@ async function getSavedDocument(gateway: StudioGateway) { describe("TechLog Studio save workflow", () => { it("shows save pending and success states and creates a fresh idempotency key for each command", async () => { const user = userEvent.setup(); - const base = createTechLogFeatureInstalledInput().input.createStudioGateway(); + const base = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway(); const first = deferred(); const keys: string[] = []; let calls = 0; @@ -144,7 +145,7 @@ describe("TechLog Studio save workflow", () => { it("keeps the local draft and disables overwrite after a revision conflict", async () => { const user = userEvent.setup(); - const gateway = createTechLogFeatureInstalledInput().input.createStudioGateway(); + const gateway = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway(); renderEditor(FIXTURE_IDS.conflictCase, gateway); await user.clear(await screen.findByLabelText("제목")); @@ -161,7 +162,7 @@ describe("TechLog Studio save workflow", () => { it("surfaces a save error and retries with a new user-command key without losing input", async () => { const user = userEvent.setup(); - const base = createTechLogFeatureInstalledInput().input.createStudioGateway(); + const base = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway(); const keys: string[] = []; const retryable = new StudioGatewayError({ type: "https://techlog.local/problems/studio-unavailable", @@ -203,7 +204,7 @@ describe("TechLog Studio save workflow", () => { describe("TechLog Studio dirty navigation", () => { it("opens the source modal dialog, protects browser unload, and restores focus when staying", async () => { const user = userEvent.setup(); - const gateway = createTechLogFeatureInstalledInput().input.createStudioGateway(); + const gateway = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway(); const destinations: string[] = []; const saved = await getSavedDocument(gateway); render( @@ -240,7 +241,7 @@ describe("TechLog Studio dirty navigation", () => { it("discards the draft and follows the pending internal destination", async () => { const user = userEvent.setup(); - const gateway = createTechLogFeatureInstalledInput().input.createStudioGateway(); + const gateway = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway(); const destinations: string[] = []; const saved = await getSavedDocument(gateway); render( @@ -263,7 +264,7 @@ describe("TechLog Studio dirty navigation", () => { it("saves the draft before following the pending destination", async () => { const user = userEvent.setup(); - const base = createTechLogFeatureInstalledInput().input.createStudioGateway(); + const base = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway(); const pending = deferred(); let key = ""; const gateway = { diff --git a/tests/features/tech-log/studio-screens-smoke.test.tsx b/tests/features/tech-log/studio-screens-smoke.test.tsx index 38da632..6cace79 100644 --- a/tests/features/tech-log/studio-screens-smoke.test.tsx +++ b/tests/features/tech-log/studio-screens-smoke.test.tsx @@ -7,6 +7,7 @@ import { MemoryRouter, useLocation, useNavigate } from "react-router-dom"; import { afterEach, describe, expect, it, vi } from "vitest"; 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 type { StudioGateway } from "../../../src/features/tech-log/application/ports/studio-gateway.ts"; import type { DocumentPage } from "../../../src/features/tech-log/contracts/studio/contract.ts"; import { DocumentList } from "../../../src/features/tech-log/presentation/studio/components/document-list.tsx"; @@ -40,7 +41,7 @@ function RouterStudioProvider({ } function renderScreen(children: ReactNode, gateway: StudioGateway) { - const input = createTechLogFeatureInstalledInput().input; + const input = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input; return render( { it("shows the source dashboard totals, workflow sections, status links, and sample rows", async () => { - const gateway = createTechLogFeatureInstalledInput().input.createStudioGateway(); + const gateway = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway(); const { container } = renderScreen(, gateway); @@ -81,7 +82,7 @@ describe("TechLog Studio index screens", () => { }); it("searches and filters documents and renders the source empty state", async () => { - const gateway = createTechLogFeatureInstalledInput().input.createStudioGateway(); + const gateway = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway(); renderScreen(, gateway); expect(await screen.findByText("7개의 작업본")).toBeVisible(); @@ -96,7 +97,7 @@ describe("TechLog Studio index screens", () => { }); it("cancels an obsolete list request and advances cursor pagination", async () => { - const base = createTechLogFeatureInstalledInput().input.createStudioGateway(); + const base = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway(); const all = await base.listDocuments({ limit: 20 }); let obsoleteSignal: AbortSignal | undefined; const firstPage: DocumentPage = { items: all.items.slice(0, 1), nextCursor: "next-page" }; @@ -122,7 +123,7 @@ describe("TechLog Studio index screens", () => { it("shows a retry action after a list failure and recovers", async () => { const user = userEvent.setup(); - const base = createTechLogFeatureInstalledInput().input.createStudioGateway(); + const base = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway(); const recovered = await base.listDocuments({ limit: 20 }); const gateway = { ...base, @@ -141,7 +142,7 @@ describe("TechLog Studio index screens", () => { it("creates the selected Question in the same session and redirects to its editor", async () => { const user = userEvent.setup(); - const gateway = createTechLogFeatureInstalledInput().input.createStudioGateway(); + const gateway = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway(); renderScreen( <> diff --git a/tests/features/tech-log/studio-shell-smoke.test.tsx b/tests/features/tech-log/studio-shell-smoke.test.tsx index db63fdb..e6febe9 100644 --- a/tests/features/tech-log/studio-shell-smoke.test.tsx +++ b/tests/features/tech-log/studio-shell-smoke.test.tsx @@ -6,6 +6,7 @@ import { Outlet, RouterProvider, createMemoryRouter } from "react-router-dom"; import { afterEach, describe, expect, it, vi } from "vitest"; 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 type { StudioGateway } from "../../../src/features/tech-log/application/ports/studio-gateway.ts"; import { StudioHomePage } from "../../../src/features/tech-log/presentation/studio/pages/studio-home-page.tsx"; import { StudioNotFoundPage } from "../../../src/features/tech-log/presentation/studio/pages/studio-not-found-page.tsx"; @@ -39,7 +40,7 @@ function renderStudio( ], { initialEntries: [initialEntry] }, ); - const installed = createTechLogFeatureInstalledInput().input; + const installed = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input; const application = createTestApplication({ featureInputs: { "tech-log": { ...installed, createStudioGateway }, @@ -56,7 +57,7 @@ function renderStudio( describe("TechLog Studio shell", () => { it("creates one application-provided gateway for child navigation and exposes the source header", async () => { const user = userEvent.setup(); - const gateway = createTechLogFeatureInstalledInput().input.createStudioGateway(); + const gateway = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway(); const createGateway = vi.fn(() => gateway); const { container, router } = renderStudio("/studio", createGateway); @@ -88,8 +89,8 @@ describe("TechLog Studio shell", () => { it("recreates the provider generation and cancels obsolete work after a persisted pageshow", async () => { const user = userEvent.setup(); let firstSignal: AbortSignal | undefined; - const first = createTechLogFeatureInstalledInput().input.createStudioGateway(); - const second = createTechLogFeatureInstalledInput().input.createStudioGateway(); + const first = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway(); + const second = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway(); vi.spyOn(first, "getDashboard").mockImplementation(({ signal } = {}) => { firstSignal = signal; return new Promise(() => {}); @@ -115,7 +116,7 @@ describe("TechLog Studio shell", () => { it("keeps unknown Studio routes inside the Studio shell without authentication UI", () => { const createGateway = vi.fn( - () => createTechLogFeatureInstalledInput().input.createStudioGateway(), + () => createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway(), ); renderStudio("/studio/does-not-exist", createGateway); diff --git a/tests/features/tech-log/studio-validation-preview.test.tsx b/tests/features/tech-log/studio-validation-preview.test.tsx index b83a029..733b85e 100644 --- a/tests/features/tech-log/studio-validation-preview.test.tsx +++ b/tests/features/tech-log/studio-validation-preview.test.tsx @@ -6,6 +6,7 @@ import { MemoryRouter } from "react-router-dom"; import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; 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 { FIXTURE_IDS } from "../../../src/features/tech-log/adapters/mock/fixtures.ts"; import type { StudioGateway } from "../../../src/features/tech-log/application/ports/studio-gateway.ts"; import { DocumentEditorScreen } from "../../../src/features/tech-log/presentation/studio/components/document-editor-screen.tsx"; @@ -39,7 +40,7 @@ afterEach(() => vi.restoreAllMocks()); function renderStudio( child: React.ReactNode, - gateway: StudioGateway = createTechLogFeatureInstalledInput().input.createStudioGateway(), + gateway: StudioGateway = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway(), ) { return render( @@ -51,7 +52,7 @@ function renderStudio( describe("TechLog Studio validation workflow", () => { it("reruns stale saved validation, reports exact freshness copy, and creates a new key per command", async () => { const user = userEvent.setup(); - const base = createTechLogFeatureInstalledInput().input.createStudioGateway(); + const base = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway(); const keys: string[] = []; const gateway = { ...base, @@ -80,7 +81,7 @@ describe("TechLog Studio validation workflow", () => { it("orders validation issues and links each JSON pointer to the affected editor field", async () => { const user = userEvent.setup(); - const gateway = createTechLogFeatureInstalledInput().input.createStudioGateway(); + const gateway = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway(); const view = renderStudio( , gateway, @@ -109,7 +110,7 @@ describe("TechLog Studio validation workflow", () => { }); it("aborts a route-obsolete validation read without surfacing an abort error", async () => { - const base = createTechLogFeatureInstalledInput().input.createStudioGateway(); + const base = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway(); let firstSignal: AbortSignal | undefined; const getDocument = vi .fn() @@ -144,7 +145,7 @@ describe("TechLog Studio validation workflow", () => { describe("TechLog Studio Public Preview workflow", () => { it("creates a missing preview, shows the current state, and renders the shared Public document", async () => { const user = userEvent.setup(); - const base = createTechLogFeatureInstalledInput().input.createStudioGateway(); + const base = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway(); const keys: string[] = []; const gateway = { ...base, @@ -200,7 +201,7 @@ describe("TechLog Studio Public Preview workflow", () => { }); it("shows retryable preview-load errors and retries the read", async () => { - const base = createTechLogFeatureInstalledInput().input.createStudioGateway(); + const base = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway(); const getDocument = vi .fn() .mockRejectedValueOnce(new Error("offline")) diff --git a/tests/features/tech-log/style-contract.test.ts b/tests/features/tech-log/style-contract.test.ts index 6ad287a..e5588b6 100644 --- a/tests/features/tech-log/style-contract.test.ts +++ b/tests/features/tech-log/style-contract.test.ts @@ -9,6 +9,7 @@ import { MemoryRouter } from "react-router-dom"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; 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 { FatalErrorState } from "../../../src/features/tech-log/presentation/public/components/fatal-error-state.tsx"; import { PublicShell } from "../../../src/features/tech-log/presentation/public/public-shell.tsx"; import { ApplicationProvider } from "../../../src/presentation/providers/application-provider.tsx"; @@ -33,7 +34,7 @@ afterAll(() => { }); function renderPublicSurface(node: React.ReactNode) { - const techLog = createTechLogFeatureInstalledInput().input; + const techLog = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input; const shell = createElement(PublicShell, { children: node }); const router = createElement( MemoryRouter, diff --git a/tests/helpers/studio-install-context.ts b/tests/helpers/studio-install-context.ts new file mode 100644 index 0000000..c6ee0ed --- /dev/null +++ b/tests/helpers/studio-install-context.ts @@ -0,0 +1,15 @@ +import type { TechLogInstallContext } from "../../src/features/tech-log/adapters/create-tech-log-feature-input.ts"; + +/** + * The install context the existing Studio test suite composes against. It + * always selects the mock adapter, so `contractOperations` is a throwing stub + * — `createTechLogFeatureInstalledInput` never reads it on the MOCK branch. + */ +export const MOCK_STUDIO_INSTALL_CONTEXT: TechLogInstallContext = Object.freeze({ + studioSource: "MOCK", + contractOperations: Object.freeze({ + async execute() { + throw new Error("contract executor is not used by the mock Studio gateway"); + }, + }), +}); diff --git a/tests/runtime-schema/release-manifest.test.ts b/tests/runtime-schema/release-manifest.test.ts index 9367f75..365f61e 100644 --- a/tests/runtime-schema/release-manifest.test.ts +++ b/tests/runtime-schema/release-manifest.test.ts @@ -46,6 +46,7 @@ const runtime: Parameters[0] = { OFFLINE_COMMANDS: "DEFAULT", }, FEATURE_OVERRIDES: {}, + TECH_LOG_STUDIO_SOURCE: "MOCK", }, configSchema: "V2", validationDurationMs: 0, diff --git a/tests/runtime-schema/runtime-config.test.ts b/tests/runtime-schema/runtime-config.test.ts index c3090a0..40b00f6 100644 --- a/tests/runtime-schema/runtime-config.test.ts +++ b/tests/runtime-schema/runtime-config.test.ts @@ -165,6 +165,36 @@ describe("runtime configuration boundary", () => { expect(result.validationDurationMs).toBe(0); }); + it("accepts TECH_LOG_STUDIO_SOURCE and defaults it to MOCK", () => { + const defaulted = validateRuntimeConfig(validConfig); + expect(defaulted.success).toBe(true); + if (defaulted.success) { + expect(defaulted.data.TECH_LOG_STUDIO_SOURCE).toBe("MOCK"); + } + + const explicit = validateRuntimeConfig({ + ...validConfig, + TECH_LOG_STUDIO_SOURCE: "HTTP", + }); + expect(explicit.success).toBe(true); + if (explicit.success) { + expect(explicit.data.TECH_LOG_STUDIO_SOURCE).toBe("HTTP"); + } + + expect( + validateRuntimeConfig({ + ...validConfig, + TECH_LOG_STUDIO_SOURCE: "LIVE", + }).success, + ).toBe(false); + + // A V1 document has no such key; normalization must still land on MOCK. + expect(validateRuntimeConfig(validV1Config)).toMatchObject({ + success: true, + data: { TECH_LOG_STUDIO_SOURCE: "MOCK" }, + }); + }); + it("returns only safe boot fields on failure", async () => { await expect( loadRuntimeConfig({ diff --git a/tests/unit/release-coherence.test.ts b/tests/unit/release-coherence.test.ts index 96160c7..31957ce 100644 --- a/tests/unit/release-coherence.test.ts +++ b/tests/unit/release-coherence.test.ts @@ -75,6 +75,7 @@ const runtimeV2 = { OFFLINE_COMMANDS: "DEFAULT", }, FEATURE_OVERRIDES: {}, + TECH_LOG_STUDIO_SOURCE: "MOCK", } as const satisfies RuntimeConfigArtifact; async function releaseV2With( diff --git a/tests/unit/runtime-adapters.test.ts b/tests/unit/runtime-adapters.test.ts index 0ffaeef..fbc4b17 100644 --- a/tests/unit/runtime-adapters.test.ts +++ b/tests/unit/runtime-adapters.test.ts @@ -27,6 +27,7 @@ const runtime: Runtime = { OFFLINE_COMMANDS: "DEFAULT", }, FEATURE_OVERRIDES: {}, + TECH_LOG_STUDIO_SOURCE: "MOCK", }, configSchema: "V2", build: {