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 hosts = await import('../../src/lib/server/hosts/index.js'); const host = hosts.createHost({ name: 'Observation host' }); hosts.enrollHost(hosts.createEnrollment(host.id).token, { protocolVersion: 1 }); const { db } = await import('../../src/lib/server/db/index.js'); db().prepare("INSERT INTO servers(name,komodo_name) VALUES('test','')").run(); db().prepare(`INSERT INTO native_instances(id,server,host_id,library_id,image,port,created_at) VALUES('instance','test',?,'library','image',28960,?)`).run(host.id, new Date().toISOString()); const native = await import('../../src/lib/server/backends/native.js'); const instance = native.nativeInstance('test')!; return { ...hosts, ...native, db: db(), host, instance }; } describe('overlapping native observations', () => { it.each(['files.read', 'files.list', 'files.stat', 'files.manifest', 'logs.read', 'instance.inspect'])('shares %s only until completion', async kind => { const api = await setup(); const requests = Array.from({ length: 4 }, () => api.nativeRequest(api.instance, kind, { path: 'same' })); const operations = api.listHostOperations(api.host.id); expect(operations).toHaveLength(1); api.reportHostOperation(api.host.id, operations[0].id, { state: 'completed', result: { value: 'first' } }); expect(await Promise.all(requests)).toEqual(Array(4).fill({ value: 'first' })); const fresh = api.nativeRequest(api.instance, kind, { path: 'same' }); const next = api.listHostOperations(api.host.id)[0]; expect(next.id).not.toBe(operations[0].id); api.reportHostOperation(api.host.id, next.id, { state: 'completed', result: { value: 'changed' } }); expect(await fresh).toEqual({ value: 'changed' }); }); it('does not merge different paths, mutations, or reads across lifecycle submissions', async () => { const api = await setup(); const requests = [api.nativeRequest(api.instance, 'files.read', { path: 'one' }), api.nativeRequest(api.instance, 'files.read', { path: 'two' }), api.nativeRequest(api.instance, 'rcon.execute', { command: 'map_restart' }), api.nativeRequest(api.instance, 'rcon.execute', { command: 'map_restart' })]; api.db.prepare("INSERT INTO restart_rollout(server,requested_at) VALUES('test',?)").run(new Date().toISOString()); requests.push(api.nativeRequest(api.instance, 'files.read', { path: 'one' })); const operations = api.listHostOperations(api.host.id); expect(operations).toHaveLength(5); for (const operation of operations) api.reportHostOperation(api.host.id, operation.id, { state: 'completed', result: {} }); await Promise.all(requests); }); it('releases failed observations so the next request can recover', async () => { const api = await setup(); const first = api.nativeRequest(api.instance, 'files.read', { path: 'one' }); const rejected = expect(first).rejects.toThrow('unavailable'); api.reportHostOperation(api.host.id, api.listHostOperations(api.host.id)[0].id, { state: 'failed', error: 'unavailable' }); await rejected; const next = api.nativeRequest(api.instance, 'files.read', { path: 'one' }); expect(api.listHostOperations(api.host.id)).toHaveLength(2); api.reportHostOperation(api.host.id, api.listHostOperations(api.host.id)[0].id, { state: 'completed', result: {} }); await next; }); it('keeps replacement generations separate and rejects the old result', async () => { const api = await setup(); const old = api.nativeRequest(api.instance, 'files.read', { path: 'one' }); const rejected = expect(old).rejects.toThrow('Instance was replaced'); api.db.prepare("UPDATE native_instances SET generation=2 WHERE id='instance'").run(); const current = api.nativeRequest(api.nativeInstance('test')!, 'files.read', { path: 'one' }); const operations = api.listHostOperations(api.host.id); expect(operations).toHaveLength(2); for (const operation of operations) api.reportHostOperation(api.host.id, operation.id, { state: 'completed', result: { generation: operation.generation } }); await rejected; expect(await current).toEqual({ generation: 2 }); }); });