- Extract database schema and migrations into @openreader/database - Extract startup and orchestration scripts into @openreader/bootstrap - Move compute-worker into packages/compute-worker - Remove runtime dependency on drizzle-kit - Update Dockerfile to deploy isolated packages without merging node_modules
109 lines
3.6 KiB
TypeScript
109 lines
3.6 KiB
TypeScript
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
list: vi.fn(),
|
|
del: vi.fn(async () => undefined),
|
|
owned: [] as Array<{ id: string }>,
|
|
ownedAfterLease: [] as Array<{ id: string }>,
|
|
leaseRelease: vi.fn(async () => undefined),
|
|
}));
|
|
|
|
vi.mock('@/lib/server/storage/s3', () => ({ isS3Configured: () => true }));
|
|
vi.mock('@/lib/server/documents/blobstore', () => ({
|
|
listDocumentSourceBlobs: mocks.list,
|
|
deleteDocumentBlob: mocks.del,
|
|
}));
|
|
vi.mock('@/lib/server/documents/previews-blobstore', () => ({
|
|
deleteDocumentPreviewArtifacts: vi.fn(async () => 0),
|
|
}));
|
|
vi.mock('@/lib/server/documents/previews', () => ({
|
|
deleteDocumentPreviewRows: vi.fn(async () => undefined),
|
|
}));
|
|
vi.mock('@/lib/server/documents/blob-lease', () => ({
|
|
tryAcquireDocumentBlobLease: vi.fn(async () => ({
|
|
owner: 'lease-owner',
|
|
release: mocks.leaseRelease,
|
|
})),
|
|
}));
|
|
vi.mock('@/lib/server/errors/logging', () => ({ logDegraded: vi.fn() }));
|
|
vi.mock('@openreader/database', () => ({
|
|
db: {
|
|
select: () => ({
|
|
from: () => ({
|
|
where: () => {
|
|
const rows = mocks.owned;
|
|
mocks.owned = mocks.ownedAfterLease;
|
|
return Object.assign(Promise.resolve(rows), {
|
|
limit: () => Promise.resolve(rows.slice(0, 1)),
|
|
});
|
|
},
|
|
}),
|
|
}),
|
|
},
|
|
}));
|
|
|
|
import { reapOrphanedBlobs } from '../../src/lib/server/tasks/handlers/reap-orphaned-blobs';
|
|
|
|
const TWO_HOURS = 2 * 60 * 60 * 1000;
|
|
const context = () => ({ signal: new AbortController().signal, deadlineAt: Date.now() + 60_000 });
|
|
|
|
beforeEach(() => {
|
|
mocks.list.mockReset();
|
|
mocks.del.mockReset();
|
|
mocks.del.mockResolvedValue(undefined);
|
|
mocks.owned = [];
|
|
mocks.ownedAfterLease = [];
|
|
mocks.leaseRelease.mockClear();
|
|
});
|
|
|
|
describe('reap-orphaned-blobs', () => {
|
|
test('reaps only old blobs with no owner', async () => {
|
|
const now = Date.now();
|
|
mocks.list.mockResolvedValue([
|
|
{ id: 'orphan-old', lastModifiedMs: now - TWO_HOURS },
|
|
{ id: 'owned-old', lastModifiedMs: now - TWO_HOURS },
|
|
{ id: 'orphan-young', lastModifiedMs: now - 60_000 },
|
|
]);
|
|
mocks.owned = [{ id: 'owned-old' }];
|
|
|
|
const result = await reapOrphanedBlobs(context());
|
|
|
|
expect(mocks.del).toHaveBeenCalledTimes(1);
|
|
expect(mocks.del).toHaveBeenCalledWith('orphan-old', null);
|
|
expect(result.reaped).toBe(1);
|
|
expect(result.scanned).toBe(3);
|
|
expect(result.candidates).toBe(2);
|
|
});
|
|
|
|
test('reaps nothing when every old blob still has an owner', async () => {
|
|
const now = Date.now();
|
|
mocks.list.mockResolvedValue([{ id: 'kept', lastModifiedMs: now - TWO_HOURS }]);
|
|
mocks.owned = [{ id: 'kept' }];
|
|
|
|
const result = await reapOrphanedBlobs(context());
|
|
|
|
expect(mocks.del).not.toHaveBeenCalled();
|
|
expect(result.reaped).toBe(0);
|
|
});
|
|
|
|
test('does not delete when an owner appears before the post-lease recheck', async () => {
|
|
const now = Date.now();
|
|
mocks.list.mockResolvedValue([{ id: 'claimed-during-run', lastModifiedMs: now - TWO_HOURS }]);
|
|
mocks.ownedAfterLease = [{ id: 'claimed-during-run' }];
|
|
|
|
const result = await reapOrphanedBlobs(context());
|
|
|
|
expect(mocks.del).not.toHaveBeenCalled();
|
|
expect(result.reaped).toBe(0);
|
|
expect(mocks.leaseRelease).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test('fails the task when an orphan deletion fails', async () => {
|
|
const now = Date.now();
|
|
mocks.list.mockResolvedValue([{ id: 'failed-orphan', lastModifiedMs: now - TWO_HOURS }]);
|
|
mocks.del.mockRejectedValue(new Error('storage failed'));
|
|
|
|
await expect(reapOrphanedBlobs(context())).rejects.toThrow('Failed to reap 1');
|
|
expect(mocks.leaseRelease).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|