import { beforeEach, describe, expect, it, vi } from 'vitest' import { ApiResponseError } from '@/shared/api/http' import * as api from '../api/ownedBusiness' import { ownedBusinessFixture, ownedBusinessCommandFixture } from './OwnedBusinessFixtures' import { acceptOwnedBusinessCommand, newOwnedBusinessPending, persistOwnedBusinessPending, restoreOwnedBusinessPending, recoverOwnedBusinessCommand, ownedBusinessPendingKey } from './OwnedBusinessCommandRecovery' import type { OwnedBusinessPhoto, OwnedBusinessSave } from '../ownedBusinessTypes' beforeEach(() => sessionStorage.clear()) const no = 'DS-OWN-1' function operation() : OwnedBusinessSave { return { recordVersion: '0', idempotencyKey: 'original-key', operations: { ...ownedBusinessFixture().row.operations, money: { currency: 'USD', amount: '9999999999999999.99' } }, reason: null } } function client() { const current = ownedBusinessFixture(); current.row.recordVersion = '1' const photo: OwnedBusinessPhoto = { slot:'FUEL_START',category:'FUEL',fileId:'9007199254740993',fileName:'fuel.png',contentType:'image/png',sizeBytes:'5',uploadedAt:'2026-10-20T10:00:00Z',savedAt:null } return { ...api, getOwnedBusiness: vi.fn(async () => current), getOwnedBusinessCommand: vi.fn(async () => ownedBusinessCommandFixture()), saveOwnedBusinessOperations: vi.fn(async () => current), bindOwnedBusinessPhoto: vi.fn(async () => current), uploadOwnedBusinessPhoto: vi.fn(async () => photo) } } describe('owned business exact command recovery', () => { it('persists the complete original payload, actor and zero auxiliary version', () => { const pending = newOwnedBusinessPending('actor', no, 'SAVE_OPERATIONS', operation()) persistOwnedBusinessPending('actor', no, 'operations', pending) expect(restoreOwnedBusinessPending('actor', no, 'operations')).toEqual(pending) expect(restoreOwnedBusinessPending('another', no, 'operations')).toBeNull() const raw = JSON.parse(sessionStorage.getItem(ownedBusinessPendingKey('actor',no,'operations'))!) raw.actor = 'another'; sessionStorage.setItem(ownedBusinessPendingKey('actor',no,'operations'),JSON.stringify(raw)) expect(() => restoreOwnedBusinessPending('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 = newOwnedBusinessPending('actor', no, 'SAVE_OPERATIONS', operation()); pending.attempted = true const service = client(), persist = vi.fn() service.getOwnedBusinessCommand.mockRejectedValue(new ApiResponseError('not found','A0404',404)) await recoverOwnedBusinessCommand(pending,persist,() => {},service) expect(service.getOwnedBusinessCommand).toHaveBeenCalledWith(no,'original-key') expect(service.saveOwnedBusinessOperations).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 = newOwnedBusinessPending('actor', no, 'SAVE_OPERATIONS', operation()), service = client() service.getOwnedBusiness.mockRejectedValueOnce(new Error('read offline')) await expect(recoverOwnedBusinessCommand(pending,() => {},() => {},service)).rejects.toThrow('read offline') expect(pending.confirmed?.recordVersion).toBe('1') await recoverOwnedBusinessCommand(pending,() => {},() => {},service) expect(service.saveOwnedBusinessOperations).toHaveBeenCalledTimes(1); expect(service.getOwnedBusinessCommand).not.toHaveBeenCalled() }) it('never retransmits a file for an unknown upload, including command not-found', async () => { const pending = newOwnedBusinessPending('actor',no,'UPLOAD_PHOTO',null,'FUEL_START'); pending.attempted = true const service = client(); service.getOwnedBusinessCommand.mockRejectedValue(new ApiResponseError('not found','A0404',404)) await expect(recoverOwnedBusinessCommand(pending,() => {},() => {},service)).rejects.toThrow('禁止重复上传') expect(service.uploadOwnedBusinessPhoto).not.toHaveBeenCalled(); expect(service.bindOwnedBusinessPhoto).not.toHaveBeenCalled(); expect(pending.key).toBeTruthy() }) it('keeps the prepared file and exact binding key through unknown binding and later replacement', async () => { const photo: OwnedBusinessPhoto = { slot:'FUEL_START',category:'FUEL',fileId:'9007199254740993',fileName:'fuel.png',contentType:'image/png',sizeBytes:'5',uploadedAt:'2026-10-20T10:00:00Z',savedAt:'2026-10-20T10:01:00Z' } const pending = newOwnedBusinessPending('actor',no,'BIND_PHOTO',{recordVersion:'0',idempotencyKey:'bind-key',fileId:photo.fileId},photo.slot) pending.attempted = true const service = client(); const bound = ownedBusinessFixture(); bound.row.recordVersion='1'; bound.photos=[photo] const original = ownedBusinessCommandFixture({action:'BIND_PHOTO',idempotencyKey:'bind-key',result:bound}) service.getOwnedBusinessCommand.mockResolvedValue(original) const current = ownedBusinessFixture();current.row.recordVersion='2';current.photos=[{...photo,fileId:'9007199254740994'}] service.getOwnedBusiness.mockResolvedValue(current) const result = await recoverOwnedBusinessCommand(pending,() => {},() => {},service) expect(result.detail?.photos[0]?.fileId).toBe('9007199254740994') expect(pending.body).toEqual({recordVersion:'0',idempotencyKey:'bind-key',fileId:photo.fileId}) expect(service.uploadOwnedBusinessPhoto).not.toHaveBeenCalled();expect(service.bindOwnedBusinessPhoto).not.toHaveBeenCalled() }) it('refuses wrong command identities, stale reads, definite rejection retries and scope changes', async () => { const pending = newOwnedBusinessPending('actor',no,'SAVE_OPERATIONS',operation()), service=client() expect(() => acceptOwnedBusinessCommand(pending,ownedBusinessCommandFixture({dispatchNo:'another'}))).toThrow('不一致') pending.command=ownedBusinessCommandFixture({recordVersion:'9007199254740993'}) await expect(recoverOwnedBusinessCommand(pending,() => {},() => {},service)).rejects.toThrow('早于') pending.rejected=true;await expect(recoverOwnedBusinessCommand(pending,() => {},() => {},service)).rejects.toThrow('明确拒绝') pending.rejected=false;await expect(recoverOwnedBusinessCommand(pending,() => {},() => {throw new Error('scope changed')},service)).rejects.toThrow('scope changed') expect(service.saveOwnedBusinessOperations).not.toHaveBeenCalled() }) })