import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { newFixture, type Fixture } from '../helpers/fixture.js'; let fixture: Fixture; const now = Date.parse('2026-10-05T12:00:00Z'); const iso = (offset = 0) => new Date(now + offset).toISOString(); beforeEach(async () => { vi.resetModules(); vi.useFakeTimers({ toFake: ['Date'] }); vi.setSystemTime(now); fixture = await newFixture([]); const { db } = await import('../../src/lib/server/db/index.js'); db().transaction(() => { const server = db().prepare('INSERT INTO servers(name) VALUES (?)'); const operation = db() .prepare(`INSERT INTO server_deployments(id,server,action,state,stage,started_at,finished_at,triggered_by) VALUES(?,?,'restart',?,'Complete',?,?,'test')`); for (const name of ['one', 'two']) { server.run(name); for (let i = 0; i < 100; i++) operation.run(`${name}-${i}`, name, 'done', iso(-i), iso(-i)); operation.run(`${name}-old`, name, 'done', iso(-86_400_001), iso(-86_400_001)); operation.run(`${name}-failed`, name, 'failed', iso(), iso()); operation.run(`${name}-active`, name, 'waiting', iso(), null); } db() .prepare( "INSERT INTO deployments(id,action,started_at,finished_at) VALUES('campaign','push',?,?)" ) .run(iso(), iso()); for (const name of ['one', 'two']) { db() .prepare( `INSERT INTO deployment_servers(deployment_id,server,files_changed,restart_state,checked_at) VALUES('campaign',?,1,'done',?)` ) .run(name, iso()); } })(); }); afterEach(async () => { await fixture.cleanup(); vi.useRealTimers(); vi.restoreAllMocks(); }); describe('fleet and server response scope', () => { it('counts unattached hosts needing attention and rechecks heartbeat age with each snapshot', async () => { const hosts = await import('../../src/lib/server/hosts/index.js'); const { fleetSnapshot } = await import('../../src/lib/server/fleet/snapshot.js'); const base: import('../../src/lib/hosts.js').Host = { id: 'empty-host', name: 'Empty host', hostname: 'empty-host', portMin: 28960, portMax: 28980, bindAddress: '0.0.0.0', publicAddress: '', minimumFreeBytes: 1024, createdAt: iso(), connected: true, connectionState: 'online', lastHeartbeat: iso(), enrolled: true, credentialActive: true, mode: 'active', readiness: 'ready', readinessReasons: [], inventory: {}, decommission: null }; vi.spyOn(hosts, 'listHosts').mockReturnValue([ base, { ...base, id: 'offline', connected: false }, { ...base, id: 'degraded', readiness: 'degraded' }, { ...base, id: 'disabled', connected: false, mode: 'disabled' }, { ...base, id: 'unenrolled', connected: false, enrolled: false } ]); expect(fleetSnapshot().hostAttention).toBe(2); vi.setSystemTime(now + 91_000); expect(fleetSnapshot().hostAttention).toBe(3); // None of these hosts has an assigned instance in this fixture. expect(fleetSnapshot().servers.every((server) => !server.host_id)).toBe(true); }); it('omits completed history from fleet REST and SSR responses', async () => { const { GET } = await import('../../src/routes/api/fleet/+server.js'); const { load } = await import('../../src/routes/+page.server.js'); const body = await GET().json(); expect(body.operations.map((operation: { key: string }) => operation.key).sort()).toEqual([ 'deployment:one-active', 'deployment:two-active' ]); expect(load().fleetSnapshot.operations).toEqual(body.operations); }); it('loads only the selected server and its recent history in both REST and SSR', async () => { const { GET } = await import('../../src/routes/api/servers/[name]/+server.js'); const { load } = await import('../../src/routes/servers/[name]/+page.server.js'); const registry = await import('../../src/lib/server/fleet/servers.js'); const { fleetSnapshot } = await import('../../src/lib/server/fleet/snapshot.js'); const expected = fleetSnapshot().servers.find((server) => server.name === 'one'); const list = vi.spyOn(registry, 'listServers'); const snapshot = load({ params: { name: 'one' } }).serverSnapshot; expect(list).not.toHaveBeenCalled(); expect(snapshot.servers).toEqual([expected]); expect(snapshot.operations).toHaveLength(103); expect(snapshot.operations.every((operation) => operation.server === 'one')).toBe(true); expect(snapshot.operations.map((operation) => operation.key)).not.toContain( 'deployment:one-old' ); expect(snapshot.operations).toContainEqual( expect.objectContaining({ key: 'campaign:campaign:one', state: 'done' }) ); expect(snapshot.operations).toContainEqual( expect.objectContaining({ key: 'deployment:one-failed', state: 'attention' }) ); const body = await GET({ params: { name: 'one' } }).json(); expect(body).toEqual(JSON.parse(JSON.stringify(snapshot))); expect(list).not.toHaveBeenCalled(); }); it('returns no unrelated servers or history for missing and removed servers', async () => { const { serverSnapshot } = await import('../../src/lib/server/fleet/snapshot.js'); expect(serverSnapshot('missing')).toMatchObject({ servers: [], operations: [] }); const { db } = await import('../../src/lib/server/db/index.js'); db().prepare("UPDATE servers SET destroyed_at = ? WHERE name = 'one'").run(iso()); expect(serverSnapshot('one').servers).toEqual([]); db() .prepare("UPDATE server_deployments SET server_deleted_at = ? WHERE server = 'one'") .run(iso()); db() .prepare("UPDATE deployment_servers SET server_deleted_at = ? WHERE server = 'one'") .run(iso()); expect(serverSnapshot('one').operations).toEqual([]); }); it('uses a server index for recent lifecycle history', async () => { const { db } = await import('../../src/lib/server/db/index.js'); const { serverOperations } = await import('../../src/lib/server/fleet/overview.js'); const prepare = vi.spyOn(db(), 'prepare'); serverOperations('one', now); const sql = prepare.mock.calls .map(([sql]) => sql) .find((sql) => sql.includes('FROM server_deployments'))!; prepare.mockRestore(); const plan = db() .prepare(`EXPLAIN QUERY PLAN ${sql}`) .all({ server: 'one', since: iso(-86_400_000) }) as { detail: string }[]; const details = plan.map((row) => row.detail).join('\n'); expect(details).toMatch( /SEARCH server_deployments USING (?:COVERING )?INDEX idx_server_deployments_server/ ); expect(details).not.toMatch(/SCAN server_deployments\b/); }); });