@serfab/cadre-host 1.13.0 → 1.15.0

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 (352) hide show
  1. package/README.md +347 -423
  2. package/dist/bin/host.d.ts +6 -3
  3. package/dist/bin/host.js +332 -524
  4. package/dist/bin/host.js.map +1 -1
  5. package/dist/bin/join-output.d.ts +56 -0
  6. package/dist/bin/join-output.js +92 -0
  7. package/dist/bin/join-output.js.map +1 -0
  8. package/dist/bin/nat-output.d.ts +54 -0
  9. package/dist/bin/nat-output.js +138 -0
  10. package/dist/bin/nat-output.js.map +1 -0
  11. package/dist/bin/ui-access.d.ts +22 -0
  12. package/dist/bin/ui-access.js +44 -0
  13. package/dist/bin/ui-access.js.map +1 -0
  14. package/dist/hosted/claim-details.d.ts +66 -0
  15. package/dist/hosted/claim-details.js +60 -0
  16. package/dist/hosted/claim-details.js.map +1 -0
  17. package/dist/hosted/hosted-node-service.d.ts +325 -0
  18. package/dist/hosted/hosted-node-service.js +678 -0
  19. package/dist/hosted/hosted-node-service.js.map +1 -0
  20. package/dist/hosted/hosted-node-store.d.ts +40 -0
  21. package/dist/hosted/hosted-node-store.js +126 -0
  22. package/dist/hosted/hosted-node-store.js.map +1 -0
  23. package/dist/{donation/donation-supervisor.d.ts → hosted/hosted-node-supervisor.d.ts} +70 -53
  24. package/dist/hosted/hosted-node-supervisor.js +396 -0
  25. package/dist/hosted/hosted-node-supervisor.js.map +1 -0
  26. package/dist/hosted/hosted-node-watcher.d.ts +95 -0
  27. package/dist/hosted/hosted-node-watcher.js +260 -0
  28. package/dist/hosted/hosted-node-watcher.js.map +1 -0
  29. package/dist/hosted/index.d.ts +16 -0
  30. package/dist/hosted/index.js +13 -0
  31. package/dist/hosted/index.js.map +1 -0
  32. package/dist/hosted/node-status.d.ts +51 -0
  33. package/dist/hosted/node-status.js +77 -0
  34. package/dist/hosted/node-status.js.map +1 -0
  35. package/dist/hosted/types.d.ts +141 -0
  36. package/dist/hosted/types.js +22 -0
  37. package/dist/hosted/types.js.map +1 -0
  38. package/dist/index.d.ts +12 -24
  39. package/dist/index.js +9 -19
  40. package/dist/index.js.map +1 -1
  41. package/dist/installer/config.d.ts +27 -34
  42. package/dist/installer/config.js +46 -15
  43. package/dist/installer/config.js.map +1 -1
  44. package/dist/installer/index.d.ts +9 -19
  45. package/dist/installer/index.js +21 -85
  46. package/dist/installer/index.js.map +1 -1
  47. package/dist/installer/paths.d.ts +0 -2
  48. package/dist/installer/paths.js +0 -4
  49. package/dist/installer/paths.js.map +1 -1
  50. package/dist/installer/wizard.d.ts +12 -9
  51. package/dist/installer/wizard.js +15 -14
  52. package/dist/installer/wizard.js.map +1 -1
  53. package/dist/nat/address-resolver.d.ts +18 -17
  54. package/dist/nat/address-resolver.js +79 -25
  55. package/dist/nat/address-resolver.js.map +1 -1
  56. package/dist/nat/address-watch.d.ts +37 -0
  57. package/dist/nat/address-watch.js +84 -0
  58. package/dist/nat/address-watch.js.map +1 -0
  59. package/dist/nat/ddns/duckdns.d.ts +2 -1
  60. package/dist/nat/ddns/duckdns.js +5 -1
  61. package/dist/nat/ddns/duckdns.js.map +1 -1
  62. package/dist/nat/ddns/index.d.ts +6 -0
  63. package/dist/nat/ddns/index.js.map +1 -1
  64. package/dist/nat/index.d.ts +13 -9
  65. package/dist/nat/index.js +7 -5
  66. package/dist/nat/index.js.map +1 -1
  67. package/dist/nat/nat-service.d.ts +220 -77
  68. package/dist/nat/nat-service.js +653 -239
  69. package/dist/nat/nat-service.js.map +1 -1
  70. package/dist/nat/nat-store.d.ts +12 -5
  71. package/dist/nat/nat-store.js +124 -29
  72. package/dist/nat/nat-store.js.map +1 -1
  73. package/dist/nat/network-mode.d.ts +19 -0
  74. package/dist/nat/network-mode.js +32 -0
  75. package/dist/nat/network-mode.js.map +1 -0
  76. package/dist/nat/port-mapper.d.ts +83 -79
  77. package/dist/nat/port-mapper.js +148 -189
  78. package/dist/nat/port-mapper.js.map +1 -1
  79. package/dist/nat/reachability.d.ts +32 -15
  80. package/dist/nat/reachability.js +110 -22
  81. package/dist/nat/reachability.js.map +1 -1
  82. package/dist/nat/types.d.ts +98 -27
  83. package/dist/nat/types.js +8 -5
  84. package/dist/nat/types.js.map +1 -1
  85. package/dist/orchestrator/host-process-orchestrator.d.ts +32 -74
  86. package/dist/orchestrator/host-process-orchestrator.js +84 -233
  87. package/dist/orchestrator/host-process-orchestrator.js.map +1 -1
  88. package/dist/{installer/identity.d.ts → orchestrator/identity-file.d.ts} +5 -5
  89. package/dist/{installer/identity.js → orchestrator/identity-file.js} +6 -6
  90. package/dist/orchestrator/identity-file.js.map +1 -0
  91. package/dist/orchestrator/index.d.ts +2 -3
  92. package/dist/orchestrator/index.js +1 -1
  93. package/dist/orchestrator/index.js.map +1 -1
  94. package/dist/orchestrator/node-identity.d.ts +6 -6
  95. package/dist/orchestrator/node-identity.js +7 -7
  96. package/dist/orchestrator/node-identity.js.map +1 -1
  97. package/dist/orchestrator/port-allocator.d.ts +5 -6
  98. package/dist/orchestrator/port-allocator.js +9 -11
  99. package/dist/orchestrator/port-allocator.js.map +1 -1
  100. package/dist/orchestrator/state-store.d.ts +3 -5
  101. package/dist/orchestrator/state-store.js +2 -10
  102. package/dist/orchestrator/state-store.js.map +1 -1
  103. package/dist/orchestrator/types.d.ts +33 -22
  104. package/dist/orchestrator/types.js.map +1 -1
  105. package/dist/package-version.d.ts +1 -0
  106. package/dist/package-version.js +4 -0
  107. package/dist/package-version.js.map +1 -0
  108. package/dist/server/error-handler.d.ts +5 -25
  109. package/dist/server/error-handler.js +11 -74
  110. package/dist/server/error-handler.js.map +1 -1
  111. package/dist/server/events/types.d.ts +7 -11
  112. package/dist/server/events/types.js +2 -1
  113. package/dist/server/events/types.js.map +1 -1
  114. package/dist/server/index.d.ts +11 -37
  115. package/dist/server/index.js +30 -48
  116. package/dist/server/index.js.map +1 -1
  117. package/dist/server/routes/hosted-nodes.d.ts +28 -0
  118. package/dist/server/routes/hosted-nodes.js +68 -0
  119. package/dist/server/routes/hosted-nodes.js.map +1 -0
  120. package/dist/server/routes/nat.d.ts +4 -1
  121. package/dist/server/routes/nat.js +13 -2
  122. package/dist/server/routes/nat.js.map +1 -1
  123. package/dist/server/routes/nodes.d.ts +6 -20
  124. package/dist/server/routes/nodes.js +7 -86
  125. package/dist/server/routes/nodes.js.map +1 -1
  126. package/dist/server/routes/settings.d.ts +3 -7
  127. package/dist/server/routes/settings.js +18 -8
  128. package/dist/server/routes/settings.js.map +1 -1
  129. package/dist/server/routes/status.d.ts +2 -25
  130. package/dist/server/routes/status.js +3 -43
  131. package/dist/server/routes/status.js.map +1 -1
  132. package/dist/server/server.js +8 -8
  133. package/dist/server/settings-store.d.ts +2 -2
  134. package/dist/server/settings-store.js +2 -2
  135. package/dist/server/static.js +1 -2
  136. package/dist/server/static.js.map +1 -1
  137. package/dist/ui/assets/index-BHd8CWZA.css +1 -0
  138. package/dist/ui/assets/index-Ccr2GjPT.js +23 -0
  139. package/dist/ui/assets/index-Ccr2GjPT.js.map +1 -0
  140. package/dist/ui/index.html +2 -2
  141. package/dist/update/index.d.ts +2 -2
  142. package/dist/update/index.js +2 -2
  143. package/dist/update/store.d.ts +1 -1
  144. package/dist/update/store.js +1 -1
  145. package/dist/update/types.d.ts +1 -1
  146. package/dist/update/types.js +1 -1
  147. package/package.json +5 -7
  148. package/src/__tests__/cli-nat.smoke.test.ts +219 -212
  149. package/src/__tests__/cli.smoke.test.ts +2 -2
  150. package/src/__tests__/global-setup.ts +2 -3
  151. package/src/__tests__/orchestrator-claim-secret.test.ts +111 -0
  152. package/src/__tests__/orchestrator-env-scrub.test.ts +160 -163
  153. package/src/__tests__/orchestrator-handle-contract.ts +134 -134
  154. package/src/__tests__/orchestrator-node-identity.test.ts +177 -203
  155. package/src/__tests__/orchestrator-ports.test.ts +124 -143
  156. package/src/__tests__/orchestrator-push.test.ts +138 -146
  157. package/src/__tests__/orchestrator.test.ts +1017 -1063
  158. package/src/__tests__/ui-access.test.ts +30 -0
  159. package/src/bin/host.ts +397 -572
  160. package/src/bin/join-output.ts +120 -0
  161. package/src/bin/nat-output.ts +188 -0
  162. package/src/bin/ui-access.ts +50 -0
  163. package/src/{donation → hosted}/__tests__/fake-orchestrator.test.ts +172 -173
  164. package/src/{donation → hosted}/__tests__/fake-orchestrator.ts +43 -23
  165. package/src/hosted/__tests__/hosted-node-service.test.ts +708 -0
  166. package/src/hosted/__tests__/hosted-node-supervisor.test.ts +475 -0
  167. package/src/hosted/__tests__/test-invitation.ts +33 -0
  168. package/src/hosted/claim-details.ts +103 -0
  169. package/src/hosted/hosted-node-service.ts +814 -0
  170. package/src/hosted/hosted-node-store.ts +144 -0
  171. package/src/hosted/hosted-node-supervisor.ts +439 -0
  172. package/src/hosted/hosted-node-watcher.ts +288 -0
  173. package/src/hosted/index.ts +53 -0
  174. package/src/hosted/node-status.ts +97 -0
  175. package/src/hosted/types.ts +160 -0
  176. package/src/index.ts +72 -91
  177. package/src/installer/__tests__/config.test.ts +15 -40
  178. package/src/installer/__tests__/installer.smoke.test.ts +184 -263
  179. package/src/installer/__tests__/wizard.test.ts +25 -13
  180. package/src/installer/config.ts +201 -175
  181. package/src/installer/index.ts +252 -327
  182. package/src/installer/paths.ts +104 -109
  183. package/src/installer/wizard.ts +24 -26
  184. package/src/nat/__tests__/address-resolver.test.ts +107 -82
  185. package/src/nat/__tests__/nat-service.test.ts +608 -515
  186. package/src/nat/__tests__/nat-store.test.ts +49 -12
  187. package/src/nat/__tests__/network-mode.test.ts +38 -0
  188. package/src/nat/__tests__/port-mapper.test.ts +30 -191
  189. package/src/nat/__tests__/reachability.test.ts +96 -13
  190. package/src/nat/address-resolver.ts +94 -52
  191. package/src/nat/address-watch.ts +92 -0
  192. package/src/nat/ddns/duckdns.ts +5 -1
  193. package/src/nat/ddns/index.ts +6 -0
  194. package/src/nat/index.ts +33 -13
  195. package/src/nat/nat-service.ts +942 -501
  196. package/src/nat/nat-store.ts +126 -31
  197. package/src/nat/network-mode.ts +36 -0
  198. package/src/nat/port-mapper.ts +241 -329
  199. package/src/nat/reachability.ts +155 -34
  200. package/src/nat/types.ts +110 -37
  201. package/src/{installer/__tests__/identity.test.ts → orchestrator/__tests__/identity-file.test.ts} +34 -34
  202. package/src/orchestrator/__tests__/node-identity.test.ts +62 -62
  203. package/src/orchestrator/host-process-orchestrator.ts +1089 -1260
  204. package/src/{installer/identity.ts → orchestrator/identity-file.ts} +56 -56
  205. package/src/orchestrator/index.ts +9 -10
  206. package/src/orchestrator/node-identity.ts +41 -41
  207. package/src/orchestrator/port-allocator.ts +71 -73
  208. package/src/orchestrator/state-store.ts +76 -90
  209. package/src/orchestrator/types.ts +34 -23
  210. package/src/package-version.ts +4 -0
  211. package/src/server/__tests__/bus.test.ts +38 -38
  212. package/src/server/__tests__/error-handler.test.ts +81 -118
  213. package/src/server/__tests__/fakes.ts +15 -18
  214. package/src/server/__tests__/hosted-nodes-route.test.ts +161 -0
  215. package/src/server/__tests__/nodes-route.test.ts +114 -283
  216. package/src/server/__tests__/publishers.test.ts +193 -180
  217. package/src/server/__tests__/server.smoke.test.ts +134 -223
  218. package/src/server/__tests__/settings-route.test.ts +210 -195
  219. package/src/server/__tests__/sse-route.test.ts +119 -119
  220. package/src/server/__tests__/status-route.test.ts +82 -121
  221. package/src/server/error-handler.ts +112 -178
  222. package/src/server/events/types.ts +20 -24
  223. package/src/server/index.ts +223 -267
  224. package/src/server/routes/hosted-nodes.ts +83 -0
  225. package/src/server/routes/nat.ts +75 -63
  226. package/src/server/routes/nodes.ts +96 -188
  227. package/src/server/routes/settings.ts +20 -15
  228. package/src/server/routes/status.ts +5 -67
  229. package/src/server/server.ts +8 -8
  230. package/src/server/settings-store.ts +2 -2
  231. package/src/server/static.ts +1 -2
  232. package/src/update/index.ts +2 -2
  233. package/src/update/store.ts +1 -1
  234. package/src/update/types.ts +1 -1
  235. package/dist/auth/duration.d.ts +0 -1
  236. package/dist/auth/duration.js +0 -32
  237. package/dist/auth/duration.js.map +0 -1
  238. package/dist/auth/index.d.ts +0 -11
  239. package/dist/auth/index.js +0 -9
  240. package/dist/auth/index.js.map +0 -1
  241. package/dist/auth/self-label.d.ts +0 -36
  242. package/dist/auth/self-label.js +0 -57
  243. package/dist/auth/self-label.js.map +0 -1
  244. package/dist/auth/trust-circle-store.d.ts +0 -26
  245. package/dist/auth/trust-circle-store.js +0 -129
  246. package/dist/auth/trust-circle-store.js.map +0 -1
  247. package/dist/auth/trust-circle.d.ts +0 -156
  248. package/dist/auth/trust-circle.js +0 -307
  249. package/dist/auth/trust-circle.js.map +0 -1
  250. package/dist/auth/types.d.ts +0 -84
  251. package/dist/auth/types.js +0 -24
  252. package/dist/auth/types.js.map +0 -1
  253. package/dist/donation/donation-service.d.ts +0 -389
  254. package/dist/donation/donation-service.js +0 -828
  255. package/dist/donation/donation-service.js.map +0 -1
  256. package/dist/donation/donation-store.d.ts +0 -51
  257. package/dist/donation/donation-store.js +0 -152
  258. package/dist/donation/donation-store.js.map +0 -1
  259. package/dist/donation/donation-supervisor.js +0 -364
  260. package/dist/donation/donation-supervisor.js.map +0 -1
  261. package/dist/donation/grant-service.d.ts +0 -80
  262. package/dist/donation/grant-service.js +0 -210
  263. package/dist/donation/grant-service.js.map +0 -1
  264. package/dist/donation/grant-store.d.ts +0 -38
  265. package/dist/donation/grant-store.js +0 -135
  266. package/dist/donation/grant-store.js.map +0 -1
  267. package/dist/donation/index.d.ts +0 -20
  268. package/dist/donation/index.js +0 -17
  269. package/dist/donation/index.js.map +0 -1
  270. package/dist/donation/types.d.ts +0 -224
  271. package/dist/donation/types.js +0 -40
  272. package/dist/donation/types.js.map +0 -1
  273. package/dist/installer/identity.js.map +0 -1
  274. package/dist/owner/index.d.ts +0 -11
  275. package/dist/owner/index.js +0 -11
  276. package/dist/owner/index.js.map +0 -1
  277. package/dist/owner/owner-node-client.d.ts +0 -110
  278. package/dist/owner/owner-node-client.js +0 -184
  279. package/dist/owner/owner-node-client.js.map +0 -1
  280. package/dist/server/routes/bootstrap-node-validation.d.ts +0 -115
  281. package/dist/server/routes/bootstrap-node-validation.js +0 -197
  282. package/dist/server/routes/bootstrap-node-validation.js.map +0 -1
  283. package/dist/server/routes/grants-admin.d.ts +0 -29
  284. package/dist/server/routes/grants-admin.js +0 -58
  285. package/dist/server/routes/grants-admin.js.map +0 -1
  286. package/dist/server/routes/grants.d.ts +0 -28
  287. package/dist/server/routes/grants.js +0 -165
  288. package/dist/server/routes/grants.js.map +0 -1
  289. package/dist/server/routes/provision-request-validation.d.ts +0 -45
  290. package/dist/server/routes/provision-request-validation.js +0 -97
  291. package/dist/server/routes/provision-request-validation.js.map +0 -1
  292. package/dist/server/routes/strands.d.ts +0 -20
  293. package/dist/server/routes/strands.js +0 -38
  294. package/dist/server/routes/strands.js.map +0 -1
  295. package/dist/server/routes/trust-circle.d.ts +0 -17
  296. package/dist/server/routes/trust-circle.js +0 -35
  297. package/dist/server/routes/trust-circle.js.map +0 -1
  298. package/dist/strands/index.d.ts +0 -7
  299. package/dist/strands/index.js +0 -6
  300. package/dist/strands/index.js.map +0 -1
  301. package/dist/strands/strand-service.d.ts +0 -50
  302. package/dist/strands/strand-service.js +0 -103
  303. package/dist/strands/strand-service.js.map +0 -1
  304. package/dist/strands/types.d.ts +0 -93
  305. package/dist/strands/types.js +0 -23
  306. package/dist/strands/types.js.map +0 -1
  307. package/dist/ui/assets/index-C6nELNfC.js +0 -23
  308. package/dist/ui/assets/index-C6nELNfC.js.map +0 -1
  309. package/dist/ui/assets/index-TtIYSeZO.css +0 -1
  310. package/src/__tests__/cli-invite.smoke.test.ts +0 -192
  311. package/src/__tests__/host-owner.smoke.test.ts +0 -123
  312. package/src/__tests__/orchestrator-owner.test.ts +0 -234
  313. package/src/__tests__/orchestrator-pin-keys.test.ts +0 -152
  314. package/src/auth/__tests__/duration.test.ts +0 -28
  315. package/src/auth/__tests__/self-label.test.ts +0 -85
  316. package/src/auth/__tests__/trust-circle-integration.test.ts +0 -152
  317. package/src/auth/__tests__/trust-circle-store.test.ts +0 -115
  318. package/src/auth/__tests__/trust-circle.test.ts +0 -445
  319. package/src/auth/duration.ts +0 -33
  320. package/src/auth/index.ts +0 -21
  321. package/src/auth/self-label.ts +0 -74
  322. package/src/auth/trust-circle-store.ts +0 -155
  323. package/src/auth/trust-circle.ts +0 -395
  324. package/src/auth/types.ts +0 -99
  325. package/src/donation/__tests__/donation-service.test.ts +0 -1193
  326. package/src/donation/__tests__/donation-supervisor.test.ts +0 -548
  327. package/src/donation/__tests__/grant-service.test.ts +0 -213
  328. package/src/donation/__tests__/grant-store.test.ts +0 -113
  329. package/src/donation/donation-service.ts +0 -1033
  330. package/src/donation/donation-store.ts +0 -176
  331. package/src/donation/donation-supervisor.ts +0 -411
  332. package/src/donation/grant-service.ts +0 -252
  333. package/src/donation/grant-store.ts +0 -152
  334. package/src/donation/index.ts +0 -65
  335. package/src/donation/types.ts +0 -260
  336. package/src/owner/__tests__/owner-node-client.test.ts +0 -194
  337. package/src/owner/index.ts +0 -12
  338. package/src/owner/owner-node-client.ts +0 -259
  339. package/src/server/__tests__/bootstrap-node-validation.test.ts +0 -138
  340. package/src/server/__tests__/grants-admin-route.test.ts +0 -171
  341. package/src/server/__tests__/grants-route.test.ts +0 -394
  342. package/src/server/__tests__/strands-route.test.ts +0 -234
  343. package/src/server/routes/bootstrap-node-validation.ts +0 -206
  344. package/src/server/routes/grants-admin.ts +0 -74
  345. package/src/server/routes/grants.ts +0 -193
  346. package/src/server/routes/provision-request-validation.ts +0 -105
  347. package/src/server/routes/strands.ts +0 -54
  348. package/src/server/routes/trust-circle.ts +0 -49
  349. package/src/strands/__tests__/strand-service.test.ts +0 -165
  350. package/src/strands/index.ts +0 -14
  351. package/src/strands/strand-service.ts +0 -135
  352. package/src/strands/types.ts +0 -102
@@ -1,173 +1,172 @@
1
- /**
2
- * Contract tests for `FakeOrchestrator` itself — the handle lifecycle it borrows
3
- * from `HostProcessOrchestrator`. The donation suites lean on these semantics to
4
- * express races that only exist because the real class drops and rejects
5
- * handles, so this file exists to stop a later relaxation of the fake from
6
- * quietly turning one of those tests green.
7
- *
8
- * The rules the fake shares with the real class live in
9
- * `src/__tests__/orchestrator-handle-contract.ts` and run against both — here
10
- * and in `src/__tests__/orchestrator.test.ts` — so a disagreement between the
11
- * two fails a test. The cases in this file add what only the fake has: its
12
- * recording arrays (`stopped`, `removed`, `reclaimedWorkdirs`) and its hooks.
13
- * Each names the real behaviour it mirrors.
14
- */
15
-
16
- import { describe, expect, it } from 'vitest';
17
-
18
- import type { OrchestratorCreateRequest } from '@serfab/cadre-provider';
19
-
20
- import { describeHandleContract } from '../../__tests__/orchestrator-handle-contract.js';
21
- import { FakeOrchestrator } from './fake-orchestrator.js';
22
-
23
- const request = (containerId: string): OrchestratorCreateRequest => ({
24
- containerId,
25
- partyId: 'party-P',
26
- bootstrapNodes: ['/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWA9hbnKrRnPRSPTRkzXqTHzGE8YpJ3JHZmQ5tGwLRTMmp'],
27
- profile: 'storage',
28
- });
29
-
30
- describeHandleContract<FakeOrchestrator>('FakeOrchestrator', {
31
- make: () => new FakeOrchestrator(),
32
- request,
33
- // No process to wait for: a fake child is running as soon as it exists.
34
- started: () => Promise.resolve(),
35
- failNextCreate: (orch) => { orch.failCreate = true; },
36
- });
37
-
38
- describe('FakeOrchestrator handle lifecycle', () => {
39
- it('drops the prior handle when a re-spawn succeeds (mirrors dropStaleHandle)', async () => {
40
- const orch = new FakeOrchestrator();
41
- const first = await orch.createContainer(request('grn_1'));
42
- orch.crash(first.dockerId);
43
- const second = await orch.createContainer(request('grn_1'));
44
-
45
- await expect(orch.stopContainer(first.dockerId)).rejects.toThrow(
46
- `Container not found: ${first.dockerId}`,
47
- );
48
- // The rejected call left no trace: `stopped` records handles acted on, not
49
- // calls attempted.
50
- expect(orch.stopped).toEqual([]);
51
- // …and the container now resolves only to the new spawn.
52
- expect(orch.resolveDockerId('grn_1')).toBe(second.dockerId);
53
-
54
- await orch.stopContainer(second.dockerId);
55
- expect(orch.stopped).toEqual([second.dockerId]);
56
- });
57
-
58
- it('keeps the prior handle live across the whole pre-drop await window', async () => {
59
- const orch = new FakeOrchestrator();
60
- const first = await orch.createContainer(request('grn_1'));
61
-
62
- // The invariant the donation race tests rest on: a concurrent stop started
63
- // from `onCreate` still finds the old handle. Deferring it onto a microtask
64
- // puts it strictly after `onCreate` returns and strictly before the
65
- // `createDelayMs` timer, so this one case pins the drop behind BOTH — with
66
- // no dependence on wall-clock timing. The outcome is captured rather than
67
- // rethrown so a regression fails this assertion instead of surfacing as an
68
- // unhandled rejection.
69
- orch.createDelayMs = 1;
70
- let outcome: Promise<string> | undefined;
71
- orch.onCreate = () => {
72
- outcome ??= Promise.resolve()
73
- .then(() => orch.stopContainer(first.dockerId))
74
- .then(() => 'stopped', (err: Error) => `rejected: ${err.message}`);
75
- };
76
-
77
- const second = await orch.createContainer(request('grn_1'));
78
-
79
- expect(await outcome).toBe('stopped');
80
- expect(orch.stopped).toEqual([first.dockerId]);
81
- expect(orch.resolveDockerId('grn_1')).toBe(second.dockerId);
82
- });
83
-
84
- it('leaves the prior handle in place when the re-spawn fails (mirrors restoreDroppedHandles)', async () => {
85
- const orch = new FakeOrchestrator();
86
- const first = await orch.createContainer(request('grn_1'));
87
-
88
- orch.failCreate = true;
89
- await expect(orch.createContainer(request('grn_1'))).rejects.toThrow('spawn boom');
90
-
91
- // Host state is exactly as the failed attempt found it — which is what lets
92
- // `DonationSupervisor.giveUp` still stop the child it knows about.
93
- await expect(orch.stopContainer(first.dockerId)).resolves.toBeUndefined();
94
- expect(orch.stopped).toEqual([first.dockerId]);
95
- expect(orch.resolveDockerId('grn_1')).toBe(first.dockerId);
96
- });
97
-
98
- it('rejects a never-issued dockerId (mirrors requireHandle)', async () => {
99
- const orch = new FakeOrchestrator();
100
-
101
- await expect(orch.removeContainer('dock_nope')).rejects.toThrow(
102
- 'Container not found: dock_nope',
103
- );
104
- expect(orch.removed).toEqual([]);
105
- });
106
-
107
- it('rejects a second call once a handle has been removed (mirrors removeContainer deleting it)', async () => {
108
- const orch = new FakeOrchestrator();
109
- const { dockerId } = await orch.createContainer(request('grn_1'));
110
-
111
- await orch.removeContainer(dockerId);
112
- expect(orch.removed).toEqual([dockerId]);
113
-
114
- await expect(orch.stopContainer(dockerId)).rejects.toThrow(
115
- `Container not found: ${dockerId}`,
116
- );
117
- expect(orch.stopped).toEqual([]);
118
- });
119
-
120
- it('fires onStop for a rejected stop, so a test can still observe the attempt', async () => {
121
- const orch = new FakeOrchestrator();
122
- const attempted: string[] = [];
123
- orch.onStop = (id) => { attempted.push(id); };
124
-
125
- await expect(orch.stopContainer('dock_nope')).rejects.toThrow('Container not found');
126
- expect(attempted).toEqual(['dock_nope']);
127
- expect(orch.stopped).toEqual([]);
128
- });
129
-
130
- it('refuses to reclaim the workdir of a container that still resolves (mirrors the live-handle guard)', async () => {
131
- const orch = new FakeOrchestrator();
132
- await orch.createContainer(request('grn_1'));
133
-
134
- // A live handle owns that directory; only `removeContainer` — which stops
135
- // the child first — may delete it.
136
- expect(orch.reclaimWorkdir('grn_1')).toBe(false);
137
- expect(orch.reclaimedWorkdirs).toEqual([]);
138
- });
139
-
140
- it('reclaims the workdir of a container no handle owns', async () => {
141
- const orch = new FakeOrchestrator();
142
-
143
- // Never spawned — the stuck-`provisioning` record whose host died before a
144
- // handle ever existed.
145
- expect(orch.reclaimWorkdir('grn_orphan')).toBe(true);
146
- expect(orch.reclaimedWorkdirs).toEqual(['grn_orphan']);
147
-
148
- // …and once the container's own handle is removed, its id becomes
149
- // reclaimable too.
150
- const { dockerId } = await orch.createContainer(request('grn_1'));
151
- expect(orch.reclaimWorkdir('grn_1')).toBe(false);
152
- await orch.removeContainer(dockerId);
153
- expect(orch.reclaimWorkdir('grn_1')).toBe(true);
154
- expect(orch.reclaimedWorkdirs).toEqual(['grn_orphan', 'grn_1']);
155
- });
156
-
157
- it('fires onSpawned after the drop, with the new handle live and the old one gone', async () => {
158
- const orch = new FakeOrchestrator();
159
- const first = await orch.createContainer(request('grn_1'));
160
- orch.crash(first.dockerId);
161
-
162
- let observed: { spawned: string; resolves: string | undefined } | undefined;
163
- orch.onSpawned = (dockerId) => {
164
- observed = { spawned: dockerId, resolves: orch.resolveDockerId('grn_1') };
165
- };
166
- const second = await orch.createContainer(request('grn_1'));
167
-
168
- // The post-drop world: the container already resolves to the new handle, so
169
- // work driven from here sees what `abandonRespawn`'s caller would see.
170
- expect(observed).toEqual({ spawned: second.dockerId, resolves: second.dockerId });
171
- expect(second.dockerId).not.toBe(first.dockerId);
172
- });
173
- });
1
+ /**
2
+ * Contract tests for `FakeOrchestrator` itself — the handle lifecycle it borrows
3
+ * from `HostProcessOrchestrator`. The hosted-node suites lean on these semantics to
4
+ * express races that only exist because the real class drops and rejects
5
+ * handles, so this file exists to stop a later relaxation of the fake from
6
+ * quietly turning one of those tests green.
7
+ *
8
+ * The rules the fake shares with the real class live in
9
+ * `src/__tests__/orchestrator-handle-contract.ts` and run against both — here
10
+ * and in `src/__tests__/orchestrator.test.ts` — so a disagreement between the
11
+ * two fails a test. The cases in this file add what only the fake has: its
12
+ * recording arrays (`stopped`, `removed`, `reclaimedWorkdirs`) and its hooks.
13
+ * Each names the real behaviour it mirrors.
14
+ */
15
+
16
+ import { describe, expect, it } from 'vitest';
17
+
18
+ import type { HostedSpawnRequest } from '../../orchestrator/types.js';
19
+
20
+ import { describeHandleContract } from '../../__tests__/orchestrator-handle-contract.js';
21
+ import { FakeOrchestrator } from './fake-orchestrator.js';
22
+
23
+ const request = (containerId: string): HostedSpawnRequest => ({
24
+ containerId,
25
+ partyId: 'party-P',
26
+ profile: 'storage',
27
+ });
28
+
29
+ describeHandleContract<FakeOrchestrator>('FakeOrchestrator', {
30
+ make: () => new FakeOrchestrator(),
31
+ request,
32
+ // No process to wait for: a fake child is running as soon as it exists.
33
+ started: () => Promise.resolve(),
34
+ failNextCreate: (orch) => { orch.failCreate = true; },
35
+ });
36
+
37
+ describe('FakeOrchestrator handle lifecycle', () => {
38
+ it('drops the prior handle when a re-spawn succeeds (mirrors dropStaleHandle)', async () => {
39
+ const orch = new FakeOrchestrator();
40
+ const first = await orch.createContainer(request('hn_1'));
41
+ orch.crash(first.dockerId);
42
+ const second = await orch.createContainer(request('hn_1'));
43
+
44
+ await expect(orch.stopContainer(first.dockerId)).rejects.toThrow(
45
+ `Container not found: ${first.dockerId}`,
46
+ );
47
+ // The rejected call left no trace: `stopped` records handles acted on, not
48
+ // calls attempted.
49
+ expect(orch.stopped).toEqual([]);
50
+ // …and the container now resolves only to the new spawn.
51
+ expect(orch.resolveDockerId('hn_1')).toBe(second.dockerId);
52
+
53
+ await orch.stopContainer(second.dockerId);
54
+ expect(orch.stopped).toEqual([second.dockerId]);
55
+ });
56
+
57
+ it('keeps the prior handle live across the whole pre-drop await window', async () => {
58
+ const orch = new FakeOrchestrator();
59
+ const first = await orch.createContainer(request('hn_1'));
60
+
61
+ // The invariant the hosted-node race tests rest on: a concurrent stop started
62
+ // from `onCreate` still finds the old handle. Deferring it onto a microtask
63
+ // puts it strictly after `onCreate` returns and strictly before the
64
+ // `createDelayMs` timer, so this one case pins the drop behind BOTH — with
65
+ // no dependence on wall-clock timing. The outcome is captured rather than
66
+ // rethrown so a regression fails this assertion instead of surfacing as an
67
+ // unhandled rejection.
68
+ orch.createDelayMs = 1;
69
+ let outcome: Promise<string> | undefined;
70
+ orch.onCreate = () => {
71
+ outcome ??= Promise.resolve()
72
+ .then(() => orch.stopContainer(first.dockerId))
73
+ .then(() => 'stopped', (err: Error) => `rejected: ${err.message}`);
74
+ };
75
+
76
+ const second = await orch.createContainer(request('hn_1'));
77
+
78
+ expect(await outcome).toBe('stopped');
79
+ expect(orch.stopped).toEqual([first.dockerId]);
80
+ expect(orch.resolveDockerId('hn_1')).toBe(second.dockerId);
81
+ });
82
+
83
+ it('leaves the prior handle in place when the re-spawn fails (mirrors restoreDroppedHandles)', async () => {
84
+ const orch = new FakeOrchestrator();
85
+ const first = await orch.createContainer(request('hn_1'));
86
+
87
+ orch.failCreate = true;
88
+ await expect(orch.createContainer(request('hn_1'))).rejects.toThrow('spawn boom');
89
+
90
+ // Host state is exactly as the failed attempt found it — which is what lets
91
+ // `HostedNodeSupervisor.giveUp` still stop the child it knows about.
92
+ await expect(orch.stopContainer(first.dockerId)).resolves.toBeUndefined();
93
+ expect(orch.stopped).toEqual([first.dockerId]);
94
+ expect(orch.resolveDockerId('hn_1')).toBe(first.dockerId);
95
+ });
96
+
97
+ it('rejects a never-issued dockerId (mirrors requireHandle)', async () => {
98
+ const orch = new FakeOrchestrator();
99
+
100
+ await expect(orch.removeContainer('dock_nope')).rejects.toThrow(
101
+ 'Container not found: dock_nope',
102
+ );
103
+ expect(orch.removed).toEqual([]);
104
+ });
105
+
106
+ it('rejects a second call once a handle has been removed (mirrors removeContainer deleting it)', async () => {
107
+ const orch = new FakeOrchestrator();
108
+ const { dockerId } = await orch.createContainer(request('hn_1'));
109
+
110
+ await orch.removeContainer(dockerId);
111
+ expect(orch.removed).toEqual([dockerId]);
112
+
113
+ await expect(orch.stopContainer(dockerId)).rejects.toThrow(
114
+ `Container not found: ${dockerId}`,
115
+ );
116
+ expect(orch.stopped).toEqual([]);
117
+ });
118
+
119
+ it('fires onStop for a rejected stop, so a test can still observe the attempt', async () => {
120
+ const orch = new FakeOrchestrator();
121
+ const attempted: string[] = [];
122
+ orch.onStop = (id) => { attempted.push(id); };
123
+
124
+ await expect(orch.stopContainer('dock_nope')).rejects.toThrow('Container not found');
125
+ expect(attempted).toEqual(['dock_nope']);
126
+ expect(orch.stopped).toEqual([]);
127
+ });
128
+
129
+ it('refuses to reclaim the workdir of a container that still resolves (mirrors the live-handle guard)', async () => {
130
+ const orch = new FakeOrchestrator();
131
+ await orch.createContainer(request('hn_1'));
132
+
133
+ // A live handle owns that directory; only `removeContainer` — which stops
134
+ // the child first — may delete it.
135
+ expect(orch.reclaimWorkdir('hn_1')).toBe(false);
136
+ expect(orch.reclaimedWorkdirs).toEqual([]);
137
+ });
138
+
139
+ it('reclaims the workdir of a container no handle owns', async () => {
140
+ const orch = new FakeOrchestrator();
141
+
142
+ // Never spawned — the stuck-`spawning` record whose host died before a
143
+ // handle ever existed.
144
+ expect(orch.reclaimWorkdir('hn_orphan')).toBe(true);
145
+ expect(orch.reclaimedWorkdirs).toEqual(['hn_orphan']);
146
+
147
+ // …and once the container's own handle is removed, its id becomes
148
+ // reclaimable too.
149
+ const { dockerId } = await orch.createContainer(request('hn_1'));
150
+ expect(orch.reclaimWorkdir('hn_1')).toBe(false);
151
+ await orch.removeContainer(dockerId);
152
+ expect(orch.reclaimWorkdir('hn_1')).toBe(true);
153
+ expect(orch.reclaimedWorkdirs).toEqual(['hn_orphan', 'hn_1']);
154
+ });
155
+
156
+ it('fires onSpawned after the drop, with the new handle live and the old one gone', async () => {
157
+ const orch = new FakeOrchestrator();
158
+ const first = await orch.createContainer(request('hn_1'));
159
+ orch.crash(first.dockerId);
160
+
161
+ let observed: { spawned: string; resolves: string | undefined } | undefined;
162
+ orch.onSpawned = (dockerId) => {
163
+ observed = { spawned: dockerId, resolves: orch.resolveDockerId('hn_1') };
164
+ };
165
+ const second = await orch.createContainer(request('hn_1'));
166
+
167
+ // The post-drop world: the container already resolves to the new handle, so
168
+ // work driven from here sees what `abandonRespawn`'s caller would see.
169
+ expect(observed).toEqual({ spawned: second.dockerId, resolves: second.dockerId });
170
+ expect(second.dockerId).not.toBe(first.dockerId);
171
+ });
172
+ });
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Shared fake orchestrator for the donation unit tests — no real child
2
+ * Shared fake orchestrator for the hosted-node unit tests — no real child
3
3
  * processes. Records every create / stop / remove, hands out deterministic
4
4
  * unique spawn results, and tracks per-`dockerId` liveness so a test can crash
5
5
  * one node and let the supervisor observe exactly that one as down.
@@ -9,17 +9,16 @@
9
9
  * and run against both classes, so the two cannot come to disagree unnoticed.
10
10
  * The sibling `fake-orchestrator.test.ts` runs that contract and pins what only
11
11
  * the fake has (its recording arrays and hooks). Both exist so a future agent
12
- * cannot make a failing donation test go green by relaxing the fake.
12
+ * cannot make a failing hosted-node test go green by relaxing the fake.
13
13
  */
14
14
 
15
15
  import type {
16
16
  Orchestrator,
17
- OrchestratorCreateRequest,
18
17
  OrchestratorCreateResult,
19
18
  OrchestratorStats,
20
19
  } from '@serfab/cadre-provider';
21
20
 
22
- import type { ManagedNodeInfo, NodeStateListener } from '../../orchestrator/types.js';
21
+ import type { HostedSpawnRequest, ManagedNodeInfo, NodePorts, NodeStateListener } from '../../orchestrator/types.js';
23
22
 
24
23
  function sleep(ms: number): Promise<void> {
25
24
  return new Promise((res) => setTimeout(res, ms));
@@ -31,15 +30,18 @@ interface FakeChild {
31
30
  partyId: string;
32
31
  profile: 'storage' | 'transaction';
33
32
  running: boolean;
33
+ /** The ports a re-spawn comes back on (the real `reusedNodePorts`). */
34
+ ports: NodePorts;
34
35
  }
35
36
 
36
37
  /**
37
- * Mirrors the parts of `HostProcessOrchestrator`'s handle lifecycle the donation
38
+ * Mirrors the parts of `HostProcessOrchestrator`'s handle lifecycle the hosted-node
38
39
  * code depends on:
39
40
  *
40
41
  * - a **successful** `createContainer` drops every prior handle for the same
41
42
  * `containerId` (the real `dropStaleHandle`), so an old `dockerId` stops
42
- * resolving the moment its container re-spawns;
43
+ * resolving the moment its container re-spawns — and the new child comes back
44
+ * on the dropped handle's ports (the real `reusedNodePorts`);
43
45
  * - a **failed** `createContainer` leaves those handles exactly as it found them
44
46
  * (the real `restoreDroppedHandles`);
45
47
  * - `createContainer` refuses a container whose previous child is still running
@@ -56,7 +58,7 @@ interface FakeChild {
56
58
  * a test can observe the attempt separately from its outcome.
57
59
  */
58
60
  export class FakeOrchestrator implements Orchestrator {
59
- createCalls: OrchestratorCreateRequest[] = [];
61
+ createCalls: HostedSpawnRequest[] = [];
60
62
  stopped: string[] = [];
61
63
  removed: string[] = [];
62
64
  /**
@@ -78,7 +80,7 @@ export class FakeOrchestrator implements Orchestrator {
78
80
  * Work driven from here sees the previous handle still alive. Contrast
79
81
  * {@link onSpawned}.
80
82
  */
81
- onCreate?: (request: OrchestratorCreateRequest) => void;
83
+ onCreate?: (request: HostedSpawnRequest) => void;
82
84
  /**
83
85
  * Observation hook — fires once the drop has landed and the new child is
84
86
  * registered, immediately before `createContainer` resolves. Models the
@@ -95,7 +97,7 @@ export class FakeOrchestrator implements Orchestrator {
95
97
  private readonly listeners = new Set<NodeStateListener>();
96
98
  private counter = 0;
97
99
 
98
- async createContainer(request: OrchestratorCreateRequest): Promise<OrchestratorCreateResult> {
100
+ async createContainer(request: HostedSpawnRequest): Promise<OrchestratorCreateResult> {
99
101
  this.createCalls.push(request);
100
102
  this.onCreate?.(request);
101
103
  if (this.createDelayMs) await sleep(this.createDelayMs);
@@ -107,26 +109,30 @@ export class FakeOrchestrator implements Orchestrator {
107
109
  // Success-only, and placed here on purpose: the real drop happens after the
108
110
  // spawn's every `await` and cannot fail afterwards, and a create that throws
109
111
  // puts the handles back (`restoreDroppedHandles`) — so a failed create must
110
- // leave them untouched. `DonationSupervisor`'s give-up test depends on that.
112
+ // leave them untouched. `HostedNodeSupervisor`'s give-up test depends on that.
113
+ let ports: NodePorts | undefined;
111
114
  for (const [id, child] of this.children) {
112
- if (child.containerId === request.containerId) this.children.delete(id);
115
+ if (child.containerId === request.containerId) {
116
+ ports = child.ports;
117
+ this.children.delete(id);
118
+ }
113
119
  }
114
120
  const n = ++this.counter;
115
121
  const dockerId = `dock_${n}`;
122
+ ports ??= { health: 9000 + n, metrics: 9100 + n, p2p: 4000 + n, ws: 4100 + n };
116
123
  this.children.set(dockerId, {
117
124
  containerId: request.containerId,
118
125
  partyId: request.partyId,
119
126
  profile: request.profile,
120
127
  running: true,
128
+ ports,
121
129
  });
122
130
  this.onSpawned?.(dockerId);
123
131
  return {
124
132
  dockerId,
125
- healthEndpoint: `http://127.0.0.1:${9000 + n}/health`,
126
- metricsEndpoint: `http://127.0.0.1:${9000 + n}/metrics`,
127
- seedEndpoint: `http://127.0.0.1:${9000 + n}/seed`,
128
- seedToken: `seed-token-${n}`,
129
- p2pPort: 4000 + n,
133
+ healthEndpoint: `http://127.0.0.1:${ports.health}/health`,
134
+ metricsEndpoint: `http://127.0.0.1:${ports.metrics}/metrics`,
135
+ p2pPort: ports.p2p,
130
136
  };
131
137
  }
132
138
 
@@ -142,7 +148,7 @@ export class FakeOrchestrator implements Orchestrator {
142
148
  async removeContainer(dockerId: string): Promise<void> {
143
149
  // NOTE: the real `removeContainer` stops a still-live child first; this one
144
150
  // only deletes, so a remove aimed at a running child records no `stopped`
145
- // entry and emits no state change. Every donation caller stops before it
151
+ // entry and emits no state change. Every hosted-node caller stops before it
146
152
  // reclaims, so nothing sees the difference today — if one stops doing that,
147
153
  // stop the child here too.
148
154
  this.requireChild(dockerId);
@@ -152,8 +158,8 @@ export class FakeOrchestrator implements Orchestrator {
152
158
 
153
159
  /**
154
160
  * NOTE: this and {@link getLogs} take an unknown `dockerId` where the real
155
- * class throws (`requireHandle`). No donation path calls either, so the gap is
156
- * unobservable — route them through `requireChild` if one starts to.
161
+ * class throws (`requireHandle`). No hosted-node path calls either, so the gap
162
+ * is unobservable — route them through `requireChild` if one starts to.
157
163
  */
158
164
  async getStats(): Promise<OrchestratorStats> {
159
165
  return { cpuPercent: 0, memoryBytes: 0, networkRxBytes: 0, networkTxBytes: 0 };
@@ -173,6 +179,20 @@ export class FakeOrchestrator implements Orchestrator {
173
179
  return undefined;
174
180
  }
175
181
 
182
+ /** The node by containerId or dockerId, as the real `getNode`. */
183
+ getNode(idOrDockerId: string): ManagedNodeInfo | undefined {
184
+ const direct = this.children.get(idOrDockerId);
185
+ if (direct) return toNodeInfo(idOrDockerId, direct);
186
+ for (const [dockerId, child] of this.children) {
187
+ if (child.containerId === idOrDockerId) return toNodeInfo(dockerId, child);
188
+ }
189
+ return undefined;
190
+ }
191
+
192
+ listNodes(): ManagedNodeInfo[] {
193
+ return [...this.children].map(([dockerId, child]) => toNodeInfo(dockerId, child));
194
+ }
195
+
176
196
  /**
177
197
  * Remove the workdir of a container no handle owns, mirroring the real
178
198
  * `reclaimWorkdir`. Refuses (returns `false`, records nothing) while a child
@@ -181,7 +201,7 @@ export class FakeOrchestrator implements Orchestrator {
181
201
  *
182
202
  * There is no directory here to check for existence, so a first reclaim of an
183
203
  * unresolvable id always reports `true`; the real class returns `false` when
184
- * the path is already gone. Nothing in the donation layer branches on the
204
+ * the path is already gone. Nothing in the hosted-node layer branches on the
185
205
  * return value, so the gap is unobservable — model it if one starts to.
186
206
  */
187
207
  reclaimWorkdir(containerId: string): boolean {
@@ -207,8 +227,7 @@ export class FakeOrchestrator implements Orchestrator {
207
227
  this.emit(toNodeInfo(dockerId, child));
208
228
  }
209
229
 
210
- /** Emit an arbitrary state change (e.g. the owner node stopping). */
211
- emit(info: ManagedNodeInfo): void {
230
+ private emit(info: ManagedNodeInfo): void {
212
231
  for (const listener of this.listeners) listener(info);
213
232
  }
214
233
 
@@ -238,6 +257,7 @@ function toNodeInfo(dockerId: string, child: FakeChild): ManagedNodeInfo {
238
257
  status: child.running ? 'running' : 'stopped',
239
258
  spawnedAt: new Date(0).toISOString(),
240
259
  workdir: `/fake/${child.containerId}`,
241
- ports: { health: 0, metrics: 0, p2p: 0, admin: 0, ws: 0 },
260
+ ports: { ...child.ports },
261
+ announcedAddrs: [],
242
262
  };
243
263
  }