feat: add removable reference feature vertical slice

This commit is contained in:
donghyeon-ka
2026-07-26 14:56:34 +09:00
parent 980981bc86
commit c11be43f20
87 changed files with 1881 additions and 1114 deletions
@@ -1,7 +1,10 @@
import { describe, expect, it, vi } from "vitest";
import { createHttpClient } from "../../src/adapters/http/client.js";
import { queryKeys } from "../../src/contracts/query-keys.js";
import {
entityQueryKeys,
TEST_HTTP_CONTRACT,
} from "../helpers/http-contract-fixture.js";
/** @param {unknown} data */
function successResponse(data) {
@@ -40,6 +43,11 @@ function recordingScheduler() {
};
}
/** @param {Parameters<typeof createHttpClient>[0]} options */
function testClient(options) {
return createHttpClient({ ...TEST_HTTP_CONTRACT, ...options });
}
describe("HTTP operation execution contract", () => {
it("sends parsed search/body values and aligns canonical query identity", async () => {
const requests = /** @type {Request[]} */ ([]);
@@ -51,7 +59,7 @@ describe("HTTP operation execution contract", () => {
}
return successResponse([]);
});
const client = createHttpClient({
const client = testClient({
baseUrl: "https://api.test",
fetcher,
clock: immediateClock(),
@@ -60,21 +68,21 @@ describe("HTTP operation execution contract", () => {
const filters = { tags: ["open", "new"], cursor: "a/b", limit: 5 };
await client.execute({
operationId: "LIST_SAMPLE_RESOURCES",
routeId: "SAMPLE_RESOURCE_LIST",
operationId: "LIST_ENTITIES",
routeId: "TEST_ROUTE",
searchParams: filters,
});
await client.execute({
operationId: "CREATE_SAMPLE_RESOURCE",
routeId: "SAMPLE_RESOURCE_LIST",
operationId: "CREATE_ENTITY",
routeId: "TEST_ROUTE",
body: { name: " Trimmed " },
idempotencyKey: "logical-command",
});
expect(requests[0].url).toBe(
"https://api.test/api/sample/resources?cursor=a%2Fb&limit=5&tags=open&tags=new",
"https://api.test/api/entities?cursor=a%2Fb&limit=5&tags=open&tags=new",
);
expect(queryKeys.resource.list(filters).at(-1)).toEqual(filters);
expect(entityQueryKeys.list(filters).at(-1)).toEqual(filters);
await expect(requests[1].json()).resolves.toEqual({ name: "Trimmed" });
expect(requests[1].headers.get("Idempotency-Key")).toBe("logical-command");
expect(scheduler.setTimeout).toHaveBeenCalledTimes(2);
@@ -84,7 +92,7 @@ describe("HTTP operation execution contract", () => {
it("performs no fetch or timer work for invalid request input", async () => {
const fetcher = vi.fn();
const scheduler = recordingScheduler();
const client = createHttpClient({
const client = testClient({
baseUrl: "https://api.test",
fetcher,
scheduler,
@@ -92,8 +100,8 @@ describe("HTTP operation execution contract", () => {
await expect(
client.execute({
operationId: "CREATE_SAMPLE_RESOURCE",
routeId: "SAMPLE_RESOURCE_LIST",
operationId: "CREATE_ENTITY",
routeId: "TEST_ROUTE",
body: { name: " " },
}),
).resolves.toMatchObject({
@@ -114,7 +122,7 @@ describe("HTTP operation execution contract", () => {
async (maxRetryAttempts, totalAttempts) => {
const fetcher = vi.fn(async () => failureResponse(503));
const scheduler = recordingScheduler();
const client = createHttpClient({
const client = testClient({
baseUrl: "https://api.test",
fetcher,
clock: immediateClock(),
@@ -124,8 +132,8 @@ describe("HTTP operation execution contract", () => {
await expect(
client.execute({
operationId: "LIST_SAMPLE_RESOURCES",
routeId: "SAMPLE_RESOURCE_LIST",
operationId: "LIST_ENTITIES",
routeId: "TEST_ROUTE",
}),
).resolves.toMatchObject({
ok: false,
@@ -149,15 +157,15 @@ describe("HTTP operation execution contract", () => {
);
}),
);
const client = createHttpClient({
const client = testClient({
baseUrl: "https://api.test",
fetcher,
scheduler,
maxRetryAttempts: 0,
});
const timeoutResult = client.execute({
operationId: "LIST_SAMPLE_RESOURCES",
routeId: "SAMPLE_RESOURCE_LIST",
operationId: "LIST_ENTITIES",
routeId: "TEST_ROUTE",
});
await vi.waitFor(() => expect(scheduler.callbacks).toHaveLength(1));
scheduler.callbacks[0]();
@@ -170,8 +178,8 @@ describe("HTTP operation execution contract", () => {
const add = vi.spyOn(caller.signal, "addEventListener");
const remove = vi.spyOn(caller.signal, "removeEventListener");
const abortResult = client.execute({
operationId: "LIST_SAMPLE_RESOURCES",
routeId: "SAMPLE_RESOURCE_LIST",
operationId: "LIST_ENTITIES",
routeId: "TEST_ROUTE",
signal: caller.signal,
});
await vi.waitFor(() => expect(fetcher).toHaveBeenCalledTimes(2));
@@ -196,11 +204,11 @@ describe("HTTP operation execution contract", () => {
retry: "never",
requestSource: "none",
requestSchema: "unused",
responseSchema: "SampleResourcePayload",
responseSchema: "EntityPayload",
owner: "test",
});
const unsafeFetch = vi.fn(async () => failureResponse(503));
const unsafeClient = createHttpClient({
const unsafeClient = testClient({
baseUrl: "https://api.test",
fetcher: unsafeFetch,
clock: immediateClock(),
@@ -213,14 +221,14 @@ describe("HTTP operation execution contract", () => {
expect(unsafeFetch).toHaveBeenCalledOnce();
const statusFetch = vi.fn(async () => failureResponse(500));
const statusClient = createHttpClient({
const statusClient = testClient({
baseUrl: "https://api.test",
fetcher: statusFetch,
clock: immediateClock(),
});
await statusClient.execute({
operationId: "LIST_SAMPLE_RESOURCES",
routeId: "SAMPLE_RESOURCE_LIST",
operationId: "LIST_ENTITIES",
routeId: "TEST_ROUTE",
});
expect(statusFetch).toHaveBeenCalledOnce();
@@ -228,15 +236,15 @@ describe("HTTP operation execution contract", () => {
successResponse([{ id: "one", name: 42 }]),
);
const schemaScheduler = recordingScheduler();
const schemaClient = createHttpClient({
const schemaClient = testClient({
baseUrl: "https://api.test",
fetcher: schemaFetch,
clock: immediateClock(),
scheduler: schemaScheduler,
});
await schemaClient.execute({
operationId: "LIST_SAMPLE_RESOURCES",
routeId: "SAMPLE_RESOURCE_LIST",
operationId: "LIST_ENTITIES",
routeId: "TEST_ROUTE",
});
expect(schemaFetch).toHaveBeenCalledOnce();
expect(schemaScheduler.clearTimeout).toHaveBeenCalledOnce();