Files
electric-cherry/tests/unit/host-log-results.test.ts
T

101 lines
4.0 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { newFixture, type Fixture } from '../helpers/fixture.js';
let fixture: Fixture;
beforeEach(async () => {
vi.resetModules();
fixture = await newFixture([]);
});
afterEach(async () => {
await fixture.cleanup();
});
async function setup() {
const api = await import('../../src/lib/server/hosts/index.js');
const { db } = await import('../../src/lib/server/db/index.js');
const host = api.createHost({ name: 'Log host' });
const operation = api.queueHostOperation(host.id, 'logs.read', { path: 'logs/games_sp.log' });
return { ...api, host, operation, db };
}
describe('transient log results', () => {
it.each([
{ lines: ['private log body'], size: 17, mtime: '2026-10-08T12:00:00Z', truncated: false },
{
contentBase64: Buffer.from('private log body').toString('base64'),
size: 17,
reset: false,
cursor: { offset: 17 }
}
])('delivers full replies to current readers while storing only metadata', async (result) => {
const api = await setup();
const first = api.waitHostOperationStatus(api.operation.id);
const second = api.waitHostOperation(api.operation.id);
const receipt = api.reportHostOperationStatus(api.host.id, api.operation.id, {
state: 'completed',
result
});
expect((await first).result).toEqual(result);
expect((await second).result).toEqual(result);
expect(receipt.result).toMatchObject({ retained: false, size: 17 });
expect(api.getHostOperation(api.operation.id)?.result).toEqual(receipt.result);
const stored = api
.db()
.prepare('SELECT result FROM ec_host_operations WHERE id=?')
.get(api.operation.id);
expect(JSON.stringify(stored)).not.toMatch(/private log body|contentBase64|cursor|"lines"/);
expect(
api
.db()
.prepare(
"SELECT * FROM file_blob_references WHERE owner_type='operation-result' AND owner_id=?"
)
.all(api.operation.id)
).toEqual([]);
// Completed reads cannot serve a stale body, including duplicate receipts.
await expect(api.waitHostOperationStatus(api.operation.id)).rejects.toThrow(
'no longer available'
);
api.reportHostOperationStatus(api.host.id, api.operation.id, {
state: 'completed',
result
});
await expect(api.waitHostOperationStatus(api.operation.id)).rejects.toThrow(
'no longer available'
);
});
it('preserves missing-log results and operation failures', async () => {
const api = await setup();
const missing = api.waitHostOperationStatus(api.operation.id);
api.reportHostOperationStatus(api.host.id, api.operation.id, {
state: 'completed',
result: null
});
expect((await missing).result).toBeNull();
const failed = api.queueHostOperation(api.host.id, 'logs.read', {});
const failure = api.waitHostOperationStatus(failed.id);
api.reportHostOperationStatus(api.host.id, failed.id, {
state: 'failed',
error: 'Read failed'
});
expect(await failure).toMatchObject({ state: 'failed', error: 'Read failed' });
});
it('discards bodies arriving after the reader has timed out', async () => {
const api = await setup();
await expect(api.waitHostOperationStatus(api.operation.id, 0)).rejects.toThrow();
api.reportHostOperationStatus(api.host.id, api.operation.id, {
state: 'completed',
result: { lines: ['late body'] }
});
expect(api.getHostOperation(api.operation.id)?.result).toEqual({
retained: false,
lineCount: 1
});
await expect(api.waitHostOperationStatus(api.operation.id)).rejects.toThrow(
'no longer available'
);
});
});