fix: let the mock's dependency revision observe the Asset store
`createMockStudioGateway`'s default `dependencyRevision.current()` returned a literal constant and never consulted `dependencies.assets`, so the staleness guards in `createStudioPreview` and `publishStudioDocument` could not fire for an asset-store mutation between validate and preview. The live case: a document references an evidence key with `alt=""` and the store holds only a `decorative: true` Asset for it, so validation is correctly VALID with zero issues. A newer `decorative: false` Asset then wins that key. Preview succeeds, the figure resolves to `decorative: false, alt: ""`, and publish snapshots it verbatim -- a meaningful image with no accessible name, validated clean, with nothing anywhere reporting an error. The default now folds the Asset store into the revision. Each Asset is reduced to the fields the mock's own validation and projection read -- identity and resolution order (`id`, `assetKey`, `updatedAt`), resolvability (`managementStatus`, `publicPath`), the alt rule (`decorative`, `altText`), and what the published `ResolvedAsset` carries (`mediaType`, `width`, `height`) -- canonicalized with `stableStringify`, sorted, and folded into a 128-bit FNV-1a digest. Sorting the canonical strings is what makes it order-independent, which this mock's reproducibility across the suite depends on. It is a projection rather than the whole record because the excluded fields cost sensitivity without buying any. `usageCount` is the clearest: it counts referencing documents, so on a real backend publishing any document that uses an Asset would invalidate every other author's in-flight validation, while changing nothing the validator or renderer reads. An empty store still reports the bare catalog constant -- that is the world the seeded fixtures were validated against, and `fixtures.ts` now shares the one definition rather than retyping the literal. `findResolvableAsset` is untouched: it is a single-point-in-time predicate and is correct as it stands. Seeing a change *between* two points is the revision's job. A caller-supplied `dependencyRevision` still wins outright. The asset-picker test that reached `failureOf`'s `ContentFormatError` branch did so only because the revision could not move; it now pins its own revision to keep reaching the projection, and asserts the problem detail so the two 409 paths cannot be confused. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
cf45bcc7dc
commit
d84b57bb3f
@@ -63,6 +63,34 @@ canonical Studio API 전체는 19개 operation이다. `tech-log-studio-contract-
|
||||
|
||||
두 경로 모두 `StudioAssetUploadTransport`/`StudioAssetGateway` 포트 경계 뒤에서 일어나므로, presentation 계층(Task 11의 Asset Library UI)은 재작성하지 않는다.
|
||||
|
||||
## MOCK 의존성 리비전은 계약의 요구가 아니라 mock의 구현이다
|
||||
|
||||
`createMockStudioGateway`의 기본 `dependencyRevision.current()`가 무엇을 관찰하는지 — 그리고 그것이 **계약이 요구하는 계산이 아니라는 점** — 을 여기에 남긴다. 나중에 mock의 구현을 계약의 요구로 오독하지 않기 위해서다.
|
||||
|
||||
### 실제 백엔드
|
||||
|
||||
`DependencyRevision`(`studio-api.openapi.yaml` / `generated.ts`)은 값의 **형식**만 계약이다: 불투명한 문자열. 계약이 요구하는 것은 값이 아니라 규칙 하나뿐이다 — *검증에 사용한 dependency set을 publish 시 다시 계산해 값이 다르면 `VALIDATION_STALE`로 거절한다.* 무엇을 dependency set에 넣을지(Topic/Project 존재, relation target 상태, Asset READY/QUARANTINED 상태, slug/route ownership, catalog revision, 필요 시 renderer/content-format version), 그리고 그것을 어떻게 정규화·hash할지는 **서버가 스스로 정한다.** 프론트엔드는 이 값을 생성하지도, 해석하지도, 비교하지도 않는다. `ValidationReport.dependencyRevision`을 받아 그대로 되돌려 보내고, 서버가 내린 `VALIDATION_STALE` 판정을 표시할 뿐이다. HTTP gateway(`http-studio-gateway.ts`)에는 리비전을 계산하는 코드가 없다 — 있어서도 안 된다.
|
||||
|
||||
### MOCK
|
||||
|
||||
MOCK `studioSource`에는 그 서버가 없으므로, mock이 같은 규칙을 스스로 만족시켜야 한다. 기본 리비전은 `dependency-revision.ts`의 `mockDependencyRevision`이 계산한다.
|
||||
|
||||
- **catalog 성분**: `MOCK_CATALOG_REVISION`(`"catalog-2026-08-14"`) 상수. `createMockStudioState`가 고정 fixture catalog 하나를 싣고 변경하지 않으므로 catalog의 기여는 실제로 상수다. `fixtures.ts`의 seed validation/preview도 같은 정의를 import해 쓴다 — 두 값이 갈라지면 seed된 문서가 전부 조용히 stale이 된다.
|
||||
- **asset 성분**: Asset store를 정규화해 만든 128비트 digest. 각 Asset을 **투영(projection)** 으로 줄이고(`id`, `assetKey`, `managementStatus`, `publicPath`, `updatedAt`, `decorative`, `altText`, `mediaType`, `width`, `height`), `stableStringify`로 정규 문자열을 만든 뒤 정렬해 접는다. 따라서 `Map` 삽입 순서와 무관하게 같은 논리적 Asset 집합은 항상 같은 리비전을 낸다 — 이 mock의 재현성은 저장소 전체 테스트가 의존하는 성질이다.
|
||||
- 빈 store는 성분을 더하지 않아 `MOCK_CATALOG_REVISION` 그대로다. seed fixture가 Asset이 없는 세계에서 만들어졌고 그 문자열을 그대로 싣기 때문이다.
|
||||
|
||||
레코드 전체가 아니라 투영을 hash하는 이유: `usageCount`는 그 Asset을 참조하는 문서 수라 실제 백엔드였다면 **아무 문서나 publish할 때마다** 다른 저자의 진행 중인 검증이 전부 무효가 된다 — 검증기도 렌더러도 읽지 않는 필드인데도. `version`은 이 mock에서 `updatedAt`과 함께 움직여 신호를 더하지 않고, `kind`·`originalFilename`·`byteSize`·`createdAt`은 검증에도 render model에도 도달하지 않는다.
|
||||
|
||||
### 이 기본값이 닫는 구멍
|
||||
|
||||
기본 리비전이 리터럴 상수였을 때, `createStudioPreview`/`publishStudioDocument`의 staleness guard는 Asset store를 전혀 관찰하지 못했다. 그래서 **validate와 preview 사이의 Asset 변경이 guard에게 보이지 않았다.** 구체적으로: 어떤 evidence key의 Asset이 `decorative: true`뿐이면 `alt=""`인 directive는 정당하게 VALID다(장식용 이미지는 대체 텍스트가 없어도 된다). 그 사이에 같은 key에 `decorative: false`인 더 새로운 Asset이 도착하면, `createStudioPreview`는 성공하고 figure는 `decorative: false, alt: ""`로 해석되며 `publishDocument`가 그 render model을 그대로 snapshot한다. **의미 있는 이미지가 접근 가능한 이름 없이, 검증은 깨끗한 채로, 아무도 오류를 보고하지 않은 채 공개된다.** 이제 그 변경이 리비전을 움직여 guard가 `VALIDATION_STALE`을 내고, 저자가 재검증하면 `EVIDENCE_ALT_REQUIRED`로 진짜 문제를 듣는다.
|
||||
|
||||
수정은 `findResolvableAsset`이 아니라 리비전에 있다. `findResolvableAsset`은 *한 시점의* 술어이고 그 자체로는 옳다 — 두 시점 사이의 변화를 보는 것은 리비전의 일이다.
|
||||
|
||||
**주의**: 위 필드 목록은 이 mock이 스스로 무엇을 읽는지에 대한 서술이지, 서버가 무엇을 dependency set에 넣어야 하는지에 대한 요구가 아니다. 서버는 프론트엔드가 볼 수 없는 것(예: relation target의 게시 상태, route ownership)까지 포함할 수 있고 그래야 한다. 이 mock을 계약의 참조 구현으로 삼지 말 것.
|
||||
|
||||
고정 테스트: `tests/features/tech-log/mock-dependency-revision.test.ts`(보고된 시나리오 end-to-end, 순서 무관 결정성, 무변경 authoring loop 안정성).
|
||||
|
||||
## 검증
|
||||
|
||||
```
|
||||
@@ -71,6 +99,7 @@ corepack pnpm exec vitest run tests/features/tech-log/studio-asset-gateway.test.
|
||||
corepack pnpm exec vitest run tests/features/tech-log/runtime-composition.test.ts
|
||||
corepack pnpm exec vitest run tests/features/tech-log/studio-csrf-composition.test.ts
|
||||
corepack pnpm exec vitest run tests/features/tech-log/studio-session-csrf.test.ts
|
||||
corepack pnpm exec vitest run tests/features/tech-log/mock-dependency-revision.test.ts
|
||||
corepack pnpm check:types
|
||||
corepack pnpm test:tech-log
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user