import { afterEach, describe, expect, it, vi } from 'vitest'; import { fetchAppVersionInfo, fetchConnectorDiscovery, fetchProjectFileText, uploadProjectFiles, } from '../../src/providers/registry'; describe('fetchAppVersionInfo', () => { afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); }); it('returns version info from the daemon response', async () => { vi.stubGlobal( 'fetch', vi.fn(async () => new Response(JSON.stringify({ version: { version: '1.2.3', channel: 'beta', packaged: true, platform: 'darwin', arch: 'arm64' }, }), { status: 200 })), ); await expect(fetchAppVersionInfo()).resolves.toEqual({ version: '1.2.3', channel: 'beta', packaged: true, platform: 'darwin', arch: 'arm64', }); }); it('returns null when version info is unavailable or malformed', async () => { vi.stubGlobal( 'fetch', vi.fn(async () => new Response(JSON.stringify({ version: { version: '1.2.3' } }), { status: 200 })), ); await expect(fetchAppVersionInfo()).resolves.toBeNull(); }); }); describe('fetchProjectFileText', () => { afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); }); it('can bypass caches when fetching source text', async () => { const fetchMock = vi.fn(async () => new Response('', { status: 200 })); vi.stubGlobal('fetch', fetchMock); await expect( fetchProjectFileText('project-1', 'diagram.svg', { cache: 'no-store', cacheBustKey: '1710000000-2', }), ).resolves.toBe(''); expect(fetchMock).toHaveBeenCalledWith( '/api/projects/project-1/raw/diagram.svg?cacheBust=1710000000-2', { cache: 'no-store' }, ); }); it('logs HTTP failure context before returning null', async () => { const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); vi.stubGlobal('fetch', vi.fn(async () => new Response('missing', { status: 404, statusText: 'Not Found' }))); await expect(fetchProjectFileText('project-1', 'missing.svg')).resolves.toBeNull(); expect(warn).toHaveBeenCalledWith( '[fetchProjectFileText] failed:', expect.objectContaining({ name: 'missing.svg', projectId: 'project-1', status: 404, statusText: 'Not Found', url: '/api/projects/project-1/raw/missing.svg', }), ); }); it('logs thrown fetch errors before returning null', async () => { const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); const error = new Error('network down'); vi.stubGlobal('fetch', vi.fn(async () => { throw error; })); await expect(fetchProjectFileText('project-1', 'diagram.svg')).resolves.toBeNull(); expect(warn).toHaveBeenCalledWith( '[fetchProjectFileText] failed:', expect.objectContaining({ error, name: 'diagram.svg', projectId: 'project-1', url: '/api/projects/project-1/raw/diagram.svg', }), ); }); }); describe('fetchConnectorDiscovery', () => { afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); }); it('caches connector discovery after a successful fetch', async () => { const fetchMock = vi.fn(async () => new Response(JSON.stringify({ connectors: [{ id: 'github', name: 'GitHub', tools: [{ name: 'issues' }] }], }), { status: 200 })); vi.stubGlobal('fetch', fetchMock); await expect(fetchConnectorDiscovery({ refresh: true })).resolves.toEqual([ { id: 'github', name: 'GitHub', tools: [{ name: 'issues' }] }, ]); await expect(fetchConnectorDiscovery()).resolves.toEqual([ { id: 'github', name: 'GitHub', tools: [{ name: 'issues' }] }, ]); expect(fetchMock).toHaveBeenCalledTimes(1); expect(fetchMock).toHaveBeenCalledWith('/api/connectors/discovery?refresh=true'); }); }); describe('uploadProjectFiles', () => { afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); }); it('treats every response entry as a success regardless of originalName drift', async () => { // Simulates an encoding edge case: the browser File.name carries a // composed CJK name (NFC) but multer round-trips it through latin1 and // returns a slightly different decoded form. The old name-equality // matching marked these as failed even though the server stored them. const composed = '测试.pdf'; const decomposed = '测试.pdf'; // pretend the server returned a normalized variant const file = new File(['hello'], composed, { type: 'application/pdf' }); vi.stubGlobal( 'fetch', vi.fn(async () => new Response(JSON.stringify({ files: [ { name: 'mxk7-test.pdf', path: 'mxk7-test.pdf', size: 5, originalName: decomposed, }, ], }), { status: 200 })), ); const result = await uploadProjectFiles('project-1', [file]); expect(result.failed).toEqual([]); expect(result.uploaded).toHaveLength(1); expect(result.uploaded[0]).toMatchObject({ path: 'mxk7-test.pdf', name: decomposed, size: 5, }); }); it('marks the unmatched tail as failed when the server drops files mid-flight', async () => { const a = new File(['a'], 'a.txt', { type: 'text/plain' }); const b = new File(['b'], 'b.txt', { type: 'text/plain' }); const c = new File(['c'], 'c.txt', { type: 'text/plain' }); vi.stubGlobal( 'fetch', vi.fn(async () => new Response(JSON.stringify({ files: [ { name: 't1-a.txt', path: 't1-a.txt', size: 1, originalName: 'a.txt' }, { name: 't2-b.txt', path: 't2-b.txt', size: 1, originalName: 'b.txt' }, ], }), { status: 200 })), ); const result = await uploadProjectFiles('project-1', [a, b, c]); expect(result.uploaded).toHaveLength(2); expect(result.failed).toHaveLength(1); expect(result.failed[0]).toMatchObject({ name: 'c.txt' }); }); });