test: enforce exact TechLog contract keys

This commit is contained in:
DongHyeonka
2026-08-15 18:16:21 +09:00
parent 01ed1e9300
commit 82f94423e5
2 changed files with 39 additions and 59 deletions
+13 -19
View File
@@ -5,29 +5,23 @@ import {
TECH_LOG_FEATURE_ID, TECH_LOG_FEATURE_ID,
type TechLogFeatureInput, type TechLogFeatureInput,
} from "../../../src/features/tech-log/application/tech-log-feature-input.ts"; } from "../../../src/features/tech-log/application/tech-log-feature-input.ts";
import type { PublicContentQueries } from "../../../src/features/tech-log/application/ports/public-content-queries.ts";
import type { StudioGateway } from "../../../src/features/tech-log/application/ports/studio-gateway.ts";
function acceptsApplicationFeatureInput( type Equal<Left, Right> =
input: ApplicationFeatureInputs["tech-log"], (<Value>() => Value extends Left ? 1 : 2) extends
): TechLogFeatureInput { (<Value>() => Value extends Right ? 1 : 2)
return input; ? true
} : false;
type Assert<Condition extends true> = Condition;
type TechLogFeatureInputExposesNoMissingOrAdditionalKeys = Assert<
Equal<keyof ApplicationFeatureInputs["tech-log"], "publicContent" | "createStudioGateway">
>;
type TechLogFeatureInputRegistryValueMatchesFeatureContract = Assert<
Equal<ApplicationFeatureInputs["tech-log"], TechLogFeatureInput>
>;
describe("TechLog feature input", () => { describe("TechLog feature input", () => {
it("uses the fixed application feature identifier", () => { it("uses the fixed application feature identifier", () => {
expect(TECH_LOG_FEATURE_ID).toBe("tech-log"); expect(TECH_LOG_FEATURE_ID).toBe("tech-log");
}); });
it("accepts public queries and a Studio gateway factory through the application registry", () => {
const publicContent = {} as PublicContentQueries;
const gateway = {} as StudioGateway;
const input = acceptsApplicationFeatureInput({
publicContent,
createStudioGateway: () => gateway,
});
expect(input.publicContent).toBe(publicContent);
expect(input.createStudioGateway()).toBe(gateway);
});
}); });
+26 -40
View File
@@ -9,6 +9,13 @@ import type {
StudioGateway, StudioGateway,
} from "../../../src/features/tech-log/application/ports/studio-gateway.ts"; } from "../../../src/features/tech-log/application/ports/studio-gateway.ts";
type Equal<Left, Right> =
(<Value>() => Value extends Left ? 1 : 2) extends
(<Value>() => Value extends Right ? 1 : 2)
? true
: false;
type Assert<Condition extends true> = Condition;
type Inline = components["schemas"]["Inline"]; type Inline = components["schemas"]["Inline"];
type CaseRenderBlock = components["schemas"]["CaseRenderBlock"]; type CaseRenderBlock = components["schemas"]["CaseRenderBlock"];
@@ -40,39 +47,24 @@ const blocks: CaseRenderBlock[] = [
{ type: "EVIDENCE_FIGURE", key: "fetch-plan", alt: "Fetch plan", caption: "Measured fetch plan", zoom: true }, { type: "EVIDENCE_FIGURE", key: "fetch-plan", alt: "Fetch plan", caption: "Measured fetch plan", zoom: true },
]; ];
const gatewayMethodNames = [ type RequiredStudioGatewayOperation =
"getDashboard", | "getDashboard"
"listDocuments", | "listDocuments"
"createDocument", | "createDocument"
"getDocument", | "getDocument"
"saveDocument", | "saveDocument"
"validateDocument", | "validateDocument"
"createPreview", | "createPreview"
"getCurrentPreview", | "getCurrentPreview"
"publishDocument", | "publishDocument"
"unpublishPublication", | "unpublishPublication"
"listPublications", | "listPublications"
"getPublicationSnapshot", | "getPublicationSnapshot"
"getCatalog", | "getCatalog";
] as const;
function gatewayWithEveryRequiredMethod(): StudioGateway { type StudioGatewayExposesNoMissingOrAdditionalOperations = Assert<
return { Equal<keyof StudioGateway, RequiredStudioGatewayOperation>
getDashboard: async () => undefined as never, >;
listDocuments: async () => undefined as never,
createDocument: async () => undefined as never,
getDocument: async () => undefined as never,
saveDocument: async () => undefined as never,
validateDocument: async () => undefined as never,
createPreview: async () => undefined as never,
getCurrentPreview: async () => undefined as never,
publishDocument: async () => undefined as never,
unpublishPublication: async () => undefined as never,
listPublications: async () => undefined as never,
getPublicationSnapshot: async () => undefined as never,
getCatalog: async () => undefined as never,
};
}
describe("TechLog Studio contracts", () => { describe("TechLog Studio contracts", () => {
it("preserves every public wire discriminator exactly once", () => { it("preserves every public wire discriminator exactly once", () => {
@@ -98,26 +90,20 @@ describe("TechLog Studio contracts", () => {
expect(new Set(blocks.map((block) => block.type)).size).toBe(blocks.length); expect(new Set(blocks.map((block) => block.type)).size).toBe(blocks.length);
}); });
it("requires the exact Studio gateway operation set", () => {
expect(Object.keys(gatewayWithEveryRequiredMethod()).sort()).toEqual(
[...gatewayMethodNames].sort(),
);
});
it("keeps RFC 9457-style gateway failures inspectable at the application boundary", () => { it("keeps RFC 9457-style gateway failures inspectable at the application boundary", () => {
const error = new StudioGatewayError({ const error = new StudioGatewayError({
type: "https://techlog.dev/problems/conflict", type: "https://techlog.dev/problems/conflict",
title: "Conflict", title: "Conflict",
status: 409, status: 409,
detail: "The working copy has changed.", detail: "The working copy has changed.",
code: "DOCUMENT_VERSION_CONFLICT", code: "VERSION_CONFLICT",
retryable: false, retryable: false,
}); });
expect(error).toMatchObject({ expect(error).toMatchObject({
name: "StudioGatewayError", name: "StudioGatewayError",
status: 409, status: 409,
code: "DOCUMENT_VERSION_CONFLICT", code: "VERSION_CONFLICT",
retryable: false, retryable: false,
}); });
expect(isStudioGatewayError(error)).toBe(true); expect(isStudioGatewayError(error)).toBe(true);