feat: Studio 응답 봉투를 전송 경계에서 언랩한다
studio-v1.yaml v3.0.0(ADR-006)에 맞춰 계약을 재생성하고, 성공은
{success,data,meta}, 실패는 {success,error,meta} 봉투를 전송 경계에서
언랩하는 envelopeData/envelopeError validator를 도입한다. 앱·도메인
계층은 기존과 같은 payload/ProblemDetails 모양을 계속 받고,
StudioGateway 포트 시그니처는 무변경이다.
- tech-log-studio-contract-contribution.ts: envelopeData/envelopeError
도입, 18개 operation의 outputValidator를 passthrough에서 envelopeData로
교체
- studio-error-mapping.ts: 봉투 오류의 status(항상 0)를
outcome.metadata.status로 덮는다. SafeResponseMetadata.status가
실제 필드명이며(httpStatus 아님) PROBLEM outcome에서 필수 필드다
- contract.ts: 삭제된 ProblemDetails 생성 스키마를 손으로 유지 — 앱
계층·mock 게이트웨이가 그 모양을 계속 소비한다
- asset-upload-transport.ts: multipart 업로드는 일반 계약 런타임을
거치지 않는 별도 seam이지만 같은 wire 봉투를 쓴다 — envelopeData/
envelopeError를 재사용해 이 경로도 언랩한다 (브리프 파일 목록 밖의
발견, report에 기록)
- 테스트: 신규 studio-envelope-unwrap.test.ts(TDD) + 봉투 뼈대를 직접
만드는 기존 테스트(asset-upload-transport, studio-csrf-composition,
contract-generation)를 봉투 형태로 갱신
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d84b57bb3f
commit
25a6b63d27
@@ -43,6 +43,16 @@ beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
|
||||
afterEach(() => server.resetHandlers());
|
||||
afterAll(() => server.close());
|
||||
|
||||
// wire format은 봉투다 (ADR-006) — 이 파일은 실제 platform 계약 런타임
|
||||
// (`createContractHttpExecutor`)을 조립하므로 `outputValidator`
|
||||
// (`envelopeData`)가 그대로 걸린다. 봉투가 아닌 본문은 SUCCESS_SCHEMA_INVALID로
|
||||
// 거절된다.
|
||||
const dataEnvelope = (data: unknown) => ({
|
||||
success: true,
|
||||
data,
|
||||
meta: { requestId: "r", traceId: "t", correlationId: null, page: null },
|
||||
});
|
||||
|
||||
function scopeSnapshot() {
|
||||
return Object.freeze({
|
||||
generation: 1,
|
||||
@@ -152,13 +162,15 @@ test(
|
||||
server.use(
|
||||
http.get(`${BASE}/api/v1/studio/session`, () => {
|
||||
sessionCalls += 1;
|
||||
return HttpResponse.json({
|
||||
authenticated: true,
|
||||
displayName: "테스터",
|
||||
roles: ["editor"],
|
||||
csrfToken: "csrf-token-1",
|
||||
csrfHeaderName: "X-CSRF-TOKEN",
|
||||
});
|
||||
return HttpResponse.json(
|
||||
dataEnvelope({
|
||||
authenticated: true,
|
||||
displayName: "테스터",
|
||||
roles: ["editor"],
|
||||
csrfToken: "csrf-token-1",
|
||||
csrfHeaderName: "X-CSRF-TOKEN",
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -166,10 +178,7 @@ test(
|
||||
server.use(
|
||||
http.get(`${BASE}/api/v1/studio/dashboard`, ({ request }) => {
|
||||
jsonRequestHeader = request.headers.get("x-csrf-token");
|
||||
return HttpResponse.json({
|
||||
documentTotals: {},
|
||||
workflowSections: [],
|
||||
});
|
||||
return HttpResponse.json(dataEnvelope({ documentTotals: {}, workflowSections: [] }));
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -177,7 +186,10 @@ test(
|
||||
server.use(
|
||||
http.post(`${BASE}/api/v1/studio/assets`, ({ request }) => {
|
||||
uploadRequestHeader = request.headers.get("X-CSRF-TOKEN");
|
||||
return HttpResponse.json({ id: "a", managementStatus: "READY" }, { status: 201 });
|
||||
return HttpResponse.json(
|
||||
dataEnvelope({ id: "a", managementStatus: "READY" }),
|
||||
{ status: 201 },
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -232,13 +244,15 @@ test(
|
||||
server.use(
|
||||
http.get(`${BASE}/api/v1/studio/session`, () => {
|
||||
sessionCalls += 1;
|
||||
return HttpResponse.json({
|
||||
authenticated: true,
|
||||
displayName: "테스터",
|
||||
roles: ["editor"],
|
||||
csrfToken: `csrf-token-${sessionCalls}`,
|
||||
csrfHeaderName: "X-CSRF-TOKEN",
|
||||
});
|
||||
return HttpResponse.json(
|
||||
dataEnvelope({
|
||||
authenticated: true,
|
||||
displayName: "테스터",
|
||||
roles: ["editor"],
|
||||
csrfToken: `csrf-token-${sessionCalls}`,
|
||||
csrfHeaderName: "X-CSRF-TOKEN",
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -250,7 +264,7 @@ test(
|
||||
// Second call: succeeds with whatever token is presented.
|
||||
return dashboardHeaders.length === 1
|
||||
? new HttpResponse(null, { status: 403 })
|
||||
: HttpResponse.json({ documentTotals: {}, workflowSections: [] });
|
||||
: HttpResponse.json(dataEnvelope({ documentTotals: {}, workflowSections: [] }));
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -294,13 +308,15 @@ test(
|
||||
server.use(
|
||||
http.get(`${BASE}/api/v1/studio/session`, () => {
|
||||
sessionCalls += 1;
|
||||
return HttpResponse.json({
|
||||
authenticated: true,
|
||||
displayName: "테스터",
|
||||
roles: ["editor"],
|
||||
csrfToken: `csrf-token-${sessionCalls}`,
|
||||
csrfHeaderName: "X-CSRF-TOKEN",
|
||||
});
|
||||
return HttpResponse.json(
|
||||
dataEnvelope({
|
||||
authenticated: true,
|
||||
displayName: "테스터",
|
||||
roles: ["editor"],
|
||||
csrfToken: `csrf-token-${sessionCalls}`,
|
||||
csrfHeaderName: "X-CSRF-TOKEN",
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -308,7 +324,7 @@ test(
|
||||
server.use(
|
||||
http.get(`${BASE}/api/v1/studio/dashboard`, ({ request }) => {
|
||||
dashboardHeaders.push(request.headers.get("x-csrf-token"));
|
||||
return HttpResponse.json({ documentTotals: {}, workflowSections: [] });
|
||||
return HttpResponse.json(dataEnvelope({ documentTotals: {}, workflowSections: [] }));
|
||||
}),
|
||||
);
|
||||
server.use(
|
||||
|
||||
Reference in New Issue
Block a user