@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.
- package/LICENSE +21 -0
- package/dist/agent-readiness.d.ts +163 -0
- package/dist/agent-readiness.d.ts.map +1 -0
- package/dist/agent-readiness.js +179 -0
- package/dist/agent-readiness.js.map +1 -0
- package/dist/agent-readiness.test.d.ts +2 -0
- package/dist/agent-readiness.test.d.ts.map +1 -0
- package/dist/agent-readiness.test.js +177 -0
- package/dist/agent-readiness.test.js.map +1 -0
- package/dist/api-client.d.ts +529 -0
- package/dist/api-client.d.ts.map +1 -0
- package/dist/api-client.js +551 -0
- package/dist/api-client.js.map +1 -0
- package/dist/api-client.test.d.ts +2 -0
- package/dist/api-client.test.d.ts.map +1 -0
- package/dist/api-client.test.js +305 -0
- package/dist/api-client.test.js.map +1 -0
- package/dist/build-cost.test.d.ts +2 -0
- package/dist/build-cost.test.d.ts.map +1 -0
- package/dist/build-cost.test.js +21 -0
- package/dist/build-cost.test.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/ota-artifact.d.ts +41 -0
- package/dist/ota-artifact.d.ts.map +1 -0
- package/dist/ota-artifact.js +43 -0
- package/dist/ota-artifact.js.map +1 -0
- package/dist/ota-artifact.test.d.ts +2 -0
- package/dist/ota-artifact.test.d.ts.map +1 -0
- package/dist/ota-artifact.test.js +44 -0
- package/dist/ota-artifact.test.js.map +1 -0
- package/dist/ota-bundle-hash.d.ts +38 -0
- package/dist/ota-bundle-hash.d.ts.map +1 -0
- package/dist/ota-bundle-hash.js +40 -0
- package/dist/ota-bundle-hash.js.map +1 -0
- package/dist/ota-bundle-hash.test.d.ts +2 -0
- package/dist/ota-bundle-hash.test.d.ts.map +1 -0
- package/dist/ota-bundle-hash.test.js +28 -0
- package/dist/ota-bundle-hash.test.js.map +1 -0
- package/dist/ota-rollout.d.ts +38 -0
- package/dist/ota-rollout.d.ts.map +1 -0
- package/dist/ota-rollout.js +53 -0
- package/dist/ota-rollout.js.map +1 -0
- package/dist/ota-rollout.test.d.ts +2 -0
- package/dist/ota-rollout.test.d.ts.map +1 -0
- package/dist/ota-rollout.test.js +48 -0
- package/dist/ota-rollout.test.js.map +1 -0
- package/dist/schemas.d.ts +776 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +538 -0
- package/dist/schemas.js.map +1 -0
- package/dist/token-packs.test.d.ts +2 -0
- package/dist/token-packs.test.d.ts.map +1 -0
- package/dist/token-packs.test.js +34 -0
- package/dist/token-packs.test.js.map +1 -0
- package/dist/types.d.ts +445 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +80 -0
- package/dist/types.js.map +1 -0
- 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 @@
|
|
|
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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"}
|