windows-development-sandbox 1.20.0 → 1.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (337) hide show
  1. package/config.example.json +8 -1
  2. package/dist/app/features/access/access.def.js +5 -2
  3. package/dist/app/features/access/access.run.js +7 -3
  4. package/dist/app/features/accounts/accounts.def.d.ts +29 -1
  5. package/dist/app/features/accounts/accounts.def.js +10 -4
  6. package/dist/app/features/accounts/accounts.run.d.ts +1 -0
  7. package/dist/app/features/accounts/accounts.run.js +7 -2
  8. package/dist/app/features/accounts/services.def.d.ts +4 -1
  9. package/dist/app/features/accounts/services.def.js +15 -14
  10. package/dist/app/features/accounts/services.run.d.ts +1 -0
  11. package/dist/app/features/accounts/services.run.js +41 -15
  12. package/dist/app/features/defs.d.ts +39 -2
  13. package/dist/app/features/engine/engine.run.js +1 -1
  14. package/dist/app/features/github/github.def.d.ts +44 -3
  15. package/dist/app/features/github/github.def.js +145 -63
  16. package/dist/app/features/github/github.run.d.ts +9 -2
  17. package/dist/app/features/github/github.run.js +43 -5
  18. package/dist/app/features/health/health.def.js +8 -2
  19. package/dist/app/features/logs/logs.def.js +0 -8
  20. package/dist/app/features/projects/projects.def.js +2 -2
  21. package/dist/app/features/projects/projects.run.js +5 -5
  22. package/dist/app/features/sandbox/sandbox.def.js +7 -3
  23. package/dist/app/features/sessions/sessions.def.d.ts +4 -4
  24. package/dist/app/features/sessions/sessions.def.js +0 -5
  25. package/dist/app/features/sessions/sessions.run.d.ts +1 -1
  26. package/dist/app/features/sessions/sessions.run.js +2 -2
  27. package/dist/app/features/settings/categories.def.js +2 -0
  28. package/dist/app/features/settings/settings-rows.js +1 -1
  29. package/dist/app/features/settings/settings.def.js +1 -1
  30. package/dist/app/features/settings/settings.run.js +3 -3
  31. package/dist/app/features/storage/storage.run.js +18 -9
  32. package/dist/app/features/updates/update-lines.js +2 -2
  33. package/dist/app/features/updates/updates.def.js +1 -1
  34. package/dist/app/features/updates/updates.run.js +1 -1
  35. package/dist/app/features/web/web.def.js +2 -2
  36. package/dist/app/features/web/web.run.js +12 -4
  37. package/dist/app/kernel/errors.d.ts +7 -0
  38. package/dist/app/kernel/errors.js +18 -3
  39. package/dist/app/kernel/rpc.d.ts +5 -0
  40. package/dist/app/kernel/view.d.ts +11 -1
  41. package/dist/bin/agent-sandbox.js +2 -2
  42. package/dist/bin/{chunk-DK4NTBUF.js → chunk-7YPEDAJD.js} +172 -48
  43. package/dist/bin/{chunk-2IMT7ZCT.js → chunk-QFSJX2KW.js} +1 -1
  44. package/dist/bin/{chunk-Z2P5OGKL.js → chunk-USTRSHIE.js} +2436 -1693
  45. package/dist/bin/claude-host.js +2 -2
  46. package/dist/bin/claude-sbx.js +2 -2
  47. package/dist/bin/codex-sbx.js +2 -2
  48. package/dist/bin/gemini-sbx.js +2 -2
  49. package/dist/bin/sandbox.js +2 -2
  50. package/dist/bin/sbx-hide.js +2 -2
  51. package/dist/bin/sbx-show.js +2 -2
  52. package/dist/cli/manifest.d.ts +2 -2
  53. package/dist/cli/manifest.js +2 -1
  54. package/dist/cli/menu/coverage-exempt.d.ts +17 -0
  55. package/dist/cli/menu/coverage-exempt.js +61 -0
  56. package/dist/cli/menu/io-menu.d.ts +2 -0
  57. package/dist/cli/menu/io-menu.js +50 -5
  58. package/dist/cli/menu/pages/accounts-page.js +1 -1
  59. package/dist/cli/menu/pages/agents-page.js +8 -2
  60. package/dist/cli/menu/pages/maintenance-page.d.ts +6 -0
  61. package/dist/cli/menu/pages/maintenance-page.js +9 -2
  62. package/dist/cli/menu/pages/sessions-page.d.ts +2 -0
  63. package/dist/cli/menu/pages/sessions-page.js +12 -4
  64. package/dist/cli/menu/pages/settings-page.js +12 -1
  65. package/dist/cli/menu/remote-menu.js +1 -1
  66. package/dist/cli/menu/shell.d.ts +5 -0
  67. package/dist/cli/menu/shell.js +13 -0
  68. package/dist/cli/report.js +2 -0
  69. package/dist/cli/tui/app.d.ts +5 -0
  70. package/dist/cli/tui/app.js +2 -0
  71. package/dist/core/config/defaults.js +3 -1
  72. package/dist/core/schema/accounts.d.ts +6 -0
  73. package/dist/core/schema/accounts.js +13 -11
  74. package/dist/core/schema/config.d.ts +20 -8
  75. package/dist/core/schema/config.js +4 -4
  76. package/dist/core/schema/github.js +1 -1
  77. package/dist/core/schema/profile.js +8 -4
  78. package/dist/core/schema/resume.js +1 -1
  79. package/dist/core/schema/sandbox.js +3 -3
  80. package/dist/core/schema/vpn.js +1 -1
  81. package/dist/core/schema/web.js +3 -3
  82. package/dist/core/types.d.ts +6 -0
  83. package/dist/core/util/errors.d.ts +11 -1
  84. package/dist/core/util/errors.js +7 -1
  85. package/dist/core/util/index.d.ts +1 -1
  86. package/dist/core/util/index.js +1 -1
  87. package/dist/guest/accounts/adapter.d.ts +2 -0
  88. package/dist/guest/accounts/adapters.js +4 -0
  89. package/dist/guest/accounts/apply.d.ts +6 -2
  90. package/dist/guest/accounts/kinds/cfx.d.ts +7 -0
  91. package/dist/guest/accounts/kinds/cfx.js +33 -3
  92. package/dist/guest/accounts/kinds/comet.d.ts +7 -0
  93. package/dist/guest/accounts/kinds/comet.js +50 -0
  94. package/dist/guest/accounts/kinds/perplexity.d.ts +2 -0
  95. package/dist/guest/accounts/kinds/perplexity.js +12 -0
  96. package/dist/guest/accounts/watch.d.ts +2 -0
  97. package/dist/guest/accounts/watch.js +12 -2
  98. package/dist/guest/agents/agent-installer.js +1 -1
  99. package/dist/guest/agents/profile-links.d.ts +2 -0
  100. package/dist/guest/agents/profile-links.js +18 -4
  101. package/dist/guest/apps/after-install.js +19 -0
  102. package/dist/guest/apps/app-installer.d.ts +11 -0
  103. package/dist/guest/apps/app-installer.js +69 -6
  104. package/dist/guest/apps/comet-unpack.d.ts +20 -0
  105. package/dist/guest/apps/comet-unpack.js +127 -0
  106. package/dist/guest/apps/index.d.ts +1 -1
  107. package/dist/guest/apps/index.js +1 -1
  108. package/dist/guest/apps/resolvers-agents-web.d.ts +2 -0
  109. package/dist/guest/apps/resolvers-agents-web.js +7 -0
  110. package/dist/guest/apps/resolvers.js +2 -0
  111. package/dist/guest/apps/shortcuts.d.ts +2 -0
  112. package/dist/guest/apps/shortcuts.js +1 -0
  113. package/dist/guest/apps/unpack.js +14 -4
  114. package/dist/guest/boot/boot.d.ts +45 -16
  115. package/dist/guest/boot/boot.js +316 -145
  116. package/dist/guest/boot/graph.d.ts +252 -0
  117. package/dist/guest/boot/graph.js +257 -0
  118. package/dist/guest/boot/guide-lines.d.ts +25 -0
  119. package/dist/guest/boot/guide-lines.js +41 -0
  120. package/dist/guest/boot/guide.d.ts +1 -4
  121. package/dist/guest/boot/guide.js +6 -8
  122. package/dist/guest/boot/runner.d.ts +77 -0
  123. package/dist/guest/boot/runner.js +215 -0
  124. package/dist/guest/browser/cdp.d.ts +71 -0
  125. package/dist/guest/browser/cdp.js +306 -0
  126. package/dist/guest/browser/comet.d.ts +28 -0
  127. package/dist/guest/browser/comet.js +155 -0
  128. package/dist/guest/daemon/project-provisioner.js +6 -1
  129. package/dist/guest/devtools/fivem.d.ts +1 -1
  130. package/dist/guest/devtools/fivem.js +243 -3
  131. package/dist/guest/entries/daemon.js +2 -0
  132. package/dist/guest/runtime/log.d.ts +12 -0
  133. package/dist/guest/runtime/log.js +29 -5
  134. package/dist/guest/runtime/pools.d.ts +24 -0
  135. package/dist/guest/runtime/pools.js +60 -0
  136. package/dist/guest/runtime/process.d.ts +4 -0
  137. package/dist/guest/runtime/process.js +38 -1
  138. package/dist/guest/runtime/status.d.ts +5 -1
  139. package/dist/guest/runtime/status.js +19 -0
  140. package/dist/guest/setup/addon-mcp.d.ts +1 -1
  141. package/dist/guest/setup/addon-mcp.js +52 -12
  142. package/dist/guest/setup/agent-addons.d.ts +3 -2
  143. package/dist/guest/setup/agent-addons.js +12 -3
  144. package/dist/guest/setup/environment.d.ts +1 -1
  145. package/dist/guest/setup/environment.js +25 -12
  146. package/dist/guest/setup/headroom.d.ts +8 -3
  147. package/dist/guest/setup/headroom.js +44 -15
  148. package/dist/guest/setup/path-plan.d.ts +28 -0
  149. package/dist/guest/setup/path-plan.js +69 -2
  150. package/dist/guest/setup/shell-path.d.ts +1 -1
  151. package/dist/guest/setup/shell-path.js +3 -3
  152. package/dist/guest/toolchains/fivem/artifacts.d.ts +48 -1
  153. package/dist/guest/toolchains/fivem/artifacts.js +97 -4
  154. package/dist/guest/toolchains/fivem/fivem.d.ts +32 -1
  155. package/dist/guest/toolchains/fivem/fivem.js +168 -3
  156. package/dist/guest/toolchains/fivem/scaffold.d.ts +35 -0
  157. package/dist/guest/toolchains/fivem/scaffold.js +105 -0
  158. package/dist/guest/toolchains/fivem/serve-plan.d.ts +34 -0
  159. package/dist/guest/toolchains/fivem/serve-plan.js +61 -0
  160. package/dist/guest/toolchains/languages/go.js +1 -1
  161. package/dist/guest/toolchains/languages/node.js +2 -1
  162. package/dist/guest/toolchains/languages/php.d.ts +2 -0
  163. package/dist/guest/toolchains/languages/php.js +13 -1
  164. package/dist/guest/toolchains/languages/python-tools.js +1 -1
  165. package/dist/guest/toolchains/languages/ruby.js +1 -1
  166. package/dist/guest/toolchains/native/binstall.js +1 -1
  167. package/dist/guest/toolchains/native/msvc/packages.js +10 -4
  168. package/dist/guest/toolchains/native/rust.d.ts +2 -0
  169. package/dist/guest/toolchains/native/rust.js +11 -4
  170. package/dist/guest/toolchains/native/unreal.d.ts +2 -0
  171. package/dist/guest/toolchains/native/unreal.js +16 -6
  172. package/dist/guest/toolchains/native/vcruntime.d.ts +2 -0
  173. package/dist/guest/toolchains/native/vcruntime.js +10 -2
  174. package/dist/guest/toolchains/native/visual-studio.d.ts +3 -0
  175. package/dist/guest/toolchains/native/visual-studio.js +44 -26
  176. package/dist/guest/toolchains/native/vs-request.d.ts +2 -0
  177. package/dist/guest/toolchains/native/vs-request.js +2 -0
  178. package/dist/guest/toolchains/plan.js +19 -8
  179. package/dist/guest/toolchains/scheduler.d.ts +2 -21
  180. package/dist/guest/toolchains/scheduler.js +7 -121
  181. package/dist/guest/toolchains/setup.d.ts +15 -2
  182. package/dist/guest/toolchains/setup.js +39 -20
  183. package/dist/guest/toolchains/split.d.ts +28 -0
  184. package/dist/guest/toolchains/split.js +60 -0
  185. package/dist/guest/toolchains/steam/client.js +2 -1
  186. package/dist/guest/toolchains/step-scheduler.d.ts +51 -0
  187. package/dist/guest/toolchains/step-scheduler.js +147 -0
  188. package/dist/guest/toolchains/step.d.ts +13 -0
  189. package/dist/guest/toolchains/tools/git.js +18 -0
  190. package/dist/guest/vpn/openvpn.js +3 -4
  191. package/dist/guest/vpn/wireguard.js +11 -4
  192. package/dist/host/accounts/deliver.js +7 -0
  193. package/dist/host/accounts/handoff.js +2 -6
  194. package/dist/host/accounts/index.d.ts +1 -1
  195. package/dist/host/accounts/index.js +1 -1
  196. package/dist/host/accounts/kinds/cfx.d.ts +1 -1
  197. package/dist/host/accounts/kinds/cfx.js +47 -4
  198. package/dist/host/accounts/kinds/comet.d.ts +2 -0
  199. package/dist/host/accounts/kinds/comet.js +6 -0
  200. package/dist/host/accounts/kinds/perplexity.d.ts +2 -0
  201. package/dist/host/accounts/kinds/perplexity.js +30 -0
  202. package/dist/host/accounts/leases.d.ts +7 -1
  203. package/dist/host/accounts/leases.js +13 -2
  204. package/dist/host/accounts/source.d.ts +7 -1
  205. package/dist/host/accounts/source.js +27 -0
  206. package/dist/host/accounts/sources.js +4 -0
  207. package/dist/host/github/deliver.d.ts +1 -1
  208. package/dist/host/github/deliver.js +20 -1
  209. package/dist/host/github/index.d.ts +2 -1
  210. package/dist/host/github/index.js +1 -0
  211. package/dist/host/github/token.d.ts +13 -1
  212. package/dist/host/github/token.js +19 -2
  213. package/dist/host/github/verify.d.ts +27 -0
  214. package/dist/host/github/verify.js +61 -0
  215. package/dist/host/launch/hints/fivem.d.ts +3 -1
  216. package/dist/host/launch/hints/fivem.js +68 -2
  217. package/dist/host/launch/session-file.js +4 -0
  218. package/dist/host/launch/toolchains.d.ts +2 -1
  219. package/dist/host/launch/toolchains.js +5 -3
  220. package/dist/host/profile/claude-login.d.ts +2 -2
  221. package/dist/host/profile/claude-login.js +4 -3
  222. package/dist/host/vpn/nordvpn.d.ts +2 -4
  223. package/dist/host/vpn/nordvpn.js +2 -24
  224. package/dist/runtime/agent.js +405 -90
  225. package/dist/runtime/daemon.js +3010 -1383
  226. package/dist/runtime/db.js +333 -90
  227. package/dist/runtime/main.js +4720 -1954
  228. package/dist/runtime/netproxy.js +286 -69
  229. package/dist/runtime/tool.js +2852 -1780
  230. package/dist/runtime/view.js +286 -69
  231. package/dist/selftest/accounts.test.js +1 -1
  232. package/dist/selftest/agent-paths.test.js +39 -1
  233. package/dist/selftest/agent-settings.test.js +6 -0
  234. package/dist/selftest/agents-web.test.d.ts +2 -0
  235. package/dist/selftest/agents-web.test.js +647 -0
  236. package/dist/selftest/app.test.js +5 -2
  237. package/dist/selftest/boot-runner.test.d.ts +2 -0
  238. package/dist/selftest/boot-runner.test.js +456 -0
  239. package/dist/selftest/channel.test.js +15 -0
  240. package/dist/selftest/fivem.test.js +249 -3
  241. package/dist/selftest/github.test.js +94 -9
  242. package/dist/selftest/guest-boot.test.js +3 -0
  243. package/dist/selftest/health.test.js +1 -1
  244. package/dist/selftest/index.js +8 -0
  245. package/dist/selftest/layout.test.js +5 -0
  246. package/dist/selftest/menu-coverage.test.d.ts +2 -0
  247. package/dist/selftest/menu-coverage.test.js +46 -0
  248. package/dist/selftest/menu-screens.js +37 -1
  249. package/dist/selftest/persisted-installs.test.js +6 -3
  250. package/dist/selftest/service-accounts.test.js +3 -2
  251. package/dist/selftest/support/engine-golden.js +27 -0
  252. package/dist/selftest/support/fixtures.d.ts +6 -0
  253. package/dist/selftest/support/web-preview.d.ts +34 -1
  254. package/dist/selftest/support/web-preview.js +21 -18
  255. package/dist/selftest/toolchains.test.js +23 -1
  256. package/dist/selftest/ui-text.test.d.ts +7 -0
  257. package/dist/selftest/ui-text.test.js +300 -0
  258. package/dist/shared/accounts/anthropic.d.ts +1 -1
  259. package/dist/shared/accounts/anthropic.js +9 -8
  260. package/dist/shared/accounts/index.d.ts +2 -0
  261. package/dist/shared/accounts/index.js +2 -0
  262. package/dist/shared/accounts/kinds/comet.d.ts +12 -0
  263. package/dist/shared/accounts/kinds/comet.js +64 -0
  264. package/dist/shared/accounts/kinds/perplexity.d.ts +8 -0
  265. package/dist/shared/accounts/kinds/perplexity.js +37 -0
  266. package/dist/shared/accounts/kinds.d.ts +24 -5
  267. package/dist/shared/accounts/kinds.js +86 -4
  268. package/dist/shared/accounts/payloads.d.ts +81 -0
  269. package/dist/shared/accounts/payloads.js +39 -1
  270. package/dist/shared/apps/agent-addons.d.ts +33 -5
  271. package/dist/shared/apps/agent-addons.js +95 -3
  272. package/dist/shared/apps/catalogue-agents-web.d.ts +14 -0
  273. package/dist/shared/apps/catalogue-agents-web.js +112 -0
  274. package/dist/shared/apps/catalogue-modding.js +32 -1
  275. package/dist/shared/apps/catalogue.d.ts +27 -3
  276. package/dist/shared/apps/catalogue.js +2 -1
  277. package/dist/shared/apps/index.d.ts +3 -1
  278. package/dist/shared/apps/index.js +3 -1
  279. package/dist/shared/channel/schema.d.ts +21 -3
  280. package/dist/shared/channel/schema.js +7 -0
  281. package/dist/shared/channel/secrets.d.ts +19 -0
  282. package/dist/shared/concurrency/index.d.ts +3 -0
  283. package/dist/shared/concurrency/index.js +3 -0
  284. package/dist/shared/concurrency/installer-retry.d.ts +6 -0
  285. package/dist/shared/concurrency/installer-retry.js +10 -0
  286. package/dist/shared/concurrency/limiter.d.ts +19 -0
  287. package/dist/shared/concurrency/limiter.js +67 -0
  288. package/dist/shared/concurrency/pools.d.ts +7 -0
  289. package/dist/shared/concurrency/pools.js +9 -0
  290. package/dist/shared/download/archive.d.ts +2 -0
  291. package/dist/shared/download/archive.js +11 -1
  292. package/dist/shared/download/bounded-fetch.d.ts +13 -0
  293. package/dist/shared/download/bounded-fetch.js +40 -0
  294. package/dist/shared/download/downloader.d.ts +5 -0
  295. package/dist/shared/download/downloader.js +50 -38
  296. package/dist/shared/download/index.d.ts +2 -1
  297. package/dist/shared/download/index.js +2 -1
  298. package/dist/shared/readiness.d.ts +2 -2
  299. package/dist/shared/readiness.js +2 -2
  300. package/dist/web/index.html +1 -1
  301. package/dist/web/{main-OUUQYNK4.js → main-CCILLX2M.js} +67 -67
  302. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  303. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  304. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  305. package/package.json +1 -1
  306. package/sandbox/addons/browser-use-mcp/requirements.in +1 -0
  307. package/sandbox/addons/browser-use-mcp/requirements.txt +2350 -0
  308. package/sandbox/addons/chrome-devtools-mcp/package-lock.json +38 -0
  309. package/sandbox/addons/chrome-devtools-mcp/package.json +7 -0
  310. package/sandbox/addons/comet-mcp/package-lock.json +1277 -0
  311. package/sandbox/addons/comet-mcp/package.json +7 -0
  312. package/sandbox/addons/perplexity-mcp/package-lock.json +1655 -0
  313. package/sandbox/addons/perplexity-mcp/package.json +7 -0
  314. package/sandbox/scripts/add-firewall-port.ps1 +18 -4
  315. package/sandbox/scripts/set-machine-environment.ps1 +30 -15
  316. package/sandbox/templates/fivem/README.txt +6 -2
  317. package/sandbox/templates/fivem/cs/Client/Client.csproj +15 -0
  318. package/sandbox/templates/fivem/cs/Client/ClientMain.cs +20 -0
  319. package/sandbox/templates/fivem/cs/Server/Server.csproj +14 -0
  320. package/sandbox/templates/fivem/cs/Server/ServerMain.cs +20 -0
  321. package/sandbox/templates/fivem/cs/fxmanifest.lua +11 -0
  322. package/sandbox/templates/fivem/js/client.js +12 -0
  323. package/sandbox/templates/fivem/js/fxmanifest.lua +9 -0
  324. package/sandbox/templates/fivem/js/server.js +6 -0
  325. package/sandbox/templates/fivem/lua/client.lua +8 -0
  326. package/sandbox/templates/fivem/lua/fxmanifest.lua +10 -0
  327. package/sandbox/templates/fivem/lua/server.lua +6 -0
  328. package/sandbox/templates/fivem/server.cfg +37 -0
  329. package/sandbox/templates/fivem/ts/build.js +17 -0
  330. package/sandbox/templates/fivem/ts/fxmanifest.lua +9 -0
  331. package/sandbox/templates/fivem/ts/package.json +16 -0
  332. package/sandbox/templates/fivem/ts/src/client/client.ts +12 -0
  333. package/sandbox/templates/fivem/ts/src/client/tsconfig.json +13 -0
  334. package/sandbox/templates/fivem/ts/src/server/server.ts +6 -0
  335. package/sandbox/templates/fivem/ts/src/server/tsconfig.json +13 -0
  336. package/schemas/config.schema.json +56 -27
  337. package/schemas/sandbox-config.schema.json +37 -14
@@ -21,6 +21,8 @@ import { keepShownSecrets, settingsPage } from '../app/features/settings/index.j
21
21
  import { checkEngine, checkText, enginePage } from '../app/features/engine/index.js';
22
22
  import { runUpdate, updatesPage } from '../app/features/updates/index.js';
23
23
  import { MenuOperations } from '../cli/menu/io-menu.js';
24
+ import { sessionsMenu } from '../cli/menu/pages/sessions-page.js';
25
+ import { logsMenu } from '../cli/menu/pages/maintenance-page.js';
24
26
  import { accessNeeded, ago, allows, bytesText, Cancelled, ClientMessageSchema, defineOperation, exemption, fakeIO, fieldOf, fieldProblem, FieldSchema, fieldsOf, fromWireError, humanize, implementer, invoke, pageActions, pageHasContent, ServerMessageSchema, SURFACE_NAMES, toWireError, valueText, OWNER, } from '../app/kernel/index.js';
25
27
  import { createRpcHost, IMPLEMENTATIONS, readQuery, runDefinition } from '../app/registry.js';
26
28
  import { webPage } from '../app/features/web/index.js';
@@ -324,7 +326,8 @@ export const appSuite = {
324
326
  statusLines.includes('* api C:\\Projects\\api -> C:\\Projects\\api (started from here)') &&
325
327
  statusLines.some((l) => l.startsWith('Repo guard Watching 1 folder.')), 'status on the command line: facts as rows, projects with their details, the repo guard', statusLines.join('\n'));
326
328
  t.equal(pageLines(sessionsPage({ ...list, sessions: [] })).at(-1), 'No sessions. Start one with: sandbox claude', 'an empty page prints its command-line text');
327
- const menu = menuView(sessions);
329
+ const { wide: _wide, ...sessionsModel } = { ...list, wide: false };
330
+ const menu = sessionsMenu(sessionsModel);
328
331
  const chosen = menu.resolve({ kind: 'pick', value: 'new' });
329
332
  t.check(menu.items.length === 2 &&
330
333
  menu.actions.map((a) => a.key).join() === 'n,s,r,o,x,l' &&
@@ -858,7 +861,7 @@ export const appSuite = {
858
861
  logsPageNow.sections[0].items?.find((i) => i.id === 'log:guest-a.log')?.badges?.[0]?.text === 'showing' &&
859
862
  logsPageNow.sections[0].items?.find((i) => i.id === 'log:daemon-b.log')?.badges === undefined &&
860
863
  logsPageNow.sections[1]?.text?.length === 119, 'the logs page: one row per log, the shown one badged, the crash report last, the end of the log as text', itemIds.join());
861
- const logsMenuNow = menuView(logsPage(withCrash), { category: 'logs', refresh: false });
864
+ const logsMenuNow = logsMenu(withCrash);
862
865
  t.check(logsMenuNow.actions.map((a) => a.key).join() === 'f' &&
863
866
  logsMenuNow.enterLabel('logs', 'log:guest-a.log') === 'show' &&
864
867
  logsMenuNow.resolve({ kind: 'action', key: 'f', value: undefined })?.kind === 'action', 'the logs menu: the key f (follow in the terminal) and Enter shows', logsMenuNow.actions.map((a) => a.key).join());
@@ -0,0 +1,2 @@
1
+ import type { Suite } from './support/suite.js';
2
+ export declare const bootRunnerSuite: Suite;
@@ -0,0 +1,456 @@
1
+ // The parallel boot (docs/design/parallel-boot.md): the task runner, the pools, the boot graph and the toolchain
2
+ // scheduler's download phase and background split. Fake tasks and deferred promises only: no machine state.
3
+ import { BootRunner, bootSummary, checkGraph } from '../guest/boot/runner.js';
4
+ import { BOOT_TASKS, STAGES, stageTitle } from '../guest/boot/graph.js';
5
+ import { backgroundNames, mergeParts, runSplit } from '../guest/toolchains/split.js';
6
+ import { StepScheduler } from '../guest/toolchains/step-scheduler.js';
7
+ import { Limiter, asBackground, installerRetry, INSTALLER_RETRIES } from '../shared/concurrency/index.js';
8
+ const deferred = () => {
9
+ let resolve = () => undefined;
10
+ let reject = (_) => undefined;
11
+ const promise = new Promise((res, rej) => {
12
+ resolve = res;
13
+ reject = rej;
14
+ });
15
+ return { promise, resolve, reject };
16
+ };
17
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
18
+ /** Lets queued promise callbacks run (a few turns of the event loop). */
19
+ const settle = async () => {
20
+ for (let i = 0; i < 5; i++)
21
+ await sleep(1);
22
+ };
23
+ const def = (id, after = [], more = {}) => ({
24
+ id,
25
+ stage: 1,
26
+ title: id.toUpperCase(),
27
+ after,
28
+ critical: false,
29
+ ...more,
30
+ });
31
+ const fixture = () => {
32
+ const lines = [];
33
+ const warnings = [];
34
+ const contexts = [];
35
+ return {
36
+ lines,
37
+ warnings,
38
+ contexts,
39
+ options: (extra = {}) => ({
40
+ say: (line) => void lines.push(line),
41
+ warn: (line) => void warnings.push(line),
42
+ now: () => Date.now(),
43
+ dryRun: false,
44
+ stages: 9,
45
+ stageTitle: (n) => 'Stage ' + n,
46
+ inContext: (ctx, work) => {
47
+ contexts.push({ quiet: ctx.quiet });
48
+ return work();
49
+ },
50
+ ...extra,
51
+ }),
52
+ };
53
+ };
54
+ const rejection = async (work) => {
55
+ try {
56
+ await work;
57
+ return '';
58
+ }
59
+ catch (e) {
60
+ return e instanceof Error ? e.message : String(e);
61
+ }
62
+ };
63
+ const throws = (work) => {
64
+ try {
65
+ work();
66
+ return '';
67
+ }
68
+ catch (e) {
69
+ return e instanceof Error ? e.message : String(e);
70
+ }
71
+ };
72
+ /** The transitive dependencies of a task in the boot graph. */
73
+ const ancestors = (id) => {
74
+ const seen = new Set();
75
+ const visit = (name) => {
76
+ for (const a of BOOT_TASKS.find((d) => d.id === name)?.after ?? [])
77
+ if (!seen.has(a)) {
78
+ seen.add(a);
79
+ visit(a);
80
+ }
81
+ };
82
+ visit(id);
83
+ return seen;
84
+ };
85
+ const step = (name, o = {}) => ({
86
+ name,
87
+ enabled: true,
88
+ install: () => undefined,
89
+ ...o,
90
+ });
91
+ const sink = () => {
92
+ const warnings = [];
93
+ return { warnings, warn: (m) => void warnings.push(m), status: () => undefined };
94
+ };
95
+ export const bootRunnerSuite = {
96
+ name: 'boot-runner',
97
+ title: 'parallel boot: task runner, pools, boot graph, toolchain download phase and background split',
98
+ run: async (t) => {
99
+ t.section('runner: ordering');
100
+ {
101
+ const f = fixture();
102
+ const log = [];
103
+ const gates = new Map();
104
+ const defs = [def('a'), def('b', ['a']), def('c', ['a']), def('d', ['b', 'c']), def('e')];
105
+ const impl = Object.fromEntries(defs.map((d) => [
106
+ d.id,
107
+ async () => {
108
+ log.push('start ' + d.id);
109
+ gates.set(d.id, deferred());
110
+ await gates.get(d.id).promise;
111
+ log.push('end ' + d.id);
112
+ },
113
+ ]));
114
+ const runner = new BootRunner(defs, impl, f.options());
115
+ const running = runner.run('foreground');
116
+ for (let i = 0; i < 30 && log.filter((l) => l.startsWith('end')).length < defs.length; i++) {
117
+ await settle();
118
+ // Finish the oldest task that has started and not ended, newest dependencies last.
119
+ const open = [...gates.keys()].filter((id) => !log.includes('end ' + id));
120
+ if (open.length)
121
+ gates.get(open[open.length - 1]).resolve();
122
+ }
123
+ await running;
124
+ const at = (l) => log.indexOf(l);
125
+ t.check(defs.every((d) => d.after.every((a) => at('end ' + a) < at('start ' + d.id))), 'every task starts after all the tasks it comes after have ended', log.join(', '));
126
+ t.check(at('start e') < at('end a') && at('start a') < at('start d'), 'a task with no dependencies starts at once, next to a running one', log.join(', '));
127
+ }
128
+ {
129
+ const f = fixture();
130
+ const order = [];
131
+ // Declared out of dependency order on purpose: x needs z, z needs y.
132
+ const defs = [def('x', ['z']), def('p'), def('y'), def('z', ['y']), def('q', ['p'])];
133
+ const impl = Object.fromEntries(defs.map((d) => [d.id, () => void order.push(d.id)]));
134
+ await new BootRunner(defs, impl, f.options({ parallel: 1 })).run('foreground');
135
+ t.equal(order, ['p', 'y', 'z', 'x', 'q'], 'with one task at a time the order is the declaration-order topological sort');
136
+ }
137
+ t.section('runner: limits');
138
+ {
139
+ const f = fixture();
140
+ let now = 0;
141
+ let peak = 0;
142
+ const defs = ['a', 'b', 'c', 'd', 'e', 'f'].map((id) => def(id));
143
+ const impl = Object.fromEntries(defs.map((d) => [
144
+ d.id,
145
+ async () => {
146
+ now++;
147
+ peak = Math.max(peak, now);
148
+ await sleep(15);
149
+ now--;
150
+ },
151
+ ]));
152
+ await new BootRunner(defs, impl, f.options({ parallel: 2 })).run('foreground');
153
+ t.check(peak === 2, '6 independent tasks with parallel 2 never run more than 2 at once', 'peak ' + peak);
154
+ }
155
+ {
156
+ const f = fixture();
157
+ let now = 0;
158
+ let exclusiveSaw = -1;
159
+ let startedDuring = 0;
160
+ let exclusiveRunning = false;
161
+ const tick = (id) => async () => {
162
+ if (exclusiveRunning)
163
+ startedDuring++;
164
+ if (id === 'x') {
165
+ exclusiveRunning = true;
166
+ exclusiveSaw = now;
167
+ }
168
+ now++;
169
+ await sleep(15);
170
+ now--;
171
+ if (id === 'x')
172
+ exclusiveRunning = false;
173
+ };
174
+ const defs = [def('a'), def('b'), def('x', [], { exclusive: true }), def('c'), def('d')];
175
+ await new BootRunner(defs, Object.fromEntries(defs.map((d) => [d.id, tick(d.id)])), f.options()).run('foreground');
176
+ t.check(exclusiveSaw === 0 && startedDuring === 0, 'an exclusive task runs alone and holds back the ones declared after it', exclusiveSaw + ' / ' + startedDuring);
177
+ }
178
+ t.section('runner: failures');
179
+ {
180
+ const f = fixture();
181
+ const ran = [];
182
+ const defs = [def('a'), def('b', ['a']), def('c', ['b']), def('d')];
183
+ const impl = {
184
+ a: () => {
185
+ ran.push('a');
186
+ throw new Error('boom');
187
+ },
188
+ b: () => void ran.push('b'),
189
+ c: () => void ran.push('c'),
190
+ d: () => void ran.push('d'),
191
+ };
192
+ const out = await new BootRunner(defs, impl, f.options()).run('foreground');
193
+ t.check(f.warnings.some((w) => w.includes('A failed (continuing): boom')) &&
194
+ [...ran].sort().join('') === 'abcd' &&
195
+ out.find((o) => o.id === 'a')?.state === 'failed' &&
196
+ out.find((o) => o.id === 'c')?.state === 'done', 'a soft failure is a warning "failed (continuing)", the siblings and dependants still run, the outcome is failed', f.warnings.join(' | ') + ' / ' + ran.join(''));
197
+ }
198
+ {
199
+ const f = fixture();
200
+ const ran = [];
201
+ const slow = deferred();
202
+ const defs = [def('a', [], { critical: true }), def('b', ['a']), def('c', ['b']), def('slow')];
203
+ const impl = {
204
+ a: async () => {
205
+ await sleep(5);
206
+ ran.push('a');
207
+ throw new Error('fatal');
208
+ },
209
+ b: () => void ran.push('b'),
210
+ c: () => void ran.push('c'),
211
+ slow: async () => {
212
+ await slow.promise;
213
+ ran.push('slow');
214
+ },
215
+ };
216
+ const runner = new BootRunner(defs, impl, f.options());
217
+ const result = rejection(runner.run('foreground'));
218
+ await sleep(40);
219
+ let early = true;
220
+ void result.then(() => (early = false));
221
+ await settle();
222
+ t.check(early, 'run() waits for the tasks that are still running before it rejects');
223
+ slow.resolve();
224
+ const message = await result;
225
+ const by = new Map(runner.results().map((o) => [o.id, o]));
226
+ t.check(message === 'fatal' &&
227
+ ran.join(',') === 'a,slow' &&
228
+ by.get('b')?.state === 'skipped' &&
229
+ by.get('c')?.state === 'skipped' &&
230
+ by.get('slow')?.state === 'done' &&
231
+ f.lines.some((l) => l.includes('B: skipped (A failed)')), 'a critical failure skips what comes after it, an independent long task completes, run() rejects', message + ' / ' + ran.join(',') + ' / ' + [...by.values()].map((o) => o.id + ':' + o.state).join(' '));
232
+ }
233
+ t.section('runner: background');
234
+ {
235
+ const f = fixture();
236
+ const ran = [];
237
+ const defs = [def('a'), def('bg', ['a'], { background: true })];
238
+ const impl = { a: () => void ran.push('a'), bg: () => void ran.push('bg') };
239
+ const runner = new BootRunner(defs, impl, f.options());
240
+ await runner.run('foreground');
241
+ await settle();
242
+ const before = ran.join(',');
243
+ const quietBefore = f.contexts.map((c) => c.quiet).join(',');
244
+ await runner.run('background');
245
+ t.check(before === 'a' && quietBefore === 'false' && ran.join(',') === 'a,bg' && f.contexts[1]?.quiet === true, 'a background task starts only on run("background"), in a quiet (file-only) log context', before + ' / ' + quietBefore);
246
+ t.check(!f.lines.some((l) => l.includes('BG: started')), 'background tasks print no progress lines');
247
+ }
248
+ t.section('runner: graph checks');
249
+ t.check(throws(() => checkGraph([def('a', ['b']), def('b', ['a'])])).includes('circle'), 'a cycle is refused');
250
+ t.check(throws(() => checkGraph([def('a', ['nope'])])).includes('does not exist'), 'an unknown dependency is refused');
251
+ t.check(throws(() => checkGraph([def('a'), def('a')])).includes('twice'), 'a duplicate id is refused');
252
+ t.check(throws(() => checkGraph([def('bg', [], { background: true }), def('fg', ['bg'])])).includes('background task'), 'a foreground task that waits for a background one is refused');
253
+ t.check(throws(() => new BootRunner([def('a', ['a'])], { a: () => undefined }, fixture().options())).length > 0, 'the constructor runs the same checks');
254
+ t.section('pools');
255
+ {
256
+ const pool = new Limiter(2, 'p');
257
+ let now = 0;
258
+ await Promise.all(Array.from({ length: 6 }, () => pool.run(async () => {
259
+ now++;
260
+ await sleep(10);
261
+ now--;
262
+ })));
263
+ t.check(pool.peak === 2 && now === 0, 'Limiter(2) peaks at 2', 'peak ' + pool.peak);
264
+ }
265
+ {
266
+ const pool = new Limiter(1, 'p');
267
+ const order = [];
268
+ const hold = deferred();
269
+ const first = pool.run(() => hold.promise);
270
+ const back = asBackground(() => pool.run(() => void order.push('background')));
271
+ const front = pool.run(() => void order.push('foreground'));
272
+ await settle();
273
+ hold.resolve();
274
+ await Promise.all([first, back, front]);
275
+ t.equal(order, ['foreground', 'background'], 'foreground work queued after background work still goes first');
276
+ }
277
+ {
278
+ const a = new Limiter(1, 'a');
279
+ const b = new Limiter(1, 'b');
280
+ const message = await rejection(a.run(() => b.run(() => undefined)));
281
+ const again = await rejection(a.run(() => a.run(() => undefined)));
282
+ t.check(message.includes('nested pool') && again.includes('nested pool'), 'taking a second pool (or the same one) while holding one throws "nested pool"', message);
283
+ t.check((await a.run(() => 7)) === 7 && a.running === 0, 'the slot is free again after a nested-pool error');
284
+ }
285
+ t.check(installerRetry(1618, 1) &&
286
+ installerRetry(1618, INSTALLER_RETRIES) &&
287
+ !installerRetry(1618, INSTALLER_RETRIES + 1) &&
288
+ !installerRetry(0, 1) &&
289
+ !installerRetry(1603, 1) &&
290
+ !installerRetry(null, 1), 'an installer that exits 1618 is retried up to the limit; 0 and 1603 are not');
291
+ t.section('timing');
292
+ {
293
+ const defs = [def('a'), def('b'), def('c')];
294
+ const impl = Object.fromEntries(defs.map((d) => [d.id, () => sleep(100)]));
295
+ const began = Date.now();
296
+ await new BootRunner(defs, impl, fixture().options({ parallel: 3 })).run('foreground');
297
+ const took = Date.now() - began;
298
+ t.check(took < 250, '3 independent 100 ms tasks with parallel 3 finish in under 250 ms', took + ' ms');
299
+ const pool = new Limiter(1, 'one');
300
+ const start = Date.now();
301
+ await Promise.all([1, 2, 3].map(() => pool.run(() => sleep(100))));
302
+ const serial = Date.now() - start;
303
+ // setTimeout may fire a millisecond early: 3 x 100 ms in a row is at least 295 ms.
304
+ t.check(serial >= 295, '3 100 ms jobs on Limiter(1) take 300 ms', serial + ' ms');
305
+ }
306
+ t.section('boot graph');
307
+ {
308
+ const defs = BOOT_TASKS;
309
+ t.check(throws(() => checkGraph(defs)) === '', 'the boot graph is acyclic and every dependency exists');
310
+ const foreground = BOOT_TASKS.filter((d) => !('background' in d && d.background));
311
+ const reach = ancestors('session');
312
+ t.equal(foreground.map((d) => d.id).filter((id) => id !== 'session' && !reach.has(id)), [], 'session depends, directly or not, on every foreground task');
313
+ const pre = BOOT_TASKS.find((d) => d.id === 'hooksPreAgent');
314
+ t.check(pre?.exclusive === true, 'the pre-agent hooks run exclusively');
315
+ const before = (first, second) => ancestors(second).has(first);
316
+ const orders = [
317
+ ['network', 'toolchains'],
318
+ ['toolchains', 'allowOnly'],
319
+ ['allowOnly', 'env'],
320
+ ['vpn', 'githubLogin'],
321
+ ['vpn', 'agentProvision'],
322
+ ['gitConfig', 'githubLogin'],
323
+ ['headroomReady', 'managedSettings'],
324
+ ['managedSettings', 'guide'],
325
+ ['bootEnv', 'guide'],
326
+ ['claudeProfile', 'mcpServers'],
327
+ ['audit', 'session'],
328
+ ['profileLinks', 'agentProvision'],
329
+ ['toolchains', 'vpn'],
330
+ ['hooksPreProvision', 'toolchains'],
331
+ ['hooksPreAgent', 'session'],
332
+ ];
333
+ t.equal(orders.filter(([a, b]) => !before(a, b)).map(([a, b]) => a + ' < ' + b), [], 'the required orderings hold (network < toolchains < allowOnly < env, vpn before git and the agent, ...)');
334
+ const writers = new Map();
335
+ for (const d of BOOT_TASKS)
336
+ for (const w of d.writes)
337
+ writers.set(w, [...(writers.get(w) ?? []), d.id]);
338
+ t.equal([...writers].filter(([, w]) => w.length !== 1).map(([k]) => k), [], 'every boot state field has exactly one writing task');
339
+ const unread = BOOT_TASKS.flatMap((d) => d.reads
340
+ .filter((r) => {
341
+ const writer = writers.get(r)?.[0];
342
+ return !writer || !ancestors(d.id).has(writer);
343
+ })
344
+ .map((r) => d.id + ' reads ' + r));
345
+ t.equal(unread, [], 'every task that reads boot state comes after the task that writes it');
346
+ t.check(STAGES === 9 &&
347
+ [1, 2, 3, 4, 5, 6, 7, 8, 9].every((n) => stageTitle(n).length > 0) &&
348
+ stageTitle(4, { vpnServer: 'x' }).includes('x') &&
349
+ stageTitle(4) === 'VPN (off)', 'there are nine stage titles, the VPN one names the server or says off');
350
+ const summary = bootSummary(31800, [
351
+ { id: 'a', title: 'Headroom', state: 'done', ms: 28100 },
352
+ { id: 'b', title: 'Developer tools', state: 'done', ms: 7000 },
353
+ { id: 'c', title: 'Skipped one', state: 'skipped', ms: 0 },
354
+ ], ['Visual Studio'], 'guest.log');
355
+ t.check(summary.startsWith('Boot ready in 31.8s.') &&
356
+ summary.includes('Headroom 28.1s') &&
357
+ !summary.includes('Skipped one') &&
358
+ summary.includes('In the background: Visual Studio'), 'the boot summary names the time, the slowest tasks and the background work', summary);
359
+ }
360
+ t.section('toolchain scheduler');
361
+ {
362
+ const events = [];
363
+ const gate = deferred();
364
+ const steps = [
365
+ step('A', {
366
+ install: async () => {
367
+ events.push('A installing');
368
+ await gate.promise;
369
+ events.push('A done');
370
+ },
371
+ }),
372
+ step('B', {
373
+ after: ['A'],
374
+ fetch: async () => void events.push('B fetch'),
375
+ install: () => void events.push('B install'),
376
+ }),
377
+ ];
378
+ const scheduler = new StepScheduler(sink());
379
+ const run = scheduler.run(steps, {});
380
+ await settle();
381
+ t.check(events.includes('B fetch') && !events.includes('A done'), "a step's download starts at once, before the steps it comes after have finished", events.join(','));
382
+ gate.resolve();
383
+ await run;
384
+ t.equal(events, ['B fetch', 'A installing', 'A done', 'B install'], 'the install follows its dependencies after the download');
385
+ }
386
+ {
387
+ const s = sink();
388
+ const steps = [
389
+ step('A', {
390
+ fetch: () => Promise.reject(new Error('offline')),
391
+ install: () => undefined,
392
+ }),
393
+ ];
394
+ const envs = await new StepScheduler(s).run(steps, {});
395
+ t.check(envs.has('A') && !s.warnings.length, 'a failed download is ignored: the install downloads again and reports');
396
+ }
397
+ {
398
+ const names = backgroundNames([
399
+ step('Visual Studio', { background: true }),
400
+ step('Unreal', { after: ['Visual Studio'] }),
401
+ step('Rust', { after: ['Unreal'] }),
402
+ step('Go'),
403
+ step('Kotlin', { after: ['Go'] }),
404
+ ]);
405
+ t.equal([...names].sort(), ['Rust', 'Unreal', 'Visual Studio'], 'a step that comes after a background step is a background step too');
406
+ }
407
+ {
408
+ const events = [];
409
+ const open = deferred();
410
+ const steps = [
411
+ step('VS', {
412
+ background: true,
413
+ install: async (env) => {
414
+ events.push('VS install');
415
+ env.prepend.push('vs');
416
+ },
417
+ }),
418
+ step('Unreal', { after: ['VS'], install: () => void events.push('Unreal install') }),
419
+ step('Go', { install: () => void events.push('Go install') }),
420
+ ];
421
+ const split = await runSplit(new StepScheduler(sink()), steps, {}, { gate: open.promise });
422
+ await settle();
423
+ t.check(split.nowNames.join(',') === 'Go' &&
424
+ split.laterNames.join(',') === 'VS,Unreal' &&
425
+ [...split.now.keys()].join(',') === 'Go' &&
426
+ events.join(',') === 'Go install', 'background steps (and their dependants) are left out of the foreground result and wait for the gate', events.join(',') + ' / ' + split.nowNames.join(',') + ' / ' + split.laterNames.join(','));
427
+ open.resolve();
428
+ const later = await split.later;
429
+ t.check(events.join(',') === 'Go install,VS install,Unreal install' && [...later.keys()].join(',') === 'VS,Unreal', 'they start once the gate opens and return only their own parts', events.join(','));
430
+ }
431
+ {
432
+ // The first step finishes last: PATH order still follows the plan.
433
+ const slow = deferred();
434
+ const steps = [
435
+ step('First', {
436
+ install: async (env) => {
437
+ await slow.promise;
438
+ env.prepend.push('first');
439
+ },
440
+ }),
441
+ step('Second', {
442
+ install: (env) => {
443
+ env.prepend.push('second');
444
+ },
445
+ }),
446
+ ];
447
+ const running = new StepScheduler(sink()).run(steps, {});
448
+ await settle();
449
+ slow.resolve();
450
+ const parts = await running;
451
+ const merged = mergeParts(['First', 'Second'], parts, { environment: { X: '1' }, bin: 'bin' });
452
+ t.equal(merged.prepend, ['bin', 'first', 'second'], 'the merged PATH order equals the plan order whichever step finishes first');
453
+ t.check(merged.vars.X === '1', "the config's own variables are merged in");
454
+ }
455
+ },
456
+ };
@@ -85,6 +85,21 @@ export const channelSuite = {
85
85
  const r = parsed({ ...goodStatus, progress: p });
86
86
  return r.success && r.data.progress === undefined;
87
87
  }), hostile.map(() => true), 'an oversized or malformed progress is dropped, the status itself still counts');
88
+ const background = [{ name: 'Visual Studio', state: 'running' }];
89
+ const withBackground = parsed({ ...goodStatus, phase: 'ready', background });
90
+ t.check(JSON.stringify(withBackground.data?.background) === JSON.stringify(background) &&
91
+ parsed({ ...goodStatus, phase: 'ready' }).data?.background === undefined, 'the background work of a ready sandbox is read (and is absent from an older sandbox)');
92
+ const hostileBackground = [
93
+ [{ name: 'x'.repeat(500), state: 'running' }],
94
+ [{ name: 'VS', state: 'pwned' }],
95
+ Array.from({ length: 100 }, () => ({ name: 'a', state: 'done' })),
96
+ [{ name: 'VS', state: 'running', extra: 1 }],
97
+ 'pwned',
98
+ ];
99
+ t.equal(hostileBackground.map((b) => {
100
+ const r = parsed({ ...goodStatus, background: b });
101
+ return r.success && r.data.background === undefined && r.data.phase === 'provisioning';
102
+ }), hostileBackground.map(() => true), 'a malformed background list is dropped, the status itself still counts');
88
103
  const line = (p) => bootLine(parsed({ ...goodStatus, progress: p }).data ?? null).line;
89
104
  t.equal([
90
105
  line(progress),