import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest"; import { getTestPool, setupTestDb, cleanTestDb, teardownTestDb, uuidv4, } from "./helpers.js"; import pg from "pg"; describe("Jobs", () => { let pool: pg.Pool; let orgId: string; let userId: string; beforeAll(async () => { pool = await getTestPool(); }); afterAll(async () => { await teardownTestDb(); }); beforeEach(async () => { await cleanTestDb(pool); const seeded = await setupTestDb(); orgId = seeded.orgId; userId = seeded.userId; }); it("should create a job with targets", async () => { const deviceId = uuidv4(); await pool.query( `INSERT INTO devices (id, organization_id, name, device_type, platform, os_name, os_version, architecture, agent_version, status) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`, [deviceId, orgId, "job-test", "linux_server", "linux", "Ubuntu", "22.04", "amd64", "0.1.0", "online"] ); const { rows: jobRows } = await pool.query( `INSERT INTO jobs (organization_id, created_by_user_id, job_type, status, parameters) VALUES ($1, $2, 'update_inventory', 'queued', '{}') RETURNING id`, [orgId, userId] ); const jobId = jobRows[0].id; await pool.query( `INSERT INTO job_targets (job_id, device_id, status) VALUES ($1, $2, 'queued')`, [jobId, deviceId] ); const { rows: jobs } = await pool.query( "SELECT * FROM jobs WHERE id = $1", [jobId] ); expect(jobs.length).toBe(1); expect(jobs[0].job_type).toBe("update_inventory"); expect(jobs[0].status).toBe("queued"); const { rows: targets } = await pool.query( "SELECT * FROM job_targets WHERE job_id = $1", [jobId] ); expect(targets.length).toBe(1); expect(targets[0].device_id).toBe(deviceId); }); it("should transition job through states", async () => { const { rows: jobRows } = await pool.query( `INSERT INTO jobs (organization_id, created_by_user_id, job_type, status, parameters) VALUES ($1, $2, 'update_system_info', 'queued', '{}') RETURNING id`, [orgId, userId] ); const jobId = jobRows[0].id; await pool.query( "UPDATE jobs SET status = 'dispatched', dispatched_at = NOW() WHERE id = $1", [jobId] ); const { rows: step1 } = await pool.query("SELECT status FROM jobs WHERE id = $1", [jobId]); expect(step1[0].status).toBe("dispatched"); await pool.query( "UPDATE jobs SET status = 'running', started_at = NOW() WHERE id = $1", [jobId] ); const { rows: step2 } = await pool.query("SELECT status FROM jobs WHERE id = $1", [jobId]); expect(step2[0].status).toBe("running"); await pool.query( "UPDATE jobs SET status = 'succeeded', completed_at = NOW() WHERE id = $1", [jobId] ); const { rows: step3 } = await pool.query("SELECT status FROM jobs WHERE id = $1", [jobId]); expect(step3[0].status).toBe("succeeded"); }); it("should cancel queued jobs", async () => { const { rows: jobRows } = await pool.query( `INSERT INTO jobs (organization_id, created_by_user_id, job_type, status, parameters) VALUES ($1, $2, 'reboot_system', 'queued', '{}') RETURNING id`, [orgId, userId] ); const jobId = jobRows[0].id; const { rows: updated } = await pool.query( `UPDATE jobs SET status = 'cancelled', completed_at = NOW() WHERE id = $1 AND status = 'queued' RETURNING id`, [jobId] ); expect(updated.length).toBe(1); const { rows: step } = await pool.query("SELECT status FROM jobs WHERE id = $1", [jobId]); expect(step[0].status).toBe("cancelled"); }); it("should not cancel running jobs", async () => { const { rows: jobRows } = await pool.query( `INSERT INTO jobs (organization_id, created_by_user_id, job_type, status, parameters) VALUES ($1, $2, 'update_inventory', 'running', '{}') RETURNING id`, [orgId, userId] ); const jobId = jobRows[0].id; const { rows: updated } = await pool.query( `UPDATE jobs SET status = 'cancelled', completed_at = NOW() WHERE id = $1 AND status = 'queued' RETURNING id`, [jobId] ); expect(updated.length).toBe(0); const { rows: step } = await pool.query("SELECT status FROM jobs WHERE id = $1", [jobId]); expect(step[0].status).toBe("running"); }); it("should complete job when all targets finish", async () => { const deviceId1 = uuidv4(); const deviceId2 = uuidv4(); await pool.query( `INSERT INTO devices (id, organization_id, name, device_type, platform, os_name, os_version, architecture, agent_version, status) VALUES ($1, $2, 'multi-1', 'linux_server', 'linux', 'Ubuntu', '22.04', 'amd64', '0.1.0', 'online')`, [deviceId1, orgId] ); await pool.query( `INSERT INTO devices (id, organization_id, name, device_type, platform, os_name, os_version, architecture, agent_version, status) VALUES ($1, $2, 'multi-2', 'linux_server', 'linux', 'Ubuntu', '22.04', 'amd64', '0.1.0', 'online')`, [deviceId2, orgId] ); const { rows: jobRows } = await pool.query( `INSERT INTO jobs (organization_id, created_by_user_id, job_type, status, parameters) VALUES ($1, $2, 'update_inventory', 'dispatched', '{}') RETURNING id`, [orgId, userId] ); const jobId = jobRows[0].id; await pool.query( `INSERT INTO job_targets (job_id, device_id, status) VALUES ($1, $2, 'dispatched'), ($1, $3, 'dispatched')`, [jobId, deviceId1, deviceId2] ); await pool.query( "UPDATE job_targets SET status = 'succeeded', completed_at = NOW() WHERE job_id = $1 AND device_id = $2", [jobId, deviceId1] ); const { rows: partial } = await pool.query( "SELECT status FROM jobs WHERE id = $1", [jobId] ); expect(partial[0].status).toBe("dispatched"); await pool.query( "UPDATE job_targets SET status = 'succeeded', completed_at = NOW() WHERE job_id = $1 AND device_id = $2", [jobId, deviceId2] ); const { rows: targets } = await pool.query( "SELECT status FROM job_targets WHERE job_id = $1", [jobId] ); const allDone = targets.every((t: any) => !["queued", "dispatched", "running"].includes(t.status)); expect(allDone).toBe(true); }); it("should fail job when any target fails", async () => { const deviceId1 = uuidv4(); const deviceId2 = uuidv4(); await pool.query( `INSERT INTO devices (id, organization_id, name, device_type, platform, os_name, os_version, architecture, agent_version, status) VALUES ($1, $2, 'fail-1', 'linux_server', 'linux', 'Ubuntu', '22.04', 'amd64', '0.1.0', 'online'), ($3, $2, 'fail-2', 'linux_server', 'linux', 'Ubuntu', '22.04', 'amd64', '0.1.0', 'online')`, [deviceId1, orgId, deviceId2] ); const { rows: jobRows } = await pool.query( `INSERT INTO jobs (organization_id, created_by_user_id, job_type, status, parameters) VALUES ($1, $2, 'reboot_system', 'dispatched', '{}') RETURNING id`, [orgId, userId] ); const jobId = jobRows[0].id; await pool.query( `INSERT INTO job_targets (job_id, device_id, status) VALUES ($1, $2, 'dispatched'), ($1, $3, 'dispatched')`, [jobId, deviceId1, deviceId2] ); await pool.query( "UPDATE job_targets SET status = 'succeeded', completed_at = NOW() WHERE job_id = $1 AND device_id = $2", [jobId, deviceId1] ); await pool.query( `UPDATE job_targets SET status = 'failed', completed_at = NOW(), result = '{"error": "timeout"}' WHERE job_id = $1 AND device_id = $2`, [jobId, deviceId2] ); const { rows: targets } = await pool.query( "SELECT status FROM job_targets WHERE job_id = $1", [jobId] ); const anyFailed = targets.some((t: any) => t.status === "failed"); expect(anyFailed).toBe(true); }); });