feat: select the TechLog Studio adapter from runtime configuration
Adds TECH_LOG_STUDIO_SOURCE (MOCK | HTTP, default MOCK) to the V2 runtime
config schema so a build can switch createTechLogFeatureInstalledInput
between the mock and HTTP Studio gateways without a rebuild. V1 documents
predate the key and always normalize to MOCK. The HTTP gateway is
constructed with only { operations } per Task 4's actual signature -
no CSRF provider is wired here; that lands with attachCredentials at a
later composition-root task.
Updates every existing Studio test call site to the new required
createTechLogFeatureInstalledInput(context) signature via a shared
tests/helpers/studio-install-context.ts MOCK fixture, so the whole
existing Studio suite keeps exercising the mock adapter unchanged.
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
"SERVICE_WORKER": "DEFAULT",
|
||||
"OFFLINE_COMMANDS": "DEFAULT"
|
||||
},
|
||||
"TECH_LOG_STUDIO_SOURCE": "MOCK",
|
||||
"FEATURE_OVERRIDES": {
|
||||
"reference-feature": "DEFAULT"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"SERVICE_WORKER": "DEFAULT",
|
||||
"OFFLINE_COMMANDS": "DEFAULT"
|
||||
},
|
||||
"TECH_LOG_STUDIO_SOURCE": "MOCK",
|
||||
"FEATURE_OVERRIDES": {
|
||||
"reference-feature": "DEFAULT"
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"SERVICE_WORKER": "DEFAULT",
|
||||
"OFFLINE_COMMANDS": "DEFAULT"
|
||||
},
|
||||
"TECH_LOG_STUDIO_SOURCE": "HTTP",
|
||||
"FEATURE_OVERRIDES": {
|
||||
"reference-feature": "DEFAULT"
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"SERVICE_WORKER": "DEFAULT",
|
||||
"OFFLINE_COMMANDS": "DEFAULT"
|
||||
},
|
||||
"TECH_LOG_STUDIO_SOURCE": "HTTP",
|
||||
"FEATURE_OVERRIDES": {
|
||||
"reference-feature": "DEFAULT"
|
||||
}
|
||||
|
||||
@@ -505,6 +505,7 @@ export async function createRuntimeAdapters(
|
||||
});
|
||||
const featureInputs = createInstalledFeatureInputs({
|
||||
contractOperations,
|
||||
studioSource: config.TECH_LOG_STUDIO_SOURCE,
|
||||
});
|
||||
|
||||
return Object.freeze({
|
||||
|
||||
@@ -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
|
||||
? {}
|
||||
: {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -19,9 +19,10 @@ type InstalledFeatureInputs = Readonly<
|
||||
>;
|
||||
|
||||
export function createInstalledFeatureInputs(
|
||||
context: Parameters<typeof createReferenceFeatureInstalledInput>[0],
|
||||
context: Parameters<typeof createReferenceFeatureInstalledInput>[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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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) {
|
||||
<ApplicationProvider
|
||||
application={createTestApplication({
|
||||
session: createAnonymousSessionAdapter(),
|
||||
featureInputs: { "tech-log": createTechLogFeatureInstalledInput().input },
|
||||
featureInputs: { "tech-log": createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input },
|
||||
...(disabled
|
||||
? {
|
||||
productFeatures: createProductFeaturesStub({
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
|
||||
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 { TECH_LOG_FEATURE_ID } from "../../src/features/tech-log/application/tech-log-feature-input.ts";
|
||||
import { ApplicationProvider } from "../../src/presentation/providers/application-provider.tsx";
|
||||
import {
|
||||
@@ -102,7 +103,7 @@ function compileTimeGroupedRouteContract() {
|
||||
void compileTimeGroupedRouteContract;
|
||||
|
||||
function renderRouter() {
|
||||
const techLog = createTechLogFeatureInstalledInput();
|
||||
const techLog = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT);
|
||||
return render(
|
||||
<ApplicationProvider
|
||||
application={createTestApplication({
|
||||
|
||||
@@ -29,6 +29,7 @@ const runtime: Runtime = {
|
||||
OFFLINE_COMMANDS: "DEFAULT",
|
||||
},
|
||||
FEATURE_OVERRIDES: {},
|
||||
TECH_LOG_STUDIO_SOURCE: "MOCK",
|
||||
},
|
||||
configSchema: "V2",
|
||||
build: {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from "react-router-dom";
|
||||
|
||||
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,
|
||||
@@ -90,7 +91,7 @@ function renderDiscoveryRoute<RouteId extends DiscoveryRouteId>(
|
||||
),
|
||||
{ initialEntries: [initialEntry] },
|
||||
);
|
||||
const techLog = createTechLogFeatureInstalledInput().input;
|
||||
const techLog = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input;
|
||||
const view = render(
|
||||
<ApplicationProvider
|
||||
application={
|
||||
@@ -415,7 +416,7 @@ describe("TechLog explore discovery", () => {
|
||||
routeChunks,
|
||||
};
|
||||
const record = vi.fn();
|
||||
const techLog = createTechLogFeatureInstalledInput().input;
|
||||
const techLog = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input;
|
||||
const application = createTestApplication({
|
||||
diagnostics: { record },
|
||||
releaseInfo: {
|
||||
|
||||
@@ -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(
|
||||
<ApplicationProvider
|
||||
application={createTestApplication({
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
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 {
|
||||
TECH_LOG_ROUTE_REGISTRY,
|
||||
TECH_LOG_ROUTE_RUNTIME_CONTRACT,
|
||||
@@ -82,7 +83,7 @@ function renderPublicRoute(routeId: PublicIndexRouteId, initialEntry: string) {
|
||||
),
|
||||
{ initialEntries: [initialEntry] },
|
||||
);
|
||||
const techLog = createTechLogFeatureInstalledInput().input;
|
||||
const techLog = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input;
|
||||
const view = render(
|
||||
<ApplicationProvider
|
||||
application={createTestApplication({
|
||||
|
||||
@@ -6,6 +6,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createMemoryRouter, RouterProvider } from "react-router-dom";
|
||||
|
||||
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";
|
||||
@@ -47,7 +48,7 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
function renderShell(initialEntry = "/projects") {
|
||||
const techLog = createTechLogFeatureInstalledInput().input;
|
||||
const techLog = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input;
|
||||
const router = createMemoryRouter(
|
||||
[
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
<MemoryRouter initialEntries={["/studio/documents/new"]}>
|
||||
<RouterStudioProvider gateway={gateway}>
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
<MemoryRouter initialEntries={[`/studio/documents/${documentId}/edit`]}>
|
||||
@@ -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<StudioGateway["getDocument"]>()
|
||||
.mockRejectedValueOnce(new Error("offline"))
|
||||
|
||||
@@ -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<never>();
|
||||
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<StudioGateway["listPublications"]>()
|
||||
.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(<PublicationList />, gateway);
|
||||
await user.click(
|
||||
await screen.findByRole("button", { name: /Redis Adapter.*게시 취소/ }),
|
||||
|
||||
@@ -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<void>();
|
||||
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<WorkingCopyDetail>();
|
||||
let key = "";
|
||||
const gateway = {
|
||||
|
||||
@@ -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(
|
||||
<ApplicationProvider
|
||||
application={createTestApplication({
|
||||
@@ -56,7 +57,7 @@ function renderScreen(children: ReactNode, gateway: StudioGateway) {
|
||||
|
||||
describe("TechLog Studio index screens", () => {
|
||||
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(<StudioDashboard />, 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(<DocumentList />, 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(
|
||||
<>
|
||||
<NewDocumentForm />
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
<MemoryRouter initialEntries={["/studio"]}>
|
||||
@@ -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(
|
||||
<ValidationScreen documentId={FIXTURE_IDS.edgeTokenQuestion} />,
|
||||
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<StudioGateway["getDocument"]>()
|
||||
@@ -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<StudioGateway["getDocument"]>()
|
||||
.mockRejectedValueOnce(new Error("offline"))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
},
|
||||
}),
|
||||
});
|
||||
@@ -46,6 +46,7 @@ const runtime: Parameters<typeof loadReleaseManifest>[0] = {
|
||||
OFFLINE_COMMANDS: "DEFAULT",
|
||||
},
|
||||
FEATURE_OVERRIDES: {},
|
||||
TECH_LOG_STUDIO_SOURCE: "MOCK",
|
||||
},
|
||||
configSchema: "V2",
|
||||
validationDurationMs: 0,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -75,6 +75,7 @@ const runtimeV2 = {
|
||||
OFFLINE_COMMANDS: "DEFAULT",
|
||||
},
|
||||
FEATURE_OVERRIDES: {},
|
||||
TECH_LOG_STUDIO_SOURCE: "MOCK",
|
||||
} as const satisfies RuntimeConfigArtifact;
|
||||
|
||||
async function releaseV2With(
|
||||
|
||||
@@ -27,6 +27,7 @@ const runtime: Runtime = {
|
||||
OFFLINE_COMMANDS: "DEFAULT",
|
||||
},
|
||||
FEATURE_OVERRIDES: {},
|
||||
TECH_LOG_STUDIO_SOURCE: "MOCK",
|
||||
},
|
||||
configSchema: "V2",
|
||||
build: {
|
||||
|
||||
Reference in New Issue
Block a user