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>
67 lines
3.2 KiB
TypeScript
67 lines
3.2 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import { createHash } from "node:crypto";
|
|
import { test } from "vitest";
|
|
|
|
import canonicalSource from "../../../src/features/tech-log/contracts/studio/canonical-source.json" with { type: "json" };
|
|
import ciGates from "../../../config/ci/gates.json" with { type: "json" };
|
|
import packageDocument from "../../../package.json" with { type: "json" };
|
|
|
|
const YAML_PATH = "src/features/tech-log/contracts/studio/studio-api.openapi.yaml";
|
|
const DRIFT_GATE_SCRIPT = "check:tech-log-contract";
|
|
|
|
test("vendored contract matches the recorded canonical digest", () => {
|
|
const bytes = readFileSync(YAML_PATH);
|
|
const digest = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
assert.equal(digest, canonicalSource.digest);
|
|
});
|
|
|
|
test("canonical source records the pinned revision and version", () => {
|
|
assert.equal(canonicalSource.packageId, "@tech-log/studio-contract");
|
|
assert.equal(canonicalSource.version, "3.0.0");
|
|
// revision은 생성 시점에 기록된다. canonical 저장소는 활발히 편집 중이므로
|
|
// 특정 값을 박아두면 계약이 그대로인데도 테스트가 깨진다. 형식만 고정한다.
|
|
assert.match(canonicalSource.sourceRevision, /^[0-9a-f]{7,64}$/);
|
|
assert.match(canonicalSource.digest, /^sha256:[0-9a-f]{64}$/);
|
|
});
|
|
|
|
test("canonical source lists all 19 operationIds", () => {
|
|
assert.equal(canonicalSource.operationIds.length, 19);
|
|
assert.ok(canonicalSource.operationIds.includes("uploadStudioAsset"));
|
|
assert.ok(canonicalSource.operationIds.includes("getStudioSession"));
|
|
});
|
|
|
|
test("vendored contract declares the CSRF header", () => {
|
|
const yaml = readFileSync(YAML_PATH, "utf8");
|
|
assert.ok(yaml.includes("X-CSRF-TOKEN"));
|
|
});
|
|
|
|
// 이 브랜치의 중심 산출물은 "canonical 계약이 다시 갈라지지 못하게 빌드로 막는다"
|
|
// (§선택한 접근 A)이다. 스크립트가 존재하는 것만으로는 그 약속이 지켜지지 않는다.
|
|
// 누군가 손으로 vendor yaml이나 generated.ts를 고쳐도, 실제로 실행되는 게이트가
|
|
// 하나도 그것을 보지 않으면 digest 고정은 의미를 잃는다. 아래 두 테스트가
|
|
// "실행 경로에 실제로 연결돼 있는가"를 검증한다.
|
|
test("the contract drift check is a real CI gate command, not just a package script", () => {
|
|
const command = ciGates.commands.find((entry) => entry.script === DRIFT_GATE_SCRIPT);
|
|
assert.ok(command, `config/ci/gates.json declares no ${DRIFT_GATE_SCRIPT} command`);
|
|
assert.equal(command.expect, "pass");
|
|
|
|
const owningGates = ciGates.gates.filter((gate) =>
|
|
(gate.commandIds as readonly string[]).includes(command.id),
|
|
);
|
|
assert.equal(
|
|
owningGates.length,
|
|
1,
|
|
`${command.id} must be referenced by exactly one gate; found ${owningGates.length}`,
|
|
);
|
|
});
|
|
|
|
test("test:all runs the contract drift check alongside the TechLog suite", () => {
|
|
const segments = packageDocument.scripts["test:all"].split(" && ").map((value) => value.trim());
|
|
assert.ok(
|
|
segments.includes(`corepack pnpm ${DRIFT_GATE_SCRIPT}`),
|
|
`test:all does not run ${DRIFT_GATE_SCRIPT}: ${packageDocument.scripts["test:all"]}`,
|
|
);
|
|
assert.ok(segments.includes("corepack pnpm test:tech-log"), packageDocument.scripts["test:all"]);
|
|
});
|