refactor: 리펙토링
This commit is contained in:
@@ -0,0 +1,283 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { createContractHttpExecutor } from "../../src/adapters/http/http-execution-v3.ts";
|
||||
import type { InstalledHttpContract } from "../../src/contracts/external-contract-runtime.ts";
|
||||
import { COMPOSED_CONTRACT_CONTRIBUTIONS } from "../../src/features/installed-contract-contributions.ts";
|
||||
|
||||
const installed = (() => {
|
||||
const candidate = COMPOSED_CONTRACT_CONTRIBUTIONS.httpByOperationId.get(
|
||||
"LIST_REFERENCE_RESOURCES",
|
||||
);
|
||||
if (!candidate) throw new Error("reference list contract is not installed");
|
||||
return candidate;
|
||||
})();
|
||||
|
||||
const scope = Object.freeze({
|
||||
generation: 1,
|
||||
fingerprint: "scope-1",
|
||||
identities: Object.freeze({}) as never,
|
||||
signal: new AbortController().signal,
|
||||
isCurrent: () => true,
|
||||
});
|
||||
|
||||
const createInstalled = (() => {
|
||||
const candidate = COMPOSED_CONTRACT_CONTRIBUTIONS.httpByOperationId.get(
|
||||
"CREATE_REFERENCE_RESOURCE",
|
||||
);
|
||||
if (!candidate) throw new Error("reference create contract is not installed");
|
||||
return candidate;
|
||||
})();
|
||||
|
||||
function operation(
|
||||
overrides: Readonly<{
|
||||
deadlineMs?: number;
|
||||
responseBody?: "REQUIRED_JSON" | "OPTIONAL_JSON" | "NONE";
|
||||
}> = {},
|
||||
): InstalledHttpContract<unknown, unknown, unknown> {
|
||||
return {
|
||||
...installed,
|
||||
contract: {
|
||||
...installed.contract,
|
||||
responseBody: overrides.responseBody ?? installed.contract.responseBody,
|
||||
},
|
||||
frontend: {
|
||||
...installed.frontend,
|
||||
totalDeadlineMs: overrides.deadlineMs ?? installed.frontend.totalDeadlineMs,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function flushMicrotasks(): Promise<void> {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
}
|
||||
|
||||
describe("descriptor-driven HTTP execution lifetime", () => {
|
||||
it.each([
|
||||
[{}, "limit=20"],
|
||||
[{ limit: "7" }, "limit=7"],
|
||||
])("projects the canonical validated input %#", async (input, expectedQuery) => {
|
||||
const fetcher = vi.fn(async () => Response.json([]));
|
||||
const executor = createContractHttpExecutor({
|
||||
baseUrl: "https://api.example/",
|
||||
maxRetryAttempts: 0,
|
||||
attachCredentials: () => ({
|
||||
kind: "READY",
|
||||
headers: {},
|
||||
credentials: "omit",
|
||||
}),
|
||||
fetcher,
|
||||
});
|
||||
|
||||
await expect(executor.execute(installed, input, { scope })).resolves.toMatchObject({
|
||||
kind: "SUCCESS",
|
||||
});
|
||||
expect(String((fetcher.mock.calls as unknown[][])[0]?.[0])).toContain(
|
||||
expectedQuery,
|
||||
);
|
||||
});
|
||||
|
||||
it("contains throwing and malformed external request projections", async () => {
|
||||
const executor = createContractHttpExecutor({
|
||||
baseUrl: "https://api.example/",
|
||||
maxRetryAttempts: 0,
|
||||
attachCredentials: () => ({
|
||||
kind: "READY",
|
||||
headers: {},
|
||||
credentials: "omit",
|
||||
}),
|
||||
fetcher: vi.fn(),
|
||||
});
|
||||
const throwing = {
|
||||
...installed,
|
||||
contract: {
|
||||
...installed.contract,
|
||||
projectRequest: () => {
|
||||
throw new Error("external descriptor defect");
|
||||
},
|
||||
},
|
||||
};
|
||||
const malformed = {
|
||||
...installed,
|
||||
contract: {
|
||||
...installed.contract,
|
||||
projectRequest: () => ({ pathValues: {}, queryEntries: [["limit"]], body: null }),
|
||||
},
|
||||
} as unknown as typeof installed;
|
||||
|
||||
await expect(executor.execute(throwing, { limit: 20 }, { scope })).resolves.toMatchObject({
|
||||
kind: "CONTRACT_VIOLATION",
|
||||
effect: "NOT_APPLICABLE",
|
||||
});
|
||||
await expect(executor.execute(malformed, { limit: 20 }, { scope })).resolves.toMatchObject({
|
||||
kind: "CONTRACT_VIOLATION",
|
||||
effect: "NOT_APPLICABLE",
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves MAYBE_APPLIED for a malformed command response after dispatch", async () => {
|
||||
const executor = createContractHttpExecutor({
|
||||
baseUrl: "https://api.example/",
|
||||
maxRetryAttempts: 0,
|
||||
attachCredentials: () => ({
|
||||
kind: "READY",
|
||||
headers: {},
|
||||
credentials: "omit",
|
||||
}),
|
||||
fetcher: vi.fn(async () => Response.json({ malformed: true }, { status: 201 })),
|
||||
});
|
||||
|
||||
await expect(
|
||||
executor.execute(
|
||||
createInstalled,
|
||||
{ name: "created" },
|
||||
{
|
||||
scope,
|
||||
intent: { intentId: "intent-1", startedBy: "USER", idempotencyKey: "key-1" },
|
||||
},
|
||||
),
|
||||
).resolves.toMatchObject({
|
||||
kind: "CONTRACT_VIOLATION",
|
||||
violation: { kind: "SUCCESS_SCHEMA_INVALID" },
|
||||
effect: "MAYBE_APPLIED",
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves MAYBE_APPLIED when a command response arrives after its scope fence", async () => {
|
||||
let current = true;
|
||||
const lifetime = new AbortController();
|
||||
const fencedScope = Object.freeze({
|
||||
...scope,
|
||||
signal: lifetime.signal,
|
||||
isCurrent: () => current,
|
||||
});
|
||||
const executor = createContractHttpExecutor({
|
||||
baseUrl: "https://api.example/",
|
||||
maxRetryAttempts: 0,
|
||||
attachCredentials: () => ({
|
||||
kind: "READY",
|
||||
headers: {},
|
||||
credentials: "omit",
|
||||
}),
|
||||
fetcher: vi.fn(async () => {
|
||||
current = false;
|
||||
lifetime.abort();
|
||||
return Response.json(
|
||||
{ id: "created", name: "Created" },
|
||||
{ status: 201 },
|
||||
);
|
||||
}),
|
||||
});
|
||||
|
||||
await expect(
|
||||
executor.execute(
|
||||
createInstalled,
|
||||
{ name: "created" },
|
||||
{
|
||||
scope: fencedScope,
|
||||
intent: { intentId: "intent-2", startedBy: "USER", idempotencyKey: "key-2" },
|
||||
},
|
||||
),
|
||||
).resolves.toMatchObject({
|
||||
kind: "CONTRACT_VIOLATION",
|
||||
violation: { kind: "SCOPE_FENCED" },
|
||||
effect: "MAYBE_APPLIED",
|
||||
});
|
||||
});
|
||||
|
||||
it("settles a credential hang at the total operation deadline", async () => {
|
||||
vi.useFakeTimers();
|
||||
let settled = false;
|
||||
const executor = createContractHttpExecutor({
|
||||
baseUrl: "https://api.example/",
|
||||
maxRetryAttempts: 0,
|
||||
attachCredentials: () => new Promise(() => {}),
|
||||
});
|
||||
|
||||
const result = executor
|
||||
.execute(operation({ deadlineMs: 5 }), { limit: 20 }, { scope })
|
||||
.then((outcome) => {
|
||||
settled = true;
|
||||
return outcome;
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(5);
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(settled).toBe(true);
|
||||
await expect(result).resolves.toMatchObject({
|
||||
kind: "TRANSPORT_FAILURE",
|
||||
failure: { kind: "TIMEOUT" },
|
||||
});
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("cancels a non-cooperative retry sleep when the caller aborts", async () => {
|
||||
const caller = new AbortController();
|
||||
let sleepSignal: AbortSignal | undefined;
|
||||
let settled = false;
|
||||
const executor = createContractHttpExecutor({
|
||||
baseUrl: "https://api.example/",
|
||||
maxRetryAttempts: 2,
|
||||
attachCredentials: () => ({
|
||||
kind: "READY",
|
||||
headers: {},
|
||||
credentials: "omit",
|
||||
}),
|
||||
fetcher: vi.fn(async () =>
|
||||
Response.json(
|
||||
{ type: "about:blank", title: "temporary", status: 503 },
|
||||
{ status: 503 },
|
||||
),
|
||||
),
|
||||
sleep: (_ms, signal) => {
|
||||
sleepSignal = signal;
|
||||
return new Promise(() => {});
|
||||
},
|
||||
random: () => 0,
|
||||
});
|
||||
|
||||
const result = executor
|
||||
.execute(installed, { limit: 20 }, { scope, signal: caller.signal })
|
||||
.then((outcome) => {
|
||||
settled = true;
|
||||
return outcome;
|
||||
});
|
||||
await vi.waitFor(() => expect(sleepSignal).toBeDefined());
|
||||
caller.abort();
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(sleepSignal?.aborted).toBe(true);
|
||||
expect(settled).toBe(true);
|
||||
await expect(result).resolves.toMatchObject({ kind: "CANCELLED" });
|
||||
});
|
||||
|
||||
it("treats an unreadable forbidden-body probe as a transport failure", async () => {
|
||||
const body = new ReadableStream<Uint8Array>({
|
||||
pull(controller) {
|
||||
controller.error(new TypeError("stream failed"));
|
||||
},
|
||||
});
|
||||
const executor = createContractHttpExecutor({
|
||||
baseUrl: "https://api.example/",
|
||||
maxRetryAttempts: 0,
|
||||
attachCredentials: () => ({
|
||||
kind: "READY",
|
||||
headers: {},
|
||||
credentials: "omit",
|
||||
}),
|
||||
fetcher: vi.fn(async () => new Response(body, { status: 200 })),
|
||||
});
|
||||
|
||||
await expect(
|
||||
executor.execute(
|
||||
operation({ responseBody: "NONE" }),
|
||||
{ limit: 20 },
|
||||
{ scope },
|
||||
),
|
||||
).resolves.toMatchObject({
|
||||
kind: "TRANSPORT_FAILURE",
|
||||
failure: { kind: "RESPONSE_STREAM_FAILURE" },
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user