Files
tech-log-frontend/tests/unit/tech-log-serving-contract.test.ts
T
DongHyeonkaandClaude Opus 5 16e5b9f4ec feat: 프로젝트를 편집하고 활동을 남길 수 있게 한다
홈의 집중 카드가 제목만 보여 주고 "현재 목표"·"다음 작업" 칸이 비어 있었다.
프로젝트 페이지의 "활동" 도 늘 비어 있었다.

프로젝트를 만들 수는 있었지만 고칠 화면이 없었다. 그래서 이름과 slug 말고는 아무
값도 가질 수 없었고, 그 값을 읽는 공개 화면들은 빈칸을 그렸다 — 백엔드의
`updateProject` 는 처음부터 구현돼 있었고 채울 화면만 없었다.

`/studio/projects/:id` 를 연다. 프로젝트 필드 전부와 활동 목록을 한 화면에 둔다 —
프로젝트 밖의 활동은 존재하지 않고, 무엇이 공개 타임라인에 실리는지를 프로젝트 필드와
같은 자리에서 보는 편이 낫다.

이미 공개된 프로젝트는 저장한 뒤 투영도 함께 갱신한다. 투영은 게시할 때 세워지므로,
저장만 하면 공개 화면에는 예전 값이 남는다.

단계와 활동 유형의 선택지는 DB CHECK 제약과 같은 목록이다. 화면이 더 많은 값을
보여 주면 저장이 제약에서 터지고, 작성자는 왜 안 되는지 알 수 없다.

라우트가 하나 늘어 CI 게이트가 함께 움직였다 — FE-GATE-009 는 설치된 라우트마다 수동
접근성 증거를 하나씩 요구하고 그 집합이 정확히 일치하지 않으면 거절한다. 아티팩트
기준선 132→133, 증거 개수 111→112, 게이트 형태 다이제스트 재계산(이전 상수 187dbd96…
을 이전 gates.json 에서 먼저 재현해 계산 방법을 확인했다), 서빙 패턴 하나 추가.

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

102 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/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(["^/$"]);
});
});