// @vitest-environment jsdom import { act, renderHook, waitFor } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import { type ApplicationResult, useApplicationMutation, } from "../../src/presentation/adapters/query/application-query.ts"; import type { MutationIntentFactory } from "../../src/application/ports/mutation-intent-factory.ts"; import { RESOURCE_INVALIDATION_TOPIC, deterministicMutationIntentFactory, queryClient, scopeSnapshot, wrapper, } from "./application-query-fixture.tsx"; describe("application mutation intent admission", () => { it("retains optimistic data when execute throws after dispatch begins", async () => { const client = queryClient(); const key = ["resource", "thrown-unknown-effect"]; client.setQueryData(key, ["base"]); const hook = renderHook( () => useApplicationMutation({ execute: async () => { throw new Error("private transport defect"); }, currentData: client.getQueryData(key), optimistic: { queryKey: key, update: (previous, input) => [ ...(previous as string[]), input, ], }, }), { wrapper: wrapper(client) }, ); let outcome: ApplicationResult | undefined; await act(async () => { outcome = await hook.result.current.submit("created"); }); expect(outcome).toMatchObject({ ok: false, error: { effect: "MAYBE_APPLIED", retryable: false, action: "contact-support", }, }); expect(JSON.stringify(outcome)).not.toContain("private transport defect"); expect(client.getQueryData(key)).toEqual(["base", "created"]); expect(hook.result.current.state.indicator).toBe( "mutation-effect-unknown", ); }); it("creates a distinct logical intent for each independently admitted submit", async () => { const client = queryClient(); const scope = scopeSnapshot(); const observedIntents: unknown[] = []; const execute = vi.fn( async ( input: string, context: Readonly<{ signal: AbortSignal; intent?: unknown }>, ) => { observedIntents.push(context.intent); return { ok: true as const, value: input }; }, ); const hook = renderHook( () => useApplicationMutation({ definitionId: "independent-intent-v1", definitionVersion: 1, operationId: "CREATE_WITH_INTENT", requiresIdempotencyKey: true, owner: "platform-test", duplicatePolicy: "ALLOW_PARALLEL", scope, execute, invalidate: [], }), { wrapper: wrapper(client) }, ); await act(async () => { await hook.result.current.submit("same-input"); await hook.result.current.submit("same-input"); }); expect(observedIntents).toHaveLength(2); expect(observedIntents[0]).toMatchObject({ intentId: "intent-1", operationId: "CREATE_WITH_INTENT", idempotencyKey: "key-1", }); expect(observedIntents[1]).toMatchObject({ intentId: "intent-2", operationId: "CREATE_WITH_INTENT", idempotencyKey: "key-2", }); expect(observedIntents[0]).not.toEqual(observedIntents[1]); }); it("creates no second intent when JOIN_IDENTICAL shares an admitted submit", async () => { const client = queryClient(); const scope = scopeSnapshot(); const deterministicFactory = deterministicMutationIntentFactory(); const createIntent = vi.fn(deterministicFactory.create); const factory: MutationIntentFactory = Object.freeze({ create: createIntent, }); let complete: (value: ApplicationResult) => void = () => {}; const execute = vi.fn( () => new Promise>((resolve) => { complete = resolve; }), ); const hook = renderHook( () => useApplicationMutation({ definitionId: "joined-intent-v1", definitionVersion: 1, operationId: "CREATE_JOINED", requiresIdempotencyKey: true, owner: "platform-test", duplicatePolicy: "JOIN_IDENTICAL", scope, execute, invalidate: [], }), { wrapper: wrapper(client, factory) }, ); let first: Promise> | null = null; let joined: Promise> | null = null; act(() => { first = hook.result.current.submit("same-input"); joined = hook.result.current.submit("same-input"); }); expect(first).toBe(joined); await waitFor(() => expect(execute).toHaveBeenCalledOnce()); expect(createIntent).toHaveBeenCalledOnce(); expect(createIntent).toHaveBeenCalledWith({ operationId: "CREATE_JOINED", canonicalInputIdentity: "scope-fingerprint-0001:joined-intent-v1:scope-identity-token-0001", requiresIdempotencyKey: true, }); complete({ ok: true, value: "same-input" }); if (!first) throw new Error("expected admitted mutation"); await act(() => first); }); it("keeps the legacy raw mutation path outside the intent factory", async () => { const client = queryClient(); const createIntent = vi.fn(); const factory: MutationIntentFactory = Object.freeze({ create: createIntent, }); const execute = vi.fn(async (input: string) => ({ ok: true as const, value: input, })); const hook = renderHook( () => useApplicationMutation({ definitionId: "legacy-raw-path-v1", execute, }), { wrapper: wrapper(client, factory) }, ); await act(() => hook.result.current.submit("legacy-input")); expect(execute).toHaveBeenCalledOnce(); expect(createIntent).not.toHaveBeenCalled(); }); it("rejects a duplicate submit by default while one is active", async () => { const client = queryClient(); let complete: (value: ApplicationResult) => void = () => {}; const execute = vi.fn( () => new Promise>((resolve) => { complete = resolve; }), ); const hook = renderHook( () => useApplicationMutation({ definitionId: "legacy-duplicate-rejection-v1", execute, }), { wrapper: wrapper(client) }, ); let first: Promise> | null = null; let duplicate: Promise> | null = null; act(() => { first = hook.result.current.submit("created"); duplicate = hook.result.current.submit("created"); }); if (!first || !duplicate) throw new Error("expected two submissions"); await expect(duplicate).resolves.toMatchObject({ ok: false, error: { kind: "DUPLICATE_IN_FLIGHT" }, }); expect(execute).toHaveBeenCalledOnce(); complete({ ok: true, value: "created" }); await act(() => first as Promise>); }); it("deduplicates submit and commits one optimistic mutation", async () => { const client = queryClient(); const key = ["resource", "list"]; client.setQueryData(key, ["existing"]); let complete: (value: ApplicationResult) => void = () => {}; const execute = vi.fn( () => new Promise>((resolve) => { complete = resolve; }), ); const hook = renderHook( () => useApplicationMutation({ // ยง11.2: joining is opt-in. The default is REJECT_WHILE_ACTIVE. duplicatePolicy: "JOIN_IDENTICAL", execute, invalidate: [RESOURCE_INVALIDATION_TOPIC], currentData: client.getQueryData(key), optimistic: { queryKey: key, update: (previous, input) => [ ...(previous as string[]), input, ], }, }), { wrapper: wrapper(client) }, ); let first: Promise> | null = null; let duplicate: Promise> | null = null; act(() => { first = hook.result.current.submit("created"); duplicate = hook.result.current.submit("created"); }); expect(first).toBe(duplicate); await waitFor(() => expect(client.getQueryData(key)).toEqual(["existing", "created"]), ); await waitFor(() => expect(execute).toHaveBeenCalledOnce()); await waitFor(() => expect(hook.result.current.state.indicator).toBe("mutation-pending"), ); complete({ ok: true, value: "created" }); if (!first) throw new Error("expected pending mutation"); await act(() => first); expect(client.getQueryState(key)?.isInvalidated).toBe(true); await waitFor(() => expect(hook.result.current.state.indicator).toBeNull(), ); }); it("never joins distinct mutation inputs to the same runtime promise", async () => { const client = queryClient(); const resolvers = new Map< string, (value: ApplicationResult) => void >(); const execute = vi.fn( (input: string) => new Promise>((resolve) => { resolvers.set(input, resolve); }), ); const hook = renderHook( () => useApplicationMutation({ definitionId: "legacy-parallel-distinct-inputs-v1", execute, currentData: true, }), { wrapper: wrapper(client) }, ); let first: Promise> | undefined; let second: Promise> | undefined; act(() => { first = hook.result.current.submit("first"); second = hook.result.current.submit("second"); }); expect(first).not.toBe(second); await waitFor(() => expect(execute).toHaveBeenCalledTimes(2)); resolvers.get("first")?.({ ok: true, value: "first" }); resolvers.get("second")?.({ ok: true, value: "second" }); if (!first || !second) throw new Error("expected pending mutations"); await act(async () => { await Promise.all([first, second]); }); }); });