fix: 새 목록 항목의 id 가 계약을 건너갈 수 있게 한다
관계를 고르고 저장하면 400 이 돌아왔고, 편집기에는 "saveStudioDocument broke its contract." 한 줄만 남았다. 저장은 통째로 실패했다. 새 관계·규칙·선택지·문장의 id 를 `createLocalId` 로 만들고 있었다. 그 함수는 접두사를 붙여 `relation-<uuid>` 를 돌려준다 — React key 나 Idempotency-Key 로는 맞지만 계약이 그 자리에 요구하는 것은 uuid 다. 서버는 파싱조차 못 하고 InvalidFormatException 으로 거절했다. 목록에 고를 대상이 하나도 없던 동안에는(catalog RELATION/EVIDENCE 가 스텁이었다) 아무도 이 경로를 지나지 않아 드러나지 않았다. 두 결함이 서로를 가리고 있었다. Case 는 목록 항목이 없어 무사했다. Reference 의 규칙, Question 의 선택지, Decision 의 consequences 는 모두 같은 이유로 저장되지 않았을 것이다. `createNewItemId` 로 나눈다. 서버는 자기가 소유하지 않은 id 를 어차피 새로 부여하므로 (`StudioRelationStore.replace`) 이 값은 "이 줄은 새것"이라는 표시일 뿐이다 — 지켜야 할 것은 형식뿐이다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XEHXspz4rv5pB5wiiSsVDu
This commit is contained in:
co-authored by
Claude Opus 5
parent
c87e0a2338
commit
0eb3c86839
@@ -14,7 +14,10 @@ import {
|
||||
derivePreviewState,
|
||||
deriveValidationState,
|
||||
} from "../../../src/features/tech-log/domain/studio/document-state.ts";
|
||||
import { createLocalId } from "../../../src/features/tech-log/domain/studio/local-id.ts";
|
||||
import {
|
||||
createLocalId,
|
||||
createNewItemId,
|
||||
} from "../../../src/features/tech-log/domain/studio/local-id.ts";
|
||||
|
||||
const now = "2026-08-14T12:00:00.000Z";
|
||||
const validUntil = "2026-08-14T12:30:00.000Z";
|
||||
@@ -317,3 +320,32 @@ test("local Studio IDs are deterministic without Web Crypto and prefer UUIDs whe
|
||||
"save-fixture-uuid",
|
||||
);
|
||||
});
|
||||
|
||||
/*
|
||||
이 두 함수가 나뉜 이유는 하나는 화면 안에만 머물고 다른 하나는 계약을 건너가기 때문이다.
|
||||
|
||||
한때 새 관계·규칙·선택지의 id 를 `createLocalId` 로 만들었고, 그래서 `relation-<uuid>` 같은
|
||||
값이 요청 본문에 실렸다. 계약은 그 자리에 uuid 를 요구하므로 서버는 파싱조차 못 하고 400 을
|
||||
돌려주었다 — 편집기에는 "saveStudioDocument broke its contract." 한 줄만 남고 저장은 통째로
|
||||
실패했다. 목록에 고를 대상이 하나도 없던 시절에는 아무도 이 경로를 지나지 않아 드러나지 않았다.
|
||||
*/
|
||||
const UUID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
||||
|
||||
test("new list item IDs are bare UUIDs, because they cross the contract", () => {
|
||||
assert.equal(
|
||||
createNewItemId({ randomUUID: () => "3f1a2b4c-5d6e-4f70-8a91-b2c3d4e5f607" }),
|
||||
"3f1a2b4c-5d6e-4f70-8a91-b2c3d4e5f607",
|
||||
);
|
||||
// Web Crypto 가 없는 환경에서도 형식은 uuid 여야 한다 — 값은 서버가 새로 부여하지만, 형식이
|
||||
// 어긋나면 요청 자체가 거절된다.
|
||||
assert.match(createNewItemId(null, () => 0.5), UUID_SHAPE);
|
||||
assert.match(createNewItemId(null, () => 0), UUID_SHAPE);
|
||||
assert.match(createNewItemId(null, () => 0.999), UUID_SHAPE);
|
||||
});
|
||||
|
||||
test("prefixed local IDs never satisfy the contract's UUID shape", () => {
|
||||
assert.equal(
|
||||
UUID_SHAPE.test(createLocalId("relation", { randomUUID: () => "3f1a2b4c-5d6e-4f70-8a91-b2c3d4e5f607" })),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user