/** * Mutation queue — server-freshness vs response arrival order (same tab). */ import assert from 'node:assert/strict' import test from 'node:test' import { createPinia, setActivePinia } from 'pinia' import { createMeFixture } from './fixtures/me.fixture' import { installFetchMock, resetAppTestState } from './helpers/fetchMock' test.beforeEach(async () => { installFetchMock() await resetAppTestState() setActivePinia(createPinia()) const { resetUserStores } = await import('../src/api/sync') resetUserStores() }) test('uncoordinated late applyMe simulates stale server overwrite regression', async () => { const { applyMeResponse } = await import('../src/api/sync') const { useUserStore } = await import('../src/stores/user') applyMeResponse(createMeFixture({ coins: 10 })) const newer = createMeFixture({ coins: 200 }) const older = createMeFixture({ coins: 50 }) applyMeResponse(newer) applyMeResponse(older) assert.equal(useUserStore().coins, 50, 'older server snapshot must not win when applied last') }) test('serial queue: slower older server response then newer mutation keeps final newer state', async () => { const meOlder = createMeFixture({ coins: 50 }) const meNewer = createMeFixture({ coins: 200 }) const { applyMeResponse, runMeMutation } = await import('../src/api/sync') const { useUserStore } = await import('../src/stores/user') const { api } = await import('../src/api/client') applyMeResponse(createMeFixture({ coins: 10 })) let call = 0 globalThis.routeHandlers!.set('/store/purchases', () => { call += 1 if (call === 1) { return { status: 200, body: meOlder, delayMs: 35 } } return { status: 200, body: meNewer } }) const first = runMeMutation(() => api.purchaseProduct('castle')) const second = runMeMutation(() => api.purchaseProduct('suit')) await Promise.all([first, second]) assert.equal(useUserStore().coins, 200) }) test('failed mutation does not block the next mutation in queue', async () => { const meOk = createMeFixture({ coins: 180 }) const { applyMeResponse, runMeMutation } = await import('../src/api/sync') const { useUserStore } = await import('../src/stores/user') const { api } = await import('../src/api/client') const { ApiClientError } = await import('../src/api/errors') applyMeResponse(createMeFixture({ coins: 10 })) let call = 0 globalThis.routeHandlers!.set('/store/purchases', () => { call += 1 if (call === 1) { return { status: 400, body: { error: 'سکه کافی نداری!' } } } return { status: 200, body: meOk } }) await assert.rejects(runMeMutation(() => api.purchaseProduct('castle')), ApiClientError) await runMeMutation(() => api.purchaseProduct('suit')) assert.equal(useUserStore().coins, 180) }) test('logout prevents queued mutation from applying or sending after session change', async () => { const meStale = createMeFixture({ coins: 999 }) const meCurrent = createMeFixture({ coins: 40 }) const { applyMeResponse, runMeMutation } = await import('../src/api/sync') const { invalidateSession } = await import('../src/api/authSession') const { useUserStore } = await import('../src/stores/user') const { api } = await import('../src/api/client') const { StaleSessionError } = await import('../src/api/errors') applyMeResponse(createMeFixture({ coins: 10 })) globalThis.routeHandlers!.set('/store/purchases', () => ({ status: 200, body: meStale, delayMs: 40, })) const pending = runMeMutation(() => api.purchaseProduct('castle')) invalidateSession() applyMeResponse(meCurrent) await assert.rejects(pending, StaleSessionError) assert.equal(useUserStore().coins, 40) }) declare global { // eslint-disable-next-line no-var var routeHandlers: Map import('./helpers/fetchMock').MockResponse | Promise> }