@appforge-ci/core 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/dist/agent-readiness.d.ts +163 -0
  3. package/dist/agent-readiness.d.ts.map +1 -0
  4. package/dist/agent-readiness.js +179 -0
  5. package/dist/agent-readiness.js.map +1 -0
  6. package/dist/agent-readiness.test.d.ts +2 -0
  7. package/dist/agent-readiness.test.d.ts.map +1 -0
  8. package/dist/agent-readiness.test.js +177 -0
  9. package/dist/agent-readiness.test.js.map +1 -0
  10. package/dist/api-client.d.ts +529 -0
  11. package/dist/api-client.d.ts.map +1 -0
  12. package/dist/api-client.js +551 -0
  13. package/dist/api-client.js.map +1 -0
  14. package/dist/api-client.test.d.ts +2 -0
  15. package/dist/api-client.test.d.ts.map +1 -0
  16. package/dist/api-client.test.js +305 -0
  17. package/dist/api-client.test.js.map +1 -0
  18. package/dist/build-cost.test.d.ts +2 -0
  19. package/dist/build-cost.test.d.ts.map +1 -0
  20. package/dist/build-cost.test.js +21 -0
  21. package/dist/build-cost.test.js.map +1 -0
  22. package/dist/index.d.ts +8 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +8 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/ota-artifact.d.ts +41 -0
  27. package/dist/ota-artifact.d.ts.map +1 -0
  28. package/dist/ota-artifact.js +43 -0
  29. package/dist/ota-artifact.js.map +1 -0
  30. package/dist/ota-artifact.test.d.ts +2 -0
  31. package/dist/ota-artifact.test.d.ts.map +1 -0
  32. package/dist/ota-artifact.test.js +44 -0
  33. package/dist/ota-artifact.test.js.map +1 -0
  34. package/dist/ota-bundle-hash.d.ts +38 -0
  35. package/dist/ota-bundle-hash.d.ts.map +1 -0
  36. package/dist/ota-bundle-hash.js +40 -0
  37. package/dist/ota-bundle-hash.js.map +1 -0
  38. package/dist/ota-bundle-hash.test.d.ts +2 -0
  39. package/dist/ota-bundle-hash.test.d.ts.map +1 -0
  40. package/dist/ota-bundle-hash.test.js +28 -0
  41. package/dist/ota-bundle-hash.test.js.map +1 -0
  42. package/dist/ota-rollout.d.ts +38 -0
  43. package/dist/ota-rollout.d.ts.map +1 -0
  44. package/dist/ota-rollout.js +53 -0
  45. package/dist/ota-rollout.js.map +1 -0
  46. package/dist/ota-rollout.test.d.ts +2 -0
  47. package/dist/ota-rollout.test.d.ts.map +1 -0
  48. package/dist/ota-rollout.test.js +48 -0
  49. package/dist/ota-rollout.test.js.map +1 -0
  50. package/dist/schemas.d.ts +776 -0
  51. package/dist/schemas.d.ts.map +1 -0
  52. package/dist/schemas.js +538 -0
  53. package/dist/schemas.js.map +1 -0
  54. package/dist/token-packs.test.d.ts +2 -0
  55. package/dist/token-packs.test.d.ts.map +1 -0
  56. package/dist/token-packs.test.js +34 -0
  57. package/dist/token-packs.test.js.map +1 -0
  58. package/dist/types.d.ts +445 -0
  59. package/dist/types.d.ts.map +1 -0
  60. package/dist/types.js +80 -0
  61. package/dist/types.js.map +1 -0
  62. package/package.json +35 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AppForge CI contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,163 @@
1
+ import type { AgentInfo, Platform } from "./types.js";
2
+ /**
3
+ * The k8s-`Node`-style "Ready" condition for a Mac mini: not a raw health
4
+ * heartbeat (that's `status`), but whether its *toolchains* are complete
5
+ * enough to actually run a build for at least one native platform.
6
+ * Flutter apps still build through the Android or iOS toolchain
7
+ * underneath, so the Flutter SDK alone doesn't make a mini ready — it
8
+ * needs Flutter *and* one complete platform toolchain.
9
+ *
10
+ * Single source of truth on purpose: `appforge-admin agents add-mac`'s
11
+ * own preview, `agents list`'s READY column, and the dashboard's Fleet
12
+ * page all import this instead of each re-deriving their own notion of
13
+ * "ready" from the same five columns and silently drifting apart.
14
+ */
15
+ export interface AgentReadiness {
16
+ androidReady: boolean;
17
+ iosReady: boolean;
18
+ /** Flutter SDK present *and* at least one native platform toolchain
19
+ * complete — see this module's docblock. Flutter alone doesn't build
20
+ * anything; it still compiles through the Android or iOS toolchain
21
+ * underneath. */
22
+ flutterReady: boolean;
23
+ /** online/building and not stale — the health half of "Ready", independent
24
+ * of which toolchains are installed. `agentFitsPlatform` below gates on
25
+ * this too: a draining mini with a complete iOS toolchain still isn't
26
+ * fit to take an iOS build. */
27
+ healthy: boolean;
28
+ ready: boolean;
29
+ }
30
+ /**
31
+ * A mini's `status` column is only ever written as a side effect of an
32
+ * inbound heartbeat (see updateAgentHeartbeat in packages/db/src/agents.ts)
33
+ * — nothing ever rewrites it afterwards. So a mini that goes dark (network
34
+ * cut, process killed, lid closed) freezes forever at whatever it last
35
+ * reported instead of ever becoming "offline". Rather than add a cron
36
+ * sweep to rewrite the DB on a timer, staleness is computed on the fly from
37
+ * `lastSeenAt` every time status is read, so no extra moving part is needed
38
+ * and every reader (dashboard poll, CLI, API) sees a consistent, live
39
+ * answer. The agent heartbeats every 15s (HEARTBEAT_INTERVAL_MS in
40
+ * packages/agent/src/index.ts); this is a generous multiple of that so
41
+ * normal network jitter or a slow tick doesn't flap the badge, while a
42
+ * genuinely disconnected mini still reads as offline within about a
43
+ * minute instead of showing a frozen snapshot indefinitely.
44
+ */
45
+ export declare const AGENT_STALE_AFTER_MS = 60000;
46
+ /**
47
+ * D1/SQLite's `datetime('now')` (what every `*At` column in this schema
48
+ * is actually stamped with — see `updateAgentHeartbeat`,
49
+ * `createEnrollmentToken`, every `queuedAt`/`createdAt`/`updatedAt`
50
+ * column) returns UTC as "YYYY-MM-DD HH:MM:SS" — no "T", no "Z", no
51
+ * offset. `new Date(...)` on a non-ISO, space-separated string like that
52
+ * is parsed as *local time*, not UTC, so anywhere east of Greenwich a
53
+ * timestamp that just landed reads as hours in the past (and anywhere
54
+ * west, hours in the future — the date itself can flip near midnight).
55
+ * Force it to parse as UTC by reshaping to real ISO-8601 first; leave an
56
+ * already-ISO string (has a "T", e.g. anything built in JS via
57
+ * `.toISOString()`) alone.
58
+ *
59
+ * Exported for exactly this reason beyond this file's own use in
60
+ * `isAgentStale`: any other reader of a raw D1 timestamp column — the
61
+ * dashboard's own display code included — needs the identical fix, not
62
+ * a second, easy-to-forget reimplementation of it.
63
+ */
64
+ export declare function parseUtc(timestamp: string): Date;
65
+ export declare function isAgentStale(lastSeenAt: string, now?: Date): boolean;
66
+ /**
67
+ * The status actually worth displaying or scheduling against — the raw DB
68
+ * `status` value, overridden to "offline" once the mini has gone stale.
69
+ * Every place that shows or gates on an agent's status should read this
70
+ * instead of `agent.status` directly.
71
+ */
72
+ export declare function effectiveAgentStatus(agent: Pick<AgentInfo, "status" | "lastSeenAt">, now?: Date): AgentInfo["status"];
73
+ export declare function computeAgentReadiness(agent: Pick<AgentInfo, "xcodeVersion" | "javaVersion" | "androidSdkVersion" | "cocoapodsVersion" | "flutterVersion" | "status" | "lastSeenAt">, now?: Date): AgentReadiness;
74
+ /**
75
+ * The k8s-nodeSelector-style scheduling predicate: whether an already-ready
76
+ * mini is fit for a *specific* app `Platform`, not just "healthy" in the
77
+ * abstract. Mirrors a k8s scheduler's predicate phase filtering candidate
78
+ * Nodes down to the ones a Pod's spec actually fits before anything gets
79
+ * bound — a build queued for `platform: ios` should never land on an
80
+ * Android-only mini and fail two minutes in because `pod install` has no
81
+ * Xcode to talk to.
82
+ *
83
+ * `react-native` has no dedicated readiness flag (it needs a native
84
+ * toolchain but never the Flutter SDK), so — like `flutterReady`'s own
85
+ * "Flutter + one native toolchain" rule — either native platform being
86
+ * ready satisfies it.
87
+ */
88
+ export declare function agentFitsPlatform(readiness: AgentReadiness, platform: Platform): boolean;
89
+ /**
90
+ * The single predicate the scheduler (`apps/admin-api/src/routes/
91
+ * agents.ts`'s `/agents/lease-job` route, filtering `@appforge/db`'s
92
+ * `listQueuedBuildCandidates` before trying `claimBuildById` on each)
93
+ * filters queued build candidates against when an agent asks for work:
94
+ * healthy *and* toolchain-fit for this specific build's platform. An
95
+ * agent that fails this for every queued build just stays idle and
96
+ * long-polls again — the same as a Pod sitting `Pending` with a
97
+ * `FailedScheduling` event when no Node satisfies its selector, rather than
98
+ * getting bound somewhere it can't actually run.
99
+ */
100
+ export declare function agentCanBuildPlatform(agent: Parameters<typeof computeAgentReadiness>[0], platform: Platform, now?: Date): boolean;
101
+ /**
102
+ * The currently published packages/agent build — bump this in lockstep
103
+ * with packages/agent/package.json's own "version" field (and
104
+ * packages/agent/src/lib/version.ts's AGENT_VERSION, which is what the
105
+ * agent actually reports; version.test.ts checks those two never drift
106
+ * apart) whenever a new agent build ships. Compared against each fleet
107
+ * row's self-reported `AgentInfo.agentVersion` to flag a mini that needs
108
+ * updating — see ROADMAP.md's "OTA / self-update for the runner fleet"
109
+ * entry, Phase 1. Also this is Phase 3's own "desired fleet version":
110
+ * `shouldAutoUpdateAgent` below compares every heartbeat's reported
111
+ * version against this same constant to decide whether to auto-issue an
112
+ * `UPDATE_AGENT` command (Phase 2's remote command, reused unchanged).
113
+ */
114
+ export declare const CURRENT_AGENT_VERSION = "0.1.1";
115
+ /**
116
+ * Whether a fleet member is running the latest known agent build.
117
+ * `null` — deliberately not "outdated" — for a row with no reported
118
+ * version at all (never (re-)enrolled or heartbeated since
119
+ * migrations/0011_agent_version.sql added the column): there's nothing to
120
+ * compare, and calling an unknown version "outdated" would be a guess a
121
+ * maintainer could act on wrongly.
122
+ */
123
+ export declare function isAgentVersionCurrent(agentVersion: string | null): boolean | null;
124
+ /**
125
+ * The fleet's auto-update policy — fleet-wide, default-on (no per-mini
126
+ * opt-in): every heartbeat is a chance to bring an outdated mini current,
127
+ * with no separate cron/scheduled-worker infra needed, since the agent
128
+ * already calls in every ~15s regardless. See
129
+ * `apps/admin-api/src/routes/agents.ts`'s `/agents/heartbeat` handler,
130
+ * which calls this on every heartbeat to decide whether to auto-issue a
131
+ * fresh `UPDATE_AGENT` command (reusing Phase 2's existing drain-safe
132
+ * self-update flow unchanged — this only decides *whether* to enqueue
133
+ * one, never how the update itself runs).
134
+ *
135
+ * Two guards, both required:
136
+ * - Only a *known* outdated version — same "don't guess" restraint as
137
+ * `isAgentVersionCurrent` itself (a row with no reported version yet,
138
+ * or one already current, is never auto-updated).
139
+ * - Never while an `UPDATE_AGENT` command is already pending or acked
140
+ * for this agent, so a mini that's mid-drain/mid-self-update from an
141
+ * earlier heartbeat doesn't get a second one queued up behind the
142
+ * first on every subsequent heartbeat until the command resolves.
143
+ *
144
+ * The operator's control lever is exactly the existing one: `CURRENT_
145
+ * AGENT_VERSION` itself. A bad release simply isn't bumped to "current"
146
+ * until it's trusted, the same discipline manual `agents update --all`
147
+ * already required — no separate kill switch needed on top of that.
148
+ */
149
+ export declare function shouldAutoUpdateAgent(effectiveAgentVersion: string | null, hasActiveUpdateCommand: boolean): boolean;
150
+ /**
151
+ * `AgentInfo.activeBuildIds` is passed straight through from the D1 row
152
+ * as raw JSON text (same "not re-serialized" choice as `screenSharingEnabled`
153
+ * — see that field's docblock), so every reader (the dashboard's Fleet
154
+ * page, `appforge-admin agents list`) needs to parse it the same way
155
+ * rather than each re-deriving its own notion of "what does this string
156
+ * actually mean." Falls back to empty on anything unexpected — a
157
+ * genuinely malformed value, or a row from before migrations/0017 added
158
+ * the column (though that migration backfills existing rows with `'[]'`,
159
+ * so this is really just defense in depth) — rather than ever throwing
160
+ * mid-render over one agent's stale/unexpected value.
161
+ */
162
+ export declare function parseActiveBuildIds(raw: string): string[];
163
+ //# sourceMappingURL=agent-readiness.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-readiness.d.ts","sourceRoot":"","sources":["../src/agent-readiness.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtD;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,cAAc;IAC7B,YAAY,EAAE,OAAO,CAAC;IACtB,QAAQ,EAAE,OAAO,CAAC;IAClB;;;sBAGkB;IAClB,YAAY,EAAE,OAAO,CAAC;IACtB;;;oCAGgC;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,oBAAoB,QAAS,CAAC;AAE3C;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAEhD;AAED,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,GAAE,IAAiB,GAAG,OAAO,CAEhF;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,IAAI,CAAC,SAAS,EAAE,QAAQ,GAAG,YAAY,CAAC,EAC/C,GAAG,GAAE,IAAiB,GACrB,SAAS,CAAC,QAAQ,CAAC,CAErB;AAED,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,IAAI,CACT,SAAS,EACT,cAAc,GAAG,aAAa,GAAG,mBAAmB,GAAG,kBAAkB,GAAG,gBAAgB,GAAG,QAAQ,GAAG,YAAY,CACvH,EACD,GAAG,GAAE,IAAiB,GACrB,cAAc,CAUhB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,cAAc,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAYxF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,UAAU,CAAC,OAAO,qBAAqB,CAAC,CAAC,CAAC,CAAC,EAClD,QAAQ,EAAE,QAAQ,EAClB,GAAG,GAAE,IAAiB,GACrB,OAAO,CAET;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,qBAAqB,UAAU,CAAC;AAE7C;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,GAAG,IAAI,CAGjF;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,qBAAqB,CAAC,qBAAqB,EAAE,MAAM,GAAG,IAAI,EAAE,sBAAsB,EAAE,OAAO,GAAG,OAAO,CAEpH;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAOzD"}
@@ -0,0 +1,179 @@
1
+ /**
2
+ * A mini's `status` column is only ever written as a side effect of an
3
+ * inbound heartbeat (see updateAgentHeartbeat in packages/db/src/agents.ts)
4
+ * — nothing ever rewrites it afterwards. So a mini that goes dark (network
5
+ * cut, process killed, lid closed) freezes forever at whatever it last
6
+ * reported instead of ever becoming "offline". Rather than add a cron
7
+ * sweep to rewrite the DB on a timer, staleness is computed on the fly from
8
+ * `lastSeenAt` every time status is read, so no extra moving part is needed
9
+ * and every reader (dashboard poll, CLI, API) sees a consistent, live
10
+ * answer. The agent heartbeats every 15s (HEARTBEAT_INTERVAL_MS in
11
+ * packages/agent/src/index.ts); this is a generous multiple of that so
12
+ * normal network jitter or a slow tick doesn't flap the badge, while a
13
+ * genuinely disconnected mini still reads as offline within about a
14
+ * minute instead of showing a frozen snapshot indefinitely.
15
+ */
16
+ export const AGENT_STALE_AFTER_MS = 60_000;
17
+ /**
18
+ * D1/SQLite's `datetime('now')` (what every `*At` column in this schema
19
+ * is actually stamped with — see `updateAgentHeartbeat`,
20
+ * `createEnrollmentToken`, every `queuedAt`/`createdAt`/`updatedAt`
21
+ * column) returns UTC as "YYYY-MM-DD HH:MM:SS" — no "T", no "Z", no
22
+ * offset. `new Date(...)` on a non-ISO, space-separated string like that
23
+ * is parsed as *local time*, not UTC, so anywhere east of Greenwich a
24
+ * timestamp that just landed reads as hours in the past (and anywhere
25
+ * west, hours in the future — the date itself can flip near midnight).
26
+ * Force it to parse as UTC by reshaping to real ISO-8601 first; leave an
27
+ * already-ISO string (has a "T", e.g. anything built in JS via
28
+ * `.toISOString()`) alone.
29
+ *
30
+ * Exported for exactly this reason beyond this file's own use in
31
+ * `isAgentStale`: any other reader of a raw D1 timestamp column — the
32
+ * dashboard's own display code included — needs the identical fix, not
33
+ * a second, easy-to-forget reimplementation of it.
34
+ */
35
+ export function parseUtc(timestamp) {
36
+ return new Date(timestamp.includes("T") ? timestamp : `${timestamp.replace(" ", "T")}Z`);
37
+ }
38
+ export function isAgentStale(lastSeenAt, now = new Date()) {
39
+ return now.getTime() - parseUtc(lastSeenAt).getTime() > AGENT_STALE_AFTER_MS;
40
+ }
41
+ /**
42
+ * The status actually worth displaying or scheduling against — the raw DB
43
+ * `status` value, overridden to "offline" once the mini has gone stale.
44
+ * Every place that shows or gates on an agent's status should read this
45
+ * instead of `agent.status` directly.
46
+ */
47
+ export function effectiveAgentStatus(agent, now = new Date()) {
48
+ return isAgentStale(agent.lastSeenAt, now) ? "offline" : agent.status;
49
+ }
50
+ export function computeAgentReadiness(agent, now = new Date()) {
51
+ const androidReady = agent.javaVersion !== null && agent.androidSdkVersion !== null;
52
+ const iosReady = agent.xcodeVersion !== null && agent.cocoapodsVersion !== null;
53
+ const flutterReady = agent.flutterVersion !== null && (androidReady || iosReady);
54
+ // A draining/offline/stale mini can have every toolchain installed and
55
+ // still isn't schedulable right now — mirrors kubelet health gating pod
56
+ // scheduling regardless of a Node's labels/capacity.
57
+ const status = effectiveAgentStatus(agent, now);
58
+ const healthy = status === "online" || status === "building";
59
+ return { androidReady, iosReady, flutterReady, healthy, ready: healthy && (androidReady || iosReady) };
60
+ }
61
+ /**
62
+ * The k8s-nodeSelector-style scheduling predicate: whether an already-ready
63
+ * mini is fit for a *specific* app `Platform`, not just "healthy" in the
64
+ * abstract. Mirrors a k8s scheduler's predicate phase filtering candidate
65
+ * Nodes down to the ones a Pod's spec actually fits before anything gets
66
+ * bound — a build queued for `platform: ios` should never land on an
67
+ * Android-only mini and fail two minutes in because `pod install` has no
68
+ * Xcode to talk to.
69
+ *
70
+ * `react-native` has no dedicated readiness flag (it needs a native
71
+ * toolchain but never the Flutter SDK), so — like `flutterReady`'s own
72
+ * "Flutter + one native toolchain" rule — either native platform being
73
+ * ready satisfies it.
74
+ */
75
+ export function agentFitsPlatform(readiness, platform) {
76
+ if (!readiness.healthy)
77
+ return false;
78
+ switch (platform) {
79
+ case "android":
80
+ return readiness.androidReady;
81
+ case "ios":
82
+ return readiness.iosReady;
83
+ case "flutter":
84
+ return readiness.flutterReady;
85
+ case "react-native":
86
+ return readiness.androidReady || readiness.iosReady;
87
+ }
88
+ }
89
+ /**
90
+ * The single predicate the scheduler (`apps/admin-api/src/routes/
91
+ * agents.ts`'s `/agents/lease-job` route, filtering `@appforge/db`'s
92
+ * `listQueuedBuildCandidates` before trying `claimBuildById` on each)
93
+ * filters queued build candidates against when an agent asks for work:
94
+ * healthy *and* toolchain-fit for this specific build's platform. An
95
+ * agent that fails this for every queued build just stays idle and
96
+ * long-polls again — the same as a Pod sitting `Pending` with a
97
+ * `FailedScheduling` event when no Node satisfies its selector, rather than
98
+ * getting bound somewhere it can't actually run.
99
+ */
100
+ export function agentCanBuildPlatform(agent, platform, now = new Date()) {
101
+ return agentFitsPlatform(computeAgentReadiness(agent, now), platform);
102
+ }
103
+ /**
104
+ * The currently published packages/agent build — bump this in lockstep
105
+ * with packages/agent/package.json's own "version" field (and
106
+ * packages/agent/src/lib/version.ts's AGENT_VERSION, which is what the
107
+ * agent actually reports; version.test.ts checks those two never drift
108
+ * apart) whenever a new agent build ships. Compared against each fleet
109
+ * row's self-reported `AgentInfo.agentVersion` to flag a mini that needs
110
+ * updating — see ROADMAP.md's "OTA / self-update for the runner fleet"
111
+ * entry, Phase 1. Also this is Phase 3's own "desired fleet version":
112
+ * `shouldAutoUpdateAgent` below compares every heartbeat's reported
113
+ * version against this same constant to decide whether to auto-issue an
114
+ * `UPDATE_AGENT` command (Phase 2's remote command, reused unchanged).
115
+ */
116
+ export const CURRENT_AGENT_VERSION = "0.1.1";
117
+ /**
118
+ * Whether a fleet member is running the latest known agent build.
119
+ * `null` — deliberately not "outdated" — for a row with no reported
120
+ * version at all (never (re-)enrolled or heartbeated since
121
+ * migrations/0011_agent_version.sql added the column): there's nothing to
122
+ * compare, and calling an unknown version "outdated" would be a guess a
123
+ * maintainer could act on wrongly.
124
+ */
125
+ export function isAgentVersionCurrent(agentVersion) {
126
+ if (agentVersion === null)
127
+ return null;
128
+ return agentVersion === CURRENT_AGENT_VERSION;
129
+ }
130
+ /**
131
+ * The fleet's auto-update policy — fleet-wide, default-on (no per-mini
132
+ * opt-in): every heartbeat is a chance to bring an outdated mini current,
133
+ * with no separate cron/scheduled-worker infra needed, since the agent
134
+ * already calls in every ~15s regardless. See
135
+ * `apps/admin-api/src/routes/agents.ts`'s `/agents/heartbeat` handler,
136
+ * which calls this on every heartbeat to decide whether to auto-issue a
137
+ * fresh `UPDATE_AGENT` command (reusing Phase 2's existing drain-safe
138
+ * self-update flow unchanged — this only decides *whether* to enqueue
139
+ * one, never how the update itself runs).
140
+ *
141
+ * Two guards, both required:
142
+ * - Only a *known* outdated version — same "don't guess" restraint as
143
+ * `isAgentVersionCurrent` itself (a row with no reported version yet,
144
+ * or one already current, is never auto-updated).
145
+ * - Never while an `UPDATE_AGENT` command is already pending or acked
146
+ * for this agent, so a mini that's mid-drain/mid-self-update from an
147
+ * earlier heartbeat doesn't get a second one queued up behind the
148
+ * first on every subsequent heartbeat until the command resolves.
149
+ *
150
+ * The operator's control lever is exactly the existing one: `CURRENT_
151
+ * AGENT_VERSION` itself. A bad release simply isn't bumped to "current"
152
+ * until it's trusted, the same discipline manual `agents update --all`
153
+ * already required — no separate kill switch needed on top of that.
154
+ */
155
+ export function shouldAutoUpdateAgent(effectiveAgentVersion, hasActiveUpdateCommand) {
156
+ return isAgentVersionCurrent(effectiveAgentVersion) === false && !hasActiveUpdateCommand;
157
+ }
158
+ /**
159
+ * `AgentInfo.activeBuildIds` is passed straight through from the D1 row
160
+ * as raw JSON text (same "not re-serialized" choice as `screenSharingEnabled`
161
+ * — see that field's docblock), so every reader (the dashboard's Fleet
162
+ * page, `appforge-admin agents list`) needs to parse it the same way
163
+ * rather than each re-deriving its own notion of "what does this string
164
+ * actually mean." Falls back to empty on anything unexpected — a
165
+ * genuinely malformed value, or a row from before migrations/0017 added
166
+ * the column (though that migration backfills existing rows with `'[]'`,
167
+ * so this is really just defense in depth) — rather than ever throwing
168
+ * mid-render over one agent's stale/unexpected value.
169
+ */
170
+ export function parseActiveBuildIds(raw) {
171
+ try {
172
+ const parsed = JSON.parse(raw);
173
+ return Array.isArray(parsed) && parsed.every((v) => typeof v === "string") ? parsed : [];
174
+ }
175
+ catch {
176
+ return [];
177
+ }
178
+ }
179
+ //# sourceMappingURL=agent-readiness.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-readiness.js","sourceRoot":"","sources":["../src/agent-readiness.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=agent-readiness.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-readiness.test.d.ts","sourceRoot":"","sources":["../src/agent-readiness.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,177 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { computeAgentReadiness, agentFitsPlatform, agentCanBuildPlatform, effectiveAgentStatus, isAgentStale, isAgentVersionCurrent, shouldAutoUpdateAgent, parseActiveBuildIds, parseUtc, AGENT_STALE_AFTER_MS, CURRENT_AGENT_VERSION, } from "./agent-readiness.js";
4
+ const FRESH_AT = new Date("2026-08-26T10:00:00.000Z");
5
+ function baseAgent(overrides = {}) {
6
+ return {
7
+ xcodeVersion: null,
8
+ javaVersion: "21",
9
+ androidSdkVersion: "34",
10
+ cocoapodsVersion: null,
11
+ flutterVersion: null,
12
+ status: "online",
13
+ lastSeenAt: FRESH_AT.toISOString(),
14
+ ...overrides,
15
+ };
16
+ }
17
+ test("isAgentStale is false right at lastSeenAt", () => {
18
+ assert.equal(isAgentStale(FRESH_AT.toISOString(), FRESH_AT), false);
19
+ });
20
+ test("isAgentStale is false just under the threshold", () => {
21
+ const now = new Date(FRESH_AT.getTime() + AGENT_STALE_AFTER_MS - 1);
22
+ assert.equal(isAgentStale(FRESH_AT.toISOString(), now), false);
23
+ });
24
+ test("isAgentStale is true once the threshold has elapsed", () => {
25
+ const now = new Date(FRESH_AT.getTime() + AGENT_STALE_AFTER_MS + 1);
26
+ assert.equal(isAgentStale(FRESH_AT.toISOString(), now), true);
27
+ });
28
+ // Regression: caught live while enrolling a runner in a UTC+3 timezone — a
29
+ // heartbeat that had landed 6 seconds earlier was reported offline. D1's
30
+ // `datetime('now')` (what updateAgentHeartbeat actually stamps lastSeenAt
31
+ // with — see packages/db/src/agents.ts) returns UTC as
32
+ // "YYYY-MM-DD HH:MM:SS", with no "T"/"Z"/offset. `new Date()` parses that
33
+ // non-ISO shape as *local* time, so anywhere east of Greenwich this always
34
+ // misreads a fresh heartbeat as hours stale. Every test above used
35
+ // `.toISOString()`, which never exercised this real DB-timestamp shape.
36
+ test("isAgentStale treats a fresh D1-style timestamp (space-separated, no timezone) as UTC, not local time", () => {
37
+ const sqlLastSeenAt = "2026-08-26 15:27:33"; // D1's datetime('now') shape
38
+ const now = new Date("2026-08-26T15:27:39.000Z"); // 6s later, same instant zone as the timestamp above
39
+ assert.equal(isAgentStale(sqlLastSeenAt, now), false);
40
+ });
41
+ /**
42
+ * `parseUtc` is exported (beyond this file's own `isAgentStale` use) so
43
+ * every other reader of a raw D1 timestamp column — the dashboard's
44
+ * `Billing.tsx`/`AppDetail.tsx`/`Fleet.tsx` display code included — gets
45
+ * the identical UTC-safe parse instead of a second, easy-to-forget
46
+ * reimplementation of the exact bug this function exists to avoid (see
47
+ * this function's own docblock).
48
+ */
49
+ test("parseUtc parses a D1-style timestamp (space-separated, no timezone) as UTC, not local time", () => {
50
+ // Force a non-UTC local timezone for this assertion specifically —
51
+ // otherwise this test would pass even against a naive `new
52
+ // Date(timestamp)` (no UTC reshaping at all) whenever CI's own host
53
+ // timezone happens to already be UTC, which is exactly the failure
54
+ // mode this function exists to prevent (see this run's ROADMAP entry:
55
+ // the real dashboard bug this fix closes was invisible in this same
56
+ // UTC-hosted environment for that exact reason). Node re-reads
57
+ // `process.env.TZ` for Date objects constructed after it changes.
58
+ const originalTz = process.env.TZ;
59
+ process.env.TZ = "Asia/Tokyo"; // UTC+9 — arbitrary, just reliably non-UTC
60
+ try {
61
+ assert.equal(parseUtc("2026-08-26 15:27:33").toISOString(), "2026-08-26T15:27:33.000Z");
62
+ }
63
+ finally {
64
+ process.env.TZ = originalTz;
65
+ }
66
+ });
67
+ test("parseUtc leaves an already-ISO timestamp (has a 'T') alone", () => {
68
+ const iso = "2026-08-26T15:27:33.123Z";
69
+ assert.equal(parseUtc(iso).toISOString(), new Date(iso).toISOString());
70
+ });
71
+ test("effectiveAgentStatus passes through a fresh heartbeat's status", () => {
72
+ const now = new Date(FRESH_AT.getTime() + 1_000);
73
+ assert.equal(effectiveAgentStatus({ status: "online", lastSeenAt: FRESH_AT.toISOString() }, now), "online");
74
+ });
75
+ test("effectiveAgentStatus reports offline once stale, regardless of the frozen status column", () => {
76
+ // This is the exact bug this module fixes: a mini whose network was cut
77
+ // hours ago is still sitting in the DB with status="online" because
78
+ // nothing ever rewrites that column after the last successful heartbeat.
79
+ const now = new Date(FRESH_AT.getTime() + 3 * 60 * 60_000); // 3 hours later
80
+ assert.equal(effectiveAgentStatus({ status: "online", lastSeenAt: FRESH_AT.toISOString() }, now), "offline");
81
+ assert.equal(effectiveAgentStatus({ status: "building", lastSeenAt: FRESH_AT.toISOString() }, now), "offline");
82
+ });
83
+ test("computeAgentReadiness: fresh online mini with only Android toolchain is Android-ready, not iOS-ready, not Flutter-ready (no Flutter SDK)", () => {
84
+ const result = computeAgentReadiness(baseAgent(), FRESH_AT);
85
+ assert.deepEqual(result, { androidReady: true, iosReady: false, flutterReady: false, healthy: true, ready: true });
86
+ });
87
+ test("computeAgentReadiness: same toolchains, but hours-stale heartbeat, is not ready even though status column still says online", () => {
88
+ const now = new Date(FRESH_AT.getTime() + 3 * 60 * 60_000);
89
+ const result = computeAgentReadiness(baseAgent(), now);
90
+ assert.deepEqual(result, { androidReady: true, iosReady: false, flutterReady: false, healthy: false, ready: false });
91
+ });
92
+ // Issue #12: computeAgentReadiness's own docblock says a Flutter app needs
93
+ // "Flutter *and* one complete platform toolchain" — the Flutter SDK alone
94
+ // was never actually checked, so a mini with Flutter but no native
95
+ // toolchain at all silently reported no readiness signal for it either way.
96
+ test("computeAgentReadiness: Flutter SDK alone, no native toolchain at all, is not Flutter-ready", () => {
97
+ const result = computeAgentReadiness(baseAgent({ javaVersion: null, androidSdkVersion: null, flutterVersion: "3.47.1" }), FRESH_AT);
98
+ assert.equal(result.flutterReady, false);
99
+ });
100
+ test("computeAgentReadiness: Flutter SDK + a complete Android toolchain is Flutter-ready", () => {
101
+ const result = computeAgentReadiness(baseAgent({ flutterVersion: "3.47.1" }), FRESH_AT);
102
+ assert.equal(result.flutterReady, true);
103
+ });
104
+ test("computeAgentReadiness: Flutter SDK + a complete iOS toolchain is Flutter-ready", () => {
105
+ const result = computeAgentReadiness(baseAgent({ javaVersion: null, androidSdkVersion: null, xcodeVersion: "16", cocoapodsVersion: "1.15", flutterVersion: "3.47.1" }), FRESH_AT);
106
+ assert.equal(result.flutterReady, true);
107
+ });
108
+ test("computeAgentReadiness: draining mini is never ready even with complete toolchains and a fresh heartbeat", () => {
109
+ const result = computeAgentReadiness(baseAgent({ status: "draining", xcodeVersion: "16", cocoapodsVersion: "1.15" }), FRESH_AT);
110
+ assert.equal(result.ready, false);
111
+ });
112
+ // agentFitsPlatform / agentCanBuildPlatform — the scheduling predicate that
113
+ // gates which queued build platform a mini may claim (see docs/architecture.md's
114
+ // k8s-node-model section).
115
+ test("agentFitsPlatform: an Android-only mini fits android and react-native, not ios or flutter", () => {
116
+ const readiness = computeAgentReadiness(baseAgent(), FRESH_AT); // Android toolchain only
117
+ assert.equal(agentFitsPlatform(readiness, "android"), true);
118
+ assert.equal(agentFitsPlatform(readiness, "react-native"), true);
119
+ assert.equal(agentFitsPlatform(readiness, "ios"), false);
120
+ assert.equal(agentFitsPlatform(readiness, "flutter"), false);
121
+ });
122
+ test("agentFitsPlatform: an iOS-only mini fits ios and react-native, not android or flutter", () => {
123
+ const readiness = computeAgentReadiness(baseAgent({ javaVersion: null, androidSdkVersion: null, xcodeVersion: "16", cocoapodsVersion: "1.15" }), FRESH_AT);
124
+ assert.equal(agentFitsPlatform(readiness, "ios"), true);
125
+ assert.equal(agentFitsPlatform(readiness, "react-native"), true);
126
+ assert.equal(agentFitsPlatform(readiness, "android"), false);
127
+ assert.equal(agentFitsPlatform(readiness, "flutter"), false);
128
+ });
129
+ test("agentFitsPlatform: a draining mini fits nothing even with every toolchain installed", () => {
130
+ const readiness = computeAgentReadiness(baseAgent({ status: "draining", xcodeVersion: "16", cocoapodsVersion: "1.15", flutterVersion: "3.47.1" }), FRESH_AT);
131
+ assert.equal(agentFitsPlatform(readiness, "android"), false);
132
+ assert.equal(agentFitsPlatform(readiness, "ios"), false);
133
+ assert.equal(agentFitsPlatform(readiness, "flutter"), false);
134
+ assert.equal(agentFitsPlatform(readiness, "react-native"), false);
135
+ });
136
+ test("agentCanBuildPlatform: wraps computeAgentReadiness + agentFitsPlatform over raw agent columns", () => {
137
+ const agent = baseAgent({ flutterVersion: "3.47.1" }); // Flutter + Android toolchain
138
+ assert.equal(agentCanBuildPlatform(agent, "flutter", FRESH_AT), true);
139
+ assert.equal(agentCanBuildPlatform(agent, "ios", FRESH_AT), false);
140
+ });
141
+ test("isAgentVersionCurrent: true for a row already on CURRENT_AGENT_VERSION", () => {
142
+ assert.equal(isAgentVersionCurrent(CURRENT_AGENT_VERSION), true);
143
+ });
144
+ test("isAgentVersionCurrent: false for a row on a different (older) version", () => {
145
+ assert.equal(isAgentVersionCurrent("0.0.9"), false);
146
+ });
147
+ test("isAgentVersionCurrent: null (unknown, not outdated) for a row with no reported version", () => {
148
+ assert.equal(isAgentVersionCurrent(null), null);
149
+ });
150
+ test("shouldAutoUpdateAgent: true for a known-outdated version with no update already active", () => {
151
+ assert.equal(shouldAutoUpdateAgent("0.0.9", false), true);
152
+ });
153
+ test("shouldAutoUpdateAgent: false when already current — never re-issues an update for an up-to-date mini", () => {
154
+ assert.equal(shouldAutoUpdateAgent(CURRENT_AGENT_VERSION, false), false);
155
+ });
156
+ test("shouldAutoUpdateAgent: false for a never-reported version — same 'don't guess' restraint as isAgentVersionCurrent", () => {
157
+ assert.equal(shouldAutoUpdateAgent(null, false), false);
158
+ });
159
+ test("shouldAutoUpdateAgent: false when an UPDATE_AGENT command is already pending/acked for this agent", () => {
160
+ // Outdated, but already draining/self-updating from an earlier
161
+ // heartbeat — must not queue a second UPDATE_AGENT behind the first.
162
+ assert.equal(shouldAutoUpdateAgent("0.0.9", true), false);
163
+ });
164
+ test("parseActiveBuildIds: parses a real JSON array of build ids", () => {
165
+ assert.deepEqual(parseActiveBuildIds('["bld_a","bld_b"]'), ["bld_a", "bld_b"]);
166
+ });
167
+ test("parseActiveBuildIds: empty array for an idle agent's '[]'", () => {
168
+ assert.deepEqual(parseActiveBuildIds("[]"), []);
169
+ });
170
+ test("parseActiveBuildIds: falls back to empty for malformed JSON rather than throwing", () => {
171
+ assert.deepEqual(parseActiveBuildIds("not json"), []);
172
+ });
173
+ test("parseActiveBuildIds: falls back to empty for well-formed JSON that isn't an array of strings", () => {
174
+ assert.deepEqual(parseActiveBuildIds('{"not":"an array"}'), []);
175
+ assert.deepEqual(parseActiveBuildIds("[1,2,3]"), []);
176
+ });
177
+ //# sourceMappingURL=agent-readiness.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-readiness.test.js","sourceRoot":"","sources":["../src/agent-readiness.test.ts"],"names":[],"mappings":"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"}