편집 폼이 릴리즈 목록 아래에 열렸다. 릴리즈가 열 개 스무 개로 늘면 편집하려고 목록 전체를 지나 내려가야 하고, 저장 버튼은 그보다 더 아래에 있다. 목록이 길어질수록 편집이 멀어지는 구조다. `/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
103 lines
3.9 KiB
TypeScript
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(["^/$"]);
|
|
});
|
|
});
|