| 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; |
| 3 | |
| 4 | import type { AgentRun, RunnerActivity, UsageReport } from "@g1t/contracts"; |
| 5 | |
| 6 | import { cloudNow, cloudPrices, machineTime, ownWaiting, perMinute, runnerCost } from "./runners.ts"; |
| 7 | |
| 8 | function run(id: string, status: AgentRun["status"], over: Partial<AgentRun> = {}): AgentRun { |
| 9 | return { |
| 10 | id, |
| 11 | repo: { namespace: "acme", name: "web" }, |
| 12 | number: 12, |
| 13 | title: "Fix the flaky checkout test", |
| 14 | kind: "checks", |
| 15 | agent: "g1t", |
| 16 | model: null, |
| 17 | status, |
| 18 | step: null, |
| 19 | steps: [], |
| 20 | stepCount: 0, |
| 21 | startedBy: null, |
| 22 | error: null, |
| 23 | costUsd: null, |
| 24 | turns: null, |
| 25 | createdAt: "2026-10-10T10:00:00Z", |
| 26 | startedAt: "2026-10-10T10:01:00Z", |
| 27 | finishedAt: null, |
| 28 | updatedAt: "2026-10-10T10:02:00Z", |
| 29 | ...over, |
| 30 | }; |
| 31 | } |
| 32 | |
| 33 | const activity: RunnerActivity = { |
| 34 | cloud_jobs: [{ id: "job_1", name: "test (ubuntu)", run_id: "wr_1", repo: "acme/web", started_at: "2026-10-10T10:05:00Z" }], |
| 35 | cloud_jobs_queued: 2, |
| 36 | handed_over: [ |
| 37 | { |
| 38 | id: "rtk_1", |
| 39 | run_id: "run_mine", |
| 40 | kind: "implement", |
| 41 | title: "On my runner", |
| 42 | repo: "acme/web", |
| 43 | status: "in_progress", |
| 44 | runner_name: "mac", |
| 45 | created_at: "2026-10-10T09:00:00Z", |
| 46 | started_at: null, |
| 47 | }, |
| 48 | { |
| 49 | id: "rtk_2", |
| 50 | run_id: null, |
| 51 | kind: "checks", |
| 52 | title: "Waiting", |
| 53 | repo: "acme/web", |
| 54 | status: "queued", |
| 55 | runner_name: null, |
| 56 | created_at: "2026-10-10T09:00:00Z", |
| 57 | started_at: null, |
| 58 | }, |
| 59 | ], |
| 60 | self_hosted_jobs_queued: 3, |
| 61 | }; |
| 62 | |
| 63 | test("g1t cloud is the running sandbox runs not handed to the workspace's runners, and the jobs in g1t's sandboxes", () => { |
| 64 | const now = cloudNow([run("run_a", "running"), run("run_mine", "running"), run("run_q", "queued")], activity, 200, 100)!; |
| 65 | assert.deepEqual( |
| 66 | now.running.map((work) => [work.kind, work.id, work.href]), |
| 67 | [ |
| 68 | ["workflow", "job_1", "/acme/web/actions/runs/wr_1"], |
| 69 | ["agent", "run_a", "/acme/web/pull/12"], |
| 70 | ], |
| 71 | ); |
| 72 | // One queued sandbox run and two queued jobs. |
| 73 | assert.equal(now.queued, 3); |
| 74 | assert.equal(now.more, false); |
| 75 | }); |
| 76 | |
| 77 | test("a service that does not answer leaves only its part out", () => { |
| 78 | assert.equal(cloudNow(null, null, 200, 100), null); |
| 79 | assert.equal(cloudNow([run("run_a", "running")], null, 200, 100)!.running.length, 1); |
| 80 | assert.equal(ownWaiting(null), null); |
| 81 | assert.deepEqual(ownWaiting(activity), { tasks: 1, jobs: 3 }); |
| 82 | assert.equal(runnerCost(null), null); |
| 83 | }); |
| 84 | |
| 85 | test("a full list says there may be more", () => { |
| 86 | assert.equal(cloudNow([run("run_a", "running")], activity, 1, 100)!.more, true); |
| 87 | }); |
| 88 | |
| 89 | test("this month's time adds agent sandbox and sandbox time for g1t cloud, and self-hosted time for the workspace's own", () => { |
| 90 | const meter = (key: string, quantity: number, micros: number) => ({ |
| 91 | key, |
| 92 | label: key, |
| 93 | product: "x", |
| 94 | unit: "seconds", |
| 95 | quantity, |
| 96 | micros, |
| 97 | daily: [], |
| 98 | byProject: [], |
| 99 | }); |
| 100 | const report = { |
| 101 | from: "2026-10-01", |
| 102 | until: "2026-10-10", |
| 103 | free: false, |
| 104 | products: [ |
| 105 | { key: "agent", label: "Agent", micros: 0, meters: [meter("agent_sandbox", 600, 1_000_000), meter("agent_models", 9, 5_000_000)] }, |
| 106 | { |
| 107 | key: "sandboxes", |
| 108 | label: "Sandboxes", |
| 109 | micros: 0, |
| 110 | meters: [meter("runner_2-core", 10, 40_000), meter("runner_4-core", 2, 14_000), meter("sandbox", 120, 200_000), meter("self_hosted", 3600, 0)], |
| 111 | }, |
| 112 | ], |
| 113 | } as unknown as UsageReport; |
| 114 | // Runner minutes count as their minutes; older sandbox time by the second. |
| 115 | assert.deepEqual(runnerCost(report), { |
| 116 | from: "2026-10-01", |
| 117 | until: "2026-10-10", |
| 118 | cloudSeconds: 720 + 12 * 60, |
| 119 | cloudMicros: 1_254_000, |
| 120 | ownSeconds: 3600, |
| 121 | free: false, |
| 122 | }); |
| 123 | }); |
| 124 | |
| 125 | test("prices are each machine's rate a minute, from the catalogue", () => { |
| 126 | assert.deepEqual( |
| 127 | cloudPrices().map((p) => [p.machine, p.perMinuteMicros, p.title]), |
| 128 | [ |
| 129 | ["2-core", 4000, "2-core: 2 vCPU, 8 GiB, 16 GB"], |
| 130 | ["4-core", 7000, "4-core: 4 vCPU, 12 GiB, 20 GB"], |
| 131 | ], |
| 132 | ); |
| 133 | assert.equal(perMinute(4000), "$0.004"); |
| 134 | }); |
| 135 | |
| 136 | test("machine time and prices read as people say them", () => { |
| 137 | assert.equal(machineTime(0), "0 min"); |
| 138 | assert.equal(machineTime(45), "45s"); |
| 139 | assert.equal(machineTime(720), "12 min"); |
| 140 | assert.equal(machineTime(12_240), "3.4 h"); |
| 141 | assert.equal(perMinute(1500), "$0.0015"); |
| 142 | assert.equal(perMinute(870), "$0.00087"); |
| 143 | assert.equal(perMinute(0), "$0"); |
| 144 | }); |