@caelo-cms/provisioning 0.10.47 → 0.10.49

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 (321) hide show
  1. package/caddy/Caddyfile.production +1 -1
  2. package/caddy/Caddyfile.staging +1 -1
  3. package/dist/agent/apply.d.ts +45 -0
  4. package/dist/agent/apply.d.ts.map +1 -0
  5. package/dist/agent/apply.js +83 -0
  6. package/dist/agent/apply.js.map +1 -0
  7. package/dist/agent/cli-provenance.d.ts +42 -0
  8. package/dist/agent/cli-provenance.d.ts.map +1 -0
  9. package/dist/agent/cli-provenance.js +138 -0
  10. package/dist/agent/cli-provenance.js.map +1 -0
  11. package/dist/agent/command.d.ts +79 -0
  12. package/dist/agent/command.d.ts.map +1 -0
  13. package/dist/agent/command.js +454 -0
  14. package/dist/agent/command.js.map +1 -0
  15. package/dist/agent/config.d.ts +30 -0
  16. package/dist/agent/config.d.ts.map +1 -0
  17. package/dist/agent/config.js +60 -0
  18. package/dist/agent/config.js.map +1 -0
  19. package/dist/agent/lock.d.ts +13 -0
  20. package/dist/agent/lock.d.ts.map +1 -0
  21. package/dist/agent/lock.js +100 -0
  22. package/dist/agent/lock.js.map +1 -0
  23. package/dist/agent/queue.d.ts +35 -0
  24. package/dist/agent/queue.d.ts.map +1 -0
  25. package/dist/agent/queue.js +50 -0
  26. package/dist/agent/queue.js.map +1 -0
  27. package/dist/agent/run.d.ts +97 -0
  28. package/dist/agent/run.d.ts.map +1 -0
  29. package/dist/agent/run.js +198 -0
  30. package/dist/agent/run.js.map +1 -0
  31. package/dist/agent/systemd.d.ts +55 -0
  32. package/dist/agent/systemd.d.ts.map +1 -0
  33. package/dist/agent/systemd.js +171 -0
  34. package/dist/agent/systemd.js.map +1 -0
  35. package/dist/agent/units.d.ts +44 -0
  36. package/dist/agent/units.d.ts.map +1 -0
  37. package/dist/agent/units.js +127 -0
  38. package/dist/agent/units.js.map +1 -0
  39. package/dist/agent-db.js +18402 -0
  40. package/dist/cli.js +119 -16
  41. package/dist/cli.js.map +1 -1
  42. package/dist/cloudsql-iam-user.d.ts +39 -0
  43. package/dist/cloudsql-iam-user.d.ts.map +1 -0
  44. package/dist/cloudsql-iam-user.js +119 -0
  45. package/dist/cloudsql-iam-user.js.map +1 -0
  46. package/dist/compose.d.ts +16 -0
  47. package/dist/compose.d.ts.map +1 -1
  48. package/dist/compose.js +26 -1
  49. package/dist/compose.js.map +1 -1
  50. package/dist/dns/index.d.ts +2 -0
  51. package/dist/dns/index.d.ts.map +1 -1
  52. package/dist/dns/index.js +3 -2
  53. package/dist/dns/index.js.map +1 -1
  54. package/dist/dns/manual.d.ts +5 -0
  55. package/dist/dns/manual.d.ts.map +1 -1
  56. package/dist/dns/manual.js +28 -12
  57. package/dist/dns/manual.js.map +1 -1
  58. package/dist/gcloud.d.ts +2 -0
  59. package/dist/gcloud.d.ts.map +1 -1
  60. package/dist/gcloud.js +1 -1
  61. package/dist/gcloud.js.map +1 -1
  62. package/dist/gcp-names.d.ts +15 -0
  63. package/dist/gcp-names.d.ts.map +1 -1
  64. package/dist/gcp-names.js +23 -0
  65. package/dist/gcp-names.js.map +1 -1
  66. package/dist/index.d.ts +6 -0
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +4 -0
  69. package/dist/index.js.map +1 -1
  70. package/dist/infra-contract.d.ts +1 -1
  71. package/dist/infra-contract.js +7 -1
  72. package/dist/infra-contract.js.map +1 -1
  73. package/dist/install-job-runner.d.ts +13 -0
  74. package/dist/install-job-runner.d.ts.map +1 -0
  75. package/dist/install-job-runner.js +155 -0
  76. package/dist/install-job-runner.js.map +1 -0
  77. package/dist/install-job.d.ts +210 -0
  78. package/dist/install-job.d.ts.map +1 -0
  79. package/dist/install-job.js +449 -0
  80. package/dist/install-job.js.map +1 -0
  81. package/dist/install-state.d.ts +10 -1
  82. package/dist/install-state.d.ts.map +1 -1
  83. package/dist/install-state.js +10 -2
  84. package/dist/install-state.js.map +1 -1
  85. package/dist/lifecycle.d.ts.map +1 -1
  86. package/dist/lifecycle.js +19 -4
  87. package/dist/lifecycle.js.map +1 -1
  88. package/dist/migration-runner.d.ts +5 -0
  89. package/dist/migration-runner.d.ts.map +1 -1
  90. package/dist/migration-runner.js +1 -1
  91. package/dist/migration-runner.js.map +1 -1
  92. package/dist/operator-access.d.ts +14 -8
  93. package/dist/operator-access.d.ts.map +1 -1
  94. package/dist/operator-access.js +16 -102
  95. package/dist/operator-access.js.map +1 -1
  96. package/dist/release-manifest.d.ts +2 -41
  97. package/dist/release-manifest.d.ts.map +1 -1
  98. package/dist/release-manifest.js +18 -121
  99. package/dist/release-manifest.js.map +1 -1
  100. package/dist/run-mode.d.ts +27 -0
  101. package/dist/run-mode.d.ts.map +1 -0
  102. package/dist/run-mode.js +22 -0
  103. package/dist/run-mode.js.map +1 -0
  104. package/dist/runtime-secrets.d.ts +19 -0
  105. package/dist/runtime-secrets.d.ts.map +1 -1
  106. package/dist/runtime-secrets.js +44 -0
  107. package/dist/runtime-secrets.js.map +1 -1
  108. package/dist/self-hosted-roles.d.ts +22 -1
  109. package/dist/self-hosted-roles.d.ts.map +1 -1
  110. package/dist/self-hosted-roles.js +81 -12
  111. package/dist/self-hosted-roles.js.map +1 -1
  112. package/dist/self-hosted-upgrade.d.ts +17 -0
  113. package/dist/self-hosted-upgrade.d.ts.map +1 -1
  114. package/dist/self-hosted-upgrade.js +16 -1
  115. package/dist/self-hosted-upgrade.js.map +1 -1
  116. package/dist/stack-contract.d.ts +6 -0
  117. package/dist/stack-contract.d.ts.map +1 -1
  118. package/dist/stack-contract.js +10 -1
  119. package/dist/stack-contract.js.map +1 -1
  120. package/dist/upgrade/converge.d.ts +6 -0
  121. package/dist/upgrade/converge.d.ts.map +1 -1
  122. package/dist/upgrade/converge.js +59 -2
  123. package/dist/upgrade/converge.js.map +1 -1
  124. package/dist/upgrade/gcp-lookup.d.ts +6 -0
  125. package/dist/upgrade/gcp-lookup.d.ts.map +1 -1
  126. package/dist/upgrade/gcp-lookup.js +25 -0
  127. package/dist/upgrade/gcp-lookup.js.map +1 -1
  128. package/dist/upgrade/release-images.d.ts +26 -2
  129. package/dist/upgrade/release-images.d.ts.map +1 -1
  130. package/dist/upgrade/release-images.js +35 -2
  131. package/dist/upgrade/release-images.js.map +1 -1
  132. package/dist/upgrade/upgrade.d.ts +5 -0
  133. package/dist/upgrade/upgrade.d.ts.map +1 -1
  134. package/dist/upgrade/upgrade.js +28 -1
  135. package/dist/upgrade/upgrade.js.map +1 -1
  136. package/dist/upgrader-assess.d.ts +85 -0
  137. package/dist/upgrader-assess.d.ts.map +1 -0
  138. package/dist/upgrader-assess.js +122 -0
  139. package/dist/upgrader-assess.js.map +1 -0
  140. package/dist/upgrader-gcp.d.ts +17 -0
  141. package/dist/upgrader-gcp.d.ts.map +1 -0
  142. package/dist/upgrader-gcp.js +87 -0
  143. package/dist/upgrader-gcp.js.map +1 -0
  144. package/dist/upgrader-job.d.ts +133 -0
  145. package/dist/upgrader-job.d.ts.map +1 -0
  146. package/dist/upgrader-job.js +259 -0
  147. package/dist/upgrader-job.js.map +1 -0
  148. package/dist/upgrader-reporter.d.ts +35 -0
  149. package/dist/upgrader-reporter.d.ts.map +1 -0
  150. package/dist/upgrader-reporter.js +79 -0
  151. package/dist/upgrader-reporter.js.map +1 -0
  152. package/dist/upgrader-setup.d.ts +59 -0
  153. package/dist/upgrader-setup.d.ts.map +1 -0
  154. package/dist/upgrader-setup.js +308 -0
  155. package/dist/upgrader-setup.js.map +1 -0
  156. package/dist/wizard.d.ts +6 -0
  157. package/dist/wizard.d.ts.map +1 -1
  158. package/dist/wizard.js +32 -4
  159. package/dist/wizard.js.map +1 -1
  160. package/dist/wizards/gcp-firebase.d.ts +3 -0
  161. package/dist/wizards/gcp-firebase.d.ts.map +1 -1
  162. package/dist/wizards/gcp-firebase.js +1 -0
  163. package/dist/wizards/gcp-firebase.js.map +1 -1
  164. package/dist/wizards/gcp-install-job.d.ts +74 -0
  165. package/dist/wizards/gcp-install-job.d.ts.map +1 -0
  166. package/dist/wizards/gcp-install-job.js +398 -0
  167. package/dist/wizards/gcp-install-job.js.map +1 -0
  168. package/dist/wizards/gcp-prompts.d.ts +3 -0
  169. package/dist/wizards/gcp-prompts.d.ts.map +1 -0
  170. package/dist/wizards/gcp-prompts.js +25 -0
  171. package/dist/wizards/gcp-prompts.js.map +1 -0
  172. package/dist/wizards/gcp-pulumi.d.ts +4 -2
  173. package/dist/wizards/gcp-pulumi.d.ts.map +1 -1
  174. package/dist/wizards/gcp-pulumi.js +1 -1
  175. package/dist/wizards/gcp-pulumi.js.map +1 -1
  176. package/dist/wizards/gcp.d.ts +40 -0
  177. package/dist/wizards/gcp.d.ts.map +1 -1
  178. package/dist/wizards/gcp.js +223 -59
  179. package/dist/wizards/gcp.js.map +1 -1
  180. package/package.json +6 -4
  181. package/src/adapter.ts +97 -0
  182. package/src/adc-impersonation.ts +176 -0
  183. package/src/admin-domain.test.ts +254 -0
  184. package/src/admin-domain.ts +245 -0
  185. package/src/admin-memory.test.ts +151 -0
  186. package/src/agent/apply.test.ts +185 -0
  187. package/src/agent/apply.ts +124 -0
  188. package/src/agent/cli-provenance.test.ts +159 -0
  189. package/src/agent/cli-provenance.ts +191 -0
  190. package/src/agent/command.test.ts +77 -0
  191. package/src/agent/command.ts +564 -0
  192. package/src/agent/config.ts +86 -0
  193. package/src/agent/lock-config.test.ts +97 -0
  194. package/src/agent/lock.ts +110 -0
  195. package/src/agent/queue.test.ts +10 -0
  196. package/src/agent/queue.ts +91 -0
  197. package/src/agent/run.test.ts +437 -0
  198. package/src/agent/run.ts +295 -0
  199. package/src/agent/systemd.ts +209 -0
  200. package/src/agent/units.test.ts +223 -0
  201. package/src/agent/units.ts +163 -0
  202. package/src/azure-front-door.test.ts +119 -0
  203. package/src/azure-front-door.ts +56 -0
  204. package/src/bootstrap-token.ts +20 -0
  205. package/src/caddy.ts +112 -0
  206. package/src/cdn-copy.ts +91 -0
  207. package/src/cert-wait.test.ts +194 -0
  208. package/src/cert-wait.ts +163 -0
  209. package/src/checkpoint-secrets.test.ts +380 -0
  210. package/src/checkpoint-secrets.ts +283 -0
  211. package/src/cli-routing.test.ts +45 -0
  212. package/src/cli-routing.ts +43 -0
  213. package/src/cli.ts +1491 -0
  214. package/src/cloudsql-iam-user.ts +147 -0
  215. package/src/compose.ts +334 -0
  216. package/src/default-firewall.test.ts +411 -0
  217. package/src/default-firewall.ts +342 -0
  218. package/src/deployed-release.test.ts +231 -0
  219. package/src/deployed-release.ts +220 -0
  220. package/src/dns/cloudflare.ts +182 -0
  221. package/src/dns/groups.test.ts +76 -0
  222. package/src/dns/groups.ts +86 -0
  223. package/src/dns/index.ts +56 -0
  224. package/src/dns/manual.ts +130 -0
  225. package/src/dns/types.ts +27 -0
  226. package/src/gateway-credentials.test.ts +180 -0
  227. package/src/gateway-credentials.ts +144 -0
  228. package/src/gcloud-components.test.ts +76 -0
  229. package/src/gcloud-components.ts +85 -0
  230. package/src/gcloud-retry.ts +41 -0
  231. package/src/gcloud-runner.test.ts +61 -0
  232. package/src/gcloud.test.ts +99 -0
  233. package/src/gcloud.ts +322 -0
  234. package/src/gcp-names.ts +135 -0
  235. package/src/index.test.ts +332 -0
  236. package/src/index.ts +114 -0
  237. package/src/infra-contract.ts +32 -0
  238. package/src/install-job-runner.ts +166 -0
  239. package/src/install-job.test.ts +566 -0
  240. package/src/install-job.ts +542 -0
  241. package/src/install-state.test.ts +239 -0
  242. package/src/install-state.ts +583 -0
  243. package/src/internal-token.ts +35 -0
  244. package/src/lifecycle.ts +900 -0
  245. package/src/mcp-iap-stacks.test.ts +58 -0
  246. package/src/mcp-iap.test.ts +149 -0
  247. package/src/mcp-iap.ts +131 -0
  248. package/src/media-volume.test.ts +296 -0
  249. package/src/migration-runner.ts +365 -0
  250. package/src/operator-access-stacks.test.ts +55 -0
  251. package/src/operator-access.test.ts +332 -0
  252. package/src/operator-access.ts +554 -0
  253. package/src/ops-backend.test.ts +320 -0
  254. package/src/ops-backend.ts +414 -0
  255. package/src/provisioner-identity.test.ts +482 -0
  256. package/src/provisioner-identity.ts +407 -0
  257. package/src/redirects-emit.ts +175 -0
  258. package/src/region-data.ts +428 -0
  259. package/src/regions.test.ts +277 -0
  260. package/src/regions.ts +370 -0
  261. package/src/release-contract.ts +57 -0
  262. package/src/release-manifest.test.ts +300 -0
  263. package/src/release-manifest.ts +310 -0
  264. package/src/run-mode.ts +40 -0
  265. package/src/runtime-secrets.test.ts +627 -0
  266. package/src/runtime-secrets.ts +754 -0
  267. package/src/self-command.test.ts +14 -0
  268. package/src/self-command.ts +21 -0
  269. package/src/self-hosted-compose.test.ts +177 -0
  270. package/src/self-hosted-release.ts +240 -0
  271. package/src/self-hosted-roles.integration.test.ts +161 -0
  272. package/src/self-hosted-roles.test.ts +124 -0
  273. package/src/self-hosted-roles.ts +203 -0
  274. package/src/self-hosted-upgrade.test.ts +394 -0
  275. package/src/self-hosted-upgrade.ts +548 -0
  276. package/src/stack-contract-parity.test.ts +199 -0
  277. package/src/stack-contract.test.ts +391 -0
  278. package/src/stack-contract.ts +888 -0
  279. package/src/stack-converge-secrets.test.ts +421 -0
  280. package/src/stack-converge.test.ts +833 -0
  281. package/src/stack-converge.ts +909 -0
  282. package/src/staging-admin.test.ts +246 -0
  283. package/src/staging-admin.ts +324 -0
  284. package/src/state-attach.test.ts +211 -0
  285. package/src/state-attach.ts +132 -0
  286. package/src/state-backend.test.ts +145 -0
  287. package/src/state-backend.ts +262 -0
  288. package/src/state-command.ts +293 -0
  289. package/src/state-migrate.test.ts +479 -0
  290. package/src/state-migrate.ts +434 -0
  291. package/src/state-pulumi.ts +289 -0
  292. package/src/state-store.test.ts +212 -0
  293. package/src/state-store.ts +243 -0
  294. package/src/upgrade/app-roll.ts +486 -0
  295. package/src/upgrade/backup.ts +109 -0
  296. package/src/upgrade/converge.ts +437 -0
  297. package/src/upgrade/gcp-lookup.ts +167 -0
  298. package/src/upgrade/health.test.ts +279 -0
  299. package/src/upgrade/health.ts +305 -0
  300. package/src/upgrade/release-images.test.ts +119 -0
  301. package/src/upgrade/release-images.ts +393 -0
  302. package/src/upgrade/reporter.ts +46 -0
  303. package/src/upgrade/upgrade.test.ts +626 -0
  304. package/src/upgrade/upgrade.ts +249 -0
  305. package/src/upgrader-assess.test.ts +306 -0
  306. package/src/upgrader-assess.ts +250 -0
  307. package/src/upgrader-gcp.ts +123 -0
  308. package/src/upgrader-job.test.ts +411 -0
  309. package/src/upgrader-job.ts +502 -0
  310. package/src/upgrader-reporter.test.ts +46 -0
  311. package/src/upgrader-reporter.ts +105 -0
  312. package/src/upgrader-setup.test.ts +169 -0
  313. package/src/upgrader-setup.ts +355 -0
  314. package/src/wizard.ts +425 -0
  315. package/src/wizards/gcp-cost.ts +188 -0
  316. package/src/wizards/gcp-firebase.ts +72 -0
  317. package/src/wizards/gcp-install-job.ts +506 -0
  318. package/src/wizards/gcp-prompts.ts +26 -0
  319. package/src/wizards/gcp-pulumi.test.ts +246 -0
  320. package/src/wizards/gcp-pulumi.ts +516 -0
  321. package/src/wizards/gcp.ts +1935 -0
@@ -0,0 +1,437 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * One poll of the self-hosted host agent (agent/run.ts) against fakes:
5
+ * heartbeat, claim, the refusal matrix (nothing of the target runs before
6
+ * its signed manifest passed), the hand-over to the target release's CLI,
7
+ * and what happens when that process records nothing.
8
+ */
9
+
10
+ import { describe, expect, it } from "bun:test";
11
+ import type { StoredUpdatePolicy } from "@caelo-cms/shared";
12
+ import type { ReleaseManifest, ReleaseManifestResult } from "../release-manifest.js";
13
+ import type { SelfHostedRelease } from "../self-hosted-release.js";
14
+ import type { ClaimedUpdate, UpdateEnd, UpdateFacts } from "../upgrader-job.js";
15
+ import type { AgentHeartbeat, AgentQueue } from "./queue.js";
16
+ import { type AgentApplyRequest, type AgentRunDeps, runAgentOnce } from "./run.js";
17
+
18
+ const D = (c: string) => `sha256:${c.repeat(64)}`;
19
+
20
+ const MANIFEST: ReleaseManifest = {
21
+ manifestVersion: 1,
22
+ version: "0.11.0",
23
+ channel: "stable",
24
+ digests: { admin: D("a"), gateway: D("b"), ops: D("c") },
25
+ minFromVersion: "0.10.0",
26
+ // A raised cloud contract: irrelevant on self-hosted.
27
+ infraContractVersion: 7,
28
+ migrations: { contract: false, lastContractVersion: null, head: "0270_platform_update_agent" },
29
+ security: false,
30
+ advisories: [],
31
+ };
32
+
33
+ const POLICY: StoredUpdatePolicy = {
34
+ mode: "notify",
35
+ autoSecurity: false,
36
+ autoPatch: false,
37
+ autoMinor: false,
38
+ channel: "stable",
39
+ windowStart: null,
40
+ windowEnd: null,
41
+ windowTimezone: "UTC",
42
+ soakHours: 24,
43
+ securitySoakHours: 2,
44
+ paused: false,
45
+ pausedReason: null,
46
+ pausedAt: null,
47
+ pausedUntil: null,
48
+ pausedByOwner: false,
49
+ updatedAt: "2026-10-13T00:00:00Z",
50
+ };
51
+
52
+ const REQUEST: ClaimedUpdate = {
53
+ id: "11111111-1111-4111-8111-111111111111",
54
+ targetVersion: "0.11.0",
55
+ trigger: "manual",
56
+ policy: POLICY,
57
+ };
58
+
59
+ const INSTALLED: SelfHostedRelease = {
60
+ version: "0.10.47",
61
+ images: {
62
+ admin: `ghcr.io/caelo-cms/admin@${D("1")}`,
63
+ gateway: `ghcr.io/caelo-cms/gateway@${D("2")}`,
64
+ },
65
+ installedAt: "2026-10-01T00:00:00Z",
66
+ history: [],
67
+ };
68
+
69
+ interface Memory {
70
+ queue: AgentQueue;
71
+ beats: AgentHeartbeat[];
72
+ claims: (string | null)[];
73
+ events: { phase: string; level: string; message: string }[];
74
+ facts: UpdateFacts[];
75
+ ends: UpdateEnd[];
76
+ /** Whether the request is still running (finish only ends a running one). */
77
+ running: boolean;
78
+ }
79
+
80
+ function memoryQueue(request: ClaimedUpdate | null, opts: { heartbeatFails?: boolean } = {}) {
81
+ const m: Memory = {
82
+ beats: [],
83
+ claims: [],
84
+ events: [],
85
+ facts: [],
86
+ ends: [],
87
+ running: false,
88
+ queue: undefined as unknown as AgentQueue,
89
+ };
90
+ m.queue = {
91
+ heartbeat: async (beat) => {
92
+ if (opts.heartbeatFails) throw new Error("password authentication failed");
93
+ m.beats.push(beat);
94
+ },
95
+ claim: async (execution) => {
96
+ m.claims.push(execution);
97
+ m.running = request !== null;
98
+ return request;
99
+ },
100
+ event: async (_id, phase, level, message) => {
101
+ m.events.push({ phase, level, message });
102
+ },
103
+ record: async (_id, facts) => {
104
+ m.facts.push(facts);
105
+ },
106
+ finish: async (_id, end) => {
107
+ if (!m.running) return false;
108
+ m.running = false;
109
+ m.ends.push(end);
110
+ return true;
111
+ },
112
+ };
113
+ return m;
114
+ }
115
+
116
+ function setup(over: Partial<AgentRunDeps> = {}, request: ClaimedUpdate | null = REQUEST) {
117
+ const q = memoryQueue(request);
118
+ const applied: AgentApplyRequest[] = [];
119
+ const locks = { taken: 0, released: 0 };
120
+ const deps: AgentRunDeps = {
121
+ queue: q.queue,
122
+ agentVersion: "0.10.47",
123
+ host: "box-1",
124
+ preflight: async () => null,
125
+ readInstalled: () => INSTALLED,
126
+ fetchManifest: async (): Promise<ReleaseManifestResult> => ({ ok: true, manifest: MANIFEST }),
127
+ lock: () => {
128
+ locks.taken += 1;
129
+ return () => {
130
+ locks.released += 1;
131
+ };
132
+ },
133
+ verifyTargetCli: async (version) => ({
134
+ ok: true,
135
+ dir: `/srv/.caelo/agent/cli/caelo-${version}`,
136
+ cliPath: `/srv/.caelo/agent/cli/caelo-${version}/node_modules/@caelo-cms/provisioning/dist/cli.js`,
137
+ }),
138
+ applyTarget: async (req) => {
139
+ applied.push(req);
140
+ // The target's CLI records the end itself.
141
+ await q.queue.finish(req.requestId, {
142
+ status: "succeeded",
143
+ reason: null,
144
+ codes: [],
145
+ pauseAutoUpdates: false,
146
+ });
147
+ return { exitCode: 0, output: "" };
148
+ },
149
+ now: () => new Date("2026-10-13T12:00:00Z"),
150
+ log: () => {},
151
+ sleep: async () => {},
152
+ ...over,
153
+ };
154
+ return { deps, q, applied, locks };
155
+ }
156
+
157
+ describe("runAgentOnce", () => {
158
+ it("checks in and does nothing more without a pending request", async () => {
159
+ const { deps, q, applied } = setup({ preflight: async () => "cosign is missing" }, null);
160
+ expect(await runAgentOnce(deps)).toEqual({ exitCode: 0, requestId: null, status: null });
161
+ expect(q.beats).toEqual([
162
+ { agentVersion: "0.10.47", host: "box-1", problem: "cosign is missing" },
163
+ ]);
164
+ expect(q.claims).toEqual(["agent@box-1"]);
165
+ expect(applied).toEqual([]);
166
+ });
167
+
168
+ it("fails the poll (exit 1) when it cannot check in, and claims nothing", async () => {
169
+ const q = memoryQueue(REQUEST, { heartbeatFails: true });
170
+ const { deps } = setup({ queue: q.queue });
171
+ expect((await runAgentOnce(deps)).exitCode).toBe(1);
172
+ expect(q.claims).toEqual([]);
173
+ });
174
+
175
+ it("verifies, decides, then hands the request to the target release's CLI", async () => {
176
+ const { deps, q, applied, locks } = setup();
177
+ const r = await runAgentOnce(deps);
178
+ expect(r).toEqual({ exitCode: 0, requestId: REQUEST.id, status: null });
179
+ expect(applied).toEqual([
180
+ {
181
+ requestId: REQUEST.id,
182
+ targetVersion: "0.11.0",
183
+ trigger: "manual",
184
+ digests: { admin: D("a"), gateway: D("b") },
185
+ cliPath:
186
+ "/srv/.caelo/agent/cli/caelo-0.11.0/node_modules/@caelo-cms/provisioning/dist/cli.js",
187
+ },
188
+ ]);
189
+ expect(q.facts[0]).toMatchObject({ adminDigest: D("a"), manifestSecurity: false });
190
+ expect([...new Set(q.events.map((e) => e.phase))]).toEqual(["start", "verify", "assess"]);
191
+ expect(q.events.some((e) => /built by Caelo's release workflow/.test(e.message))).toBe(true);
192
+ expect(q.events.some((e) => /signed by Caelo's release workflow/.test(e.message))).toBe(true);
193
+ expect(q.ends).toEqual([
194
+ { status: "succeeded", reason: null, codes: [], pauseAutoUpdates: false },
195
+ ]);
196
+ expect(locks).toEqual({ taken: 1, released: 1 });
197
+ });
198
+
199
+ interface RefusalRow {
200
+ readonly name: string;
201
+ readonly over: Partial<AgentRunDeps>;
202
+ readonly request?: ClaimedUpdate;
203
+ /** `<status>/<codes>`. */
204
+ readonly expect: string;
205
+ readonly reason?: RegExp;
206
+ }
207
+ const refusals: readonly RefusalRow[] = [
208
+ {
209
+ name: "a host problem (preflight)",
210
+ over: { preflight: async () => "Docker is not reachable for the agent's user." },
211
+ expect: "failed/preflight",
212
+ reason: /Nothing was changed: Docker is not reachable/,
213
+ },
214
+ {
215
+ name: "another upgrade holds the lock",
216
+ over: {
217
+ lock: () => {
218
+ throw new Error("`cms-provision upgrade` is running on this install (pid 42).");
219
+ },
220
+ },
221
+ expect: "failed/state-locked",
222
+ },
223
+ {
224
+ name: "a malformed release.json",
225
+ over: {
226
+ readInstalled: () => {
227
+ throw new Error("release.json is malformed");
228
+ },
229
+ },
230
+ expect: "failed/install-state",
231
+ },
232
+ {
233
+ name: "a bad signature",
234
+ over: {
235
+ fetchManifest: async () => ({ ok: false, error: { code: "signature", message: "no" } }),
236
+ },
237
+ expect: "refused/signature",
238
+ },
239
+ {
240
+ name: "the registry is unreachable",
241
+ over: {
242
+ fetchManifest: async () => ({ ok: false, error: { code: "registry", message: "dns" } }),
243
+ },
244
+ expect: "failed/registry",
245
+ },
246
+ {
247
+ name: "a downgrade",
248
+ over: { readInstalled: () => ({ ...INSTALLED, version: "0.12.0" }) },
249
+ expect: "refused/not-newer",
250
+ },
251
+ {
252
+ name: "below minFromVersion",
253
+ over: {
254
+ fetchManifest: async () => ({
255
+ ok: true,
256
+ manifest: { ...MANIFEST, minFromVersion: "0.10.50" },
257
+ }),
258
+ },
259
+ expect: "refused/min-from-version",
260
+ },
261
+ {
262
+ name: "no recorded release (an install from before prebuilt images)",
263
+ over: { readInstalled: () => null },
264
+ expect: "needs_cloud_shell/install-version-unknown",
265
+ reason: /upgrade command on the server/,
266
+ },
267
+ {
268
+ name: "contract migrations on an automatic run",
269
+ over: {
270
+ fetchManifest: async () => ({
271
+ ok: true,
272
+ manifest: { ...MANIFEST, migrations: { ...MANIFEST.migrations, contract: true } },
273
+ }),
274
+ },
275
+ request: {
276
+ ...REQUEST,
277
+ trigger: "auto",
278
+ policy: { ...POLICY, mode: "auto", autoMinor: true },
279
+ },
280
+ expect: "refused/contract-migrations",
281
+ },
282
+ {
283
+ name: "a target CLI whose npm provenance does not check out",
284
+ over: {
285
+ verifyTargetCli: async () => ({
286
+ ok: false,
287
+ error: { code: "provenance", message: "built by https://github.com/evil/caelo-cms" },
288
+ }),
289
+ },
290
+ expect: "refused/cli-provenance",
291
+ reason: /not the one Caelo's release workflow published/,
292
+ },
293
+ {
294
+ name: "npm missing on the server",
295
+ over: {
296
+ verifyTargetCli: async () => ({
297
+ ok: false,
298
+ error: { code: "npm-missing", message: "npm is not installed" },
299
+ }),
300
+ },
301
+ expect: "failed/cli-npm-missing",
302
+ },
303
+ {
304
+ name: "an automatic run the policy does not allow",
305
+ over: {},
306
+ request: { ...REQUEST, trigger: "auto" },
307
+ expect: "refused/policy",
308
+ },
309
+ ];
310
+
311
+ it.each(refusals.map((r) => [r.name, r] as const))(
312
+ "stops before any target code runs: %s",
313
+ async (_name, row) => {
314
+ const { deps, q, applied, locks } = setup(row.over, row.request ?? REQUEST);
315
+ const r = await runAgentOnce(deps);
316
+ expect(applied).toEqual([]);
317
+ const end = q.ends[0];
318
+ expect(`${end?.status}/${end?.codes.join(",")}`).toBe(row.expect);
319
+ if (row.reason) expect(end?.reason ?? "").toMatch(row.reason);
320
+ expect(r.status).toBe(end?.status ?? null);
321
+ expect(r.exitCode).toBe(end?.status === "failed" ? 1 : 0);
322
+ // Whatever the lock did, it is not left held.
323
+ expect(locks.released).toBe(locks.taken);
324
+ expect(q.events.at(-1)?.phase).toBe("finish");
325
+ },
326
+ );
327
+
328
+ it("ends the request as failed with the output when the target's CLI recorded nothing", async () => {
329
+ const { deps, q } = setup({
330
+ applyTarget: async () => ({ exitCode: 1, output: "error: package not found" }),
331
+ });
332
+ const r = await runAgentOnce(deps);
333
+ expect(r).toEqual({ exitCode: 1, requestId: REQUEST.id, status: "failed" });
334
+ expect(q.ends[0]?.status).toBe("failed");
335
+ expect(q.ends[0]?.codes).toEqual(["apply"]);
336
+ expect(q.ends[0]?.reason).toContain("error: package not found");
337
+ expect(q.events.at(-1)).toMatchObject({ phase: "finish", level: "error" });
338
+ });
339
+
340
+ it("leaves a rolled-back end the target's CLI recorded alone", async () => {
341
+ const { deps, q } = setup();
342
+ const rolled: AgentRunDeps = {
343
+ ...deps,
344
+ applyTarget: async (req) => {
345
+ await q.queue.finish(req.requestId, {
346
+ status: "rolled_back",
347
+ reason: "unhealthy",
348
+ codes: ["roll-admin"],
349
+ pauseAutoUpdates: false,
350
+ });
351
+ return { exitCode: 1, output: "" };
352
+ },
353
+ };
354
+ const r = await runAgentOnce(rolled);
355
+ expect(r).toEqual({ exitCode: 1, requestId: REQUEST.id, status: null });
356
+ expect(q.ends.map((e) => e.status)).toEqual(["rolled_back"]);
357
+ expect(q.events.some((e) => e.phase === "finish")).toBe(false);
358
+ });
359
+
360
+ it("keeps checking in while the target's CLI runs", async () => {
361
+ const { deps, q } = setup();
362
+ const slow: AgentRunDeps = {
363
+ ...deps,
364
+ heartbeatEveryMs: 5,
365
+ applyTarget: async (req) => {
366
+ await new Promise((resolve) => setTimeout(resolve, 40));
367
+ return deps.applyTarget(req);
368
+ },
369
+ };
370
+ await runAgentOnce(slow);
371
+ const during = q.beats.length;
372
+ expect(during).toBeGreaterThan(2);
373
+ await new Promise((resolve) => setTimeout(resolve, 20));
374
+ expect(q.beats.length).toBe(during);
375
+ });
376
+
377
+ // #696 Phase 5: the admin's scheduler queues automatic requests in the
378
+ // same table (trigger 'auto', auto_basis, requested_by NULL); on a
379
+ // self-hosted install this agent is what installs them.
380
+ const AUTO_POLICY: StoredUpdatePolicy = { ...POLICY, mode: "auto", autoMinor: true };
381
+ const AUTO_REQUEST: ClaimedUpdate = {
382
+ ...REQUEST,
383
+ trigger: "auto",
384
+ autoBasis: "minor",
385
+ policy: AUTO_POLICY,
386
+ };
387
+
388
+ it("installs an automatic request the scheduler queued, as an automatic run", async () => {
389
+ const { deps, q, applied } = setup({}, AUTO_REQUEST);
390
+ const r = await runAgentOnce(deps);
391
+ expect(r).toEqual({ exitCode: 0, requestId: REQUEST.id, status: null });
392
+ expect(applied.map((a) => [a.targetVersion, a.trigger])).toEqual([["0.11.0", "auto"]]);
393
+ expect(q.ends.map((e) => e.status)).toEqual(["succeeded"]);
394
+ });
395
+
396
+ it("installs it once an Owner pause has run out", async () => {
397
+ const { deps, applied } = setup(
398
+ {},
399
+ {
400
+ ...AUTO_REQUEST,
401
+ policy: {
402
+ ...AUTO_POLICY,
403
+ paused: true,
404
+ pausedAt: "2026-10-06T12:00:00Z",
405
+ pausedUntil: "2026-10-13T11:00:00Z",
406
+ pausedByOwner: true,
407
+ },
408
+ },
409
+ );
410
+ await runAgentOnce(deps);
411
+ expect(applied.map((a) => a.trigger)).toEqual(["auto"]);
412
+ });
413
+
414
+ it("refuses an automatic request queued as a security fix the signed manifest does not back", async () => {
415
+ const { deps, q, applied } = setup(
416
+ {},
417
+ {
418
+ ...AUTO_REQUEST,
419
+ autoBasis: "security",
420
+ policy: { ...AUTO_POLICY, autoSecurity: true },
421
+ },
422
+ );
423
+ await runAgentOnce(deps);
424
+ expect(applied).toEqual([]);
425
+ expect(q.ends[0]).toMatchObject({ status: "refused", codes: ["policy"] });
426
+ expect(q.ends[0]?.reason).toMatch(/queued as a security fix/);
427
+ });
428
+
429
+ it("pauses automatic installs when an automatic run fails", async () => {
430
+ const { deps, q } = setup(
431
+ { preflight: async () => "cosign is missing." },
432
+ { ...REQUEST, trigger: "auto" },
433
+ );
434
+ await runAgentOnce(deps);
435
+ expect(q.ends[0]).toMatchObject({ status: "failed", pauseAutoUpdates: true });
436
+ });
437
+ });