import { beforeEach, describe, expect, it, vi } from 'vitest' import { ApiResponseError } from '@/shared/api/http' import * as api from '../api/outsourcedBusiness' import { outsourcedBusinessFixture, outsourcedBusinessCommandFixture } from './OutsourcedBusinessFixtures' import { acceptOutsourcedBusinessCommand, newOutsourcedBusinessPending, persistOutsourcedBusinessPending, restoreOutsourcedBusinessPending, recoverOutsourcedBusinessCommand, outsourcedBusinessPendingKey } from './OutsourcedBusinessCommandRecovery' import type { OutsourcedBusinessPhoto, OutsourcedBusinessSave } from '../outsourcedBusinessTypes' beforeEach(() => sessionStorage.clear()) const no = 'DS-OUT-1' function operation() : OutsourcedBusinessSave { return { recordVersion: '0', idempotencyKey: 'original-key', operations: { ...outsourcedBusinessFixture().row.operations, money: { currency: 'USD', amount: '9999999999999999.99' } }, reason: null } } function client() { const current = outsourcedBusinessFixture(); current.row.recordVersion = '1' const photo: OutsourcedBusinessPhoto = { slot:'LOADING_SCENE',category:'LOADING',fileId:'9007199254740993',fileName:'loading.png',contentType:'image/png',sizeBytes:'5',uploadedAt:'2026-10-20T10:00:00Z',savedAt:null } return { ...api, getOutsourcedBusiness: vi.fn(async () => current), getOutsourcedBusinessCommand: vi.fn(async () => outsourcedBusinessCommandFixture()), saveOutsourcedBusinessOperations: vi.fn(async () => current), bindOutsourcedBusinessPhoto: vi.fn(async () => current), uploadOutsourcedBusinessPhoto: vi.fn(async () => photo) } } describe('outsourced business exact command recovery', () => { it('persists the complete original payload, actor and zero auxiliary version', () => { const pending = newOutsourcedBusinessPending('actor', no, 'SAVE_OPERATIONS', operation()) persistOutsourcedBusinessPending('actor', no, 'operations', pending) expect(restoreOutsourcedBusinessPending('actor', no, 'operations')).toEqual(pending) expect(restoreOutsourcedBusinessPending('another', no, 'operations')).toBeNull() const raw = JSON.parse(sessionStorage.getItem(outsourcedBusinessPendingKey('actor',no,'operations'))!) raw.actor = 'another'; sessionStorage.setItem(outsourcedBusinessPendingKey('actor',no,'operations'),JSON.stringify(raw)) expect(() => restoreOutsourcedBusinessPending('actor',no,'operations')).toThrow('损坏') }) it('looks up an unknown write first and retries only the unchanged original body on explicit not-found', async () => { const pending = newOutsourcedBusinessPending('actor', no, 'SAVE_OPERATIONS', operation()); pending.attempted = true const service = client(), persist = vi.fn() service.getOutsourcedBusinessCommand.mockRejectedValue(new ApiResponseError('not found','A0404',404)) await recoverOutsourcedBusinessCommand(pending,persist,() => {},service) expect(service.getOutsourcedBusinessCommand).toHaveBeenCalledWith(no,'original-key') expect(service.saveOutsourcedBusinessOperations).toHaveBeenCalledExactlyOnceWith(no,operation()) expect(pending.body).toEqual(operation()); expect(pending.body?.recordVersion).toBe('0') }) it('retains a proven success through failed fresh reads without writing it a second time', async () => { const pending = newOutsourcedBusinessPending('actor', no, 'SAVE_OPERATIONS', operation()), service = client() service.getOutsourcedBusiness.mockRejectedValueOnce(new Error('read offline')) await expect(recoverOutsourcedBusinessCommand(pending,() => {},() => {},service)).rejects.toThrow('read offline') expect(pending.confirmed?.recordVersion).toBe('1') await recoverOutsourcedBusinessCommand(pending,() => {},() => {},service) expect(service.saveOutsourcedBusinessOperations).toHaveBeenCalledTimes(1); expect(service.getOutsourcedBusinessCommand).not.toHaveBeenCalled() }) it('never retransmits a file for an unknown upload, including command not-found', async () => { const pending = newOutsourcedBusinessPending('actor',no,'UPLOAD_PHOTO',null,'LOADING_SCENE'); pending.attempted = true const service = client(); service.getOutsourcedBusinessCommand.mockRejectedValue(new ApiResponseError('not found','A0404',404)) await expect(recoverOutsourcedBusinessCommand(pending,() => {},() => {},service)).rejects.toThrow('禁止重复上传') expect(service.uploadOutsourcedBusinessPhoto).not.toHaveBeenCalled(); expect(service.bindOutsourcedBusinessPhoto).not.toHaveBeenCalled(); expect(pending.key).toBeTruthy() }) it('keeps the prepared file and exact binding key through unknown binding and later replacement', async () => { const photo: OutsourcedBusinessPhoto = { slot:'LOADING_SCENE',category:'LOADING',fileId:'9007199254740993',fileName:'loading.png',contentType:'image/png',sizeBytes:'5',uploadedAt:'2026-10-20T10:00:00Z',savedAt:'2026-10-20T10:01:00Z' } const pending = newOutsourcedBusinessPending('actor',no,'BIND_PHOTO',{recordVersion:'0',idempotencyKey:'bind-key',fileId:photo.fileId},photo.slot) pending.attempted = true const service = client(); const bound = outsourcedBusinessFixture(); bound.row.recordVersion='1'; bound.photos=[photo] const original = outsourcedBusinessCommandFixture({action:'BIND_PHOTO',idempotencyKey:'bind-key',result:bound}) service.getOutsourcedBusinessCommand.mockResolvedValue(original) const current = outsourcedBusinessFixture();current.row.recordVersion='2';current.photos=[{...photo,fileId:'9007199254740994'}] service.getOutsourcedBusiness.mockResolvedValue(current) const result = await recoverOutsourcedBusinessCommand(pending,() => {},() => {},service) expect(result.detail?.photos[0]?.fileId).toBe('9007199254740994') expect(pending.body).toEqual({recordVersion:'0',idempotencyKey:'bind-key',fileId:photo.fileId}) expect(service.uploadOutsourcedBusinessPhoto).not.toHaveBeenCalled();expect(service.bindOutsourcedBusinessPhoto).not.toHaveBeenCalled() }) it('refuses wrong command identities, stale reads, definite rejection retries and scope changes', async () => { const pending = newOutsourcedBusinessPending('actor',no,'SAVE_OPERATIONS',operation()), service=client() expect(() => acceptOutsourcedBusinessCommand(pending,outsourcedBusinessCommandFixture({dispatchNo:'another'}))).toThrow('不一致') pending.command=outsourcedBusinessCommandFixture({recordVersion:'9007199254740993'}) await expect(recoverOutsourcedBusinessCommand(pending,() => {},() => {},service)).rejects.toThrow('早于') pending.rejected=true;await expect(recoverOutsourcedBusinessCommand(pending,() => {},() => {},service)).rejects.toThrow('明确拒绝') pending.rejected=false;await expect(recoverOutsourcedBusinessCommand(pending,() => {},() => {throw new Error('scope changed')},service)).rejects.toThrow('scope changed') expect(service.saveOutsourcedBusinessOperations).not.toHaveBeenCalled() }) })