import { describe, it, expect, vi, beforeAll, beforeEach } from 'vitest'; import { fileURLToPath } from 'url'; import { resetDb } from '../helpers/db.js'; import { migrateUp } from '../../lib/db/migrate.js'; import * as aa from '../../lib/db/repos/agent_actions.js'; import { makeActionService } from '../../lib/actions/service.js'; import { loadActions } from '../../lib/actions/registry.js'; const FIX = fileURLToPath(new URL('../fixtures/actions.test.json', import.meta.url)); const owner = { kind: 'user', id: null }; let svc, channels; beforeAll(async () => { await resetDb(); await migrateUp(); }); beforeEach(() => { channels = { powerGuest: vi.fn(async () => ({ ok: true, upid: 'U' })), restartService: vi.fn(async () => ({ ok: true, output: 'done' })) }; svc = makeActionService({ registry: loadActions(FIX), channels }); }); describe('action service', () => { it('safe action executes immediately + audits', async () => { const out = await svc.run('restart-caddy-ct100', owner); expect(out.executed).toBe(true); expect(channels.restartService).toHaveBeenCalledOnce(); }); it('risky action queues, does NOT execute', async () => { const out = await svc.run('stop-ct107', owner); expect(out.queued).toBe(true); expect(channels.powerGuest).not.toHaveBeenCalled(); expect((await aa.listPending()).some(r => r.id === out.action_row_id)).toBe(true); }); it('approve executes the queued risky action; reject does not', async () => { const q = await svc.run('stop-ct107', owner); const done = await svc.approve(q.action_row_id, owner); expect(done.status).toBe('executed'); expect(channels.powerGuest).toHaveBeenCalledOnce(); const q2 = await svc.run('stop-ct107', owner); const rej = await svc.reject(q2.action_row_id, owner); expect(rej.status).toBe('rejected'); expect(channels.powerGuest).toHaveBeenCalledOnce(); // unchanged }); it('unknown action → error', async () => { await expect(svc.run('ghost', owner)).rejects.toThrow(/unknown action/i); }); });