Vendors the canonical studio-v1.yaml, generates types via an isolated `pnpm dlx` toolchain (openapi-typescript needs TypeScript 5's classic compiler API; this repo pins TypeScript 7.0.2 per VD-01, whose root export has none), and adds an offline drift gate that checks the vendored yaml/generated types/canonical-source.json against each other without touching the sibling design-package repo or the network. Regenerating from canonical surfaces real, new required fields on existing schemas (WorkingCopyDetail.nextAction, PreviewDetail/PublicPreview .dependencyRevision, StudioDashboard.totals.needsValidation, PublicationSnapshot.contentFormatVersion/rendererContractVersion) and a new required EvidenceFigureBlock.asset. The mock gateway and fixtures are updated to satisfy the former; the latter exposes a real authoring- vs-rendering conflation in the content-format parser (it declared its output as the server's fully-resolved PublicRenderModel type, which it has no asset catalog to satisfy). Split that boundary: the parser now produces an authoring block type omitting the resolved asset, and each of its three consumers (the mock gateway, the Studio instant preview, and the static Case demo page) attaches the resolved descriptor from its own asset source through a shared, pure domain-level resolver. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
128 lines
3.7 KiB
TypeScript
128 lines
3.7 KiB
TypeScript
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 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 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,
|
|
asset: {
|
|
assetId: "44444444-4444-4444-8444-444444444441",
|
|
assetKey: "fetch-plan",
|
|
mediaType: "image/svg+xml",
|
|
publicPath: "/media/fetch-plan.svg",
|
|
width: 1080,
|
|
height: 420,
|
|
decorative: false,
|
|
},
|
|
},
|
|
];
|
|
|
|
type RequiredStudioGatewayOperation =
|
|
| "getDashboard"
|
|
| "listDocuments"
|
|
| "createDocument"
|
|
| "getDocument"
|
|
| "saveDocument"
|
|
| "validateDocument"
|
|
| "createPreview"
|
|
| "getCurrentPreview"
|
|
| "publishDocument"
|
|
| "unpublishPublication"
|
|
| "listPublications"
|
|
| "getPublicationSnapshot"
|
|
| "getCatalog";
|
|
|
|
type StudioGatewayExposesNoMissingOrAdditionalOperations = Assert<
|
|
Equal<keyof StudioGateway, RequiredStudioGatewayOperation>
|
|
>;
|
|
|
|
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("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: "VERSION_CONFLICT",
|
|
retryable: false,
|
|
});
|
|
|
|
expect(error).toMatchObject({
|
|
name: "StudioGatewayError",
|
|
status: 409,
|
|
code: "VERSION_CONFLICT",
|
|
retryable: false,
|
|
});
|
|
expect(isStudioGatewayError(error)).toBe(true);
|
|
expect(isStudioGatewayError(new DOMException("Aborted", "AbortError"))).toBe(false);
|
|
});
|
|
});
|