import { beforeEach, expect, it, vi } from 'vitest' import { ApiResponseError } from '@/shared/api/http' import { useDispatchCommandRecovery } from './dispatchCommandRecovery' import { command } from './dispatchTestFixtures' beforeEach(() => sessionStorage.clear()) it.each([new Error('network unknown'), new ApiResponseError('server failure','S0500',200), new ApiResponseError('gateway timeout','HTTP_504',504), new ApiResponseError('version changed','A0409',200)])('releases failed $message and permits a new request with edited input', async failure => { const owner=crypto.randomUUID(), state=useDispatchCommandRecovery(()=>owner) state.begin('PC/1','EDIT',{recordVersion:0,idempotencyKey:'first',remark:'original'}) await expect(state.execute(vi.fn().mockRejectedValue(failure),vi.fn())).rejects.toBe(failure) expect(state.pending.value).toBeNull();expect(state.busy.value).toBe(false) expect(sessionStorage.getItem(`dispatch-command:${owner}`)).toBeNull() const reopened=useDispatchCommandRecovery(()=>owner) reopened.begin('PC/1','EDIT',{recordVersion:2,idempotencyKey:'second',remark:'edited'}) const send=vi.fn().mockResolvedValue({...command('EDIT','second'),recordVersion:3}) await reopened.execute(send,vi.fn().mockResolvedValue({dispatchNo:'PC/1',recordVersion:3})) expect(send.mock.calls[0]![0].body).toEqual({recordVersion:2,idempotencyKey:'second',remark:'edited'}) expect(reopened.pending.value).toBeNull() }) it.each(['{broken',JSON.stringify({attempted:true,result:null,action:'EDIT'})])('clears saved failed requests without restoring them',saved=>{ const owner=crypto.randomUUID() sessionStorage.setItem(`dispatch-command:${owner}`,saved) const state=useDispatchCommandRecovery(()=>owner) expect(state.pending.value).toBeNull();expect(state.busy.value).toBe(false) expect(sessionStorage.getItem(`dispatch-command:${owner}`)).toBeNull() }) it('prevents double clicks only during the active request',async()=>{ const owner=crypto.randomUUID(),state=useDispatchCommandRecovery(()=>owner) state.begin('PC/1','EDIT',{recordVersion:0,idempotencyKey:'first'}) let finish!:(result:ReturnType)=>void const send=vi.fn().mockReturnValue(new Promise(done=>{finish=done})) const read=vi.fn().mockResolvedValue({dispatchNo:'PC/1',recordVersion:1}) const running=state.execute(send,read),another=useDispatchCommandRecovery(()=>owner) expect(another.busy.value).toBe(true) expect(()=>another.begin('PC/2','EDIT',{recordVersion:0,idempotencyKey:'second'})).toThrow('正在提交') await another.execute(send,read);expect(send).toHaveBeenCalledTimes(1) finish(command('EDIT','first'));await running expect(another.pending.value).toBeNull();expect(another.busy.value).toBe(false) expect(()=>another.begin('PC/2','EDIT',{recordVersion:0,idempotencyKey:'second'})).not.toThrow() }) it('unlocks even when a successful write is followed by a failed detail read',async()=>{ const owner=crypto.randomUUID(),state=useDispatchCommandRecovery(()=>owner) state.begin('PC/1','EDIT',{recordVersion:0,idempotencyKey:'key'}) await expect(state.execute(vi.fn().mockResolvedValue(command('EDIT')),vi.fn().mockRejectedValue(new Error('read failed')))).rejects.toThrow('read failed') expect(state.pending.value).toBeNull();expect(state.busy.value).toBe(false) }) it('keeps loading-stage and response identity checks without retaining failed requests',async()=>{ const owner=crypto.randomUUID(),state=useDispatchCommandRecovery(()=>owner),read=vi.fn() state.begin('PC/1','LOADING_COMPLETE',{recordVersion:0,idempotencyKey:'key'}) await expect(state.execute(vi.fn().mockResolvedValue({...command('LOADING_COMPLETE'),stage:'LOADING'}),read)).rejects.toThrow('装车完成') expect(read).not.toHaveBeenCalled();expect(state.pending.value).toBeNull() state.begin('PC/1','EDIT',{recordVersion:0,idempotencyKey:'key'}) await expect(state.execute(vi.fn().mockResolvedValue(command('EDIT','different')),read)).rejects.toThrow('不一致') expect(read).not.toHaveBeenCalled();expect(state.pending.value).toBeNull() }) it('preserves large version precision and unlocks after a mismatched detail',async()=>{ const owner=crypto.randomUUID(),state=useDispatchCommandRecovery(()=>owner) state.begin('PC/1','EDIT',{recordVersion:'9007199254740992',idempotencyKey:'key'}) await expect(state.execute(vi.fn().mockResolvedValue({...command('EDIT'),recordVersion:'9007199254740993'}),vi.fn().mockResolvedValue({dispatchNo:'PC/1',recordVersion:'9007199254740992'}))).rejects.toThrow('详情版本早于') expect(state.pending.value).toBeNull() }) it('rejects an account change before sending and releases state',async()=>{ let owner=crypto.randomUUID();const state=useDispatchCommandRecovery(()=>owner) state.begin('PC/1','EDIT',{recordVersion:0,idempotencyKey:'key'});owner=crypto.randomUUID() const send=vi.fn() await expect(state.execute(send,vi.fn())).rejects.toThrow('账号已变化') expect(send).not.toHaveBeenCalled();expect(state.pending.value).toBeNull();expect(state.busy.value).toBe(false) })