feat: register the TechLog Studio contract contribution
Registers the TECH_LOG_STUDIO_SESSION SAME_ORIGIN_COOKIE auth profile and declares the tech-log-studio-http-v1 contribution covering all 18 JSON Studio operations (everything except the multipart uploadStudioAsset), built from two shared builders (safeOperation/keyedOperation) so every KEYED command gets IDEMPOTENCY_REPLAY recovery and a zero retry budget, and every SAFE read gets a plain retry budget, without repeating the declaration shape 18 times. Rescopes the canonical package identity from "tech-log-studio-contract" to "@tech-log/studio-contract" (generator, canonical-source.json, contract-generation.test.ts) because the platform's contribution composer requires an npm-scoped packageId; the unscoped form failed composition. Updates the release-manifest test fixture, which hardcoded an empty expected contract set, to derive its expected packages from the real installed set now that TechLog is always installed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a6fc536d8a
commit
66c047cec8
@@ -14,7 +14,7 @@ test("vendored contract matches the recorded canonical digest", () => {
|
||||
});
|
||||
|
||||
test("canonical source records the pinned revision and version", () => {
|
||||
assert.equal(canonicalSource.packageId, "tech-log-studio-contract");
|
||||
assert.equal(canonicalSource.packageId, "@tech-log/studio-contract");
|
||||
assert.equal(canonicalSource.version, "2.0.0");
|
||||
// revision은 생성 시점에 기록된다. canonical 저장소는 활발히 편집 중이므로
|
||||
// 특정 값을 박아두면 계약이 그대로인데도 테스트가 깨진다. 형식만 고정한다.
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "vitest";
|
||||
|
||||
import canonicalSource from "../../../src/features/tech-log/contracts/studio/canonical-source.json" with { type: "json" };
|
||||
import {
|
||||
TECH_LOG_STUDIO_CONTRIBUTION,
|
||||
TECH_LOG_STUDIO_OPERATION_IDS,
|
||||
} from "../../../src/features/tech-log/contracts/tech-log-studio-contract-contribution.ts";
|
||||
import { composeContractContributions } from "../../../src/contracts/external-contract-runtime.ts";
|
||||
|
||||
const UPLOAD = "uploadStudioAsset";
|
||||
|
||||
test("declares every canonical operation except the multipart upload", () => {
|
||||
const expected = canonicalSource.operationIds.filter((id) => id !== UPLOAD);
|
||||
assert.equal(expected.length, 18);
|
||||
assert.deepEqual(
|
||||
[...TECH_LOG_STUDIO_OPERATION_IDS].sort(),
|
||||
[...expected].sort(),
|
||||
);
|
||||
});
|
||||
|
||||
test("pins the canonical digest and revision as external package provenance", () => {
|
||||
const source = TECH_LOG_STUDIO_CONTRIBUTION.source;
|
||||
assert.equal(source.kind, "EXTERNAL_PACKAGE");
|
||||
if (source.kind !== "EXTERNAL_PACKAGE") return;
|
||||
assert.equal(source.package.packageId, canonicalSource.packageId);
|
||||
assert.equal(source.package.version, canonicalSource.version);
|
||||
assert.equal(source.package.digest, canonicalSource.digest);
|
||||
assert.equal(source.package.sourceRevision, canonicalSource.sourceRevision);
|
||||
assert.equal(source.package.runtimeProtocolVersion, 1);
|
||||
});
|
||||
|
||||
test("composes without violating the platform contract runtime", () => {
|
||||
const composed = composeContractContributions([TECH_LOG_STUDIO_CONTRIBUTION]);
|
||||
assert.equal(composed.externalPackages.length, 1);
|
||||
});
|
||||
|
||||
test("every mutating operation replays by idempotency key and never auto-retries", () => {
|
||||
for (const entry of TECH_LOG_STUDIO_CONTRIBUTION.http) {
|
||||
if (entry.contract.retrySemantics !== "KEYED") continue;
|
||||
assert.deepEqual(
|
||||
entry.contract.commandRecovery,
|
||||
{
|
||||
mode: "IDEMPOTENCY_REPLAY",
|
||||
operationIdentityField: "idempotencyKey",
|
||||
},
|
||||
`${entry.contract.operationId} recovery`,
|
||||
);
|
||||
assert.equal(
|
||||
entry.frontend.retryBudget,
|
||||
0,
|
||||
`${entry.contract.operationId} budget`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("path templates match the canonical /api/v1/studio prefix", () => {
|
||||
for (const entry of TECH_LOG_STUDIO_CONTRIBUTION.http) {
|
||||
assert.ok(
|
||||
entry.contract.pathTemplate.startsWith("/api/v1/studio/"),
|
||||
`${entry.contract.operationId}: ${entry.contract.pathTemplate}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
composeRuntimeSchemaCodecs,
|
||||
validateWithRuntimeSchemaRegistry,
|
||||
} from "../../src/contracts/schema-registry.ts";
|
||||
import { INSTALLED_REST_AUTH_PROFILES } from "../../src/contracts/rest-profiles.ts";
|
||||
|
||||
describe("HTTP platform schema boundary", () => {
|
||||
it("rejects an invalid top-level envelope", () => {
|
||||
@@ -68,3 +69,20 @@ describe("runtime schema codec contribution", () => {
|
||||
).toThrow("duplicate runtime schema codec");
|
||||
});
|
||||
});
|
||||
|
||||
describe("REST auth profile registry", () => {
|
||||
it("installs the TechLog Studio session auth profile", () => {
|
||||
const profile = INSTALLED_REST_AUTH_PROFILES.get(
|
||||
"TECH_LOG_STUDIO_SESSION",
|
||||
);
|
||||
expect(profile).toBeTruthy();
|
||||
expect(profile?.transport).toBe("SAME_ORIGIN_COOKIE");
|
||||
expect(profile?.credentials).toBe("include");
|
||||
expect([...(profile?.requiredCredentialHeaders ?? [])]).toEqual([
|
||||
"x-csrf-token",
|
||||
]);
|
||||
expect([...(profile?.allowedCredentialHeaders ?? [])]).toEqual([
|
||||
"x-csrf-token",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,9 +4,22 @@ import {
|
||||
loadReleaseManifest,
|
||||
ReleaseManifestError,
|
||||
} from "../../src/bootstrap/load-release-manifest.ts";
|
||||
import { computeContractSetDigest } from "../../src/contracts/contract-set-canonical.ts";
|
||||
import {
|
||||
computeContractSetDigest,
|
||||
type ContractSetPackage,
|
||||
} from "../../src/contracts/contract-set-canonical.ts";
|
||||
import { EXPECTED_CONTRACT_SET_PACKAGES } from "../../src/features/installed-contract-contributions.ts";
|
||||
|
||||
const EMPTY_SET_DIGEST = await computeContractSetDigest([]);
|
||||
/**
|
||||
* TechLog's Studio contribution is always installed (Task 3), so the build's
|
||||
* real expected contract set is no longer empty. A coherent manifest fixture
|
||||
* must declare exactly what this build actually compiled in, or the boot-time
|
||||
* `verifyContractSet` check rejects it as `CONTRACT_SET_PACKAGE_MISSING`.
|
||||
*/
|
||||
const EXPECTED_PACKAGES =
|
||||
EXPECTED_CONTRACT_SET_PACKAGES as readonly ContractSetPackage[];
|
||||
const EXPECTED_SET_DIGEST = await computeContractSetDigest(EXPECTED_PACKAGES);
|
||||
|
||||
const runtime: Parameters<typeof loadReleaseManifest>[0] = {
|
||||
build: {
|
||||
@@ -49,8 +62,8 @@ const manifest = {
|
||||
routeChunks: { "route-home": "assets/home.js" },
|
||||
contractSet: {
|
||||
setAlgorithm: "CA_CONTRACT_SET_V1",
|
||||
setDigest: EMPTY_SET_DIGEST,
|
||||
packages: [],
|
||||
setDigest: EXPECTED_SET_DIGEST,
|
||||
packages: EXPECTED_PACKAGES,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user