Public: anyone can read this; the crew can change it.
The forge says why a rollout is blocked: /api/v1/health surfaces it and the board shows it #540
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/476-the-forge-says-why-a-rollout-is-blocked → main 5 commits fork
9 files
changed +304
−17
charts/app/templates/deployment.yaml +8
| @@ -125,6 +125,9 @@ spec: | ||
| 125 | 125 | - name: plat-ca |
| 126 | 126 | mountPath: /etc/plat-ca |
| 127 | 127 | readOnly: true |
| 128 | - name: forge-guard | |
| 129 | mountPath: /etc/forge-guard | |
| 130 | readOnly: true | |
| 128 | 131 | # `/` answers 200 for anyone; it is the landing page. Boot reconciles |
| 129 | 132 | # the index from git first, which grows with the number of repos. |
| 130 | 133 | startupProbe: |
| @@ -148,4 +151,9 @@ spec: | ||
| 148 | 151 | - name: plat-ca |
| 149 | 152 | secret: |
| 150 | 153 | secretName: plat-local-ca-tls |
| 154 | # The watchdog's view of the platform; absent is fine. | |
| 155 | - name: forge-guard | |
| 156 | configMap: | |
| 157 | name: forge-guard | |
| 158 | optional: true | |
| 151 | 159 | optional: true |
docs/ARCHITECTURE.md +8 −3
| @@ -514,9 +514,14 @@ spike #339) implements the same interface and must provide: | ||
| 514 | 514 | |
| 515 | 515 | ## The forge's standing |
| 516 | 516 | |
| 517 | `GET /api/v1/health` (`kernel/vitals.ts`) answers a site admin, by session or | |
| 518 | any token scope, with `{ ok, needs, disk, memory, queue, stuck, apps, at }`; | |
| 519 | everyone else gets a 404. `needs` is one sentence per thing off: disk under | |
| 517 | `GET /api/v1/health` answers anyone with `{ ok, version, draining, rollout: | |
| 518 | { state: ok|blocked|held, reason }, incidents, stale, checkedAt }` — the | |
| 519 | platform's word, read from the read-only `forge-guard` ConfigMap mounted at | |
| 520 | `FORGE_GUARD_DIR` (default `/etc/forge-guard`, `kernel/guard.ts`): a `stuck` | |
| 521 | incident blocks, `held` holds, a view older than ten minutes is stale. A | |
| 522 | site admin, by session or any token scope, also gets `{ needs, disk, memory, | |
| 523 | queue, stuck, apps, at }` (`kernel/vitals.ts`), and the board shows | |
| 524 | `rollout blocked: <reason>` in its status line. `needs` is one sentence per thing off: disk under | |
| 520 | 525 | 10% free on the `FORGE_DATA` volume, a run running with a lapsed lease (or |
| 521 | 526 | past six hours), a queue whose oldest run has waited over thirty minutes. |
| 522 | 527 | It is a fixed handful of queries however many runs and apps exist, measured |
forge.json +1 −1
| @@ -6,7 +6,7 @@ | ||
| 6 | 6 | "ready": 90 |
| 7 | 7 | }, |
| 8 | 8 | "check": { |
| 9 | "run": "bun x tsc --noEmit && bun scripts/test-shards.ts --timeout 60000 -- crew crew-wrapup crew-limits crew-sandbox steer landing decide ledger checks dependencies autonomy studio studio-words forks fork-flow cross-repo-pull pulls apps apps-adversarial apps-busy apps-token web git-trust helpers shards migrate vitals asks feed feed-page offline share cloud run-secrets forge-cloudflare usage standing board-windows spawn-plan spawn web-apps fleet-ops meter", | |
| 9 | "run": "bun x tsc --noEmit && bun scripts/test-shards.ts --timeout 60000 -- crew crew-wrapup crew-limits crew-sandbox steer landing decide ledger checks dependencies autonomy studio studio-words forks fork-flow cross-repo-pull pulls apps apps-adversarial apps-busy apps-token web git-trust helpers shards migrate vitals guard asks feed feed-page offline share cloud run-secrets forge-cloudflare usage standing board-windows spawn-plan spawn web-apps fleet-ops meter", | |
| 10 | 10 | "timeout": 1800 |
| 11 | 11 | } |
| 12 | 12 | } |
src/http/api.ts +8 −6
| @@ -8,6 +8,7 @@ | ||
| 8 | 8 | * back to continue); errors are `{ error, message }`. |
| 9 | 9 | */ |
| 10 | 10 | |
| 11 | import { forgeHealth } from "../kernel/guard.ts"; | |
| 11 | 12 | import { listSecrets, putSecret, removeSecret } from "../actions/secrets.ts"; |
| 12 | 13 | import { proposeUpstream } from "../actions/upstream.ts"; |
| 13 | 14 | import { relationOf, syncFork, takeUpdates } from "../actions/forks.ts"; |
| @@ -2752,17 +2753,18 @@ export function apiRoutes(router: Router<Handler>): void { | ||
| 2752 | 2753 | |
| 2753 | 2754 | // ---- apps, runs, and the crew (docs/APPS.md) ---- |
| 2754 | 2755 | |
| 2755 | // How the forge itself is: a site admin's to read, and nobody else's to | |
| 2756 | // know exists — a 404, never the numbers. | |
| 2756 | // How the forge is, and the platform's word on its rollout: anyone may | |
| 2757 | // read it (a monitor has no account). A site admin also gets the forge's | |
| 2758 | // own vitals alongside, `ok` being both. | |
| 2757 | 2759 | router.add("GET", "/api/v1/health", (ctx) => { |
| 2758 | if (!ctx.auth.user?.isAdmin) return notFound(); | |
| 2760 | const forge = forgeHealth(); | |
| 2761 | if (!ctx.auth.user?.isAdmin) return json(forge); | |
| 2759 | 2762 | const runtime = appsRuntime(ctx.app); |
| 2760 | 2763 | const envs = runtime |
| 2761 | 2764 | ? listApps(ctx.app.db).flatMap((record) => runtime.status(record)) |
| 2762 | 2765 | : []; |
| 2763 | return json( | |
| 2764 | vitals({ db: ctx.app.db, dataDir: ctx.app.config.dataDir, envs }), | |
| 2765 | ); | |
| 2766 | const v = vitals({ db: ctx.app.db, dataDir: ctx.app.config.dataDir, envs }); | |
| 2767 | return json({ ...v, ...forge, ok: forge.ok && v.ok }); | |
| 2766 | 2768 | }); |
| 2767 | 2769 | |
| 2768 | 2770 | router.add("GET", "/api/v1/apps", (ctx) => { |
src/kernel/guard.ts added +131
| @@ -0,0 +1,131 @@ | ||
| 1 | /** | |
| 2 | * The platform's view of the forge, as the droplet's watchdog leaves it: a | |
| 3 | * ConfigMap (`forge-guard`) mounted read-only, no Kubernetes API needed. | |
| 4 | * `status.json` is `{ incidents: {key: message}, public, service }` and | |
| 5 | * `checkedAt` is an ISO time. Keys: `down`, `edge`, `stuck` (a rollout | |
| 6 | * cannot finish), `held` (the reconciler's preflight refused a release), | |
| 7 | * `oom:*`. Missing or empty means nothing is wrong; a view older than ten | |
| 8 | * minutes is said to be stale, never "all fine". | |
| 9 | */ | |
| 10 | ||
| 11 | import { readFileSync } from "node:fs"; | |
| 12 | import { join } from "node:path"; | |
| 13 | import pkg from "../../package.json" with { type: "json" }; | |
| 14 | import { health } from "../http/health.ts"; | |
| 15 | ||
| 16 | export const GUARD_STALE_MS = 10 * 60_000; | |
| 17 | const MAX_INCIDENTS = 50; | |
| 18 | const MAX_MESSAGE = 800; | |
| 19 | ||
| 20 | export interface Incident { | |
| 21 | readonly key: string; | |
| 22 | readonly message: string; | |
| 23 | } | |
| 24 | ||
| 25 | export interface Guard { | |
| 26 | readonly incidents: Incident[]; | |
| 27 | readonly checkedAt: string | null; | |
| 28 | } | |
| 29 | ||
| 30 | export interface Rollout { | |
| 31 | readonly state: "ok" | "blocked" | "held"; | |
| 32 | readonly reason: string; | |
| 33 | } | |
| 34 | ||
| 35 | export interface ForgeHealth { | |
| 36 | readonly ok: boolean; | |
| 37 | readonly version: string; | |
| 38 | readonly draining: boolean; | |
| 39 | readonly rollout: Rollout; | |
| 40 | readonly incidents: Incident[]; | |
| 41 | readonly stale: boolean; | |
| 42 | readonly checkedAt: string | null; | |
| 43 | } | |
| 44 | ||
| 45 | /** Where the ConfigMap is mounted. */ | |
| 46 | export function guardDir(): string { | |
| 47 | return process.env["FORGE_GUARD_DIR"] || "/etc/forge-guard"; | |
| 48 | } | |
| 49 | ||
| 50 | function readText(path: string): string | null { | |
| 51 | try { | |
| 52 | return readFileSync(path, "utf8"); | |
| 53 | } catch { | |
| 54 | return null; | |
| 55 | } | |
| 56 | } | |
| 57 | ||
| 58 | /** Parse the two keys; anything malformed is treated as absent. */ | |
| 59 | export function parseGuard( | |
| 60 | statusJson: string | null, | |
| 61 | checkedAtText: string | null, | |
| 62 | ): Guard { | |
| 63 | const incidents: Incident[] = []; | |
| 64 | if (statusJson) { | |
| 65 | try { | |
| 66 | const raw = (JSON.parse(statusJson) as { incidents?: unknown }) | |
| 67 | .incidents; | |
| 68 | if (raw && typeof raw === "object" && !Array.isArray(raw)) { | |
| 69 | for (const [key, message] of Object.entries(raw)) { | |
| 70 | if (incidents.length >= MAX_INCIDENTS) break; | |
| 71 | if (!/^[a-z][a-z0-9:._-]{0,63}$/i.test(key)) continue; | |
| 72 | if (typeof message !== "string") continue; | |
| 73 | incidents.push({ key, message: message.trim().slice(0, MAX_MESSAGE) }); | |
| 74 | } | |
| 75 | } | |
| 76 | } catch { | |
| 77 | // a half-written file reads as nothing | |
| 78 | } | |
| 79 | } | |
| 80 | const at = checkedAtText?.trim() ?? ""; | |
| 81 | const checkedAt = | |
| 82 | at && Number.isFinite(Date.parse(at)) ? new Date(at).toISOString() : null; | |
| 83 | return { incidents, checkedAt }; | |
| 84 | } | |
| 85 | ||
| 86 | export function readGuard(dir: string = guardDir()): Guard { | |
| 87 | return parseGuard( | |
| 88 | readText(join(dir, "status.json")), | |
| 89 | readText(join(dir, "checkedAt")), | |
| 90 | ); | |
| 91 | } | |
| 92 | ||
| 93 | /** The forge's view plus the platform's, as plain data. */ | |
| 94 | export function forgeHealth( | |
| 95 | guard: Guard = readGuard(), | |
| 96 | now: number = Date.now(), | |
| 97 | ): ForgeHealth { | |
| 98 | const stale = | |
| 99 | guard.checkedAt !== null && now - Date.parse(guard.checkedAt) > GUARD_STALE_MS; | |
| 100 | const find = (key: string) => guard.incidents.find((i) => i.key === key); | |
| 101 | const stuck = find("stuck"); | |
| 102 | const held = find("held"); | |
| 103 | const rollout: Rollout = stuck | |
| 104 | ? { state: "blocked", reason: stuck.message } | |
| 105 | : held | |
| 106 | ? { state: "held", reason: held.message } | |
| 107 | : stale | |
| 108 | ? { | |
| 109 | state: "ok", | |
| 110 | reason: "the platform's view is stale: its last check was " + | |
| 111 | `${guard.checkedAt}`, | |
| 112 | } | |
| 113 | : { state: "ok", reason: "" }; | |
| 114 | return { | |
| 115 | ok: | |
| 116 | !health.draining && !stale && guard.incidents.length === 0, | |
| 117 | version: pkg.version, | |
| 118 | draining: health.draining, | |
| 119 | rollout, | |
| 120 | incidents: guard.incidents, | |
| 121 | stale, | |
| 122 | checkedAt: guard.checkedAt, | |
| 123 | }; | |
| 124 | } | |
| 125 | ||
| 126 | /** The status-line words for a rollout that is not simply fine, else null. */ | |
| 127 | export function rolloutLine(h: ForgeHealth): string | null { | |
| 128 | if (h.rollout.state === "blocked") return `rollout blocked: ${h.rollout.reason}`; | |
| 129 | if (h.rollout.state === "held") return `rollout held: ${h.rollout.reason}`; | |
| 130 | return h.stale ? h.rollout.reason : null; | |
| 131 | } |
src/web/pages/board.ts +12 −2
| @@ -24,6 +24,7 @@ import { efficiencyForRepos } from "../../core/efficiency.ts"; | ||
| 24 | 24 | import { standings, WINDOW_MS } from "../../core/standing.ts"; |
| 25 | 25 | import { listSubscriptions } from "../../core/subscriptions.ts"; |
| 26 | 26 | import { listApps } from "../../core/apps.ts"; |
| 27 | import { forgeHealth, rolloutLine } from "../../kernel/guard.ts"; | |
| 27 | 28 | import { vitals, type Vitals } from "../../kernel/vitals.ts"; |
| 28 | 29 | import { getIssue, getIssuesByIds, type Issue } from "../../core/issues.ts"; |
| 29 | 30 | import { looksForApps } from "../../core/looks.ts"; |
| @@ -270,15 +271,24 @@ function forgeStrip(ctx: Ctx): Html | null { | ||
| 270 | 271 | ? listApps(app.db).flatMap((record) => runtime.status(record)) |
| 271 | 272 | : [], |
| 272 | 273 | }); |
| 274 | const fh = forgeHealth(); | |
| 275 | const rollout = rolloutLine(fh); | |
| 276 | const others = fh.incidents.filter( | |
| 277 | (i) => i.key !== "stuck" && i.key !== "held", | |
| 278 | ); | |
| 279 | const ok = v.ok && fh.ok; | |
| 273 | 280 | return html`<div |
| 274 | 281 | class="board-forge" |
| 275 | 282 | id="forge" |
| 276 | 283 | data-live="run.*,app.*" |
| 277 | data-ok="${v.ok ? "yes" : "no"}" | |
| 284 | data-ok="${ok ? "yes" : "no"}" | |
| 285 | data-rollout="${fh.rollout.state}" | |
| 278 | 286 | role="status" |
| 279 | 287 | > |
| 288 | ${rollout === null ? "" : html`<span class="board-forge-need">${rollout}</span>`} | |
| 289 | ${others.map((i) => html`<span class="board-forge-need">${i.key}: ${i.message}</span>`)} | |
| 280 | 290 | ${ |
| 281 | v.ok | |
| 291 | ok | |
| 282 | 292 | ? html`<span class="board-forge-ok">All fine</span>` |
| 283 | 293 | : v.needs.map((need) => { |
| 284 | 294 | const href = needHref(need, v); |
test/board.test.ts +21 −1
| @@ -5,6 +5,7 @@ | ||
| 5 | 5 | * anonymous is sent to sign in. |
| 6 | 6 | */ |
| 7 | 7 | import { afterAll, beforeAll, describe, expect, test } from "bun:test"; |
| 8 | import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; | |
| 8 | 9 | import { mkdtemp, rm } from "node:fs/promises"; |
| 9 | 10 | import { tmpdir } from "node:os"; |
| 10 | 11 | import { join } from "node:path"; |
| @@ -371,7 +372,7 @@ describe("the board's forge strip", () => { | ||
| 371 | 372 | if (username === "root") admin = u.value; |
| 372 | 373 | cookies[username] = await signIn(username); |
| 373 | 374 | } |
| 374 | }); | |
| 375 | }, 30_000); | |
| 375 | 376 | |
| 376 | 377 | afterAll(async () => { |
| 377 | 378 | servers.stop(); |
| @@ -402,4 +403,23 @@ describe("the board's forge strip", () => { | ||
| 402 | 403 | expect(other).not.toContain("All fine"); |
| 403 | 404 | expect(other).not.toContain("stuck."); |
| 404 | 405 | }); |
| 406 | ||
| 407 | test("a blocked rollout shows in the status line, escaped, for an admin only", async () => { | |
| 408 | const guard = mkdtempSync(join(tmpdir(), "forge-board-guard-")); | |
| 409 | process.env["FORGE_GUARD_DIR"] = guard; | |
| 410 | try { | |
| 411 | writeFileSync( | |
| 412 | join(guard, "status.json"), | |
| 413 | JSON.stringify({ incidents: { stuck: "quota refused <i>8Gi</i>" } }), | |
| 414 | ); | |
| 415 | writeFileSync(join(guard, "checkedAt"), new Date().toISOString()); | |
| 416 | const blocked = await board("root"); | |
| 417 | expect(blocked).toContain("rollout blocked: quota refused <i>8Gi</i>"); | |
| 418 | expect(blocked).not.toContain("<i>8Gi</i>"); | |
| 419 | expect(await board("bob")).not.toContain("rollout blocked"); | |
| 420 | } finally { | |
| 421 | delete process.env["FORGE_GUARD_DIR"]; | |
| 422 | rmSync(guard, { recursive: true, force: true }); | |
| 423 | } | |
| 424 | }); | |
| 405 | 425 | }); |
test/guard.test.ts added +102
| @@ -0,0 +1,102 @@ | ||
| 1 | /** The platform's word on the rollout: ok, blocked, held — and stale. */ | |
| 2 | ||
| 3 | import { afterAll, beforeAll, expect, test } from "bun:test"; | |
| 4 | import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; | |
| 5 | import { mkdtemp, rm } from "node:fs/promises"; | |
| 6 | import { tmpdir } from "node:os"; | |
| 7 | import { join } from "node:path"; | |
| 8 | import { createApp, type App } from "../src/app.ts"; | |
| 9 | import { serve, type Servers } from "../src/http/server.ts"; | |
| 10 | import { | |
| 11 | forgeHealth, | |
| 12 | parseGuard, | |
| 13 | readGuard, | |
| 14 | rolloutLine, | |
| 15 | } from "../src/kernel/guard.ts"; | |
| 16 | ||
| 17 | const NOW = Date.parse("2026-10-02T12:00:00Z"); | |
| 18 | const fresh = "2026-10-02T11:59:00Z"; | |
| 19 | const status = (incidents: Record<string, string>) => | |
| 20 | JSON.stringify({ incidents, public: 200, service: 200 }); | |
| 21 | ||
| 22 | test("no ConfigMap, or an empty one, reports ok", () => { | |
| 23 | const none = forgeHealth(readGuard("/nonexistent/forge-guard"), NOW); | |
| 24 | expect(none.ok).toBe(true); | |
| 25 | expect(none.rollout).toEqual({ state: "ok", reason: "" }); | |
| 26 | expect(none.checkedAt).toBeNull(); | |
| 27 | const empty = forgeHealth(parseGuard(status({}), fresh), NOW); | |
| 28 | expect(empty.ok).toBe(true); | |
| 29 | expect(empty.incidents).toEqual([]); | |
| 30 | expect(rolloutLine(empty)).toBeNull(); | |
| 31 | }); | |
| 32 | ||
| 33 | test("a stuck incident blocks the rollout with its reason", () => { | |
| 34 | const reason = "pod refused: exceeded quota; raise requests.memory to 10Gi"; | |
| 35 | const h = forgeHealth(parseGuard(status({ stuck: reason }), fresh), NOW); | |
| 36 | expect(h.rollout).toEqual({ state: "blocked", reason }); | |
| 37 | expect(h.ok).toBe(false); | |
| 38 | expect(rolloutLine(h)).toBe(`rollout blocked: ${reason}`); | |
| 39 | }); | |
| 40 | ||
| 41 | test("a held incident holds the rollout", () => { | |
| 42 | const h = forgeHealth( | |
| 43 | parseGuard(status({ held: "preflight: no room on node" }), fresh), | |
| 44 | NOW, | |
| 45 | ); | |
| 46 | expect(h.rollout).toEqual({ state: "held", reason: "preflight: no room on node" }); | |
| 47 | expect(rolloutLine(h)).toBe("rollout held: preflight: no room on node"); | |
| 48 | }); | |
| 49 | ||
| 50 | test("a stale view says so instead of all fine", () => { | |
| 51 | const h = forgeHealth(parseGuard(status({}), "2026-10-02T11:40:00Z"), NOW); | |
| 52 | expect(h.stale).toBe(true); | |
| 53 | expect(h.ok).toBe(false); | |
| 54 | expect(h.rollout.reason).toContain("stale"); | |
| 55 | expect(rolloutLine(h)).toContain("stale"); | |
| 56 | }); | |
| 57 | ||
| 58 | test("malformed input reads as nothing, and odd keys are dropped", () => { | |
| 59 | expect(parseGuard("{not json", "nonsense")).toEqual({ incidents: [], checkedAt: null }); | |
| 60 | const g = parseGuard(status({ "bad key!": "x", "oom:web": "killed", n: 5 as never }), fresh); | |
| 61 | expect(g.incidents).toEqual([{ key: "oom:web", message: "killed" }]); | |
| 62 | }); | |
| 63 | ||
| 64 | let dir: string; | |
| 65 | let guardDir: string; | |
| 66 | let app: App; | |
| 67 | let servers: Servers; | |
| 68 | ||
| 69 | beforeAll(async () => { | |
| 70 | dir = await mkdtemp(join(tmpdir(), "forge-guard-")); | |
| 71 | guardDir = mkdtempSync(join(tmpdir(), "forge-guard-cm-")); | |
| 72 | process.env["FORGE_GUARD_DIR"] = guardDir; | |
| 73 | app = createApp({ dataDir: dir, port: 0 }); | |
| 74 | servers = await serve(app); | |
| 75 | }); | |
| 76 | ||
| 77 | afterAll(async () => { | |
| 78 | delete process.env["FORGE_GUARD_DIR"]; | |
| 79 | servers.stop(); | |
| 80 | app.close(); | |
| 81 | await rm(dir, { recursive: true, force: true }); | |
| 82 | rmSync(guardDir, { recursive: true, force: true }); | |
| 83 | }); | |
| 84 | ||
| 85 | test("/api/v1/health answers anyone, from the mounted ConfigMap", async () => { | |
| 86 | let res = await fetch(servers.url + "/api/v1/health"); | |
| 87 | expect(res.status).toBe(200); | |
| 88 | let body = (await res.json()) as any; | |
| 89 | expect(body.ok).toBe(true); | |
| 90 | expect(body.rollout.state).toBe("ok"); | |
| 91 | expect(body.draining).toBe(false); | |
| 92 | expect(body.version).toMatch(/^\d+\.\d+\.\d+/); | |
| 93 | ||
| 94 | mkdirSync(guardDir, { recursive: true }); | |
| 95 | writeFileSync(join(guardDir, "status.json"), status({ stuck: "quota refused <b>" })); | |
| 96 | writeFileSync(join(guardDir, "checkedAt"), new Date().toISOString()); | |
| 97 | res = await fetch(servers.url + "/api/v1/health"); | |
| 98 | body = await res.json(); | |
| 99 | expect(body.rollout).toEqual({ state: "blocked", reason: "quota refused <b>" }); | |
| 100 | expect(body.ok).toBe(false); | |
| 101 | expect(body.incidents).toEqual([{ key: "stuck", message: "quota refused <b>" }]); | |
| 102 | }); |
test/vitals.test.ts +13 −4
| @@ -73,18 +73,27 @@ afterAll(async () => { | ||
| 73 | 73 | await rm(dataDir, { recursive: true, force: true }); |
| 74 | 74 | }); |
| 75 | 75 | |
| 76 | test("a site admin with a read-only token gets the shape; others get a 404", async () => { | |
| 76 | test("a site admin with a read-only token gets the shape; others get the short one", async () => { | |
| 77 | 77 | const res = await health(admin); |
| 78 | 78 | expect(res.status).toBe(200); |
| 79 | 79 | const v = (await res.json()) as Record<string, any>; |
| 80 | 80 | expect(Object.keys(v).sort()).toEqual( |
| 81 | ["apps", "at", "disk", "errors", "memory", "needs", "ok", "queue", "stuck"].sort(), | |
| 81 | [ | |
| 82 | "apps", "at", "checkedAt", "disk", "draining", "errors", "incidents", | |
| 83 | "memory", "needs", "ok", "queue", "rollout", "stale", "stuck", "version", | |
| 84 | ].sort(), | |
| 82 | 85 | ); |
| 83 | 86 | expect(v.disk.totalBytes).toBeGreaterThan(0); |
| 84 | 87 | expect(v.memory.rssBytes).toBeGreaterThan(0); |
| 85 | 88 | expect(v.apps).toEqual({ serving: 0, asleep: 0, previews: 0 }); |
| 86 | expect((await health(plain)).status).toBe(404); | |
| 87 | expect((await health()).status).toBe(404); | |
| 89 | // Everyone else gets the platform's short shape, never the numbers. | |
| 90 | for (const token of [plain, undefined]) { | |
| 91 | const res = await health(token); | |
| 92 | expect(res.status).toBe(200); | |
| 93 | const short = (await res.json()) as Record<string, unknown>; | |
| 94 | expect(short["disk"]).toBeUndefined(); | |
| 95 | expect(short["rollout"]).toBeDefined(); | |
| 96 | } | |
| 88 | 97 | expect((await fetch(servers.url + "/healthz")).status).toBe(200); |
| 89 | 98 | }); |
| 90 | 99 |
new above
offline