Files
tech-log-frontend/tests/features/tech-log/route-contract.test.ts
T
DongHyeonkaandClaude Opus 5 fd73bc88a1 fix: Decision 미리보기의 결정일 요구를 풀고, 화면 테스트가 실제 동작을 다시 말하게 한다
Decision 은 결정일이 없으면 미리보기가 열리지 않았다. 검증은 그것을 경고로만 다루므로
날짜 없이 게시할 수 있는데 렌더 모델이 필수로 요구했다 — 작성자는 "경고라면서 왜 안
되냐"를 만난다. 계약을 nullable 로 열고 화면이 "결정일 미정"이라고 말하게 한다.

한 칸의 실패가 화면을 통째로 날리지 않게 한다. `Promise.all([gateway.foo()])` 은 foo 가
거절하는 것만 잡는다 — 호출이 동기적으로 던지면 배열을 만드는 중에 터져 rejection
handler 를 지나지 못하고, 그러면 홈 focus 한 칸 때문에 대시보드 전체가 빈 화면이 된다.
프로젝트 편집도 같은 모양이라 함께 고친다.

`IntersectionObserver` 가 없는 환경을 견딘다. 목차는 픽스처 Case 하나에서만 쓰여 그런
환경을 만난 적이 없었는데, 모든 Case 가 목차를 받게 되면서 jsdom 에서 문서가 통째로
깨졌다. 없으면 "지금 읽는 절" 표시만 못 할 뿐이다.

픽스처의 최근 기록에서 릴리스를 뺀다. 서버의 `latestEntries` 는 공개 투영에서 고르므로
릴리스가 없고, 홈이 릴리스를 따로 읽어 합친다 — 픽스처가 넣으면 같은 릴리스가 두 번
나온다.

화면 테스트는 `test:unit` 이 아니라 `test:tech-log` 가 돌린다. 그것을 돌리지 않아 위
두 결함과, 라우트 두 개·`--body-copy`·Case 배치 통합·활동 링크 제거처럼 의도한 변경에
고정돼 있던 단언들이 23건 빨간 채로 여러 커밋을 지나갔다. 단언을 실제 동작으로 옮긴다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEHXspz4rv5pB5wiiSsVDu
2026-08-24 18:03:11 +09:00

235 lines
10 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
TECH_LOG_ROUTE_REGISTRY,
TECH_LOG_ROUTE_RUNTIME_CONTRACT,
} from "../../../src/features/tech-log/contracts/tech-log-route-contract.ts";
import { TECH_LOG_MESSAGE_CATALOGS } from "../../../src/features/tech-log/contracts/tech-log-message-catalog.ts";
import { TECH_LOG_ROUTE_CODECS } from "../../../src/features/tech-log/presentation/tech-log-route-codecs.ts";
import { ROUTE_REGISTRY } from "../../../src/features/installed-feature-contracts.ts";
import { ROUTE_RUNTIME } from "../../../src/features/installed-feature-runtimes.tsx";
import { buildRouteUrlFromContract } from "../../../src/presentation/routes/route-codecs.ts";
import { PLATFORM_ROUTE_CODECS } from "../../../src/presentation/routes/platform-route-codecs.ts";
const expectedRoutes = [
["TECH_LOG_HOME", "/", "PUBLIC", null, "TechLogHomeSearch"],
["TECH_LOG_EXPLORE", "/explore", "PUBLIC", null, "TechLogExploreSearch"],
["TECH_LOG_EXPLORE_KIND", "/explore/:kind", "PUBLIC", "TechLogExploreKindParams", "TechLogExploreKindSearch"],
["TECH_LOG_CASE", "/cases/:slug", "PUBLIC", "TechLogSlugParams", "TechLogCaseStateSearch"],
["TECH_LOG_REFERENCE", "/references/:slug", "PUBLIC", "TechLogSlugParams", null],
["TECH_LOG_QUESTION", "/questions/:slug", "PUBLIC", "TechLogSlugParams", null],
["TECH_LOG_TOPIC", "/topics/:slug", "PUBLIC", "TechLogSlugParams", null],
["TECH_LOG_PROJECTS", "/projects", "PUBLIC", null, null],
["TECH_LOG_PROJECT", "/projects/:slug", "PUBLIC", "TechLogSlugParams", null],
["TECH_LOG_PROJECT_RECORDS", "/projects/:slug/records", "PUBLIC", "TechLogSlugParams", null],
["TECH_LOG_PROJECT_DECISIONS", "/projects/:slug/decisions", "PUBLIC", "TechLogSlugParams", null],
["TECH_LOG_PROJECT_ACTIVITY", "/projects/:slug/activity", "PUBLIC", "TechLogSlugParams", null],
["TECH_LOG_RELEASES", "/releases", "PUBLIC", null, null],
["TECH_LOG_RELEASE", "/releases/:version", "PUBLIC", "TechLogVersionParams", null],
["TECH_LOG_PROFILE", "/profile", "PUBLIC", null, null],
["TECH_LOG_SEARCH", "/search", "PUBLIC", null, "TechLogSearchQuery"],
["TECH_LOG_STUDIO_HOME", "/studio", "STUDIO", null, null],
["TECH_LOG_STUDIO_DOCUMENTS", "/studio/documents", "STUDIO", null, null],
["TECH_LOG_STUDIO_DOCUMENT_NEW", "/studio/documents/new", "STUDIO", null, null],
["TECH_LOG_STUDIO_DOCUMENT_EDIT", "/studio/documents/:id/edit", "STUDIO", "TechLogDocumentIdParams", null],
["TECH_LOG_STUDIO_DOCUMENT_VALIDATION", "/studio/documents/:id/validation", "STUDIO", "TechLogDocumentIdParams", null],
["TECH_LOG_STUDIO_DOCUMENT_PREVIEW", "/studio/documents/:id/preview", "STUDIO", "TechLogDocumentIdParams", null],
["TECH_LOG_STUDIO_DOCUMENT_PUBLISH", "/studio/documents/:id/publish", "STUDIO", "TechLogDocumentIdParams", null],
["TECH_LOG_STUDIO_PUBLICATIONS", "/studio/publications", "STUDIO", null, null],
["TECH_LOG_STUDIO_PUBLICATION_PREVIEW", "/studio/publications/:publicationEventId/preview", "STUDIO", "TechLogPublicationEventIdParams", null],
["TECH_LOG_STUDIO_ASSETS", "/studio/assets", "STUDIO", null, null],
["TECH_LOG_STUDIO_TAXONOMY", "/studio/taxonomy", "STUDIO", null, null],
["TECH_LOG_STUDIO_PROJECT_EDIT", "/studio/projects/:id", "STUDIO", "TechLogDocumentIdParams", null],
["TECH_LOG_STUDIO_RELEASES", "/studio/releases", "STUDIO", null, null],
["TECH_LOG_STUDIO_RELEASE_EDIT", "/studio/releases/:id", "STUDIO", "TechLogDocumentIdParams", null],
["TECH_LOG_STUDIO_NOT_FOUND", "/studio/*", "STUDIO", "TechLogStudioSplat", null],
["NOT_FOUND", "*", "PUBLIC", "NotFoundSplat", null],
] as const;
const expectedTitles = {
TECH_LOG_HOME: "TechLog",
TECH_LOG_EXPLORE: "탐색",
TECH_LOG_EXPLORE_KIND: "유형별 탐색",
TECH_LOG_CASE: "Case",
TECH_LOG_REFERENCE: "Reference",
TECH_LOG_QUESTION: "Open Question",
TECH_LOG_TOPIC: "Topic",
TECH_LOG_PROJECTS: "프로젝트",
TECH_LOG_PROJECT: "프로젝트",
TECH_LOG_PROJECT_RECORDS: "프로젝트 기록",
TECH_LOG_PROJECT_DECISIONS: "프로젝트 결정",
TECH_LOG_PROJECT_ACTIVITY: "프로젝트 활동",
TECH_LOG_RELEASES: "변경 기록",
TECH_LOG_RELEASE: "변경 기록",
TECH_LOG_PROFILE: "프로필",
TECH_LOG_SEARCH: "검색",
TECH_LOG_STUDIO_HOME: "TechLog Studio",
TECH_LOG_STUDIO_DOCUMENTS: "작업본",
TECH_LOG_STUDIO_DOCUMENT_NEW: "새 문서",
TECH_LOG_STUDIO_DOCUMENT_EDIT: "문서 편집",
TECH_LOG_STUDIO_DOCUMENT_VALIDATION: "문서 검증",
TECH_LOG_STUDIO_DOCUMENT_PREVIEW: "Public Preview",
TECH_LOG_STUDIO_DOCUMENT_PUBLISH: "게시",
TECH_LOG_STUDIO_PUBLICATIONS: "게시 기록",
TECH_LOG_STUDIO_PUBLICATION_PREVIEW: "게시 Snapshot",
TECH_LOG_STUDIO_ASSETS: "Asset",
TECH_LOG_STUDIO_TAXONOMY: "주제와 프로젝트",
TECH_LOG_STUDIO_PROJECT_EDIT: "프로젝트 편집",
TECH_LOG_STUDIO_RELEASES: "릴리즈",
TECH_LOG_STUDIO_RELEASE_EDIT: "릴리즈 편집",
TECH_LOG_STUDIO_NOT_FOUND: "Studio 화면을 찾을 수 없습니다",
NOT_FOUND: "페이지를 찾을 수 없습니다.",
} as const;
describe("TechLog route boundary contract", () => {
it("freezes the standalone 31-route inventory before runtime installation", () => {
expect(
Object.values(TECH_LOG_ROUTE_REGISTRY).map((definition) => [
definition.routeId,
definition.path,
definition.layoutGroup,
definition.paramsSchema,
definition.searchSchema,
]),
).toEqual(expectedRoutes);
expect(
Object.values(TECH_LOG_ROUTE_REGISTRY).map((definition) => [
definition.routeId,
definition.access,
definition.chunkId,
]),
).toEqual(
// Studio is the authenticated surface, Public is not. Derived here from
// the frozen inventory's own layoutGroup column rather than restated per
// route, so a Studio route added without a gate fails this table too —
// every TechLog route used to be registered "public", which made the
// router's access decision a no-op for Studio.
expectedRoutes.map(([routeId, , layoutGroup]) => [
routeId,
layoutGroup === "STUDIO" ? "session-required" : "public",
routeId === "NOT_FOUND"
? "route-not-found"
: `route-${routeId.toLowerCase().replaceAll("_", "-")}`,
]),
);
});
it("keeps runtime module and codec bindings aligned with every dormant route", () => {
expect(Object.keys(TECH_LOG_ROUTE_RUNTIME_CONTRACT)).toEqual(
expectedRoutes.map(([routeId]) => routeId),
);
for (const [routeId, , , paramsSchema, searchSchema] of expectedRoutes) {
expect(TECH_LOG_ROUTE_RUNTIME_CONTRACT[routeId]).toEqual({
routeId,
moduleId:
routeId === "NOT_FOUND"
? "route-not-found"
: `route-${routeId.toLowerCase().replaceAll("_", "-")}`,
paramsCodec: paramsSchema ?? "none",
searchCodec: searchSchema ?? "none",
});
}
});
it("preserves source navigation labels and the complete route message catalog", () => {
expect(
Object.values(TECH_LOG_ROUTE_REGISTRY)
.filter((definition) => definition.navigationLabel !== null)
.map(({ routeId, layoutGroup, navigationLabel, navigationOrder }) => [
routeId,
layoutGroup,
navigationLabel,
navigationOrder,
]),
).toEqual([
["TECH_LOG_EXPLORE", "PUBLIC", "탐색", 10],
["TECH_LOG_PROJECTS", "PUBLIC", "프로젝트", 20],
["TECH_LOG_RELEASES", "PUBLIC", "변경 기록", 30],
["TECH_LOG_PROFILE", "PUBLIC", "프로필", 40],
["TECH_LOG_STUDIO_DOCUMENTS", "STUDIO", "작업본", 10],
["TECH_LOG_STUDIO_DOCUMENT_NEW", "STUDIO", "새 문서", 30],
["TECH_LOG_STUDIO_PUBLICATIONS", "STUDIO", "게시 기록", 20],
["TECH_LOG_STUDIO_TAXONOMY", "STUDIO", "주제·프로젝트", 40],
["TECH_LOG_STUDIO_RELEASES", "STUDIO", "릴리즈", 50],
]);
for (const locale of ["ko-KR", "en-US"] as const) {
const catalog: Readonly<Record<string, string>> =
TECH_LOG_MESSAGE_CATALOGS[locale];
expect(Object.keys(catalog)).toHaveLength(64);
for (const [routeId, title] of Object.entries(expectedTitles)) {
expect(catalog[`route.${routeId}.title`]).toBe(title);
expect(catalog[`route.${routeId}.navigation`]).toBe(title);
}
}
});
it("normalizes source search semantics and rejects empty path parameters", () => {
expect(
TECH_LOG_ROUTE_CODECS.TechLogExploreSearch.parse({
type: ["CASE", "REFERENCE"],
topic: [" JPA ", "Redis"],
project: " tech-log ",
unknown: "drop-me",
}),
).toEqual({ type: "CASE", topic: "JPA", project: "tech-log" });
expect(
TECH_LOG_ROUTE_CODECS.TechLogHomeSearch.parse({
focus: ["architecture", "delivery"],
state: ["latest-empty", "site-error"],
}),
).toEqual({ focus: "architecture", state: "latest-empty" });
expect(
TECH_LOG_ROUTE_CODECS.TechLogSearchQuery.parse({ q: [" gateway ", "ignored"] }),
).toEqual({ q: "gateway" });
expect(
TECH_LOG_ROUTE_CODECS.TechLogSlugParams.safeParse({ slug: "" }).success,
).toBe(false);
expect(
TECH_LOG_ROUTE_CODECS.TechLogDocumentIdParams.safeParse({ id: "" }).success,
).toBe(false);
});
it("builds canonical TechLog URLs with non-empty string codecs", () => {
const route = TECH_LOG_ROUTE_REGISTRY.TECH_LOG_STUDIO_PUBLICATION_PREVIEW;
const runtime =
TECH_LOG_ROUTE_RUNTIME_CONTRACT.TECH_LOG_STUDIO_PUBLICATION_PREVIEW;
expect(
buildRouteUrlFromContract(route, runtime, {
...PLATFORM_ROUTE_CODECS,
...TECH_LOG_ROUTE_CODECS,
}, {
params: { publicationEventId: "event/01" },
}),
).toBe("/studio/publications/event%2F01/preview");
expect(() =>
buildRouteUrlFromContract(route, runtime, {
...PLATFORM_ROUTE_CODECS,
...TECH_LOG_ROUTE_CODECS,
}, {
params: { publicationEventId: "" },
}),
).toThrow();
});
it("registers the studio asset library outside primary navigation", () => {
const route = TECH_LOG_ROUTE_REGISTRY.TECH_LOG_STUDIO_ASSETS;
expect(route).toBeTruthy();
expect(route.path).toBe("/studio/assets");
expect(route.layoutGroup).toBe("STUDIO");
expect(route.navigationLabel).toBeNull();
expect(route.navigationOrder).toBeNull();
});
it("atomically installs the complete TechLog route and runtime inventories", () => {
expect(Object.keys(ROUTE_REGISTRY)).toEqual(
expectedRoutes.map(([routeId]) => routeId),
);
expect(Object.keys(ROUTE_RUNTIME)).toEqual(Object.keys(ROUTE_REGISTRY));
expect(ROUTE_REGISTRY).toEqual(TECH_LOG_ROUTE_REGISTRY);
});
});