import { beforeEach, describe, expect, it, vi } from 'vitest'; const apiRequestMock = vi.fn(); vi.mock('../../src/api/client.js', () => ({ getPath(key) { const paths = { items: 'kitchen/items', changes: 'kitchen/changes', }; return paths[key]; }, apiRequest: (...args) => apiRequestMock(...args), })); const { applyItemUpsert, listKitchenChanges, previewItemUpsert, useStockItem, } = await import('../../src/api/stock.js'); describe('api/stock', () => { beforeEach(() => { apiRequestMock.mockReset(); }); it('listKitchenChanges returns normalized changes payload', async () => { apiRequestMock.mockResolvedValueOnce({ since: 'cursor-1', next_cursor: 'cursor-2', changes: [{ type: 'item', action: 'upsert', timestamp: '2026-04-10T10:00:00Z' }], }); const result = await listKitchenChanges({ config: { database: 'db' } }, { limit: 10 }); expect(result).toEqual({ since: 'cursor-1', nextCursor: 'cursor-2', changes: [{ type: 'item', action: 'upsert', timestamp: '2026-04-10T10:00:00Z' }], }); expect(apiRequestMock).toHaveBeenCalledWith( { config: { database: 'db' } }, 'kitchen/changes', { query: { since: undefined, limit: 10 } }, ); }); it('listKitchenChanges falls back to empty shape when changes are missing', async () => { apiRequestMock.mockResolvedValueOnce({}); const result = await listKitchenChanges({ config: { database: 'db' } }, {}); expect(result).toEqual({ since: null, nextCursor: null, changes: [], }); }); it('previewItemUpsert normalizes preview response', async () => { apiRequestMock.mockResolvedValueOnce({ status: 'ok', mode: 'preview', operation: 'update', match_type: 'uuid_b64', matched_item: { uuid_b64: 'abc', name: 'Rice' }, payload: { name: 'Rice' }, }); const response = await previewItemUpsert({ config: { database: 'db' } }, { item: { name: 'Rice' } }); expect(response).toEqual({ status: 'ok', mode: 'preview', operation: 'update', matchType: 'uuid_b64', matchedItem: { uuid_b64: 'abc', name: 'Rice' }, item: null, payload: { name: 'Rice' }, }); expect(apiRequestMock).toHaveBeenCalledWith( { config: { database: 'db' } }, 'kitchen/items/upsert', { method: 'POST', body: { item: { name: 'Rice' } }, query: { mode: 'preview' }, }, ); }); it('applyItemUpsert normalizes apply response', async () => { apiRequestMock.mockResolvedValueOnce({ status: 'ok', mode: 'apply', operation: 'create', match_type: null, item: { uuid_b64: 'new1', name: 'Beans' }, }); const response = await applyItemUpsert({ config: { database: 'db' } }, { item: { name: 'Beans' } }); expect(response).toEqual({ status: 'ok', mode: 'apply', operation: 'create', matchType: null, matchedItem: null, item: { uuid_b64: 'new1', name: 'Beans' }, payload: null, }); }); it('useStockItem returns used on 204', async () => { apiRequestMock.mockResolvedValueOnce(null); const result = await useStockItem({ config: { database: 'db' } }, 'item-1'); expect(result).toEqual({ status: 'used' }); expect(apiRequestMock).toHaveBeenCalledWith( { config: { database: 'db' } }, 'kitchen/items/item-1/use', { method: 'POST' }, ); }); it('useStockItem returns already_gone on 409', async () => { apiRequestMock.mockRejectedValueOnce({ status: 409, message: 'Item is out of stock.' }); const result = await useStockItem({ config: { database: 'db' } }, 'item-1'); expect(result).toEqual({ status: 'already_gone' }); }); it('useStockItem falls back to delete on 404/405', async () => { apiRequestMock .mockRejectedValueOnce({ status: 404 }) .mockResolvedValueOnce(null); const result = await useStockItem({ config: { database: 'db' } }, 'item-1'); expect(result).toEqual({ status: 'fallback_delete' }); expect(apiRequestMock).toHaveBeenNthCalledWith( 2, { config: { database: 'db' } }, 'kitchen/items/item-1', { method: 'DELETE' }, ); }); it('useStockItem does not fallback on unrelated client errors', async () => { apiRequestMock.mockRejectedValueOnce({ status: 422, message: 'validation_error' }); await expect(useStockItem({ config: { database: 'db' } }, 'item-1')).rejects.toMatchObject({ status: 422, message: 'validation_error', }); expect(apiRequestMock).toHaveBeenCalledTimes(1); }); });