initial commit
This commit is contained in:
198
src/__tests__/sync.test.ts
Normal file
198
src/__tests__/sync.test.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
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<string, GenericNode[]>): 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<string, GenericNode[]> = {
|
||||
'/': [
|
||||
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<string, GenericNode[]> = {
|
||||
'/': [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<string, GenericNode[]> = {
|
||||
'/': [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<string, GenericNode[]> = {
|
||||
'/': [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<string, GenericNode[]> = {
|
||||
'/': [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<string, GenericNode[]> = {
|
||||
'/': [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<string, GenericNode[]> = {
|
||||
'/': [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)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user