// @vitest-environment jsdom import { act, renderHook, waitFor, } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import type { ReactNode } from "react"; import { renderToString } from "react-dom/server"; import { describe, expect, it, vi } from "vitest"; import { type ApplicationResult, useApplicationMutation, useApplicationQuery, } from "../../src/presentation/adapters/query/application-query.ts"; import type { MutationIntentFactory } from "../../src/application/ports/mutation-intent-factory.ts"; import { MutationIntentProvider } from "../../src/presentation/adapters/query/mutation-intent-provider.tsx"; import { QueryInvalidationProvider } from "../../src/presentation/adapters/query/query-invalidation-provider.tsx"; import { defineQueryInvalidationTopic, type QueryInvalidationCoordinator, } from "../../src/contracts/query-invalidation.ts"; import { createFailure } from "../../src/contracts/errors.ts"; import { createQueryInvalidationPrefix, createRuntimeIdentityRegistry, defineQueryNamespaceIdentity, } from "../../src/contracts/query-keys.ts"; import { bindQuery, MUTATION_COORDINATOR_BOUNDS, type QueryResultMeasure, } from "../../src/contracts/server-state.ts"; import type { CacheScopeSnapshot } from "../../src/contracts/server-state-scope.ts"; const RESOURCE_INVALIDATION_TOPIC = defineQueryInvalidationTopic("resource"); /** * §24.12: the scope-bound commit fence is common runtime, so it is verified * here with a local scope fixture rather than through the removable sample * feature. */ function scopeSnapshot( generation = 1, fingerprint = "scope-fingerprint-0001", ): CacheScopeSnapshot & { fence(): void } { let current = true; const lifetime = new AbortController(); const identities = createRuntimeIdentityRegistry({ tokenFactory: () => "scope-identity-token-0001", }); return { generation, fingerprint, identities, signal: lifetime.signal, isCurrent: () => current, fence() { current = false; lifetime.abort(); }, }; } function measureOne(): QueryResultMeasure { return { itemCount: 1, estimatedBytes: 8 }; } function queryClient() { return new QueryClient({ defaultOptions: { queries: { retry: false, staleTime: 0, gcTime: Infinity }, mutations: { retry: false }, }, }); } function deterministicMutationIntentFactory(): MutationIntentFactory { let sequence = 0; return Object.freeze({ create(input) { sequence += 1; return Object.freeze({ intentId: `intent-${sequence}`, operationId: input.operationId, canonicalInputIdentity: input.canonicalInputIdentity, ...(input.requiresIdempotencyKey ? { idempotencyKey: `key-${sequence}` } : {}), createdAtMonotonicMs: sequence, }); }, }); } function wrapper( client: QueryClient, mutationIntentFactory = deterministicMutationIntentFactory(), ) { const coordinator: QueryInvalidationCoordinator = { async invalidate(topics) { for (const topic of topics) { await client.invalidateQueries({ queryKey: [topic], exact: false, refetchType: "active", }); } }, beginMutation() { return { release: async () => {} }; }, async resetLocal() { await client.cancelQueries(); client.clear(); }, dispose() {}, }; return function QueryWrapper({ children }: { children: ReactNode }) { return ( {children} ); }; } describe("application query inbound bridge", () => { it("latches a background failure over stale data and clears it on retry success", async () => { const client = queryClient(); const responses: ApplicationResult[] = [ { ok: true, value: ["first"] }, { ok: false, error: createFailure("SERVER_FAILURE", "LIST", 0), }, { ok: true, value: ["recovered"] }, ]; const execute = vi.fn(async () => responses.shift() ?? responses[0]); const hook = renderHook( () => useApplicationQuery({ queryKey: ["resource", "list"], execute, }), { wrapper: wrapper(client) }, ); await waitFor(() => expect(hook.result.current.data).toEqual(["first"])); await act(() => hook.result.current.retry()); await waitFor(() => expect(hook.result.current.state.indicator).toBe("stale-degraded"), ); expect(hook.result.current.state.base).toBe("success"); expect(hook.result.current.data).toEqual(["first"]); await act(() => hook.result.current.retry()); await waitFor(() => expect(hook.result.current.data).toEqual(["recovered"]), ); expect(hook.result.current.state.indicator).toBeNull(); expect(execute).toHaveBeenCalledTimes(3); }); it("projects an initial application failure into terminal state", async () => { const client = queryClient(); const failure = createFailure("FORBIDDEN", "LIST", 0); const hook = renderHook( () => useApplicationQuery({ queryKey: ["forbidden"], execute: async () => ({ ok: false, error: failure }), }), { wrapper: wrapper(client) }, ); await waitFor(() => expect(hook.result.current.state.base).toBe("terminal-error"), ); expect(hook.result.current.state.failure).toBe(failure); }); it("normalizes an unexpected execute rejection into a safe terminal failure", async () => { const client = queryClient(); const hook = renderHook( () => useApplicationQuery({ queryKey: ["unexpected-rejection"], execute: async () => { throw new Error("private upstream detail"); }, }), { wrapper: wrapper(client) }, ); await waitFor(() => expect(hook.result.current.state.base).toBe("terminal-error"), ); expect(hook.result.current.state.failure).toMatchObject({ kind: "UNKNOWN_FAILURE", operationId: "APPLICATION_QUERY", userMessageKey: "error.unknown_failure", action: "contact-support", }); expect(JSON.stringify(hook.result.current.state.failure)).not.toContain( "private upstream detail", ); }); it("passes cancellation to the application and does not retain an unmounted error", async () => { const client = queryClient(); let aborted = false; const execute = vi.fn( ({ signal }: { signal: AbortSignal }) => new Promise>((resolve) => { signal.addEventListener( "abort", () => { aborted = true; resolve({ ok: false, error: createFailure("REQUEST_ABORTED", "LIST", 0), }); }, { once: true }, ); }), ); const hook = renderHook( () => useApplicationQuery({ queryKey: ["cancelled"], execute, }), { wrapper: wrapper(client) }, ); await waitFor(() => expect(execute).toHaveBeenCalledOnce()); hook.unmount(); await waitFor(() => expect(aborted).toBe(true)); expect(client.getQueryState(["cancelled"])?.status).not.toBe("error"); }); }); describe("scope-bound query commit fence", () => { it("binds the namespace-first V2 query key", () => { const scope = scopeSnapshot(); const definition = { definitionId: "resource-detail-v1", definitionVersion: 1, owner: "platform-test", namespace: "resource", namespaceVersion: 1, operationId: "GET_RESOURCE", profileId: "DETAIL_STANDARD" as const, measureResult: measureOne, execute: async () => ({ ok: true as const, value: "value" }), }; const bound = bindQuery(definition, "resource-1", scope); expect(bound.queryKey).toEqual([ "query", 2, "resource", 1, "scope-fingerprint-0001", 1, "scope-identity-token-0001", ]); expect(bound.queryKey.slice(0, 4)).toEqual( createQueryInvalidationPrefix( defineQueryNamespaceIdentity("resource", 1), ), ); }); it("discards a successful result whose scope was fenced during execution", async () => { const client = queryClient(); const scope = scopeSnapshot(); let complete: (value: ApplicationResult) => void = () => {}; const hook = renderHook( () => useApplicationQuery( bindQuery( { definitionId: "fenced-detail-v1", definitionVersion: 1, owner: "platform-test", namespace: "fenced", namespaceVersion: 1, operationId: "GET_FENCED", profileId: "DETAIL_STANDARD", measureResult: measureOne, execute: () => new Promise>((resolve) => { complete = resolve; }), }, "input", scope, ), ), { wrapper: wrapper(client) }, ); // §10.8: the scope goes stale after dispatch but before commit. scope.fence(); await act(async () => { complete({ ok: true, value: "late" }); }); await waitFor(() => expect(hook.result.current.state.failure?.kind).toBe( "SCOPE_GENERATION_CHANGED", ), ); expect(hook.result.current.data).toBeUndefined(); }); it("refuses to start when the captured scope is already stale", async () => { const client = queryClient(); const scope = scopeSnapshot(); const execute = vi.fn(async () => ({ ok: true as const, value: "v" })); scope.fence(); const hook = renderHook( () => useApplicationQuery( bindQuery( { definitionId: "stale-detail-v1", definitionVersion: 1, owner: "platform-test", namespace: "stale", namespaceVersion: 1, operationId: "GET_STALE", profileId: "DETAIL_STANDARD", measureResult: measureOne, execute, }, "input", scope, ), ), { wrapper: wrapper(client) }, ); await waitFor(() => expect(hook.result.current.state.failure?.kind).toBe( "SCOPE_GENERATION_CHANGED", ), ); expect(execute).not.toHaveBeenCalled(); }); it("rejects a result that exceeds the profile budget instead of caching it", async () => { const client = queryClient(); const scope = scopeSnapshot(); const hook = renderHook( () => useApplicationQuery( bindQuery( { definitionId: "oversized-list-v1", definitionVersion: 1, owner: "platform-test", namespace: "oversized", namespaceVersion: 1, operationId: "LIST_OVERSIZED", profileId: "VOLATILE_STATUS", // §10.4: VOLATILE_STATUS admits 1 item and 64KiB. measureResult: (): QueryResultMeasure => ({ itemCount: 2, estimatedBytes: 8, }), execute: async () => ({ ok: true as const, value: "value" }), }, "input", scope, ), ), { wrapper: wrapper(client) }, ); await waitFor(() => expect(hook.result.current.state.failure?.kind).toBe( "RESULT_LIMIT_EXCEEDED", ), ); expect(hook.result.current.data).toBeUndefined(); }); it("treats a throwing measurement as a measurement failure, not a cache commit", async () => { const client = queryClient(); const scope = scopeSnapshot(); const hook = renderHook( () => useApplicationQuery( bindQuery( { definitionId: "unmeasurable-v1", definitionVersion: 1, owner: "platform-test", namespace: "unmeasurable", namespaceVersion: 1, operationId: "GET_UNMEASURABLE", profileId: "DETAIL_STANDARD", measureResult: (): QueryResultMeasure => { throw new Error("estimator defect"); }, execute: async () => ({ ok: true as const, value: "value" }), }, "input", scope, ), ), { wrapper: wrapper(client) }, ); await waitFor(() => expect(hook.result.current.state.failure?.kind).toBe( "RESULT_LIMIT_EXCEEDED", ), ); expect(hook.result.current.data).toBeUndefined(); }); }); 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", }, }); }); }); describe("application mutation inbound bridge", () => { it.each([ ["NOT_STARTED", ["existing"], 0, null], ["NOT_APPLIED", ["existing"], 0, null], ["APPLIED_CONFIRMED", ["existing", "created"], 1, null], [ "MAYBE_APPLIED", ["existing", "created"], 0, "mutation-effect-unknown", ], ] as const)( "settles an optimistic failure with %s certainty", async (effect, expectedData, expectedInvalidations, expectedIndicator) => { const client = queryClient(); const key = ["resource", `certainty-${effect}`]; client.setQueryData(key, ["existing"]); const invalidateQueries = vi.spyOn(client, "invalidateQueries"); const failure = createFailure("SERVER_FAILURE", "CREATE", 0, { effect, }); const hook = renderHook( () => useApplicationMutation({ execute: async () => ({ ok: false, error: failure }), invalidate: [RESOURCE_INVALIDATION_TOPIC], 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: { kind: failure.kind, effect }, }); expect(client.getQueryData(key)).toEqual([...expectedData]); expect(invalidateQueries).toHaveBeenCalledTimes(expectedInvalidations); expect(hook.result.current.state.indicator).toBe(expectedIndicator); if (effect === "MAYBE_APPLIED") { expect(outcome).toMatchObject({ ok: false, error: { retryable: false, action: "contact-support" }, }); expect(hook.result.current.state.overlay).toMatchObject({ mutationEffectUnknown: true, mutationPending: false, mutationConflict: false, }); } if (effect === "APPLIED_CONFIRMED") { expect(outcome).toMatchObject({ ok: false, error: { retryable: false, action: "none" }, }); } }, ); it("keeps an applied-confirmed layer committed when invalidation fails", async () => { const client = queryClient(); const key = ["resource", "applied-invalidation-failure"]; client.setQueryData(key, ["base"]); const invalidateQueries = vi .spyOn(client, "invalidateQueries") .mockImplementation(async () => { expect(client.getQueryData(key)).toEqual(["base", "created"]); throw new Error("refresh failed"); }); const failure = createFailure("SERVER_FAILURE", "CREATE", 0, { effect: "APPLIED_CONFIRMED", }); const hook = renderHook( () => useApplicationMutation({ definitionId: "legacy-applied-invalidation-failure-v1", execute: async () => ({ ok: false, error: failure }), invalidate: [RESOURCE_INVALIDATION_TOPIC], 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: "APPLIED_CONFIRMED", retryable: false, action: "none", }, }); expect(invalidateQueries).toHaveBeenCalledOnce(); expect(client.getQueryData(key)).toEqual(["base", "created"]); }); it("treats a command failure with missing effect certainty as maybe applied", async () => { const client = queryClient(); const key = ["resource", "missing-effect"]; client.setQueryData(key, ["existing"]); const invalidateQueries = vi.spyOn(client, "invalidateQueries"); const failure = createFailure("SERVER_FAILURE", "CREATE", 0); const execute = vi.fn(async () => ({ ok: false as const, error: failure })); const hook = renderHook( () => useApplicationMutation({ execute, invalidate: [RESOURCE_INVALIDATION_TOPIC], 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(execute).toHaveBeenCalledOnce(); expect(outcome).toMatchObject({ ok: false, error: { effect: "MAYBE_APPLIED", retryable: false, action: "contact-support", }, }); expect(client.getQueryData(key)).toEqual(["existing", "created"]); expect(invalidateQueries).not.toHaveBeenCalled(); expect(hook.result.current.state.indicator).toBe( "mutation-effect-unknown", ); }); it.each([ ["APPLIED", ["existing", "created"], 1], ["NOT_APPLIED", ["existing"], 0], ] as const)( "reconciles an unknown optimistic effect as %s exactly once", async (resolution, expectedData, expectedInvalidations) => { const client = queryClient(); const scope = scopeSnapshot(); const key = ["resource", `reconcile-${resolution}`]; client.setQueryData(key, ["existing"]); const invalidateQueries = vi.spyOn(client, "invalidateQueries"); const failure = createFailure("SERVER_FAILURE", "CREATE", 0, { effect: "MAYBE_APPLIED", }); const hook = renderHook( () => useApplicationMutation({ definitionId: `reconcile-${resolution}-v1`, definitionVersion: 1, operationId: "CREATE", requiresIdempotencyKey: true, owner: "platform-test", duplicatePolicy: "REJECT_WHILE_ACTIVE", scope, execute: async () => ({ ok: false, error: failure }), invalidate: [RESOURCE_INVALIDATION_TOPIC], optimistic: { queryKey: key, update: (previous, input) => [ ...(previous as string[]), input, ], }, }), { wrapper: wrapper(client) }, ); await act(() => hook.result.current.submit("created")); expect(hook.result.current.state.indicator).toBe( "mutation-effect-unknown", ); await act(() => hook.result.current.reconcileUnknownEffect(resolution)); await act(() => hook.result.current.reconcileUnknownEffect( resolution === "APPLIED" ? "NOT_APPLIED" : "APPLIED", ), ); expect(client.getQueryData(key)).toEqual([...expectedData]); expect(invalidateQueries).toHaveBeenCalledTimes(expectedInvalidations); expect(hook.result.current.state.indicator).toBeNull(); }, ); it("reconciles parallel unknown effects without losing either lease", async () => { const client = queryClient(); const scope = scopeSnapshot(); const key = ["resource", "parallel-unknown"]; client.setQueryData(key, ["base"]); const invalidateQueries = vi.spyOn(client, "invalidateQueries"); const failure = createFailure("SERVER_FAILURE", "CREATE", 0, { effect: "MAYBE_APPLIED", }); const resolvers: Array<(value: ApplicationResult) => void> = []; const execute = vi.fn( (_input: string) => new Promise>((resolve) => { resolvers.push(resolve); }), ); const hook = renderHook( () => useApplicationMutation({ definitionId: "parallel-unknown-v1", definitionVersion: 1, operationId: "CREATE", requiresIdempotencyKey: true, owner: "platform-test", duplicatePolicy: "ALLOW_PARALLEL", scope, execute, invalidate: [RESOURCE_INVALIDATION_TOPIC], optimistic: { queryKey: key, update: (previous, input) => [ ...(previous as string[]), input, ], }, }), { wrapper: wrapper(client) }, ); let first: Promise> | undefined; let second: Promise> | undefined; act(() => { first = hook.result.current.submit("created"); second = hook.result.current.submit("created"); }); await waitFor(() => expect(resolvers).toHaveLength(2)); act(() => resolvers[0]?.({ ok: false, error: failure })); if (!first) throw new Error("expected first mutation"); await act(() => first); expect(hook.result.current.state.indicator).toBe( "mutation-effect-unknown", ); act(() => resolvers[1]?.({ ok: false, error: failure })); if (!second) throw new Error("expected second mutation"); await act(() => second); expect(client.getQueryData(key)).toEqual(["base", "created", "created"]); await act(() => hook.result.current.reconcileUnknownEffect("NOT_APPLIED")); expect(client.getQueryData(key)).toEqual(["base", "created"]); expect(hook.result.current.state.indicator).toBe( "mutation-effect-unknown", ); await act(() => hook.result.current.reconcileUnknownEffect("APPLIED")); expect(client.getQueryData(key)).toEqual(["base", "created"]); expect(invalidateQueries).toHaveBeenCalledOnce(); expect(hook.result.current.state.indicator).toBeNull(); }); it("closes an unknown optimistic effect safely after scope expiry", async () => { const client = queryClient(); const scope = scopeSnapshot(); const key = ["resource", "expired-unknown"]; client.setQueryData(key, ["existing"]); const invalidateQueries = vi.spyOn(client, "invalidateQueries"); const failure = createFailure("SERVER_FAILURE", "CREATE", 0, { effect: "MAYBE_APPLIED", }); const hook = renderHook( () => useApplicationMutation({ definitionId: "expired-unknown-v1", definitionVersion: 1, operationId: "CREATE", requiresIdempotencyKey: true, owner: "platform-test", duplicatePolicy: "REJECT_WHILE_ACTIVE", scope, execute: async () => ({ ok: false, error: failure }), invalidate: [RESOURCE_INVALIDATION_TOPIC], optimistic: { queryKey: key, update: (previous, input) => [ ...(previous as string[]), input, ], }, }), { wrapper: wrapper(client) }, ); await act(() => hook.result.current.submit("created")); scope.fence(); await act(() => hook.result.current.reconcileUnknownEffect("APPLIED")); expect(client.getQueryState(key)).toBeUndefined(); expect(invalidateQueries).not.toHaveBeenCalled(); expect(hook.result.current.state.indicator).toBeNull(); }); it("reconciles a legacy unknown effect without an optimistic layer", async () => { const client = queryClient(); const invalidateQueries = vi.spyOn(client, "invalidateQueries"); const failure = createFailure("SERVER_FAILURE", "CREATE", 0, { effect: "MAYBE_APPLIED", }); const hook = renderHook( () => useApplicationMutation({ definitionId: "legacy-no-layer-reconcile-v1", execute: async () => ({ ok: false, error: failure }), invalidate: [RESOURCE_INVALIDATION_TOPIC], }), { wrapper: wrapper(client) }, ); await act(() => hook.result.current.submit("created")); expect(invalidateQueries).not.toHaveBeenCalled(); await act(() => hook.result.current.reconcileUnknownEffect("APPLIED")); expect(invalidateQueries).toHaveBeenCalledOnce(); expect(hook.result.current.state.indicator).toBeNull(); }); it("preserves a later legacy commit when an earlier unknown effect is not applied", async () => { const client = queryClient(); const key = ["resource", "legacy-ordered-unknown"]; client.setQueryData(key, ["base"]); const uncertainFailure = createFailure("SERVER_FAILURE", "CREATE", 0, { effect: "MAYBE_APPLIED", }); 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-active-after-unknown-v1", duplicatePolicy: "ALLOW_PARALLEL", execute, currentData: client.getQueryData(key), optimistic: { queryKey: key, update: (previous, input) => [ ...(previous as string[]), input, ], }, }), { wrapper: wrapper(client) }, ); let first: Promise> | undefined; let second: Promise> | undefined; act(() => { first = hook.result.current.submit("first"); second = hook.result.current.submit("second"); }); await waitFor(() => expect(resolvers.size).toBe(2)); act(() => resolvers.get("first")?.({ ok: false, error: uncertainFailure }), ); if (!first) throw new Error("expected first mutation"); await act(() => first); act(() => resolvers.get("second")?.({ ok: true, value: "second" }), ); if (!second) throw new Error("expected second mutation"); await act(() => second); expect(client.getQueryData(key)).toEqual(["base", "first", "second"]); await act(() => hook.result.current.reconcileUnknownEffect("NOT_APPLIED")); expect(client.getQueryData(key)).toEqual(["base", "second"]); }); it("keeps a newer submit pending while reconciling an older unknown effect", async () => { const client = queryClient(); const failure = createFailure("SERVER_FAILURE", "CREATE", 0, { effect: "MAYBE_APPLIED", }); let completeSecond: (value: ApplicationResult) => void = () => {}; const execute = vi.fn((input: string) => input === "first" ? Promise.resolve({ ok: false as const, error: failure }) : new Promise>((resolve) => { completeSecond = resolve; }), ); const hook = renderHook( () => useApplicationMutation({ definitionId: "legacy-active-after-unknown-no-layer-v1", duplicatePolicy: "ALLOW_PARALLEL", execute, }), { wrapper: wrapper(client) }, ); await act(() => hook.result.current.submit("first")); let second: Promise> | undefined; act(() => { second = hook.result.current.submit("second"); }); await waitFor(() => expect(execute).toHaveBeenCalledTimes(2)); expect(hook.result.current.state.indicator).toBe( "mutation-effect-unknown", ); await act(() => hook.result.current.reconcileUnknownEffect("NOT_APPLIED")); expect(hook.result.current.state.indicator).toBe("mutation-pending"); completeSecond({ ok: true, value: "second" }); if (!second) throw new Error("expected second mutation"); await act(() => second); await waitFor(() => expect(hook.result.current.state.indicator).toBeNull()); }); it("serializes reconciliation so a double action cannot settle the next intent", async () => { const client = queryClient(); const failure = createFailure("SERVER_FAILURE", "CREATE", 0, { effect: "MAYBE_APPLIED", }); let finishInvalidation: () => void = () => {}; const invalidation = new Promise((resolve) => { finishInvalidation = resolve; }); vi.spyOn(client, "invalidateQueries").mockImplementation( async () => invalidation, ); const hook = renderHook( () => useApplicationMutation({ definitionId: "legacy-applied-double-action-v1", duplicatePolicy: "ALLOW_PARALLEL", execute: async () => ({ ok: false, error: failure }), invalidate: [RESOURCE_INVALIDATION_TOPIC], }), { wrapper: wrapper(client) }, ); await act(async () => { await Promise.all([ hook.result.current.submit("first"), hook.result.current.submit("second"), ]); }); let firstResolution: Promise | undefined; let duplicateResolution: Promise | undefined; act(() => { firstResolution = hook.result.current.reconcileUnknownEffect("APPLIED"); duplicateResolution = hook.result.current.reconcileUnknownEffect( "NOT_APPLIED", ); }); await expect(duplicateResolution).resolves.toBeUndefined(); expect(hook.result.current.state.indicator).toBe("mutation-pending"); finishInvalidation(); if (!firstResolution) throw new Error("expected reconciliation"); await act(() => firstResolution); expect(hook.result.current.state.indicator).toBe( "mutation-effect-unknown", ); await act(() => hook.result.current.reconcileUnknownEffect("NOT_APPLIED")); expect(hook.result.current.state.indicator).toBeNull(); }); it("keeps the next intent unresolved after a synchronous not-applied double action", async () => { const client = queryClient(); const failure = createFailure("SERVER_FAILURE", "CREATE", 0, { effect: "MAYBE_APPLIED", }); const hook = renderHook( () => useApplicationMutation({ definitionId: "legacy-not-applied-double-action-v1", duplicatePolicy: "ALLOW_PARALLEL", execute: async () => ({ ok: false, error: failure }), }), { wrapper: wrapper(client) }, ); await act(async () => { await Promise.all([ hook.result.current.submit("first"), hook.result.current.submit("second"), ]); }); let firstResolution: Promise | undefined; let duplicateResolution: Promise | undefined; act(() => { firstResolution = hook.result.current.reconcileUnknownEffect( "NOT_APPLIED", ); duplicateResolution = hook.result.current.reconcileUnknownEffect( "APPLIED", ); }); await expect(duplicateResolution).resolves.toBeUndefined(); if (!firstResolution) throw new Error("expected reconciliation"); await act(() => firstResolution); expect(hook.result.current.state.indicator).toBe( "mutation-effect-unknown", ); await act(() => hook.result.current.reconcileUnknownEffect("NOT_APPLIED")); expect(hook.result.current.state.indicator).toBeNull(); }); it("clears every stale-scope unknown effect without touching new data", async () => { const client = queryClient(); const scope = scopeSnapshot(); const key = ["resource", "scope-unknown-queue"]; client.setQueryData(key, ["base"]); const invalidateQueries = vi.spyOn(client, "invalidateQueries"); const failure = createFailure("SERVER_FAILURE", "CREATE", 0, { effect: "MAYBE_APPLIED", }); const hook = renderHook( () => useApplicationMutation({ definitionId: "scope-unknown-queue-v1", definitionVersion: 1, operationId: "CREATE", requiresIdempotencyKey: true, owner: "platform-test", duplicatePolicy: "ALLOW_PARALLEL", scope, execute: async () => ({ ok: false, error: failure }), invalidate: [RESOURCE_INVALIDATION_TOPIC], optimistic: { queryKey: key, update: (previous, input) => [ ...(previous as string[]), input, ], }, }), { wrapper: wrapper(client) }, ); await act(async () => { await Promise.all([ hook.result.current.submit("created"), hook.result.current.submit("created"), ]); }); expect(hook.result.current.state.indicator).toBe( "mutation-effect-unknown", ); act(() => scope.fence()); await waitFor(() => expect(hook.result.current.state.indicator).toBeNull()); expect(client.getQueryState(key)).toBeUndefined(); client.setQueryData(key, ["new-generation"]); await act(() => hook.result.current.reconcileUnknownEffect("APPLIED")); expect(client.getQueryData(key)).toEqual(["new-generation"]); expect(invalidateQueries).not.toHaveBeenCalled(); }); it("restores a bound unknown-effect head after controller remount", async () => { const client = queryClient(); const scope = scopeSnapshot(); const key = ["resource", "durable-unknown"]; client.setQueryData(key, ["base"]); const failure = createFailure("SERVER_FAILURE", "CREATE", 0, { effect: "MAYBE_APPLIED", }); const mutationOptions = { definitionId: "durable-unknown-v1", definitionVersion: 1, operationId: "CREATE", requiresIdempotencyKey: true, owner: "platform-test", duplicatePolicy: "REJECT_WHILE_ACTIVE" as const, scope, execute: vi.fn(async () => ({ ok: false as const, error: failure })), invalidate: [] as const, optimistic: { queryKey: key, update: (previous: unknown, input: string) => [ ...(previous as string[]), input, ], }, }; const first = renderHook( () => useApplicationMutation(mutationOptions), { wrapper: wrapper(client) }, ); await act(() => first.result.current.submit("created")); expect(first.result.current.state.indicator).toBe( "mutation-effect-unknown", ); first.unmount(); const remounted = renderHook( () => useApplicationMutation(mutationOptions), { wrapper: wrapper(client) }, ); expect(remounted.result.current.state.indicator).toBe( "mutation-effect-unknown", ); await act(() => remounted.result.current.reconcileUnknownEffect("NOT_APPLIED"), ); expect(client.getQueryData(key)).toEqual(["base"]); expect(remounted.result.current.state.indicator).toBeNull(); expect(mutationOptions.execute).toHaveBeenCalledOnce(); }); it("restores a non-optimistic legacy unknown effect by stable definition after remount", async () => { const client = queryClient(); const failure = createFailure("SERVER_FAILURE", "CREATE", 0, { effect: "MAYBE_APPLIED", }); const mutationOptions = { definitionId: "legacy-durable-create-v1", execute: vi.fn(async () => ({ ok: false as const, error: failure })), }; const first = renderHook( () => useApplicationMutation(mutationOptions), { wrapper: wrapper(client) }, ); await act(() => first.result.current.submit("created")); expect(first.result.current.state.indicator).toBe( "mutation-effect-unknown", ); first.unmount(); const remounted = renderHook( () => useApplicationMutation(mutationOptions), { wrapper: wrapper(client) }, ); expect(remounted.result.current.state.indicator).toBe( "mutation-effect-unknown", ); await act(() => remounted.result.current.reconcileUnknownEffect("NOT_APPLIED"), ); expect(remounted.result.current.state.indicator).toBeNull(); expect(mutationOptions.execute).toHaveBeenCalledOnce(); }); it("does not allocate unknown-effect channels for abandoned server renders", async () => { const client = queryClient(); const selectedScopes = Array.from( { length: MUTATION_COORDINATOR_BOUNDS.activeDefinitionsPerRuntime }, (_, index) => scopeSnapshot(1, `server-render-scope-${index}`), ); function AbandonedMutation({ index }: Readonly<{ index: number }>) { useApplicationMutation({ definitionId: `server-render-abandoned-${index}`, definitionVersion: 1, operationId: "CREATE", requiresIdempotencyKey: true, owner: "platform-test", duplicatePolicy: "REJECT_WHILE_ACTIVE", scope: selectedScopes[index]!, execute: async (input) => ({ ok: true, value: input }), invalidate: [], }); return null; } const ServerWrapper = wrapper(client); renderToString( {selectedScopes.map((_, index) => ( ))} , ); const scope = scopeSnapshot(1, "committed-after-server-render"); const execute = vi.fn(async (input: string) => ({ ok: true as const, value: input, })); const committed = renderHook( () => useApplicationMutation({ definitionId: "committed-after-server-render", definitionVersion: 1, operationId: "CREATE", requiresIdempotencyKey: true, owner: "platform-test", duplicatePolicy: "REJECT_WHILE_ACTIVE", scope, execute, invalidate: [], }), { wrapper: wrapper(client) }, ); await expect(committed.result.current.submit("created")).resolves.toEqual({ ok: true, value: "created", }); expect(execute).toHaveBeenCalledOnce(); }); it("isolates channels when a reused fingerprint advances generation", async () => { const client = queryClient(); const firstScope = scopeSnapshot(1, "reused-scope-fingerprint"); const secondScope = scopeSnapshot(2, "reused-scope-fingerprint"); const failure = createFailure("SERVER_FAILURE", "CREATE", 0, { effect: "MAYBE_APPLIED", }); const definition = { definitionId: "generation-channel-v1", definitionVersion: 1, operationId: "CREATE", requiresIdempotencyKey: true, owner: "platform-test", duplicatePolicy: "REJECT_WHILE_ACTIVE" as const, invalidate: [] as const, }; const first = renderHook( () => useApplicationMutation({ ...definition, scope: firstScope, execute: async () => ({ ok: false, error: failure }), }), { wrapper: wrapper(client) }, ); await act(() => first.result.current.submit("first")); expect(first.result.current.state.indicator).toBe( "mutation-effect-unknown", ); const secondExecute = vi.fn(async () => ({ ok: true as const, value: "second", })); const second = renderHook( () => useApplicationMutation({ ...definition, scope: secondScope, execute: secondExecute, }), { wrapper: wrapper(client) }, ); expect(second.result.current.state.indicator).toBeNull(); await expect(second.result.current.submit("second")).resolves.toEqual({ ok: true, value: "second", }); expect(secondExecute).toHaveBeenCalledOnce(); expect(first.result.current.state.indicator).toBe( "mutation-effect-unknown", ); }); it("isolates unrelated non-optimistic legacy controllers", async () => { const client = queryClient(); const failure = createFailure("SERVER_FAILURE", "CREATE", 0, { effect: "MAYBE_APPLIED", }); const first = renderHook( () => useApplicationMutation({ definitionId: "legacy-isolation-first-v1", execute: async () => ({ ok: false, error: failure }), }), { wrapper: wrapper(client) }, ); const second = renderHook( () => useApplicationMutation({ definitionId: "legacy-isolation-second-v1", execute: async (input) => ({ ok: true, value: input }), }), { wrapper: wrapper(client) }, ); await act(() => first.result.current.submit("first")); expect(first.result.current.state.indicator).toBe( "mutation-effect-unknown", ); expect(second.result.current.state.indicator).toBeNull(); await act(() => second.result.current.reconcileUnknownEffect("APPLIED")); expect(first.result.current.state.indicator).toBe( "mutation-effect-unknown", ); }); it("fails closed when identical bound metadata is mounted with a different scope owner", async () => { const client = queryClient(); const firstScope = scopeSnapshot(1, "same-owner-fingerprint"); const secondScope = scopeSnapshot(1, "same-owner-fingerprint"); const failure = createFailure("SERVER_FAILURE", "CREATE", 0, { effect: "MAYBE_APPLIED", }); const definition = { definitionId: "scope-owner-channel-v1", definitionVersion: 1, operationId: "CREATE", requiresIdempotencyKey: true, owner: "platform-test", duplicatePolicy: "REJECT_WHILE_ACTIVE" as const, invalidate: [] as const, }; const first = renderHook( () => useApplicationMutation({ ...definition, scope: firstScope, execute: async () => ({ ok: false, error: failure }), }), { wrapper: wrapper(client) }, ); await act(() => first.result.current.submit("first")); const secondExecute = vi.fn(async () => ({ ok: true as const, value: "second", })); const second = renderHook( () => useApplicationMutation({ ...definition, scope: secondScope, execute: secondExecute, }), { wrapper: wrapper(client) }, ); expect(second.result.current.state.indicator).toBeNull(); await expect(second.result.current.submit("second")).resolves.toMatchObject({ ok: false, error: { kind: "IDENTITY_INTERN_LIMIT_EXCEEDED", code: "UNKNOWN_EFFECT_CHANNEL_LIMIT_EXCEEDED", effect: "NOT_STARTED", }, }); expect(secondExecute).not.toHaveBeenCalled(); expect(first.result.current.state.indicator).toBe( "mutation-effect-unknown", ); }); it("releases empty unmounted channels after their active mutation settles", async () => { const client = queryClient(); const firstScope = scopeSnapshot(1, "release-owner-fingerprint"); const secondScope = scopeSnapshot(1, "release-owner-fingerprint"); let resolveFirst: (value: ApplicationResult) => void = () => {}; const definition = { definitionId: "release-channel-v1", definitionVersion: 1, operationId: "CREATE", requiresIdempotencyKey: true, owner: "platform-test", duplicatePolicy: "ALLOW_PARALLEL" as const, invalidate: [] as const, }; const first = renderHook( () => useApplicationMutation({ ...definition, scope: firstScope, execute: () => new Promise((resolve) => { resolveFirst = resolve; }), }), { wrapper: wrapper(client) }, ); let pending: Promise> | undefined; act(() => { pending = first.result.current.submit("created"); }); await waitFor(() => expect(first.result.current.state.indicator).toBe("mutation-pending"), ); first.unmount(); resolveFirst({ ok: true, value: "created" }); if (!pending) throw new Error("expected active mutation"); await act(() => pending); const execute = vi.fn(async () => ({ ok: true as const, value: "after-release", })); const afterRelease = renderHook( () => useApplicationMutation({ ...definition, scope: secondScope, execute, }), { wrapper: wrapper(client) }, ); await expect(afterRelease.result.current.submit("created")).resolves.toEqual({ ok: true, value: "after-release", }); expect(execute).toHaveBeenCalledOnce(); }); it("enforces the active intent bound across all QueryClient channels", async () => { const client = queryClient(); const scope = scopeSnapshot(); const never = () => new Promise>(() => {}); const options = (definitionId: string) => ({ definitionId, definitionVersion: 1, operationId: "CREATE", requiresIdempotencyKey: true, owner: "platform-test", duplicatePolicy: "ALLOW_PARALLEL" as const, scope, execute: never, invalidate: [] as const, }); const first = renderHook( () => useApplicationMutation(options("bound-a")), { wrapper: wrapper(client) }, ); const second = renderHook( () => useApplicationMutation(options("bound-b")), { wrapper: wrapper(client) }, ); const half = MUTATION_COORDINATOR_BOUNDS.activeIntentsTotal / 2; act(() => { for (let index = 0; index < half; index += 1) { void first.result.current.submit("same-input"); void second.result.current.submit("same-input"); } }); await expect(first.result.current.submit("same-input")).resolves.toMatchObject({ ok: false, error: { kind: "IDENTITY_INTERN_LIMIT_EXCEEDED", code: "UNKNOWN_EFFECT_CHANNEL_LIMIT_EXCEEDED", effect: "NOT_STARTED", }, }); act(() => scope.fence()); }); it("reconciles parallel unknown effects in submit order after reverse completion", async () => { const client = queryClient(); const key = ["resource", "reverse-completion"]; client.setQueryData(key, ["base"]); const failure = createFailure("SERVER_FAILURE", "CREATE", 0, { effect: "MAYBE_APPLIED", }); const resolvers = new Map< string, (value: ApplicationResult) => void >(); const hook = renderHook( () => useApplicationMutation({ duplicatePolicy: "ALLOW_PARALLEL", execute: (input) => new Promise((resolve) => { resolvers.set(input, resolve); }), currentData: client.getQueryData(key), optimistic: { queryKey: key, update: (previous, input) => [ ...(previous as string[]), input, ], }, }), { wrapper: wrapper(client) }, ); let first: Promise> | undefined; let second: Promise> | undefined; act(() => { first = hook.result.current.submit("first"); second = hook.result.current.submit("second"); }); await waitFor(() => expect(resolvers.size).toBe(2)); act(() => resolvers.get("second")?.({ ok: false, error: failure })); if (!second) throw new Error("expected second mutation"); await act(() => second); expect(hook.result.current.state.indicator).toBe("mutation-pending"); act(() => resolvers.get("first")?.({ ok: false, error: failure })); if (!first) throw new Error("expected first mutation"); await act(() => first); expect(hook.result.current.state.indicator).toBe( "mutation-effect-unknown", ); await act(() => hook.result.current.reconcileUnknownEffect("NOT_APPLIED")); expect(client.getQueryData(key)).toEqual(["base", "second"]); expect(hook.result.current.state.indicator).toBe( "mutation-effect-unknown", ); await act(() => hook.result.current.reconcileUnknownEffect("APPLIED")); expect(client.getQueryData(key)).toEqual(["base", "second"]); expect(hook.result.current.state.indicator).toBeNull(); }); it("removes committed old-scope data when scope fences during reconciliation invalidation", async () => { const client = queryClient(); const scope = scopeSnapshot(); const key = ["resource", "reconcile-fence-race"]; client.setQueryData(key, ["base"]); let finishInvalidation: () => void = () => {}; const invalidation = new Promise((resolve) => { finishInvalidation = resolve; }); const invalidateQueries = vi .spyOn(client, "invalidateQueries") .mockImplementation(async () => invalidation); const failure = createFailure("SERVER_FAILURE", "CREATE", 0, { effect: "MAYBE_APPLIED", }); const hook = renderHook( () => useApplicationMutation({ definitionId: "reconcile-fence-race-v1", definitionVersion: 1, operationId: "CREATE", requiresIdempotencyKey: true, owner: "platform-test", duplicatePolicy: "REJECT_WHILE_ACTIVE", scope, execute: async () => ({ ok: false, error: failure }), invalidate: [RESOURCE_INVALIDATION_TOPIC], optimistic: { queryKey: key, update: (previous, input) => [ ...(previous as string[]), input, ], }, }), { wrapper: wrapper(client) }, ); await act(() => hook.result.current.submit("created")); let reconciliation: Promise | undefined; act(() => { reconciliation = hook.result.current.reconcileUnknownEffect("APPLIED"); }); await waitFor(() => expect(invalidateQueries).toHaveBeenCalledOnce()); act(() => scope.fence()); finishInvalidation(); if (!reconciliation) throw new Error("expected reconciliation"); await act(() => reconciliation); expect(client.getQueryState(key)).toBeUndefined(); expect(hook.result.current.state.indicator).toBeNull(); }); it.each(["NOT_APPLICABLE", undefined] as const)( "treats %s conflict effect certainty as unknown without conflict retry", async (effect) => { const client = queryClient(); const key = ["resource", `ambiguous-conflict-${String(effect)}`]; client.setQueryData(key, ["base"]); const conflict = createFailure("CONFLICT", "CREATE", 0, { ...(effect ? { effect } : {}), }); const hook = renderHook( () => useApplicationMutation({ execute: async () => ({ ok: false, error: conflict }), 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(client.getQueryData(key)).toEqual(["base", "created"]); expect(hook.result.current.state.indicator).toBe( "mutation-effect-unknown", ); }, ); 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]); }); }); it("cancels an in-flight query before taking the optimistic snapshot", async () => { const client = queryClient(); const key = ["resource", "ordered-update"]; client.setQueryData(key, ["existing"]); let finishCancellation: () => void = () => {}; const cancellation = new Promise((resolve) => { finishCancellation = resolve; }); const cancelQueries = vi .spyOn(client, "cancelQueries") .mockImplementation(async () => cancellation); const getQueryData = vi.spyOn(client, "getQueryData"); const update = vi.fn((previous, input) => [ ...(previous as string[]), input, ]); const execute = vi.fn(async () => ({ ok: true as const, value: "created", })); const hook = renderHook( () => useApplicationMutation({ execute, currentData: ["existing"], optimistic: { queryKey: key, update }, }), { wrapper: wrapper(client) }, ); let pending: Promise> | undefined; act(() => { pending = hook.result.current.submit("created"); }); expect(cancelQueries).toHaveBeenCalledWith({ queryKey: key, exact: true, }); expect(getQueryData).not.toHaveBeenCalled(); expect(update).not.toHaveBeenCalled(); expect(execute).not.toHaveBeenCalled(); finishCancellation(); if (!pending) throw new Error("expected pending mutation"); await act(() => pending); expect(getQueryData).toHaveBeenCalledWith(key); expect(update).toHaveBeenCalledWith(["existing"], "created"); expect(execute).toHaveBeenCalledOnce(); expect(client.getQueryData(key)).toEqual(["existing", "created"]); }); it("keeps a committed optimistic update when invalidation fails", async () => { const client = queryClient(); const key = ["resource", "committed-update"]; client.setQueryData(key, ["existing"]); vi.spyOn(client, "invalidateQueries").mockRejectedValue( new Error("cache refresh failed"), ); const hook = renderHook( () => useApplicationMutation({ execute: async () => ({ ok: true, value: "created" }), invalidate: [RESOURCE_INVALIDATION_TOPIC], currentData: ["existing"], optimistic: { queryKey: key, update: (previous, input) => [ ...(previous as string[]), input, ], }, }), { wrapper: wrapper(client) }, ); let outcome; await act(async () => { outcome = await hook.result.current.submit("created"); }); expect(outcome).toEqual({ ok: true, value: "created" }); expect(client.getQueryData(key)).toEqual(["existing", "created"]); }); it("normalizes an optimistic preparation defect without running the command", async () => { const client = queryClient(); const key = ["resource", "invalid-optimistic-update"]; client.setQueryData(key, ["existing"]); const execute = vi.fn(async () => ({ ok: true as const, value: "created", })); const hook = renderHook( () => useApplicationMutation({ execute, currentData: ["existing"], optimistic: { queryKey: key, update: () => { throw new Error("private optimistic detail"); }, }, }), { wrapper: wrapper(client) }, ); let outcome; await act(async () => { outcome = await hook.result.current.submit("created"); }); expect(outcome).toMatchObject({ ok: false, error: { kind: "UNKNOWN_FAILURE", effect: "NOT_STARTED", operationId: "APPLICATION_MUTATION", userMessageKey: "error.unknown_failure", }, }); expect(JSON.stringify(outcome)).not.toContain("private optimistic detail"); expect(execute).not.toHaveBeenCalled(); expect(client.getQueryData(key)).toEqual(["existing"]); }); it("removes an optimistic cache entry when no prior data existed", async () => { const client = queryClient(); const key = ["resource", "new-optimistic-entry"]; const failure = createFailure("SERVER_FAILURE", "CREATE", 0, { effect: "NOT_APPLIED", }); const hook = renderHook( () => useApplicationMutation({ execute: async () => ({ ok: false, error: failure }), currentData: true, optimistic: { queryKey: key, update: (_previous, input) => [input], }, }), { wrapper: wrapper(client) }, ); let outcome; await act(async () => { outcome = await hook.result.current.submit("temporary"); }); expect(outcome).toEqual({ ok: false, error: failure }); expect(client.getQueryData(key)).toBeUndefined(); expect(client.getQueryState(key)).toBeUndefined(); }); it("rolls optimistic data back and exposes a resolvable conflict", async () => { const client = queryClient(); const key = ["resource", "list"]; client.setQueryData(key, ["existing"]); const conflict = createFailure("CONFLICT", "CREATE", 0, { effect: "NOT_APPLIED", }); const hook = renderHook( () => useApplicationMutation({ execute: async () => ({ ok: false, error: conflict }), invalidate: [RESOURCE_INVALIDATION_TOPIC], currentData: client.getQueryData(key), optimistic: { queryKey: key, update: (previous, input) => [ ...(previous as string[]), input, ], }, }), { wrapper: wrapper(client) }, ); let outcome; await act(async () => { outcome = await hook.result.current.submit("conflicting"); }); expect(outcome).toEqual({ ok: false, error: conflict }); expect(client.getQueryData(key)).toEqual(["existing"]); expect(hook.result.current.state.indicator).toBe("mutation-conflict"); expect(hook.result.current.state.overlay).toMatchObject({ mutationPending: false, mutationConflict: true, }); await act(() => hook.result.current.resolveConflict()); expect(hook.result.current.state.indicator).toBeNull(); expect(client.getQueryState(key)?.isInvalidated).toBe(true); }); });