Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; | |
| 3 | ||
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 4 | import { HEARTBEAT_MS, type DesktopRunnerView, backendOf, diagnosisOf, jobHref, reportDue, reportKey, reportOf } from "./desktop-runner.ts"; |
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 5 | |
| 6 | function view(over: Partial<DesktopRunnerView> = {}): DesktopRunnerView { | |
| 7 | return { | |
| 8 | diagnosis: { code: "no_docker", tone: "problem", title: "Not running: Docker isn't running", cause: "Docker isn't running.", fix: "Start Docker Desktop.", actions: [{ id: "open_docker", label: "Open Docker" }] }, | |
| 9 | closed: { code: "off", tone: "off", title: "g1t is closed on build-box", cause: null, fix: "Open g1t on build-box.", actions: [] }, | |
| 10 | running: false, | |
| 11 | version: "0.2.3", | |
| 12 | backend: "docker", | |
| 13 | workspace: "acme", | |
| 14 | workspaces: [{ slug: "acme", name: "Acme" }], | |
| 15 | enabled: true, | |
| 16 | paused: false, | |
| 17 | link: "https://g1t.sh/acme/-/runners", | |
| 18 | machine: "build-box", | |
| 19 | platform: "windows", | |
| 20 | log: ["2026-10-12T08:00:00Z a line"], | |
| 21 | ...over, | |
| 22 | }; | |
| 23 | } | |
| 24 | ||
| 25 | test("a report carries the app's words, without its buttons or log", () => { | |
| 26 | const report = reportOf(view()); | |
| 27 | assert.deepEqual(report, { | |
| 28 | workspace: "acme", | |
| 29 | machine: "build-box", | |
| 30 | backend: "docker", | |
| 31 | version: "0.2.3", | |
| 32 | diagnosis: { code: "no_docker", tone: "problem", title: "Not running: Docker isn't running", cause: "Docker isn't running.", fix: "Start Docker Desktop." }, | |
| 33 | closed: { code: "off", tone: "off", title: "g1t is closed on build-box", cause: null, fix: "Open g1t on build-box." }, | |
| 34 | }); | |
| 35 | assert.equal(reportOf(view({ workspace: null })), null); | |
| 36 | }); | |
| 37 | ||
| 38 | test("a report goes when what the page shows changes, and as a heartbeat", () => { | |
| 39 | const report = reportOf(view())!; | |
| 40 | const at = 1_000_000; | |
| 41 | assert.equal(reportDue(null, report, at), true); | |
| 42 | const last = { key: reportKey(report), at }; | |
| 43 | assert.equal(reportDue(last, report, at + 1000), false); | |
| 44 | assert.equal(reportDue(last, report, at + HEARTBEAT_MS), true); | |
| 45 | const busy = reportOf(view({ diagnosis: { ...view().diagnosis, code: "busy", tone: "working", title: "Running Build (acme/web)" } }))!; | |
| 46 | assert.equal(reportDue(last, busy, at + 1000), true); | |
| 47 | }); | |
| 48 | ||
| Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it. | 49 | test("a sandboxed desktop reports each kind of job, without its buttons", () => { |
| 50 | const linux = { id: "linux", label: "Linux jobs", tone: "ok" as const, title: "Waiting for jobs", cause: null, fix: null, actions: [] }; | |
| 51 | const windows = { | |
| 52 | id: "windows", | |
| 53 | label: "Windows jobs", | |
| 54 | tone: "off" as const, | |
| 55 | title: "Not available", | |
| 56 | cause: "Windows Sandbox is off, or this edition of Windows has none: Home doesn't.", | |
| 57 | fix: "Linux jobs still run.", | |
| 58 | actions: [{ id: "windows_features", label: "Open Windows features" }], | |
| 59 | }; | |
| 60 | const sandboxed = view({ backend: "sandboxed", diagnosis: { code: "idle", tone: "ok", title: "Waiting for jobs", cause: null, fix: null, actions: [], capabilities: [linux, windows] } }); | |
| 61 | const report = reportOf(sandboxed)!; | |
| 62 | assert.deepEqual(report.diagnosis.capabilities, [ | |
| 63 | { id: "linux", label: "Linux jobs", tone: "ok", title: "Waiting for jobs", cause: null, fix: null }, | |
| 64 | { id: "windows", label: "Windows jobs", tone: "off", title: "Not available", cause: windows.cause, fix: "Linux jobs still run." }, | |
| 65 | ]); | |
| 66 | // One kind of job changing is a report due. | |
| 67 | const last = { key: reportKey(report), at: 0 }; | |
| 68 | const running = reportOf(view({ ...sandboxed, diagnosis: { ...sandboxed.diagnosis, capabilities: [linux, { ...windows, tone: "ok", title: "Waiting for jobs", cause: null, fix: null }] } }))!; | |
| 69 | assert.equal(reportDue(last, running, 1000), true); | |
| 70 | // What the server keeps: only kinds of job that read, and none for a desktop without any. | |
| 71 | const kept = diagnosisOf({ ...report.diagnosis, capabilities: [...report.diagnosis.capabilities!, { id: "x", tone: "loud" }, "linux"] })!; | |
| 72 | assert.equal(kept.capabilities?.length, 2); | |
| 73 | assert.equal(diagnosisOf({ code: "idle", tone: "ok", title: "Waiting for jobs" })!.capabilities, undefined); | |
| 74 | assert.equal(reportOf(view())!.diagnosis.capabilities, undefined); | |
| 75 | }); | |
| 76 | ||
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 77 | test("each kind of job reports its state, whether the person stopped it, and its job, without what only works here", () => { |
| 78 | const linux = { | |
| 79 | id: "linux", | |
| 80 | label: "Linux jobs", | |
| 81 | tone: "working" as const, | |
| 82 | title: "Running a job", | |
| 83 | cause: null, | |
| 84 | fix: null, | |
| 85 | state: "busy" as const, | |
| 86 | about: "A Linux VM of its own, each job in a fresh container", | |
| 87 | enabled: true, | |
| 88 | job: { kind: "workflow", name: "build", repo: "acme/web", link: "https://g1t.sh/acme/web/actions" }, | |
| 89 | runner: "build-box-linux", | |
| 90 | isolation: "nested-container", | |
| 91 | version: "0.3.0", | |
| 92 | available: true, | |
| 93 | can_toggle: true, | |
| 94 | progress: null, | |
| 95 | actions: [], | |
| 96 | }; | |
| 97 | const windows = { | |
| 98 | id: "windows", | |
| 99 | label: "Windows jobs", | |
| 100 | tone: "off" as const, | |
| 101 | title: "Stopped", | |
| 102 | cause: "You stopped Windows jobs on this computer. They stay stopped until you start them.", | |
| 103 | fix: null, | |
| 104 | state: "stopped" as const, | |
| 105 | enabled: false, | |
| 106 | can_toggle: true, | |
| 107 | actions: [], | |
| 108 | }; | |
| 109 | const sandboxed = view({ | |
| 110 | backend: "sandboxed", | |
| 111 | diagnosis: { code: "busy", tone: "working", title: "Running build (acme/web)", cause: null, fix: null, actions: [], capabilities: [linux, windows], in_capabilities: true }, | |
| 112 | }); | |
| 113 | const report = reportOf(sandboxed)!; | |
| 114 | assert.equal(report.diagnosis.in_capabilities, true); | |
| 115 | assert.deepEqual(report.diagnosis.capabilities![0], { | |
| 116 | id: "linux", | |
| 117 | label: "Linux jobs", | |
| 118 | tone: "working", | |
| 119 | title: "Running a job", | |
| 120 | cause: null, | |
| 121 | fix: null, | |
| 122 | state: "busy", | |
| 123 | about: "A Linux VM of its own, each job in a fresh container", | |
| 124 | enabled: true, | |
| 125 | job: { kind: "workflow", name: "build", repo: "acme/web" }, | |
| 126 | runner: "build-box-linux", | |
| 127 | isolation: "nested-container", | |
| 128 | version: "0.3.0", | |
| 129 | }); | |
| 130 | assert.equal(report.diagnosis.capabilities![1].enabled, false); | |
| 131 | // Starting Windows jobs again is a report due. | |
| 132 | const last = { key: reportKey(report), at: 0 }; | |
| 133 | const started = reportOf(view({ ...sandboxed, diagnosis: { ...sandboxed.diagnosis, capabilities: [linux, { ...windows, enabled: true, state: "starting", title: "Starting", tone: "working" }] } }))!; | |
| 134 | assert.equal(reportDue(last, started, 1000), true); | |
| 135 | // What the server keeps reads back the same, and drops what isn't one of the app's. | |
| 136 | const kept = diagnosisOf(JSON.parse(JSON.stringify(report.diagnosis)))!; | |
| 137 | assert.deepEqual(kept, report.diagnosis); | |
| 138 | const odd = diagnosisOf({ ...report.diagnosis, capabilities: [{ ...report.diagnosis.capabilities![0], state: "dancing", enabled: "yes", job: { name: "x" } }] })!; | |
| 139 | assert.equal(odd.capabilities![0].state, undefined); | |
| 140 | assert.equal(odd.capabilities![0].enabled, undefined); | |
| 141 | assert.equal(odd.capabilities![0].job, undefined); | |
| 142 | // Only a diagnosis with kinds of job says they say it all. | |
| 143 | assert.equal(diagnosisOf({ code: "idle", tone: "ok", title: "Waiting for jobs", in_capabilities: true })!.in_capabilities, undefined); | |
| 144 | }); | |
| 145 | ||
| 146 | test("a kind of job's work links to its run when the runner says which", () => { | |
| 147 | const capability = { runner: "build-box-linux", job: { kind: "workflow", name: "build", repo: "acme/web" } }; | |
| 148 | const runners = [{ name: "build-box-linux", work: { kind: "workflow" as const, id: "job_1", name: "build", repo: "acme/web", runId: "run_9", startedAt: null } }]; | |
| 149 | assert.equal(jobHref(capability, runners), "/acme/web/actions/runs/run_9"); | |
| 150 | assert.equal(jobHref(capability, []), "/acme/web/actions"); | |
| The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404. | 151 | assert.equal(jobHref({ runner: null, job: { kind: "agent", name: "fix", repo: "acme/web" } }, runners), "/acme/web/pulls"); |
| Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page. | 152 | assert.equal(jobHref({ runner: "x", job: null }, runners), null); |
| 153 | }); | |
| 154 | ||
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 155 | test("only a diagnosis and a backend the app makes are kept", () => { |
| 156 | assert.deepEqual(diagnosisOf({ code: "idle", tone: "ok", title: "Waiting for jobs", cause: "Running sandboxed.", extra: 1 }), { | |
| 157 | code: "idle", | |
| 158 | tone: "ok", | |
| 159 | title: "Waiting for jobs", | |
| 160 | cause: "Running sandboxed.", | |
| 161 | fix: null, | |
| 162 | }); | |
| 163 | assert.equal(diagnosisOf({ code: "idle", tone: "loud", title: "x" }), null); | |
| 164 | assert.equal(diagnosisOf("idle"), null); | |
| 165 | assert.equal(backendOf("host"), "host"); | |
| 166 | assert.equal(backendOf("vm"), null); | |
| 167 | }); | |
| The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report. | 168 | |
| 169 | test("a report says how the runner's updates stand, without the app's own words and button", () => { | |
| 170 | const update = { | |
| 171 | checked_at: "2026-10-12T12:00:00Z", | |
| 172 | checked: "Checked 12 min ago", | |
| 173 | checking: false, | |
| 174 | ready: "0.3.2", | |
| 175 | note: null, | |
| 176 | check: { id: "check_updates", label: "Check for runner updates" }, | |
| 177 | }; | |
| 178 | const linux = { id: "linux", label: "Linux jobs", tone: "working" as const, title: "Running a job", cause: null, fix: null, actions: [], version: "0.3.1", update: "0.3.2 ready, installs after this job" }; | |
| 179 | const sandboxed = view({ backend: "sandboxed", diagnosis: { code: "busy", tone: "working", title: "Running build (acme/web)", cause: null, fix: null, actions: [], capabilities: [linux], in_capabilities: true, update } }); | |
| 180 | const report = reportOf(sandboxed)!; | |
| 181 | assert.deepEqual(report.diagnosis.update, { checked_at: "2026-10-12T12:00:00Z", ready: "0.3.2", note: null }); | |
| 182 | assert.equal(report.diagnosis.capabilities![0].update, "0.3.2 ready, installs after this job"); | |
| 183 | // A look that found something, or a newer one put in, is reported at once. | |
| 184 | const last = { key: reportKey(report), at: 0 }; | |
| 185 | const looked = reportOf(view({ ...sandboxed, diagnosis: { ...sandboxed.diagnosis, update: { ...update, checked_at: "2026-10-12T13:00:00Z" } } }))!; | |
| 186 | assert.equal(reportDue(last, looked, 1000), true); | |
| 187 | const installed = reportOf(view({ ...sandboxed, diagnosis: { ...sandboxed.diagnosis, capabilities: [{ ...linux, version: "0.3.2", update: undefined }] } }))!; | |
| 188 | assert.equal(reportDue(last, installed, 1000), true); | |
| 189 | // As the server reads it back: a time that isn't one is dropped, and an update that says nothing is none. | |
| 190 | const kept = diagnosisOf(JSON.parse(JSON.stringify(report.diagnosis)))!; | |
| 191 | assert.deepEqual(kept.update, report.diagnosis.update); | |
| 192 | assert.equal(kept.capabilities![0].update, "0.3.2 ready, installs after this job"); | |
| 193 | assert.deepEqual(diagnosisOf({ ...report.diagnosis, update: { checked_at: "soon", ready: "0.3.2" } })!.update, { checked_at: null, ready: "0.3.2", note: null }); | |
| 194 | assert.equal(diagnosisOf({ ...report.diagnosis, update: { checked_at: 5 } })!.update, undefined); | |
| 195 | // An app from before updates were reported says nothing of them. | |
| 196 | assert.equal(reportOf(view())!.diagnosis.update, undefined); | |
| 197 | }); |
This file's history is long; its oldest lines are credited to the oldest commit read.