// @vitest-environment jsdom import { 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 { createFailure } from "../../src/contracts/errors.ts"; import { queryClient, scopeSnapshot, wrapper, } from "./application-query-fixture.tsx"; describe("scope-bound mutation fence", () => { it("rejects a submit whose scope is already fenced", async () => { const client = queryClient(); const scope = scopeSnapshot(); const execute = vi.fn(async () => ({ ok: true as const, value: "v" })); const hook = renderHook( () => useApplicationMutation({ definitionId: "fenced-mutation-v1", definitionVersion: 1, operationId: "CREATE_FENCED", requiresIdempotencyKey: false, owner: "platform-test", duplicatePolicy: "REJECT_WHILE_ACTIVE", scope, execute, invalidate: [], }), { wrapper: wrapper(client) }, ); scope.fence(); const outcome = await hook.result.current.submit("value"); expect(outcome).toMatchObject({ ok: false, error: { kind: "SCOPE_GENERATION_CHANGED", effect: "NOT_STARTED", }, }); expect(execute).not.toHaveBeenCalled(); }); it("discards a mutation result whose scope was fenced after dispatch", async () => { const client = queryClient(); const scope = scopeSnapshot(); const execute = vi.fn(async () => { scope.fence(); return { ok: true as const, value: "committed" }; }); const hook = renderHook( () => useApplicationMutation({ definitionId: "late-mutation-v1", definitionVersion: 1, operationId: "CREATE_LATE", requiresIdempotencyKey: false, owner: "platform-test", duplicatePolicy: "ALLOW_PARALLEL", scope, execute, invalidate: [], }), { wrapper: wrapper(client) }, ); const outcome = await hook.result.current.submit("value"); expect(outcome).toMatchObject({ ok: false, error: { kind: "SCOPE_GENERATION_CHANGED" }, }); expect(execute).toHaveBeenCalledOnce(); }); it("aborts a hung mutation when its captured scope is fenced", async () => { const client = queryClient(); const scope = scopeSnapshot(); let observedSignal: AbortSignal | undefined; const execute = vi.fn( (_input: string, context: Readonly<{ signal: AbortSignal }>) => new Promise>((resolve) => { observedSignal = context.signal; context.signal.addEventListener( "abort", () => resolve({ ok: false, error: createFailure("REQUEST_ABORTED", "CREATE_HUNG", 0), }), { once: true }, ); }), ); const hook = renderHook( () => useApplicationMutation({ definitionId: "hung-mutation-v1", definitionVersion: 1, operationId: "CREATE_HUNG", requiresIdempotencyKey: false, owner: "platform-test", duplicatePolicy: "REJECT_WHILE_ACTIVE", scope, execute, invalidate: [], }), { wrapper: wrapper(client) }, ); const outcome = hook.result.current.submit("value"); await waitFor(() => expect(observedSignal).toBe(scope.signal)); scope.fence(); expect(observedSignal?.aborted).toBe(true); await expect(outcome).resolves.toMatchObject({ ok: false, error: { kind: "SCOPE_GENERATION_CHANGED", effect: "MAYBE_APPLIED", retryable: false, action: "contact-support", }, }); }); });