홈의 집중 카드가 제목만 보여 주고 "현재 목표"·"다음 작업" 칸이 비어 있었다. 프로젝트 페이지의 "활동" 도 늘 비어 있었다. 프로젝트를 만들 수는 있었지만 고칠 화면이 없었다. 그래서 이름과 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
102 lines
3.9 KiB
TypeScript
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(["^/$"]);
|
|
});
|
|
});
|