Files
tech-log-frontend/tests/unit/contract-registry-immutability.test.ts
T
DongHyeonkaandClaude Opus 5 2cab4974b7 fix: break the TechLog CSRF bootstrap cycle and close the review's fix-round-1 items
C1 (Critical): getStudioSession was stamped with the same
TECH_LOG_STUDIO_SESSION auth profile as every other Studio operation, and
that profile requires the CSRF header it is getStudioSession's own job to
issue -- an unconditional cycle that recursed without bound in HTTP mode.
Fixed with a credential-free TECH_LOG_STUDIO_BOOTSTRAP auth profile for
getStudioSession alone, a synchronous re-entrancy guard in
createCsrfTokenProvider as defense in depth, and a throwing stub in place of
the prior `let x!: T` assertion. Added a composition-level regression test
that wires the real executor, CSRF provider, and credential-attach function
together and proves getStudioSession dispatches exactly once while its token
reaches both a JSON operation and the multipart upload.

Also: invalidate the cached CSRF token on a 401/403 from the upload path
(I2), a throwing useStudioAssetGateway() accessor so Task 11 cannot silently
compile a null-gateway UI (I3), and the M1-M5 minors from the review (guard
a malformed success body, cover the untested error fallbacks, align aborted
uploads with the JSON path's non-retryable CANCELLED mapping, derive the
credential header name from one source instead of two, and correct the
adapter review doc's operation count).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 03:07:02 +09:00

340 lines
11 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
composeContractContributions,
type ComposedContractContributions,
} from "../../src/contracts/external-contract-runtime.ts";
import {
installRestAuthProfileRegistry,
INSTALLED_REST_AUTH_PROFILES,
REST_AUTH_PROFILES,
} from "../../src/contracts/rest-profiles.ts";
import { TEST_CONTRACT_CONTRIBUTION } from "../helpers/external-contract-fixture.ts";
/**
* LIVE-02 / LIVE-03. `Object.freeze(new Map(...))` freezes the wrapper object,
* not the backing store: `set`, `delete` and `clear` keep working. Every
* registry the executor consults after composition must therefore be a read
* facade over a private store, and the rows it hands back must be exact
* own-data snapshots that a later mutation of the source cannot reach.
*/
const MUTATORS = ["set", "delete", "clear"] as const;
function borrowedMapMutation(
facade: unknown,
mutator: (typeof MUTATORS)[number],
): "THREW" | "MUTATED" {
try {
switch (mutator) {
case "set":
Map.prototype.set.call(facade as never, "INJECTED", {} as never);
break;
case "delete":
Map.prototype.delete.call(facade as never, "ANONYMOUS");
break;
case "clear":
Map.prototype.clear.call(facade as never);
break;
}
return "MUTATED";
} catch {
return "THREW";
}
}
describe("LIVE-02 installed REST auth profile registry", () => {
it("exposes no mutation API", () => {
const registry = INSTALLED_REST_AUTH_PROFILES as unknown as Record<
string,
unknown
>;
for (const mutator of MUTATORS) {
expect(registry[mutator]).toBeUndefined();
}
});
it("survives a cast mutation and a borrowed Map.prototype mutator", () => {
const registry = installRestAuthProfileRegistry(REST_AUTH_PROFILES);
const before = registry.size;
const identity = registry.get("ANONYMOUS");
expect(identity).toBeDefined();
for (const mutator of MUTATORS) {
expect(borrowedMapMutation(registry, mutator)).toBe("THREW");
}
expect(registry.size).toBe(before);
expect(registry.get("ANONYMOUS")).toBe(identity);
expect(registry.get("REFERENCE_EXTERNAL_BEARER")?.credentials).toBe("omit");
});
it("does not observe a post-installation mutation of the source record", () => {
const source: Record<string, (typeof REST_AUTH_PROFILES)["ANONYMOUS"]> = {
ANONYMOUS: {
authProfileId: "ANONYMOUS",
transport: "ANONYMOUS",
credentials: "omit",
allowedCredentialHeaders: [],
requiredCredentialHeaders: [],
},
};
const registry = installRestAuthProfileRegistry(source);
delete source.ANONYMOUS;
expect(registry.get("ANONYMOUS")?.transport).toBe("ANONYMOUS");
});
it("keeps read APIs the executor depends on", () => {
const registry = INSTALLED_REST_AUTH_PROFILES;
expect(registry.has("ANONYMOUS")).toBe(true);
expect(registry.has("NO_SUCH_PROFILE")).toBe(false);
expect([...registry.keys()].sort()).toEqual([
"ANONYMOUS",
"REFERENCE_EXTERNAL_BEARER",
"TECH_LOG_STUDIO_BOOTSTRAP",
"TECH_LOG_STUDIO_SESSION",
]);
expect([...registry.entries()].length).toBe(registry.size);
expect([...registry.values()].length).toBe(registry.size);
});
});
describe("LIVE-03 composed contract registry", () => {
function compose(): ComposedContractContributions {
return composeContractContributions([TEST_CONTRACT_CONTRIBUTION] as never);
}
it("exposes no mutation API on either lookup", () => {
const composed = compose();
for (const facade of [composed.httpByOperationId, composed.eventByType]) {
const record = facade as unknown as Record<string, unknown>;
for (const mutator of MUTATORS) {
expect(record[mutator]).toBeUndefined();
}
for (const mutator of MUTATORS) {
expect(borrowedMapMutation(facade, mutator)).toBe("THREW");
}
}
expect(composed.httpByOperationId.size).toBeGreaterThan(0);
});
it("snapshots the frontend policy so a later source mutation cannot reach it", () => {
const mutablePolicy = {
...TEST_CONTRACT_CONTRIBUTION.http[0]!.frontend,
};
const contribution = {
...TEST_CONTRACT_CONTRIBUTION,
http: [
{
...TEST_CONTRACT_CONTRIBUTION.http[0]!,
frontend: mutablePolicy,
},
],
};
const composed = composeContractContributions([contribution] as never);
const operationId = [...composed.httpByOperationId.keys()][0]!;
const installedBefore =
composed.httpByOperationId.get(operationId)!.frontend.totalDeadlineMs;
mutablePolicy.totalDeadlineMs = 999_999;
expect(
composed.httpByOperationId.get(operationId)!.frontend.totalDeadlineMs,
).toBe(installedBefore);
expect(installedBefore).not.toBe(999_999);
});
it("rejects an accessor or inherited policy field", () => {
const inherited = Object.create({ diagnosticsOperation: "INHERITED" }) as
Record<string, unknown>;
for (const [key, value] of Object.entries(
TEST_CONTRACT_CONTRIBUTION.http[0]!.frontend,
)) {
if (key === "diagnosticsOperation") continue;
inherited[key] = value;
}
const accessor = { ...TEST_CONTRACT_CONTRIBUTION.http[0]!.frontend };
Object.defineProperty(accessor, "totalDeadlineMs", {
configurable: true,
enumerable: true,
get: () => 10_000,
});
for (const frontend of [inherited, accessor]) {
expect(() =>
composeContractContributions([
{
...TEST_CONTRACT_CONTRIBUTION,
http: [{ ...TEST_CONTRACT_CONTRIBUTION.http[0]!, frontend }],
},
] as never),
).toThrow();
}
});
/**
* NS-02. Composition validated the caller's object and then read it again to
* copy it. Between those two reads a stateful answer could swap a deadline or
* a retry budget, so the value that passed the ceiling check and the value the
* executor ran with were two different things.
*/
describe("validate the snapshot, never the source", () => {
/** Answers a safe value to a plain read and a hostile one to a copy. */
const statefulFrontend = () =>
new Proxy(
{ ...TEST_CONTRACT_CONTRIBUTION.http[0]!.frontend },
{
get(target, key, receiver) {
if (key === "totalDeadlineMs") return 10_000;
return Reflect.get(target, key, receiver);
},
getOwnPropertyDescriptor(target, key) {
if (key === "totalDeadlineMs") {
return {
configurable: true,
enumerable: true,
value: 999_999,
};
}
return Reflect.getOwnPropertyDescriptor(target, key);
},
},
);
const contributionWith = (row: unknown) => [
{ ...TEST_CONTRACT_CONTRIBUTION, http: [row] },
];
it("validates the deadline it will install, not the one it was shown", () => {
// The copied value exceeds the hard ceiling, so composition must stop
// rather than install a deadline no check ever saw.
expect(() =>
composeContractContributions(
contributionWith({
...TEST_CONTRACT_CONTRIBUTION.http[0]!,
frontend: statefulFrontend(),
}) as never,
),
).toThrow();
});
it("keys the registry by the operation id it copied, not the one it was shown", () => {
const base = { ...TEST_CONTRACT_CONTRIBUTION.http[0]!.contract };
const contract = new Proxy(base, {
get(target, key, receiver) {
if (key === "operationId") return base.operationId;
return Reflect.get(target, key, receiver);
},
getOwnPropertyDescriptor(target, key) {
if (key === "operationId") {
return {
configurable: true,
enumerable: true,
value: "SwappedOperation",
};
}
return Reflect.getOwnPropertyDescriptor(target, key);
},
});
const composed = composeContractContributions(
contributionWith({
...TEST_CONTRACT_CONTRIBUTION.http[0]!,
contract,
}) as never,
);
expect([...composed.httpByOperationId.keys()]).toEqual([
"SwappedOperation",
]);
expect(
composed.httpByOperationId.get("SwappedOperation")!.contract
.operationId,
).toBe("SwappedOperation");
});
const hostileRows = [
{
label: "an installed row with an extra own field",
row: () => ({
...TEST_CONTRACT_CONTRIBUTION.http[0]!,
injected: true,
}),
},
{
label: "an installed row with a symbol field",
row: () => ({
...TEST_CONTRACT_CONTRIBUTION.http[0]!,
[Symbol.for("injected")]: true,
}),
},
{
label: "an installed row with a custom prototype",
row: () =>
Object.assign(Object.create({ injected: true }), {
...TEST_CONTRACT_CONTRIBUTION.http[0]!,
}),
},
{
label: "an installed row hiding a non-enumerable own field",
row: () =>
Object.defineProperty(
{ ...TEST_CONTRACT_CONTRIBUTION.http[0]! },
"injected",
{ enumerable: false, value: true },
),
},
{
label: "an installed row behind a throwing ownKeys trap",
row: () =>
new Proxy(
{ ...TEST_CONTRACT_CONTRIBUTION.http[0]! },
{
ownKeys() {
throw new TypeError("hostile ownKeys trap");
},
},
),
},
];
for (const { label, row } of hostileRows) {
it(`refuses to compose ${label}`, () => {
expect(() =>
composeContractContributions(contributionWith(row()) as never),
).toThrow();
});
}
it("refuses a contribution whose own shape is not exact", () => {
for (const contribution of [
{ ...TEST_CONTRACT_CONTRIBUTION, injected: true },
Object.assign(Object.create({ injected: true }), {
...TEST_CONTRACT_CONTRIBUTION,
}),
{
...TEST_CONTRACT_CONTRIBUTION,
[Symbol.for("injected")]: true,
},
]) {
expect(() =>
composeContractContributions([contribution] as never),
).toThrow();
}
});
it("refuses an event contract whose own shape is not exact", () => {
const events = TEST_CONTRACT_CONTRIBUTION.events;
if (events.length === 0) return;
for (const event of [
{ ...events[0]!, injected: true },
Object.assign(Object.create({ injected: true }), { ...events[0]! }),
]) {
expect(() =>
composeContractContributions([
{ ...TEST_CONTRACT_CONTRIBUTION, events: [event] },
] as never),
).toThrow();
}
});
});
});