@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,134 +1,134 @@
1
- /**
2
- * The handle rules `HostProcessOrchestrator` and its test stand-in
3
- * `FakeOrchestrator` (`donation/__tests__/fake-orchestrator.ts`) must both obey,
4
- * written once and run against each: `orchestrator.test.ts` registers them over
5
- * the real class, `fake-orchestrator.test.ts` over the fake. A rule one of them
6
- * stops obeying fails in that file's run.
7
- *
8
- * Not a `.test.ts` file, so vitest does not collect it on its own.
9
- *
10
- * Only what both classes can satisfy belongs here — no ports, no files on disk,
11
- * none of the fake's recording arrays. Those stay in the two calling files.
12
- */
13
-
14
- import { describe, expect, it } from 'vitest';
15
-
16
- import type { OrchestratorCreateRequest, OrchestratorCreateResult } from '@serfab/cadre-provider';
17
-
18
- import type { HostProcessOrchestrator } from '../orchestrator/host-process-orchestrator.js';
19
-
20
- /** What the contract needs from either orchestrator. */
21
- export type ContractOrchestrator = Pick<
22
- HostProcessOrchestrator,
23
- 'createContainer' | 'stopContainer' | 'removeContainer' | 'resolveDockerId'
24
- >;
25
-
26
- export interface HandleContractHarness<O extends ContractOrchestrator> {
27
- /** A fresh orchestrator. Teardown is the calling file's own afterEach. */
28
- make(): O;
29
- request(containerId: string): OrchestratorCreateRequest;
30
- /** Resolve once the spawn named by `dockerId` reads as running. */
31
- started(orch: O, dockerId: string): Promise<void>;
32
- /**
33
- * Make `createContainer` for `containerId` throw from here on. Only called
34
- * after that container has been spawned once and stopped, and no case creates
35
- * again afterwards.
36
- */
37
- failNextCreate(orch: O, containerId: string): void;
38
- }
39
-
40
- const CONTAINER = 'grn_contract_a';
41
- const OTHER_CONTAINER = 'grn_contract_b';
42
-
43
- export function describeHandleContract<O extends ContractOrchestrator>(
44
- label: string,
45
- harness: HandleContractHarness<O>,
46
- ): void {
47
- async function spawn(orch: O, containerId: string): Promise<OrchestratorCreateResult> {
48
- const result = await orch.createContainer(harness.request(containerId));
49
- await harness.started(orch, result.dockerId);
50
- return result;
51
- }
52
-
53
- // Stops first: a container whose previous child is still running is refused.
54
- async function respawn(orch: O, containerId: string, previousDockerId: string): Promise<OrchestratorCreateResult> {
55
- await orch.stopContainer(previousDockerId);
56
- return spawn(orch, containerId);
57
- }
58
-
59
- describe(`${label} handle contract`, () => {
60
- it('rejects a dockerId it never issued', async () => {
61
- const orch = harness.make();
62
-
63
- await expectNotFound(orch.stopContainer('never-issued'), 'never-issued');
64
- await expectNotFound(orch.removeContainer('never-issued'), 'never-issued');
65
- });
66
-
67
- it('rejects a dockerId once its container has been removed', async () => {
68
- const orch = harness.make();
69
- const { dockerId } = await spawn(orch, CONTAINER);
70
- await orch.stopContainer(dockerId);
71
-
72
- await orch.removeContainer(dockerId);
73
-
74
- await expectNotFound(orch.stopContainer(dockerId), dockerId);
75
- await expectNotFound(orch.removeContainer(dockerId), dockerId);
76
- });
77
-
78
- it('replaces the first spawn\'s dockerId when the container re-spawns', async () => {
79
- const orch = harness.make();
80
- const first = await spawn(orch, CONTAINER);
81
-
82
- const second = await respawn(orch, CONTAINER, first.dockerId);
83
-
84
- await expectNotFound(orch.stopContainer(first.dockerId), first.dockerId);
85
- expect(orch.resolveDockerId(CONTAINER)).toBe(second.dockerId);
86
- await expect(orch.stopContainer(second.dockerId)).resolves.toBeUndefined();
87
- });
88
-
89
- // Launching over a live child hands its ports to a second child that then
90
- // dies on them, while the caller records the new dockerId and seed token.
91
- it('refuses to re-spawn a container whose previous child is still running', async () => {
92
- const orch = harness.make();
93
- const first = await spawn(orch, CONTAINER);
94
-
95
- await expect(orch.createContainer(harness.request(CONTAINER)))
96
- .rejects.toHaveProperty('message', `container ${CONTAINER} is still running`);
97
-
98
- expect(orch.resolveDockerId(CONTAINER)).toBe(first.dockerId);
99
- await expect(orch.stopContainer(first.dockerId)).resolves.toBeUndefined();
100
- });
101
-
102
- // The drop on re-spawn filters on containerId; an orchestrator that cleared
103
- // every handle would still pass the same-container case above.
104
- it('leaves another container\'s dockerId alone when one container re-spawns', async () => {
105
- const orch = harness.make();
106
- const other = await spawn(orch, OTHER_CONTAINER);
107
- const first = await spawn(orch, CONTAINER);
108
-
109
- await respawn(orch, CONTAINER, first.dockerId);
110
-
111
- expect(orch.resolveDockerId(OTHER_CONTAINER)).toBe(other.dockerId);
112
- await expect(orch.stopContainer(other.dockerId)).resolves.toBeUndefined();
113
- });
114
-
115
- // What lets a caller still clean up the node it knows about after a re-spawn
116
- // that did not happen.
117
- it('keeps the first spawn\'s dockerId when the re-spawn fails', async () => {
118
- const orch = harness.make();
119
- const first = await spawn(orch, CONTAINER);
120
- await orch.stopContainer(first.dockerId);
121
-
122
- harness.failNextCreate(orch, CONTAINER);
123
- await expect(orch.createContainer(harness.request(CONTAINER))).rejects.toThrow();
124
-
125
- expect(orch.resolveDockerId(CONTAINER)).toBe(first.dockerId);
126
- await expect(orch.removeContainer(first.dockerId)).resolves.toBeUndefined();
127
- });
128
- });
129
- }
130
-
131
- /** The whole message, not a substring, so the two classes cannot come to word it differently. */
132
- async function expectNotFound(call: Promise<void>, dockerId: string): Promise<void> {
133
- await expect(call).rejects.toHaveProperty('message', `Container not found: ${dockerId}`);
134
- }
1
+ /**
2
+ * The handle rules `HostProcessOrchestrator` and its test stand-in
3
+ * `FakeOrchestrator` (`hosted/__tests__/fake-orchestrator.ts`) must both obey,
4
+ * written once and run against each: `orchestrator.test.ts` registers them over
5
+ * the real class, `fake-orchestrator.test.ts` over the fake. A rule one of them
6
+ * stops obeying fails in that file's run.
7
+ *
8
+ * Not a `.test.ts` file, so vitest does not collect it on its own.
9
+ *
10
+ * Only what both classes can satisfy belongs here — no ports, no files on disk,
11
+ * none of the fake's recording arrays. Those stay in the two calling files.
12
+ */
13
+
14
+ import { describe, expect, it } from 'vitest';
15
+
16
+ import type { OrchestratorCreateRequest, OrchestratorCreateResult } from '@serfab/cadre-provider';
17
+
18
+ import type { HostProcessOrchestrator } from '../orchestrator/host-process-orchestrator.js';
19
+
20
+ /** What the contract needs from either orchestrator. */
21
+ export type ContractOrchestrator = Pick<
22
+ HostProcessOrchestrator,
23
+ 'createContainer' | 'stopContainer' | 'removeContainer' | 'resolveDockerId'
24
+ >;
25
+
26
+ export interface HandleContractHarness<O extends ContractOrchestrator> {
27
+ /** A fresh orchestrator. Teardown is the calling file's own afterEach. */
28
+ make(): O;
29
+ request(containerId: string): OrchestratorCreateRequest;
30
+ /** Resolve once the spawn named by `dockerId` reads as running. */
31
+ started(orch: O, dockerId: string): Promise<void>;
32
+ /**
33
+ * Make `createContainer` for `containerId` throw from here on. Only called
34
+ * after that container has been spawned once and stopped, and no case creates
35
+ * again afterwards.
36
+ */
37
+ failNextCreate(orch: O, containerId: string): void;
38
+ }
39
+
40
+ const CONTAINER = 'hn_contract_a';
41
+ const OTHER_CONTAINER = 'hn_contract_b';
42
+
43
+ export function describeHandleContract<O extends ContractOrchestrator>(
44
+ label: string,
45
+ harness: HandleContractHarness<O>,
46
+ ): void {
47
+ async function spawn(orch: O, containerId: string): Promise<OrchestratorCreateResult> {
48
+ const result = await orch.createContainer(harness.request(containerId));
49
+ await harness.started(orch, result.dockerId);
50
+ return result;
51
+ }
52
+
53
+ // Stops first: a container whose previous child is still running is refused.
54
+ async function respawn(orch: O, containerId: string, previousDockerId: string): Promise<OrchestratorCreateResult> {
55
+ await orch.stopContainer(previousDockerId);
56
+ return spawn(orch, containerId);
57
+ }
58
+
59
+ describe(`${label} handle contract`, () => {
60
+ it('rejects a dockerId it never issued', async () => {
61
+ const orch = harness.make();
62
+
63
+ await expectNotFound(orch.stopContainer('never-issued'), 'never-issued');
64
+ await expectNotFound(orch.removeContainer('never-issued'), 'never-issued');
65
+ });
66
+
67
+ it('rejects a dockerId once its container has been removed', async () => {
68
+ const orch = harness.make();
69
+ const { dockerId } = await spawn(orch, CONTAINER);
70
+ await orch.stopContainer(dockerId);
71
+
72
+ await orch.removeContainer(dockerId);
73
+
74
+ await expectNotFound(orch.stopContainer(dockerId), dockerId);
75
+ await expectNotFound(orch.removeContainer(dockerId), dockerId);
76
+ });
77
+
78
+ it('replaces the first spawn\'s dockerId when the container re-spawns', async () => {
79
+ const orch = harness.make();
80
+ const first = await spawn(orch, CONTAINER);
81
+
82
+ const second = await respawn(orch, CONTAINER, first.dockerId);
83
+
84
+ await expectNotFound(orch.stopContainer(first.dockerId), first.dockerId);
85
+ expect(orch.resolveDockerId(CONTAINER)).toBe(second.dockerId);
86
+ await expect(orch.stopContainer(second.dockerId)).resolves.toBeUndefined();
87
+ });
88
+
89
+ // Launching over a live child hands its ports to a second child that then
90
+ // dies on them, while the caller records the new dockerId.
91
+ it('refuses to re-spawn a container whose previous child is still running', async () => {
92
+ const orch = harness.make();
93
+ const first = await spawn(orch, CONTAINER);
94
+
95
+ await expect(orch.createContainer(harness.request(CONTAINER)))
96
+ .rejects.toHaveProperty('message', `container ${CONTAINER} is still running`);
97
+
98
+ expect(orch.resolveDockerId(CONTAINER)).toBe(first.dockerId);
99
+ await expect(orch.stopContainer(first.dockerId)).resolves.toBeUndefined();
100
+ });
101
+
102
+ // The drop on re-spawn filters on containerId; an orchestrator that cleared
103
+ // every handle would still pass the same-container case above.
104
+ it('leaves another container\'s dockerId alone when one container re-spawns', async () => {
105
+ const orch = harness.make();
106
+ const other = await spawn(orch, OTHER_CONTAINER);
107
+ const first = await spawn(orch, CONTAINER);
108
+
109
+ await respawn(orch, CONTAINER, first.dockerId);
110
+
111
+ expect(orch.resolveDockerId(OTHER_CONTAINER)).toBe(other.dockerId);
112
+ await expect(orch.stopContainer(other.dockerId)).resolves.toBeUndefined();
113
+ });
114
+
115
+ // What lets a caller still clean up the node it knows about after a re-spawn
116
+ // that did not happen.
117
+ it('keeps the first spawn\'s dockerId when the re-spawn fails', async () => {
118
+ const orch = harness.make();
119
+ const first = await spawn(orch, CONTAINER);
120
+ await orch.stopContainer(first.dockerId);
121
+
122
+ harness.failNextCreate(orch, CONTAINER);
123
+ await expect(orch.createContainer(harness.request(CONTAINER))).rejects.toThrow();
124
+
125
+ expect(orch.resolveDockerId(CONTAINER)).toBe(first.dockerId);
126
+ await expect(orch.removeContainer(first.dockerId)).resolves.toBeUndefined();
127
+ });
128
+ });
129
+ }
130
+
131
+ /** The whole message, not a substring, so the two classes cannot come to word it differently. */
132
+ async function expectNotFound(call: Promise<void>, dockerId: string): Promise<void> {
133
+ await expect(call).rejects.toHaveProperty('message', `Container not found: ${dockerId}`);
134
+ }
@@ -1,203 +1,177 @@
1
- /**
2
- * HostProcessOrchestrator per-node identity tests.
3
- *
4
- * A donated node must keep the peer id its host cadre approved across restarts,
5
- * and needs a durable on-disk home for its node-local stores (`cadre-cli start`
6
- * opens the file-backed bootstrap-peer / trusted-owner stores only when a
7
- * protobuf identity key file is configured). The orchestrator therefore writes
8
- * `<workdir>/identity.key` once per container and passes it as
9
- * `--identity-file`.
10
- *
11
- * A fake CLI records the `--identity-file` argument it was launched with, so
12
- * the spawn wiring is observable without a real cadre node.
13
- */
14
-
15
- import { afterEach, beforeEach, describe, expect, it } from 'vitest';
16
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
17
- import { tmpdir } from 'node:os';
18
- import { join } from 'node:path';
19
-
20
- import { HostProcessOrchestrator, OWNER_CONTAINER_ID } from '../orchestrator/host-process-orchestrator.js';
21
- import { loadIdentity } from '../installer/identity.js';
22
- import { removeAllNodes } from './orchestrator-teardown.js';
23
-
24
- // Writes the --identity-file argument (as seen on its own command line) next
25
- // to the startup token, then behaves like a minimal long-lived node.
26
- const FAKE_CLI = `
27
- import fs from 'node:fs';
28
- import path from 'node:path';
29
- const args = process.argv.slice(2);
30
- const get = (flag) => { const i = args.indexOf(flag); return i >= 0 ? args[i + 1] : undefined; };
31
- const tokenPath = get('--startup-token-file');
32
- const token = process.env.CADRE_STARTUP_TOKEN ?? '';
33
- if (tokenPath) {
34
- const dir = path.dirname(tokenPath);
35
- try { fs.writeFileSync(path.join(dir, 'identity-arg.txt'), get('--identity-file') ?? '', 'utf8'); } catch (e) { console.error(e); }
36
- if (token) { try { fs.writeFileSync(tokenPath, token, 'utf8'); } catch (e) { console.error(e); } }
37
- }
38
- process.on('SIGTERM', () => process.exit(0));
39
- setInterval(() => {}, 1 << 30);
40
- `;
41
-
42
- let tmpRoot: string;
43
- let scriptPath: string;
44
- const orchestrators: HostProcessOrchestrator[] = [];
45
-
46
- beforeEach(() => {
47
- tmpRoot = mkdtempSync(join(tmpdir(), 'cadre-host-nodeid-'));
48
- scriptPath = join(tmpRoot, 'fake-cli.mjs');
49
- writeFileSync(scriptPath, FAKE_CLI, 'utf8');
50
- });
51
-
52
- afterEach(async () => {
53
- try {
54
- await removeAllNodes(orchestrators);
55
- } finally {
56
- await sleep(50);
57
- try { rmSync(tmpRoot, { recursive: true, force: true }); } catch { /* ignore */ }
58
- }
59
- });
60
-
61
- function makeOrchestrator(
62
- rootDir: string,
63
- portRange: { start: number; end: number } = { start: 17500, end: 17999 },
64
- ): HostProcessOrchestrator {
65
- mkdirSync(rootDir, { recursive: true });
66
- const orch = new HostProcessOrchestrator({
67
- rootDir,
68
- portRange,
69
- stopTimeoutMs: 1500,
70
- spawn: { entrypoint: scriptPath },
71
- });
72
- orchestrators.push(orch);
73
- return orch;
74
- }
75
-
76
- async function waitForFile(path: string, timeoutMs = 5000): Promise<string> {
77
- const deadline = Date.now() + timeoutMs;
78
- while (Date.now() < deadline) {
79
- if (existsSync(path)) return readFileSync(path, 'utf8');
80
- await sleep(50);
81
- }
82
- throw new Error(`file never appeared: ${path}`);
83
- }
84
-
85
- function sleep(ms: number): Promise<void> {
86
- return new Promise((res) => setTimeout(res, ms));
87
- }
88
-
89
- describe('HostProcessOrchestrator node identity', () => {
90
- it('passes --identity-file pointing at a real key inside the node workdir', async () => {
91
- const rootDir = join(tmpRoot, 'a');
92
- const orch = makeOrchestrator(rootDir);
93
- await orch.init();
94
-
95
- await orch.createContainer({
96
- containerId: 'donated-1',
97
- partyId: 'foreign-P',
98
- bootstrapNodes: [],
99
- profile: 'storage',
100
- pinnedOwnerKeys: ['key-a'],
101
- });
102
-
103
- const workdir = join(rootDir, 'donated-1');
104
- const arg = await waitForFile(join(workdir, 'identity-arg.txt'));
105
- expect(arg).toBe(join(workdir, 'identity.key'));
106
- // Decodes as a libp2p protobuf private key → the child gets a usable identity.
107
- expect(loadIdentity(arg).peerId).toMatch(/^12D3Koo/);
108
- });
109
-
110
- it('reuses the same identity when the same containerId is spawned again', async () => {
111
- const rootDir = join(tmpRoot, 'b');
112
- const orch = makeOrchestrator(rootDir);
113
- await orch.init();
114
-
115
- const first = await orch.createContainer({
116
- containerId: 'donated-2',
117
- partyId: 'foreign-P',
118
- bootstrapNodes: [],
119
- profile: 'storage',
120
- pinnedOwnerKeys: ['key-a'],
121
- });
122
- const workdir = join(rootDir, 'donated-2');
123
- await waitForFile(join(workdir, 'identity-arg.txt'));
124
- const before = loadIdentity(join(workdir, 'identity.key')).peerId;
125
-
126
- // Stop (not remove — removal deletes the workdir) and re-spawn, as a restart
127
- // of a live loan would.
128
- await orch.stopContainer(first.dockerId);
129
- await orch.createContainer({
130
- containerId: 'donated-2',
131
- partyId: 'foreign-P',
132
- bootstrapNodes: [],
133
- profile: 'storage',
134
- pinnedOwnerKeys: ['key-a'],
135
- });
136
-
137
- const after = loadIdentity(join(workdir, 'identity.key')).peerId;
138
- expect(after).toBe(before);
139
- });
140
-
141
- it('destroys the identity with the workdir when the loan is terminated', async () => {
142
- const rootDir = join(tmpRoot, 'c');
143
- const orch = makeOrchestrator(rootDir);
144
- await orch.init();
145
-
146
- const created = await orch.createContainer({
147
- containerId: 'donated-3',
148
- partyId: 'foreign-P',
149
- bootstrapNodes: [],
150
- profile: 'storage',
151
- pinnedOwnerKeys: ['key-a'],
152
- });
153
- const identityPath = join(rootDir, 'donated-3', 'identity.key');
154
- await waitForFile(identityPath);
155
-
156
- await orch.removeContainer(created.dockerId);
157
- expect(existsSync(identityPath)).toBe(false);
158
- expect(existsSync(join(rootDir, 'donated-3'))).toBe(false);
159
- });
160
-
161
- // The failure the identity step can actually produce is a damaged key file,
162
- // and `DonationService.provision` turns every such throw into an `error`
163
- // record and lets the grantee retry. If the spawn path reserved its ports
164
- // before the step that throws, each retry would burn five more ports out of a
165
- // bounded range until provisioning stopped working altogether.
166
- it('reserves no ports when the identity step fails', async () => {
167
- const rootDir = join(tmpRoot, 'd');
168
- // Exactly one node's worth of ports (health, metrics, p2p, admin, ws).
169
- const orch = makeOrchestrator(rootDir, { start: 18100, end: 18104 });
170
- await orch.init();
171
-
172
- mkdirSync(join(rootDir, 'donated-bad'), { recursive: true });
173
- writeFileSync(join(rootDir, 'donated-bad', 'identity.key'), 'not a protobuf private key', 'utf8');
174
-
175
- const request = {
176
- partyId: 'foreign-P',
177
- bootstrapNodes: [],
178
- profile: 'storage' as const,
179
- pinnedOwnerKeys: ['key-a'],
180
- };
181
- await expect(orch.createContainer({ ...request, containerId: 'donated-bad' })).rejects.toThrow();
182
-
183
- // The whole range is still free, so a healthy container still gets its five.
184
- const ok = await orch.createContainer({ ...request, containerId: 'donated-good' });
185
- expect(ok.p2pPort).toBeGreaterThanOrEqual(18100);
186
- });
187
-
188
- // The owner node carries the HOST's installer identity from <dataDir>, not a
189
- // per-workdir key — a second key in its workdir would mean the host's own
190
- // cadre node quietly changed peer id.
191
- it('leaves the owner node on its own installer identity', async () => {
192
- const rootDir = join(tmpRoot, 'e');
193
- const orch = makeOrchestrator(rootDir);
194
- await orch.init();
195
-
196
- const hostIdentity = join(tmpRoot, 'host-identity.key');
197
- await orch.ensureOwnerNode({ identityPath: hostIdentity, partyId: 'host-P', libp2pPort: 4101 });
198
-
199
- const ownerWorkdir = join(rootDir, OWNER_CONTAINER_ID);
200
- expect(await waitForFile(join(ownerWorkdir, 'identity-arg.txt'))).toBe(hostIdentity);
201
- expect(existsSync(join(ownerWorkdir, 'identity.key'))).toBe(false);
202
- });
203
- });
1
+ /**
2
+ * HostProcessOrchestrator per-node identity tests.
3
+ *
4
+ * A hosted node must keep the peer id the claim named across restarts,
5
+ * and needs a durable on-disk home for its node-local stores (`cadre-cli start`
6
+ * opens the file-backed bootstrap-peer / trusted-owner stores only when a
7
+ * protobuf identity key file is configured). The orchestrator therefore writes
8
+ * `<workdir>/identity.key` once per container and passes it as
9
+ * `--identity-file`.
10
+ *
11
+ * A fake CLI records the `--identity-file` argument it was launched with, so
12
+ * the spawn wiring is observable without a real cadre node.
13
+ */
14
+
15
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
16
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
17
+ import { tmpdir } from 'node:os';
18
+ import { join } from 'node:path';
19
+
20
+ import { HostProcessOrchestrator } from '../orchestrator/host-process-orchestrator.js';
21
+ import { loadIdentity } from '../orchestrator/identity-file.js';
22
+ import { removeAllNodes } from './orchestrator-teardown.js';
23
+
24
+ // Writes the --identity-file argument (as seen on its own command line) next
25
+ // to the startup token, then behaves like a minimal long-lived node.
26
+ const FAKE_CLI = `
27
+ import fs from 'node:fs';
28
+ import path from 'node:path';
29
+ const args = process.argv.slice(2);
30
+ const get = (flag) => { const i = args.indexOf(flag); return i >= 0 ? args[i + 1] : undefined; };
31
+ const tokenPath = get('--startup-token-file');
32
+ const token = process.env.CADRE_STARTUP_TOKEN ?? '';
33
+ if (tokenPath) {
34
+ const dir = path.dirname(tokenPath);
35
+ try { fs.writeFileSync(path.join(dir, 'identity-arg.txt'), get('--identity-file') ?? '', 'utf8'); } catch (e) { console.error(e); }
36
+ if (token) { try { fs.writeFileSync(tokenPath, token, 'utf8'); } catch (e) { console.error(e); } }
37
+ }
38
+ process.on('SIGTERM', () => process.exit(0));
39
+ setInterval(() => {}, 1 << 30);
40
+ `;
41
+
42
+ let tmpRoot: string;
43
+ let scriptPath: string;
44
+ const orchestrators: HostProcessOrchestrator[] = [];
45
+
46
+ beforeEach(() => {
47
+ tmpRoot = mkdtempSync(join(tmpdir(), 'cadre-host-nodeid-'));
48
+ scriptPath = join(tmpRoot, 'fake-cli.mjs');
49
+ writeFileSync(scriptPath, FAKE_CLI, 'utf8');
50
+ });
51
+
52
+ afterEach(async () => {
53
+ try {
54
+ await removeAllNodes(orchestrators);
55
+ } finally {
56
+ await sleep(50);
57
+ try { rmSync(tmpRoot, { recursive: true, force: true }); } catch { /* ignore */ }
58
+ }
59
+ });
60
+
61
+ function makeOrchestrator(
62
+ rootDir: string,
63
+ portRange: { start: number; end: number } = { start: 17500, end: 17999 },
64
+ ): HostProcessOrchestrator {
65
+ mkdirSync(rootDir, { recursive: true });
66
+ const orch = new HostProcessOrchestrator({
67
+ rootDir,
68
+ portRange,
69
+ stopTimeoutMs: 1500,
70
+ spawn: { entrypoint: scriptPath },
71
+ });
72
+ orchestrators.push(orch);
73
+ return orch;
74
+ }
75
+
76
+ async function waitForFile(path: string, timeoutMs = 5000): Promise<string> {
77
+ const deadline = Date.now() + timeoutMs;
78
+ while (Date.now() < deadline) {
79
+ if (existsSync(path)) return readFileSync(path, 'utf8');
80
+ await sleep(50);
81
+ }
82
+ throw new Error(`file never appeared: ${path}`);
83
+ }
84
+
85
+ function sleep(ms: number): Promise<void> {
86
+ return new Promise((res) => setTimeout(res, ms));
87
+ }
88
+
89
+ describe('HostProcessOrchestrator node identity', () => {
90
+ it('passes --identity-file pointing at a real key inside the node workdir', async () => {
91
+ const rootDir = join(tmpRoot, 'a');
92
+ const orch = makeOrchestrator(rootDir);
93
+ await orch.init();
94
+
95
+ await orch.createContainer({
96
+ containerId: 'hosted-1',
97
+ partyId: 'party-P',
98
+ profile: 'storage',
99
+ });
100
+
101
+ const workdir = join(rootDir, 'hosted-1');
102
+ const arg = await waitForFile(join(workdir, 'identity-arg.txt'));
103
+ expect(arg).toBe(join(workdir, 'identity.key'));
104
+ // Decodes as a libp2p protobuf private key → the child gets a usable identity.
105
+ expect(loadIdentity(arg).peerId).toMatch(/^12D3Koo/);
106
+ });
107
+
108
+ it('reuses the same identity when the same containerId is spawned again', async () => {
109
+ const rootDir = join(tmpRoot, 'b');
110
+ const orch = makeOrchestrator(rootDir);
111
+ await orch.init();
112
+
113
+ const first = await orch.createContainer({
114
+ containerId: 'hosted-2',
115
+ partyId: 'party-P',
116
+ profile: 'storage',
117
+ });
118
+ const workdir = join(rootDir, 'hosted-2');
119
+ await waitForFile(join(workdir, 'identity-arg.txt'));
120
+ const before = loadIdentity(join(workdir, 'identity.key')).peerId;
121
+
122
+ // Stop (not remove — removal deletes the workdir) and re-spawn, as a restart
123
+ // of a live hosted node would.
124
+ await orch.stopContainer(first.dockerId);
125
+ await orch.createContainer({
126
+ containerId: 'hosted-2',
127
+ partyId: 'party-P',
128
+ profile: 'storage',
129
+ });
130
+
131
+ const after = loadIdentity(join(workdir, 'identity.key')).peerId;
132
+ expect(after).toBe(before);
133
+ });
134
+
135
+ it('destroys the identity with the workdir when the node is removed', async () => {
136
+ const rootDir = join(tmpRoot, 'c');
137
+ const orch = makeOrchestrator(rootDir);
138
+ await orch.init();
139
+
140
+ const created = await orch.createContainer({
141
+ containerId: 'hosted-3',
142
+ partyId: 'party-P',
143
+ profile: 'storage',
144
+ });
145
+ const identityPath = join(rootDir, 'hosted-3', 'identity.key');
146
+ await waitForFile(identityPath);
147
+
148
+ await orch.removeContainer(created.dockerId);
149
+ expect(existsSync(identityPath)).toBe(false);
150
+ expect(existsSync(join(rootDir, 'hosted-3'))).toBe(false);
151
+ });
152
+
153
+ // The failure the identity step can actually produce is a damaged key file,
154
+ // and `HostedNodeService.join` turns every such throw into an `error`
155
+ // record and lets the admin retry. If the spawn path reserved its ports
156
+ // before the step that throws, each retry would burn four more ports out of a
157
+ // bounded range until provisioning stopped working altogether.
158
+ it('reserves no ports when the identity step fails', async () => {
159
+ const rootDir = join(tmpRoot, 'd');
160
+ // Exactly one node's worth of ports (health, metrics, p2p, ws).
161
+ const orch = makeOrchestrator(rootDir, { start: 18100, end: 18103 });
162
+ await orch.init();
163
+
164
+ mkdirSync(join(rootDir, 'hosted-bad'), { recursive: true });
165
+ writeFileSync(join(rootDir, 'hosted-bad', 'identity.key'), 'not a protobuf private key', 'utf8');
166
+
167
+ const request = {
168
+ partyId: 'party-P',
169
+ profile: 'storage' as const,
170
+ };
171
+ await expect(orch.createContainer({ ...request, containerId: 'hosted-bad' })).rejects.toThrow();
172
+
173
+ // The whole range is still free, so a healthy container still gets its four.
174
+ const ok = await orch.createContainer({ ...request, containerId: 'hosted-good' });
175
+ expect(ok.p2pPort).toBeGreaterThanOrEqual(18100);
176
+ });
177
+ });