Files
clean-architecture-frontend…/tests/unit/contract-registry-immutability.test.ts
T
DongHyeonkaandClaude Opus 5 f4bfdf0365 fix: close the live V3 authority findings from the adapter re-review
LIVE-01. A credential collaborator that returns UNAVAILABLE, throws, rejects
or answers off-contract is an outage of the auth integration, not evidence
about the user's session. Each of those now closes as AUTH_INTEGRATION_FAILURE
with zero fetches, so the composition root's logout path stays reserved for a
genuinely absent session. The synchronous and asynchronous failure sites share
one classifier.

LIVE-02 / LIVE-03. Object.freeze(new Map(...)) freezes the wrapper, not the
backing store, so an exported registry could still be cleared or replaced after
composition. Both the installed REST auth profile registry and the composed
HTTP/event lookups are now read facades over private stores, and every composed
row is an exact own-data snapshot that rejects accessors, inherited and
symbol-keyed fields.

LIVE-04. The total deadline now bounds the physical waits rather than being
checked between them: dispatch and response admission race the attempt signal,
the bounded reader takes that signal, and an abandoned operation is still
observed once so a late native rejection cannot surface unhandled. A body that
completes after the deadline or the caller owns the execution is no longer
admitted; a stale generation keeps its more specific SCOPE_FENCED verdict.

LIVE-05. DEADLINE is no longer treated as a caller-owned cancellation, so a
timeout reaches api.request.failed exactly once while caller, route, scope and
shutdown aborts stay excluded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 13:43:59 +09:00

173 lines
5.6 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",
]);
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();
}
});
});