fix: give each API surface its own error-code enum

The public site answered every screen with the terminal error surface. Three
defects stacked, and each one hid the next.

The first refused the request outright: `attachCredentials` asks the Studio
helper, which returns null for a profile it does not own, and the fallback
below read the session and rejected anything not authenticated. Public reads
declare the ANONYMOUS profile, so a signed-out visitor — the public site's
entire audience — never got a request out of the browser. An anonymous profile
carries no credentials by definition and must never consult the session.

With requests flowing, the second surfaced: `envelopeError()` pinned
`ApiError.code` to the Studio enum and all three surfaces shared it. Public and
Management each declare their own enum in their own contract, so every error
they returned failed validation and arrived as a CONTRACT_VIOLATION — an
unclassifiable transport fault — rather than the domain error it was. A strict
enum checked against the wrong surface's contract still looks strict, which is
why no gate caught it. Each surface now passes its own contract's codes.

The third was the not-found path: it read `status` and `code` off the problem
body, but the envelope has no `status` and names the code for its surface
(PUBLIC_RESOURCE_NOT_FOUND, not NOT_FOUND). The HTTP status from the transport
is the authoritative signal and the only one that holds across both shapes.

The regression test composes the real runtime adapters against the deployed
backend's actual 404 body. Neither the gateway tests (which stub the executor)
nor the screen tests (which stub the gateway) cover this seam, and the whole
outage lived in it.

Two page-level fixes came out of the same investigation: the profile page asked
for two project slugs that only ever existed in the static fixture, and the
index pages held their fixed header copy behind a request that had nothing to
do with it. Headers now paint immediately; only the sections that are actually
waiting show a fallback, and an empty list says so instead of rendering blank.
This commit is contained in:
DongHyeonka
2026-08-21 01:14:20 +09:00
parent 03986da3d6
commit 760071156d
11 changed files with 270 additions and 43 deletions
@@ -1,6 +1,6 @@
// @vitest-environment jsdom
import { render, screen, within } from "@testing-library/react";
import { render, screen, waitFor, within } from "@testing-library/react";
import { type ComponentType } from "react";
import {
createMemoryRouter,
@@ -97,7 +97,14 @@ async function renderPublicRoute(routeId: PublicIndexRouteId, initialEntry: stri
));
// 포트가 async 가 되면서 첫 페인트에는 데이터가 없다. 화면이 정착한 뒤
// 단언하도록 여기서 한 번 기다린다 — 각 테스트에 흩어 놓으면 빠뜨린 곳이 생긴다.
//
// `main` 이 있다는 것만으로는 더 이상 정착이 아니다. index 화면들은 고정 카피인
// 헤더를 네트워크와 무관하게 즉시 그리므로 (그게 목적이다), `main` 은 데이터가
// 오기 전에 존재한다. 기다려야 하는 것은 대기 중이던 구역이 대기를 멈추는 것이다.
await screen.findByRole("main");
await waitFor(() => {
expect(document.querySelector('[aria-busy="true"]')).toBeNull();
});
return { ...view, router };
}