feat: add TechLog feature contracts

This commit is contained in:
DongHyeonka
2026-08-15 18:07:12 +09:00
parent 5479101c8c
commit 01ed1e9300
9 changed files with 2787 additions and 0 deletions
@@ -0,0 +1,33 @@
import { describe, expect, it } from "vitest";
import type { ApplicationFeatureInputs } from "../../../src/application/ports/in/application-api.ts";
import {
TECH_LOG_FEATURE_ID,
type TechLogFeatureInput,
} 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(
input: ApplicationFeatureInputs["tech-log"],
): TechLogFeatureInput {
return input;
}
describe("TechLog feature input", () => {
it("uses the fixed application feature identifier", () => {
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);
});
});
@@ -0,0 +1,126 @@
import { describe, expect, it } from "vitest";
import type { components } from "../../../src/features/tech-log/contracts/studio/generated.ts";
import {
isStudioGatewayError,
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";
type Inline = components["schemas"]["Inline"];
type CaseRenderBlock = components["schemas"]["CaseRenderBlock"];
const inlines: Inline[] = [
{ type: "TEXT", text: "plain" },
{ type: "EMPHASIS", children: [{ type: "TEXT", text: "emphasis" }] },
{ type: "STRONG", children: [{ type: "TEXT", text: "strong" }] },
{ type: "INLINE_CODE", code: "findById" },
{ type: "LINK", label: "TechLog", href: "https://example.com" },
{ type: "STATUS", label: "검증 필요", tone: "warning" },
];
const blocks: CaseRenderBlock[] = [
{ type: "HEADING", id: "problem", level: 2, content: [] },
{ type: "PARAGRAPH", content: [] },
{ type: "BLOCKQUOTE", content: [] },
{ type: "UNORDERED_LIST", items: [] },
{ type: "ORDERED_LIST", items: [] },
{ type: "CODE_BLOCK", code: "select 1", language: "sql", label: null },
{
type: "DATA_TABLE",
id: "query-counts",
caption: "Query counts",
rowHeaderColumn: null,
columns: [{ id: "query-counts-column-1", label: "Case", alignment: "LEFT" }],
rows: [],
},
{ type: "CALLOUT", tone: "warning", label: "주의", content: [] },
{ type: "EVIDENCE_FIGURE", key: "fetch-plan", alt: "Fetch plan", caption: "Measured fetch plan", zoom: true },
];
const gatewayMethodNames = [
"getDashboard",
"listDocuments",
"createDocument",
"getDocument",
"saveDocument",
"validateDocument",
"createPreview",
"getCurrentPreview",
"publishDocument",
"unpublishPublication",
"listPublications",
"getPublicationSnapshot",
"getCatalog",
] as const;
function gatewayWithEveryRequiredMethod(): StudioGateway {
return {
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", () => {
it("preserves every public wire discriminator exactly once", () => {
expect(inlines.map((inline) => inline.type)).toEqual([
"TEXT",
"EMPHASIS",
"STRONG",
"INLINE_CODE",
"LINK",
"STATUS",
]);
expect(blocks.map((block) => block.type)).toEqual([
"HEADING",
"PARAGRAPH",
"BLOCKQUOTE",
"UNORDERED_LIST",
"ORDERED_LIST",
"CODE_BLOCK",
"DATA_TABLE",
"CALLOUT",
"EVIDENCE_FIGURE",
]);
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", () => {
const error = new StudioGatewayError({
type: "https://techlog.dev/problems/conflict",
title: "Conflict",
status: 409,
detail: "The working copy has changed.",
code: "DOCUMENT_VERSION_CONFLICT",
retryable: false,
});
expect(error).toMatchObject({
name: "StudioGatewayError",
status: 409,
code: "DOCUMENT_VERSION_CONFLICT",
retryable: false,
});
expect(isStudioGatewayError(error)).toBe(true);
expect(isStudioGatewayError(new DOMException("Aborted", "AbortError"))).toBe(false);
});
});