Files
tech-log-frontend/tests/unit/tech-log-serving-contract.test.ts
T
DongHyeonkaandClaude Opus 5 84d72c4f60 feat: 릴리즈 편집을 자기 주소로 옮긴다
편집 폼이 릴리즈 목록 아래에 열렸다. 릴리즈가 열 개 스무 개로 늘면 편집하려고 목록
전체를 지나 내려가야 하고, 저장 버튼은 그보다 더 아래에 있다. 목록이 길어질수록 편집이
멀어지는 구조다.

`/studio/releases/:id` 를 연다 — 문서와 프로젝트가 각자 편집 주소를 갖는 것과 같은
이유다. 목록의 "편집" 은 토글이 아니라 링크가 되고, 새 릴리즈를 만들면 곧바로 그 화면으로
간다(만들자마자 편집할 것이 분명하다).

라우트가 하나 늘어 CI 게이트가 함께 움직였다 — 아티팩트 기준선 133→134, 증거 개수
112→113, 게이트 형태 다이제스트 재계산(f9e7e521… 을 이전 gates.json 에서 먼저 재현해
계산 방법을 확인했다), 서빙 패턴 하나 추가.

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

103 lines
3.9 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { createTechLogServingContract } from "../../scripts/lib/tech-log-serving-contract.ts";
import { TECH_LOG_ROUTE_REGISTRY } from "../../src/features/tech-log/contracts/tech-log-route-contract.ts";
const publicRoutePaths = Object.values(TECH_LOG_ROUTE_REGISTRY)
.filter((route) => route.layoutGroup === "PUBLIC")
.map((route) => route.path);
const studioRoutePaths = Object.values(TECH_LOG_ROUTE_REGISTRY)
.filter((route) => route.layoutGroup === "STUDIO")
.map((route) => route.path);
describe("TechLog production serving contract", () => {
it("derives one pattern per registered Public route", () => {
const contract = createTechLogServingContract({ publicRoutePaths, studioRoutePaths });
expect(contract.schemaVersion).toBe(2);
expect(contract.publicSpaPathPatterns).toEqual([
"^/$",
"^/cases/[^/]+$",
"^/explore$",
"^/explore/[^/]+$",
"^/profile$",
"^/projects$",
"^/projects/[^/]+$",
"^/projects/[^/]+/activity$",
"^/projects/[^/]+/decisions$",
"^/projects/[^/]+/records$",
"^/questions/[^/]+$",
"^/references/[^/]+$",
"^/releases$",
"^/releases/[^/]+$",
"^/search$",
"^/topics/[^/]+$",
]);
expect(contract.studioPathPrefix).toBe("/studio");
// Derived from the route contract and sorted, exactly like the public half.
// The old hand-written array folded the four document sub-screens into one
// alternation; deriving them yields one pattern per route, which is the
// point — a route that exists is served, without anyone remembering to add it.
expect(contract.studioSpaPathPatterns).toEqual([
"^/studio$",
"^/studio/assets$",
"^/studio/documents$",
"^/studio/documents/[^/]+/edit$",
"^/studio/documents/[^/]+/preview$",
"^/studio/documents/[^/]+/publish$",
"^/studio/documents/[^/]+/validation$",
"^/studio/documents/new$",
"^/studio/projects/[^/]+$",
"^/studio/publications$",
"^/studio/publications/[^/]+/preview$",
"^/studio/releases$",
"^/studio/releases/[^/]+$",
"^/studio/taxonomy$",
]);
expect(contract.notFound).toEqual({
status: 404,
contentType: "text/plain;charset=UTF-8",
body: "Not Found",
});
});
/**
* The reason this migration happened: a slug that did not exist at build time
* used to be 404ed by the web server before the SPA was asked. A published
* record must be served whatever its slug.
*/
it("serves a slug the build never saw", () => {
const { publicSpaPathPatterns } = createTechLogServingContract({ publicRoutePaths, studioRoutePaths });
const matches = (pathname: string) =>
publicSpaPathPatterns.some((pattern) => new RegExp(pattern).test(pathname));
expect(matches("/cases/a-record-published-yesterday")).toBe(true);
expect(matches("/projects/a-new-project/decisions")).toBe(true);
expect(matches("/topics/a-new-topic")).toBe(true);
});
/** A parameter is one segment. Extra depth is a 404, not a soft 200. */
it("does not let a parameter swallow a slash", () => {
const { publicSpaPathPatterns } = createTechLogServingContract({ publicRoutePaths, studioRoutePaths });
const matches = (pathname: string) =>
publicSpaPathPatterns.some((pattern) => new RegExp(pattern).test(pathname));
expect(matches("/cases/a/b")).toBe(false);
expect(matches("/projects/one/two/three")).toBe(false);
expect(matches("/nope")).toBe(false);
});
/**
* The catch-all belongs to the SPA, not to the edge. Serving index.html for
* every unmatched URL would turn a 404 into a soft 200 and hide broken links.
*/
it("drops the catch-all route", () => {
const { publicSpaPathPatterns } = createTechLogServingContract({
publicRoutePaths: ["/", "*"],
studioRoutePaths,
});
expect(publicSpaPathPatterns).toEqual(["^/$"]);
});
});