Read chat logs incrementally and drain retention (Refs #62)

This commit is contained in:
2026-10-08 17:28:49 -04:00
parent 0cc4498b53
commit c10f602e8e
18 changed files with 646 additions and 39 deletions
+56
View File
@@ -0,0 +1,56 @@
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');
});
});