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 () => { vi.restoreAllMocks(); await fixture.cleanup(); }); async function seedHistory(count = 53) { const hosts = await import('../../src/lib/server/hosts/index.js'); const { db } = await import('../../src/lib/server/db/index.js'); const first = hosts.createHost({ name: 'Alpha host' }); const second = hosts.createHost({ name: 'Beta host' }); const deleted = hosts.createHost({ name: 'Removed host' }); const handle = db(); const ids: string[] = []; handle.transaction(() => { const insert = handle.prepare(`INSERT INTO ec_host_operations (id,host_id,kind,instance_id,generation,payload,result,state,error,created_at,updated_at) VALUES(?,?,'instance.inspect','instance-one',2,?,?,'completed',NULL,?,?)`); for (let index = 0; index < count; index++) { // Reverse IDs and tied timestamps ensure insertion order breaks ties. const id = `operation-${String(count - index).padStart(4, '0')}`; const timestamp = index < 3 ? '2026-10-01T12:00:00.000Z' : '2026-10-05T12:00:00.000Z'; ids.push(id); insert.run( id, [first, second, deleted][index % 3].id, '{"credential":"payload-secret"}', '{"credential":"result-secret"}', timestamp, timestamp ); } handle .prepare('UPDATE ec_hosts SET deleted_at=? WHERE id=?') .run('2026-10-05T12:30:00.000Z', deleted.id); })(); return { ...hosts, handle, ids, first, second, deleted }; } async function operationHistory() { const api = await seedHistory(0); function add( kind: string, options: { payload?: Record; state?: string; result?: unknown; error?: string; } = {} ) { const operation = api.queueHostOperation(api.first.id, kind, options.payload ?? {}); if (options.state !== 'pending') api.reportHostOperationStatus(api.first.id, operation.id, { state: options.state ?? 'completed', result: options.result, error: options.error }); return operation.id; } return { ...api, add }; } describe('host operation history', () => { it('maintains exact scoped totals through receipts, expiration, rollback, deletion and reopening without recounting history', async () => { const api = await operationHistory(); const { importantOperationSql } = await import('../../src/lib/server/hosts/observations.js'); const { closeDb, db } = await import('../../src/lib/server/db/index.js'); const ids = Array.from({ length: 35 }, (_, i) => api.add(i % 2 ? 'instance.inspect' : 'instance.restart', { state: 'pending' }) ); const verify = () => { for (const host of [undefined, api.first.id, api.second.id]) for (const server of [undefined, 'server-one']) { for (const filter of ['all', 'routine', 'important'] as const) { const where = ['o.history_expired_at IS NULL']; const args: string[] = []; if (host) { where.push('o.host_id=?'); args.push(host); } if (server) { where.push('o.server_name=?'); args.push(server); } if (filter !== 'all') where.push( filter === 'routine' ? 'o.routine=1' : importantOperationSql ); const expected = db() .prepare( `SELECT COUNT(*) total FROM ec_host_operations o WHERE ${where.join(' AND ')}` ) .get(...args) as { total: number }; expect( api.listHostOperationHistory(1, filter, { host, server }).total ).toBe(expected.total); } } }; verify(); for (const [index, id] of ids.entries()) api.reportHostOperationStatus( api.first.id, id, index % 3 ? { state: 'completed', result: { healthy: index % 5 !== 0 } } : { state: 'failed', error: 'failure' } ); api.handle .prepare( "UPDATE ec_host_operations SET server_name='server-one',host_id=? WHERE rowid%2=0" ) .run(api.second.id); api.handle .prepare('UPDATE ec_host_operations SET history_expired_at=? WHERE rowid%4=0') .run(new Date().toISOString()); api.handle.exec( 'DELETE FROM ec_host_operation_events WHERE operation_id IN (SELECT id FROM ec_host_operations WHERE rowid%7=0); DELETE FROM ec_host_operations WHERE rowid%7=0' ); expect(() => api.handle.transaction(() => { api.handle.exec( 'DELETE FROM ec_host_operation_events; DELETE FROM ec_host_operations' ); throw new Error('rollback'); })() ).toThrow('rollback'); verify(); closeDb(); verify(); const prepare = vi.spyOn(db(), 'prepare'); api.listHostOperationHistory(1); api.listHostOperationHistory(2); expect(prepare.mock.calls.some(([sql]) => /COUNT\(/i.test(sql))).toBe(false); const count = prepare.mock.calls.find(([sql]) => sql.includes('FROM ec_host_operation_history_counts') )![0]; expect( db() .prepare('EXPLAIN QUERY PLAN ' + count) .all('', '') ).toEqual( expect.arrayContaining([ expect.objectContaining({ detail: expect.stringContaining('USING PRIMARY KEY') }) ]) ); }); it('scopes counts and pages to one host or server and preserves deleted-host history', async () => { const api = await seedHistory(90); api.handle .prepare("UPDATE ec_host_operations SET server_name='server-one' WHERE host_id=?") .run(api.first.id); const first = api.listHostOperationHistory(1, 'all', { host: api.first.id }); const second = api.listHostOperationHistory(2, 'all', { host: api.first.id }); expect(first).toMatchObject({ total: 30, totalPages: 2 }); expect([...first.operations, ...second.operations]).toHaveLength(30); expect(first.operations.every((operation) => operation.hostId === api.first.id)).toBe(true); expect(api.listHostOperationHistory(1, 'all', { server: 'server-one' })).toEqual(first); expect( api.listHostOperationHistory(1, 'all', { host: api.second.id, server: 'server-one' }) .total ).toBe(0); expect( api .listHostOperationHistory(1, 'all', { host: api.deleted.id }) .operations.every((operation) => operation.hostName === null) ).toBe(true); const { GET } = await import('../../src/routes/api/hosts/operations/+server.js'); const response = GET({ request: new Request( `https://ec.test/api/hosts/operations?filter=all&host=${api.first.id}&page=2` ) }); expect(await response.json()).toEqual(second); }); it('applies host scope to failures as well as ordinary actions', async () => { const api = await operationHistory(); const first = api.add('instance.restart'); const other = api.queueHostOperation(api.second.id, 'instance.inspect', {}); api.reportHostOperationStatus(api.second.id, other.id, { state: 'failed', error: 'Unreachable' }); expect( api .listHostOperationHistory(1, 'important', { host: api.first.id }) .operations.map((operation) => operation.id) ).toEqual([first]); expect( api .listHostOperationHistory(1, 'important', { host: api.second.id }) .operations.map((operation) => operation.id) ).toEqual([other.id]); }); it('finds an exact old operation without walking pages and keeps all scope and retention filters', async () => { const api = await seedHistory(1003); const operation = api.ids[0]; api.handle .prepare("UPDATE ec_host_operations SET server_name='server-one' WHERE id=?") .run(operation); const prepare = vi.spyOn(api.handle, 'prepare'); const history = api.listHostOperationHistory(999, 'all', { operation, host: api.first.id, server: 'server-one' }); const exactCount = prepare.mock.calls .map(([sql]) => sql) .find((sql) => sql.startsWith('SELECT 1 AS total'))!; prepare.mockRestore(); expect(history).toMatchObject({ page: 1, total: 1, totalPages: 1 }); expect(history.operations.map((row) => row.id)).toEqual([operation]); expect(JSON.stringify(history)).not.toContain('secret'); const plan = api.handle .prepare(`EXPLAIN QUERY PLAN ${exactCount}`) .all(api.first.id, 'server-one', operation) as { detail: string }[]; expect( plan.some((row) => row.detail.includes('USING INDEX sqlite_autoindex_ec_host_operations_1') ) ).toBe(true); expect( api.listHostOperationHistory(1, 'all', { operation, host: api.second.id }).total ).toBe(0); expect( api.listHostOperationHistory(1, 'all', { operation, server: 'another-server' }).total ).toBe(0); expect(api.listHostOperationHistory(1, 'routine', { operation }).total).toBe(0); expect( api.listHostOperationHistory(1, 'all', { operation: 'missing-operation' }).total ).toBe(0); api.handle .prepare('UPDATE ec_host_operations SET history_expired_at=? WHERE id=?') .run(new Date().toISOString(), operation); expect(api.listHostOperationHistory(1, 'all', { operation })).toMatchObject({ operations: [], page: 1, total: 0 }); }); it('pages all hosts in stable newest-first order, retaining deleted-host records', async () => { const api = await seedHistory(); const pages = [1, 2, 3].map((page) => api.listHostOperationHistory(page, 'all')); expect(pages.map((page) => page.operations.length)).toEqual([25, 25, 3]); pages.forEach((page, index) => expect(page).toMatchObject({ page: index + 1, pageSize: 25, total: 53, totalPages: 3, filter: 'all' }) ); const operations = pages.flatMap((page) => page.operations); expect(operations.map((operation) => operation.id)).toEqual([...api.ids].reverse()); expect(operations.find((operation) => operation.hostId === api.first.id)?.hostName).toBe( 'Alpha host' ); expect(operations.find((operation) => operation.hostId === api.second.id)?.hostName).toBe( 'Beta host' ); expect( operations.filter((operation) => operation.hostId === api.deleted.id).length ).toBeGreaterThan(0); expect( operations .filter((operation) => operation.hostId === api.deleted.id) .every((operation) => operation.hostName === null) ).toBe(true); expect(api.listHostOperationHistory(999, 'all')).toEqual(pages[2]); }); it('bounds reads in SQLite using the history index and omits private payloads and results', async () => { const api = await seedHistory(1003); const prepare = vi.spyOn(api.handle, 'prepare'); const history = api.listHostOperationHistory(2, 'all'); api.listHostOperationHistory(1, 'important'); api.listHostOperationHistory(1, 'routine'); const statements = prepare.mock.calls.map(([sql]) => sql); const query = prepare.mock.calls .map(([sql]) => sql) .find((sql) => sql.includes('FROM ec_host_operations o LEFT JOIN'))!; prepare.mockRestore(); expect( statements.every( (sql) => !/\b(?:payload|result|json_extract|ec_host_operation_payloads)\b/.test(sql) ) ).toBe(true); const projection = query.split('FROM ec_host_operations')[0]; expect(projection).not.toContain('payload'); expect(projection).not.toContain('result'); expect(projection).not.toContain('*'); expect(query).toContain('LIMIT ? OFFSET ?'); const plan = api.handle.prepare(`EXPLAIN QUERY PLAN ${query}`).all(25, 25) as { detail: string; }[]; expect( plan.some((row) => row.detail.includes('USING INDEX ec_host_operations_history')) ).toBe(true); expect(plan.some((row) => row.detail.includes('TEMP B-TREE'))).toBe(false); expect(history.operations).toHaveLength(25); expect(history.operations[0]).toMatchObject({ instanceId: 'instance-one', generation: 2, state: 'completed' }); expect( history.operations.every( (operation) => !('payload' in operation) && !('result' in operation) ) ).toBe(true); expect(JSON.stringify(history)).not.toContain('secret'); }); it('seeks through the selected history index without sorting or reading private payloads', async () => { const api = await seedHistory(1003); for (const filter of ['important', 'routine'] as const) { const prepare = vi.spyOn(api.handle, 'prepare'); api.listHostOperationHistory(2, filter); const query = prepare.mock.calls .map(([sql]) => sql) .find((sql) => sql.includes('FROM ec_host_operations o LEFT JOIN'))!; prepare.mockRestore(); const plan = api.handle.prepare(`EXPLAIN QUERY PLAN ${query}`).all(25, 25) as { detail: string; }[]; expect( plan.some((row) => row.detail.includes(`USING INDEX ec_host_operations_${filter}_history`) ) ).toBe(true); expect(plan.some((row) => row.detail.includes('TEMP B-TREE'))).toBe(false); expect(query).not.toMatch(/\b(?:payload|result|json_extract)\b/); } }); it('returns an empty first page and normalizes invalid internal page numbers', async () => { const api = await seedHistory(0); const empty = { operations: [], page: 1, pageSize: 25, total: 0, totalPages: 1, filter: 'important' }; expect(api.listHostOperationHistory()).toEqual(empty); for (const page of [0, -1, 1.5, NaN, Infinity, Number.MAX_SAFE_INTEGER + 1, 999]) { expect(api.listHostOperationHistory(page)).toEqual(empty); } }); it('defaults to actions and errors, with independent counts and pagination for every filter', async () => { const api = await operationHistory(); const actions = Array.from({ length: 26 }, () => api.add('instance.restart')); actions.push(api.add('rcon.execute', { payload: { command: 'map_restart' } })); const successful = Array.from({ length: 26 }, () => api.add('files.manifest', { result: { files: [] } }) ); const pending = ['pending', 'accepted', 'running'].map((state) => api.add('files.read', { state }) ); const failures = ['failed', 'unknown'].map((state) => api.add('files.read', { state, error: 'Observation unavailable' }) ); const negative = [ api.add('rcon.execute', { payload: { command: 'status' }, result: { ok: false, reason: 'timeout' } }), api.add('instance.inspect', { result: { healthy: false, state: 'stopped' } }), api.add('files.read', { result: { error: 'Read was incomplete' } }), api.add('files.stat', { error: 'Could not inspect the file' }) ]; const ids = [...actions, ...successful, ...pending, ...failures, ...negative]; for (const [filter, matching] of [ ['important', [...actions, ...failures, ...negative]], ['routine', [...successful, ...pending, ...failures, ...negative]], ['all', ids] ] as const) { const pages = Array.from({ length: Math.ceil(matching.length / 25) }, (_, index) => api.listHostOperationHistory(index + 1, filter) ); expect(pages[0]).toMatchObject({ filter, total: matching.length, totalPages: pages.length, pageSize: 25 }); expect( pages.flatMap((page) => page.operations.map((operation) => operation.id)) ).toEqual([...matching].reverse()); expect(api.listHostOperationHistory(999, filter)).toEqual(pages.at(-1)); } expect(api.listHostOperationHistory()).toEqual( api.listHostOperationHistory(1, 'important') ); const routine = api.listHostOperationHistory(1, 'routine').operations; expect(routine.every((operation) => operation.routine)).toBe(true); expect( api .listHostOperationHistory(2, 'important') .operations.every((operation) => !operation.routine) ).toBe(true); }); it('keeps manual RCON commands visible and only exposes safe read command names', async () => { const api = await operationHistory(); const reads = ['status', 'getstatus', 'version'].map((command) => api.add('rcon.execute', { payload: { command } }) ); const manual = api.add('rcon.execute', { payload: { command: 'say credential-secret' } }); const combined = api.add('rcon.execute', { payload: { command: 'status; map_restart' } }); const important = api.listHostOperationHistory(); expect(important.operations.map((operation) => operation.id)).toEqual([combined, manual]); expect( important.operations.every( (operation) => !operation.routine && operation.targetPath === null ) ).toBe(true); const routine = api.listHostOperationHistory(1, 'routine'); expect(routine.operations.map((operation) => operation.id)).toEqual([...reads].reverse()); expect(routine.operations.map((operation) => operation.targetPath)).toEqual([ 'version', 'getstatus', 'status' ]); expect(JSON.stringify(api.listHostOperationHistory(1, 'all'))).not.toContain( 'credential-secret' ); }); it('persists server, target, and source summaries without depending on current instance records', async () => { const api = await operationHistory(); const { withOperationSource } = await import('../../src/lib/server/hosts/operation-context.js'); api.handle .prepare( `INSERT INTO native_instances(id,server,host_id,library_id,image,port,created_at) VALUES('history-instance','t5_zombie_theater_1',?,'library','plutainer:test',28960,?)` ) .run(api.first.id, new Date().toISOString()); const read = withOperationSource('Routine status', () => api.queueHostOperation( api.first.id, 'files.read', { path: 'configs/server.cfg', credential: 'payload-secret' }, { instanceId: 'history-instance', generation: 1 } ) ); const manifest = withOperationSource('Watchdog drift scan', () => api.queueHostOperation( api.first.id, 'files.manifest', { roots: ['scripts', 'configs', 'plugins'], ignoreNames: [], paths: ['configs/server.cfg'] }, { instanceId: 'history-instance', generation: 1 } ) ); api.reportHostOperationStatus(api.first.id, read.id, { state: 'completed', result: { contentBase64: 'result-secret' } }); api.handle.prepare('DELETE FROM native_instances WHERE id=?').run('history-instance'); const history = api.listHostOperationHistory(1, 'all'); expect(history.operations.find((operation) => operation.id === read.id)).toMatchObject({ serverName: 't5_zombie_theater_1', targetPath: 'configs/server.cfg', source: 'Routine status', routine: true }); expect(history.operations.find((operation) => operation.id === manifest.id)).toMatchObject({ serverName: 't5_zombie_theater_1', targetPath: 'scripts, configs, plugins', source: 'Watchdog drift scan', routine: true }); expect(JSON.stringify(history)).not.toContain('secret'); }); it('refuses unsafe target summaries and never derives labels from arbitrary payload fields', async () => { const api = await operationHistory(); const unsafe = [ '/etc/private-secret', '../private-secret', 'configs/../../private-secret', 'configs/line\nprivate-secret', 'configs\\private-secret' ]; const ids = unsafe.map((path) => api.add('files.read', { payload: { path, command: 'command-secret', name: 'name-secret' } }) ); ids.push( api.add('files.manifest', { payload: { roots: ['scripts', '../private-secret'] } }) ); ids.push( api.add('instance.restart', { payload: { path: 'configs/private-secret', command: 'command-secret' } }) ); const history = api.listHostOperationHistory(1, 'all'); expect(history.operations).toHaveLength(ids.length); expect( history.operations.every( (operation) => operation.targetPath === null && operation.serverName === null ) ).toBe(true); expect(JSON.stringify(history)).not.toContain('secret'); }); it('backfills safe legacy summaries from immutable payloads while leaving source unknown', async () => { const api = await operationHistory(); const { closeDb } = await import('../../src/lib/server/db/index.js'); api.handle .prepare( `INSERT INTO native_instances(id,server,host_id,library_id,image,port,created_at) VALUES('legacy-instance','t5_zombie_theater_1',?,'library','plutainer:test',28960,?)` ) .run(api.first.id, new Date().toISOString()); const read = api.queueHostOperationSerialized( api.first.id, 'files.read', Buffer.from( JSON.stringify({ path: 'configs/server.cfg', credential: 'payload-secret' }) ), { instanceId: 'legacy-instance', generation: 1 }, { path: 'configs/routing-secret' } ); api.reportHostOperationStatus(api.first.id, read.id, { state: 'completed', result: { credential: 'result-secret' } }); const command = api.queueHostOperationSerialized( api.first.id, 'rcon.execute', Buffer.from('{"command":"status"}'), {}, { command: 'command-secret' } ); const manual = api.add('rcon.execute', { payload: { command: 'say command-secret' } }); const unsafe = api.add('files.read', { payload: { path: '../private-secret' } }); api.handle.exec(`DROP INDEX ec_host_operations_routine_history; DROP TRIGGER operation_history_count_insert; DROP TRIGGER operation_history_count_delete; DROP TRIGGER operation_history_count_update; DROP TABLE ec_host_operation_history_counts; DELETE FROM ec_host_schema_migrations WHERE name='history-counts-v1'; DROP INDEX ec_host_operations_important_history; DROP INDEX ec_host_operations_server_history; ALTER TABLE ec_host_operations DROP COLUMN routine; ALTER TABLE ec_host_operations DROP COLUMN server_name; ALTER TABLE ec_host_operations DROP COLUMN target_path; ALTER TABLE ec_host_operations DROP COLUMN source;`); closeDb(); const history = api.listHostOperationHistory(1, 'all'); expect(history.operations).toHaveLength(4); expect(history.operations.find((operation) => operation.id === read.id)).toMatchObject({ routine: true, serverName: 't5_zombie_theater_1', targetPath: 'configs/server.cfg', source: null }); expect(history.operations.find((operation) => operation.id === command.id)).toMatchObject({ routine: true, targetPath: 'status', source: null }); expect(history.operations.find((operation) => operation.id === manual)).toMatchObject({ routine: false, targetPath: null, source: null }); expect(history.operations.find((operation) => operation.id === unsafe)).toMatchObject({ routine: true, targetPath: null, source: null }); expect(JSON.stringify(history)).not.toContain('secret'); closeDb(); expect(api.listHostOperationHistory(1, 'all')).toEqual(history); }); it('serves fixed-size API pages without caching or exposing private operation data', async () => { const api = await seedHistory(); const { GET } = await import('../../src/routes/api/hosts/operations/+server.js'); const first = GET({ request: new Request('https://ec.test/api/hosts/operations?filter=all') }); expect(first.headers.get('cache-control')).toBe('no-store'); expect(await first.json()).toEqual(api.listHostOperationHistory(1, 'all')); const second = GET({ request: new Request( 'https://ec.test/api/hosts/operations?page=2&pageSize=500&filter=all' ) }); const body = await second.json(); expect(body).toEqual(api.listHostOperationHistory(2, 'all')); expect(body.operations).toHaveLength(25); expect(JSON.stringify(body)).not.toContain('secret'); const last = GET({ request: new Request('https://ec.test/api/hosts/operations?page=999&filter=all') }); expect(await last.json()).toEqual(api.listHostOperationHistory(3, 'all')); }); it('serves the selected filter and defaults the API to actions and errors', async () => { const api = await operationHistory(); const action = api.add('instance.restart'); const read = api.add('instance.inspect', { result: { healthy: true } }); const { GET } = await import('../../src/routes/api/hosts/operations/+server.js'); const request = (query = '') => GET({ request: new Request(`https://ec.test/api/hosts/operations${query}`) }).json(); expect(await request()).toEqual(api.listHostOperationHistory()); expect( (await request()).operations.map((operation: { id: string }) => operation.id) ).toEqual([action]); expect( (await request('?filter=routine')).operations.map( (operation: { id: string }) => operation.id ) ).toEqual([read]); expect(await request('?filter=all')).toEqual(api.listHostOperationHistory(1, 'all')); }); it('serves an exact old operation through the API without exposing command data', async () => { const api = await seedHistory(); const operation = api.ids[0]; const { GET } = await import('../../src/routes/api/hosts/operations/+server.js'); const response = GET({ request: new Request( `https://ec.test/api/hosts/operations?filter=all&operation=${operation}` ) }); const body = await response.json(); expect(body).toMatchObject({ page: 1, total: 1, totalPages: 1 }); expect(body.operations.map((row: { id: string }) => row.id)).toEqual([operation]); expect(JSON.stringify(body)).not.toContain('secret'); }); it('rejects invalid API pages and keeps host management access restrictions', async () => { const { GET } = await import('../../src/routes/api/hosts/operations/+server.js'); const url = 'https://ec.test/api/hosts/operations'; for (const page of ['', '0', '-1', '1.5', 'nope', 'Infinity', '9007199254740992']) { expect(() => GET({ request: new Request(`${url}?page=${page}`) })).toThrow( expect.objectContaining({ status: 400 }) ); } for (const filter of ['', 'unknown', 'ALL', 'important%27%20OR%201=1']) { expect(() => GET({ request: new Request(`${url}?filter=${filter}`) })).toThrow( expect.objectContaining({ status: 400 }) ); } for (const operation of ['', 'x'.repeat(513), 'operation%00bad', 'operation%0Abad']) { expect(() => GET({ request: new Request(`${url}?operation=${operation}`) })).toThrow( expect.objectContaining({ status: 400 }) ); } const blockedHeaders: HeadersInit[] = [ { origin: 'https://other.test' }, { 'sec-fetch-site': 'cross-site' }, { authorization: 'Bearer ec_host_secret' }, { authorization: 'Bearer ec_enroll_secret' } ]; for (const headers of blockedHeaders) { expect(() => GET({ request: new Request(url, { headers }) })).toThrow( expect.objectContaining({ status: 403 }) ); } }); it('keeps operation history out of host detail responses', async () => { const api = await seedHistory(); const { GET } = await import('../../src/routes/api/hosts/[id]/+server.js'); const body = await GET({ request: new Request(`https://ec.test/api/hosts/${api.first.id}`), params: { id: api.first.id } }).json(); expect(body.host.id).toBe(api.first.id); expect(body).not.toHaveProperty('operations'); expect(JSON.stringify(body)).not.toContain('secret'); }); });