import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest' import { SyncWorker, type SyncableClient } from '../sync.js' import { MemoryCache } from '../adapters/cache/memory.js' import type { GenericNode } from '../types.js' /** Suppress logger output during tests. */ vi.mock('../logger.js', () => ({ logger: { info: vi.fn(), error: vi.fn(), warn: vi.fn(), }, })) function makeNode(path: string, isDir = false): GenericNode { const parts = path.split('/') return { path, name: parts[parts.length - 1] || '/', isDir, size: isDir ? 0 : 1024, modified: new Date('2025-01-01'), etag: `etag-${path}`, } } /** * Creates a mock client that returns a pre-configured directory tree. * The tree is a map from directory path to its direct children. */ function createMockClient(tree: Record): SyncableClient { return { list: vi.fn(async (path: string) => { return tree[path] ?? [] }), } } describe('SyncWorker', () => { let cache: MemoryCache beforeEach(() => { cache = new MemoryCache() }) afterEach(() => { vi.restoreAllMocks() }) it('should sync root directory contents to cache', async () => { const tree: Record = { '/': [ makeNode('/docs', true), makeNode('/readme.txt'), ], '/docs': [ makeNode('/docs/file.txt'), ], } const client = createMockClient(tree) const worker = new SyncWorker(client, cache, 2) worker.start() await worker.waitForDrain() worker.stop() expect(await cache.get('/docs')).toBeTruthy() expect(await cache.get('/readme.txt')).toBeTruthy() expect(await cache.get('/docs/file.txt')).toBeTruthy() }) it('should remove deleted nodes from cache', async () => { // Pre-populate cache with a node that no longer exists on remote await cache.set('/old-file.txt', makeNode('/old-file.txt')) const tree: Record = { '/': [makeNode('/new-file.txt')], } const client = createMockClient(tree) const worker = new SyncWorker(client, cache, 1) worker.start() await worker.waitForDrain() worker.stop() expect(await cache.get('/old-file.txt')).toBeNull() expect(await cache.get('/new-file.txt')).toBeTruthy() }) it('should update changed nodes based on etag', async () => { const originalNode = makeNode('/file.txt') originalNode.etag = 'old-etag' await cache.set('/file.txt', originalNode) const updatedNode = makeNode('/file.txt') updatedNode.etag = 'new-etag' const tree: Record = { '/': [updatedNode], } const client = createMockClient(tree) const worker = new SyncWorker(client, cache, 1) worker.start() await worker.waitForDrain() worker.stop() const cached = await cache.get('/file.txt') expect(cached?.etag).toBe('new-etag') }) it('should handle forceSync', async () => { const tree: Record = { '/': [makeNode('/docs', true)], '/docs': [makeNode('/docs/new.txt')], } const client = createMockClient(tree) const worker = new SyncWorker(client, cache, 2) worker.start() await worker.forceSync('/docs') worker.stop() expect(await cache.get('/docs/new.txt')).toBeTruthy() }) it('should handle prioritise without blocking', async () => { const tree: Record = { '/': [makeNode('/docs', true)], '/docs': [makeNode('/docs/file.txt')], } const client = createMockClient(tree) const worker = new SyncWorker(client, cache, 2) worker.start() worker.prioritise('/docs') await worker.waitForDrain() worker.stop() expect(await cache.get('/docs/file.txt')).toBeTruthy() }) it('should recursively sync nested directories', async () => { const tree: Record = { '/': [makeNode('/a', true)], '/a': [makeNode('/a/b', true)], '/a/b': [makeNode('/a/b/c', true)], '/a/b/c': [makeNode('/a/b/c/deep.txt')], } const client = createMockClient(tree) const worker = new SyncWorker(client, cache, 1) worker.start() await worker.waitForDrain() worker.stop() expect(await cache.get('/a/b/c/deep.txt')).toBeTruthy() }) it('should handle empty directories', async () => { const tree: Record = { '/': [makeNode('/empty', true)], '/empty': [], } const client = createMockClient(tree) const worker = new SyncWorker(client, cache, 1) worker.start() await worker.waitForDrain() worker.stop() const children = await cache.children('/empty') expect(children).toHaveLength(0) }) it('should handle client.list errors gracefully', async () => { const client: SyncableClient = { list: vi.fn(async () => { throw new Error('Network failure') }), } const worker = new SyncWorker(client, cache, 1) worker.start() await worker.waitForDrain() worker.stop() // Should not throw, just log the error expect(await cache.all()).toHaveLength(0) }) })