@highstate/backend 0.9.18 → 0.9.20

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 (331) hide show
  1. package/dist/{chunk-OU5OQBLB.js → chunk-I7BWSAN6.js} +3 -28
  2. package/dist/{chunk-OU5OQBLB.js.map → chunk-I7BWSAN6.js.map} +1 -1
  3. package/dist/chunk-RC6Q3XQQ.js +1547 -0
  4. package/dist/chunk-RC6Q3XQQ.js.map +1 -0
  5. package/dist/chunk-VB4YL327.js +139 -0
  6. package/dist/chunk-VB4YL327.js.map +1 -0
  7. package/dist/database/local/prisma.config.js +26 -0
  8. package/dist/database/local/prisma.config.js.map +1 -0
  9. package/dist/highstate.manifest.json +2 -1
  10. package/dist/index.js +7590 -7289
  11. package/dist/index.js.map +1 -1
  12. package/dist/library/package-resolution-worker.js +1 -1
  13. package/dist/library/package-resolution-worker.js.map +1 -1
  14. package/dist/library/worker/main.js +35 -29
  15. package/dist/library/worker/main.js.map +1 -1
  16. package/dist/shared/index.js +2 -2
  17. package/package.json +18 -9
  18. package/prisma/backend/_schema/layout.prisma +7 -0
  19. package/prisma/backend/_schema/library.prisma +17 -0
  20. package/prisma/backend/_schema/project.prisma +101 -0
  21. package/prisma/backend/_schema/pulumi.prisma +17 -0
  22. package/prisma/backend/postgresql/main.prisma +17 -0
  23. package/prisma/backend/sqlite/main.prisma +17 -0
  24. package/prisma/backend/sqlite/migrations/20250817070609_initiial/migration.sql +34 -0
  25. package/prisma/backend/sqlite/migrations/20250817104948_add_fields/migration.sql +59 -0
  26. package/prisma/backend/sqlite/migrations/20250818082732_add_models/migration.sql +41 -0
  27. package/prisma/backend/sqlite/migrations/20250818083106_a/migration.sql +19 -0
  28. package/prisma/backend/sqlite/migrations/20250818101945_hi/migration.sql +1 -0
  29. package/prisma/backend/sqlite/migrations/20250819082315_a/migration.sql +5 -0
  30. package/prisma/backend/sqlite/migrations/migration_lock.toml +3 -0
  31. package/prisma/project/api-key.prisma +32 -0
  32. package/prisma/project/artifact.prisma +52 -0
  33. package/prisma/project/custom-status.prisma +46 -0
  34. package/prisma/project/evaluation.prisma +45 -0
  35. package/prisma/project/instance.prisma +157 -0
  36. package/prisma/project/layout.prisma +23 -0
  37. package/prisma/project/lock.prisma +18 -0
  38. package/prisma/project/main.prisma +17 -0
  39. package/prisma/project/migrations/20250816081310_initial/migration.sql +300 -0
  40. package/prisma/project/migrations/20250816082523_test/migration.sql +72 -0
  41. package/prisma/project/migrations/20250818065643_update/migration.sql +42 -0
  42. package/prisma/project/migrations/20250818070758_a/migration.sql +8 -0
  43. package/prisma/project/migrations/20250818070913_a/migration.sql +8 -0
  44. package/prisma/project/migrations/20250818082720_add_motels/migration.sql +11 -0
  45. package/prisma/project/migrations/20250818112523_hello/migration.sql +35 -0
  46. package/prisma/project/migrations/20250819082305_a/migration.sql +14 -0
  47. package/prisma/project/migrations/20250819165004_add_missing_fields/migration.sql +216 -0
  48. package/prisma/project/migrations/20250819171309_a/migration.sql +22 -0
  49. package/prisma/project/migrations/20250820113949_a/migration.sql +66 -0
  50. package/prisma/project/migrations/20250820144256_b/migration.sql +31 -0
  51. package/prisma/project/migrations/20250820145547_a/migration.sql +24 -0
  52. package/prisma/project/migrations/20250820182517_b/migration.sql +2 -0
  53. package/prisma/project/migrations/20250821172324_a/migration.sql +2 -0
  54. package/prisma/project/migrations/20250822081339_a/migration.sql +219 -0
  55. package/prisma/project/migrations/20250822083742_b/migration.sql +1 -0
  56. package/prisma/project/migrations/20250822105134_boom/migration.sql +1 -0
  57. package/prisma/project/migrations/20250822141028_b/migration.sql +1 -0
  58. package/prisma/project/migrations/20250822142342_b/migration.sql +16 -0
  59. package/prisma/project/migrations/20250824072720_a/migration.sql +1 -0
  60. package/prisma/project/migrations/20250824093656_b/migration.sql +21 -0
  61. package/prisma/project/migrations/20250825082518_a/migration.sql +1 -0
  62. package/prisma/project/migrations/20250825085343_b/migration.sql +1 -0
  63. package/prisma/project/migrations/20250825091312_a/migration.sql +1 -0
  64. package/prisma/project/migrations/20250903095431_hi/migration.sql +44 -0
  65. package/prisma/project/migrations/20250903174255_a/migration.sql +24 -0
  66. package/prisma/project/migrations/20250908095205_hi/migration.sql +18 -0
  67. package/prisma/project/migrations/20250909155857_hi/migration.sql +15 -0
  68. package/prisma/project/migrations/migration_lock.toml +3 -0
  69. package/prisma/project/model.prisma +37 -0
  70. package/prisma/project/operation.prisma +148 -0
  71. package/prisma/project/page.prisma +49 -0
  72. package/prisma/project/secret.prisma +54 -0
  73. package/prisma/project/service-account.prisma +42 -0
  74. package/prisma/project/terminal.prisma +107 -0
  75. package/prisma/project/trigger.prisma +37 -0
  76. package/prisma/project/unlock-method.prisma +46 -0
  77. package/prisma/project/worker.prisma +169 -0
  78. package/src/artifact/abstractions.ts +13 -13
  79. package/src/artifact/encryption.ts +30 -54
  80. package/src/artifact/factory.ts +6 -9
  81. package/src/artifact/local.ts +33 -46
  82. package/src/business/api-key.ts +24 -36
  83. package/src/business/artifact.test.ts +978 -0
  84. package/src/business/artifact.ts +136 -216
  85. package/src/business/evaluation.ts +328 -0
  86. package/src/business/index.ts +5 -2
  87. package/src/business/instance-lock.test.ts +1060 -0
  88. package/src/business/instance-lock.ts +387 -78
  89. package/src/business/instance-state.test.ts +735 -0
  90. package/src/business/instance-state.ts +582 -337
  91. package/src/business/operation.test.ts +439 -0
  92. package/src/business/operation.ts +174 -208
  93. package/src/business/project-model.ts +258 -0
  94. package/src/business/project-unlock.ts +168 -126
  95. package/src/business/project.ts +287 -179
  96. package/src/business/secret.test.ts +469 -130
  97. package/src/business/secret.ts +177 -217
  98. package/src/business/settings.test.ts +695 -0
  99. package/src/business/settings.ts +855 -0
  100. package/src/business/terminal-session.ts +90 -0
  101. package/src/business/unit-extra.test.ts +539 -0
  102. package/src/business/unit-extra.ts +160 -0
  103. package/src/business/worker.test.ts +356 -579
  104. package/src/business/worker.ts +238 -339
  105. package/src/common/codebase.ts +65 -0
  106. package/src/common/index.ts +3 -5
  107. package/src/common/logger.ts +5 -0
  108. package/src/common/utils.ts +4 -3
  109. package/src/config.ts +10 -11
  110. package/src/database/_generated/backend/postgresql/client.ts +72 -0
  111. package/src/database/_generated/backend/postgresql/commonInputTypes.ts +350 -0
  112. package/src/database/_generated/backend/postgresql/enums.ts +13 -0
  113. package/src/database/_generated/backend/postgresql/internal/class.ts +320 -0
  114. package/src/database/_generated/backend/postgresql/internal/prismaNamespace.ts +1238 -0
  115. package/src/database/_generated/backend/postgresql/models/Library.ts +1263 -0
  116. package/src/database/_generated/backend/postgresql/models/Project.ts +2175 -0
  117. package/src/database/_generated/backend/postgresql/models/ProjectModelStorage.ts +1263 -0
  118. package/src/database/_generated/backend/postgresql/models/ProjectSpace.ts +1602 -0
  119. package/src/database/_generated/backend/postgresql/models/PulumiBackend.ts +1263 -0
  120. package/src/database/_generated/backend/postgresql/models/UserWorkspaseLayout.ts +1065 -0
  121. package/src/database/_generated/backend/postgresql/models.ts +16 -0
  122. package/src/database/_generated/backend/postgresql/pjtg.ts +182 -0
  123. package/src/database/_generated/backend/sqlite/client.ts +72 -0
  124. package/src/database/_generated/backend/sqlite/commonInputTypes.ts +331 -0
  125. package/src/database/_generated/backend/sqlite/enums.ts +13 -0
  126. package/src/database/_generated/backend/sqlite/internal/class.ts +318 -0
  127. package/src/database/_generated/backend/sqlite/internal/prismaNamespace.ts +1207 -0
  128. package/src/database/_generated/backend/sqlite/models/Library.ts +1261 -0
  129. package/src/database/_generated/backend/sqlite/models/Project.ts +2169 -0
  130. package/src/database/_generated/backend/sqlite/models/ProjectModelStorage.ts +1261 -0
  131. package/src/database/_generated/backend/sqlite/models/ProjectSpace.ts +1599 -0
  132. package/src/database/_generated/backend/sqlite/models/PulumiBackend.ts +1261 -0
  133. package/src/database/_generated/backend/sqlite/models/UserWorkspaseLayout.ts +1063 -0
  134. package/src/database/_generated/backend/sqlite/models.ts +16 -0
  135. package/src/database/_generated/backend/sqlite/pjtg.ts +182 -0
  136. package/src/database/_generated/project/client.ts +204 -0
  137. package/src/database/_generated/project/commonInputTypes.ts +827 -0
  138. package/src/database/_generated/project/enums.ts +104 -0
  139. package/src/database/_generated/project/internal/class.ts +479 -0
  140. package/src/database/_generated/project/internal/prismaNamespace.ts +2974 -0
  141. package/src/database/_generated/project/models/ApiKey.ts +1506 -0
  142. package/src/database/_generated/project/models/Artifact.ts +2051 -0
  143. package/src/database/_generated/project/models/HubModel.ts +1125 -0
  144. package/src/database/_generated/project/models/InstanceCustomStatus.ts +1713 -0
  145. package/src/database/_generated/project/models/InstanceEvaluationState.ts +1312 -0
  146. package/src/database/_generated/project/models/InstanceLock.ts +1268 -0
  147. package/src/database/_generated/project/models/InstanceModel.ts +1125 -0
  148. package/src/database/_generated/project/models/InstanceOperationState.ts +1707 -0
  149. package/src/database/_generated/project/models/InstanceState.ts +4613 -0
  150. package/src/database/_generated/project/models/Operation.ts +1647 -0
  151. package/src/database/_generated/project/models/OperationLog.ts +1455 -0
  152. package/src/database/_generated/project/models/Page.ts +1838 -0
  153. package/src/database/_generated/project/models/Secret.ts +1692 -0
  154. package/src/database/_generated/project/models/ServiceAccount.ts +2165 -0
  155. package/src/database/_generated/project/models/Terminal.ts +2038 -0
  156. package/src/database/_generated/project/models/TerminalSession.ts +1454 -0
  157. package/src/database/_generated/project/models/TerminalSessionLog.ts +1280 -0
  158. package/src/database/_generated/project/models/Trigger.ts +1430 -0
  159. package/src/database/_generated/project/models/UnlockMethod.ts +1220 -0
  160. package/src/database/_generated/project/models/UserCompositeViewport.ts +1280 -0
  161. package/src/database/_generated/project/models/UserProjectViewport.ts +1059 -0
  162. package/src/database/_generated/project/models/Worker.ts +1459 -0
  163. package/src/database/_generated/project/models/WorkerUnitRegistration.ts +1524 -0
  164. package/src/database/_generated/project/models/WorkerVersion.ts +1974 -0
  165. package/src/database/_generated/project/models/WorkerVersionLog.ts +1318 -0
  166. package/src/database/_generated/project/models.ts +35 -0
  167. package/src/database/_generated/project/pjtg.ts +182 -0
  168. package/src/database/abstractions.ts +19 -0
  169. package/src/database/factory.ts +37 -0
  170. package/src/database/index.ts +6 -0
  171. package/src/database/local/backend.ts +134 -0
  172. package/src/database/local/index.ts +3 -0
  173. package/src/database/local/meta.ts +46 -0
  174. package/src/database/local/prisma.config.ts +25 -0
  175. package/src/database/local/project.ts +39 -0
  176. package/src/database/manager.ts +181 -0
  177. package/src/database/migrate.ts +35 -0
  178. package/src/database/prisma.ts +56 -0
  179. package/src/database/well-known.ts +38 -0
  180. package/src/index.ts +4 -4
  181. package/src/library/abstractions.ts +3 -5
  182. package/src/library/factory.ts +1 -1
  183. package/src/library/local.ts +81 -26
  184. package/src/library/package-resolution-worker.ts +1 -1
  185. package/src/library/worker/evaluator.ts +40 -23
  186. package/src/library/worker/loader.lite.ts +1 -1
  187. package/src/library/worker/main.ts +3 -10
  188. package/src/library/worker/protocol.ts +0 -1
  189. package/src/lock/index.ts +0 -1
  190. package/src/lock/manager.ts +0 -10
  191. package/src/orchestrator/manager.ts +190 -104
  192. package/src/orchestrator/operation-context.ts +357 -0
  193. package/src/orchestrator/operation-plan.destroy.test.md +357 -0
  194. package/src/orchestrator/operation-plan.destroy.test.ts +775 -0
  195. package/src/orchestrator/operation-plan.fixtures.ts +213 -0
  196. package/src/orchestrator/operation-plan.md +198 -0
  197. package/src/orchestrator/operation-plan.refresh.test.md +199 -0
  198. package/src/orchestrator/operation-plan.refresh.test.ts +367 -0
  199. package/src/orchestrator/operation-plan.ts +709 -0
  200. package/src/orchestrator/operation-plan.update.test.md +485 -0
  201. package/src/orchestrator/operation-plan.update.test.ts +1066 -0
  202. package/src/orchestrator/operation-workset.ts +233 -578
  203. package/src/orchestrator/operation.ts +440 -948
  204. package/src/orchestrator/plan-test-builder.ts +267 -0
  205. package/src/project-model/abstractions.ts +118 -0
  206. package/src/project-model/backends/codebase.ts +365 -0
  207. package/src/project-model/backends/database.ts +440 -0
  208. package/src/project-model/errors.ts +81 -0
  209. package/src/project-model/factory.ts +24 -0
  210. package/src/project-model/index.ts +4 -0
  211. package/src/project-model/utils.test.ts +544 -0
  212. package/src/project-model/utils.ts +242 -0
  213. package/src/pubsub/abstractions.ts +10 -1
  214. package/src/pubsub/factory.ts +4 -4
  215. package/src/pubsub/index.ts +1 -0
  216. package/src/pubsub/manager.ts +29 -13
  217. package/src/pubsub/memory.ts +31 -0
  218. package/src/runner/abstractions.ts +40 -41
  219. package/src/runner/artifact-env.ts +19 -8
  220. package/src/runner/factory.ts +6 -6
  221. package/src/runner/force-abort.ts +3 -6
  222. package/src/runner/local.ts +74 -67
  223. package/src/runner/pulumi.ts +23 -63
  224. package/src/services.ts +181 -123
  225. package/src/shared/models/backend/index.ts +3 -1
  226. package/src/shared/models/backend/library.ts +9 -1
  227. package/src/shared/models/backend/project.ts +43 -42
  228. package/src/shared/models/backend/pulumi.ts +14 -0
  229. package/src/shared/models/backend/unlock-method.ts +1 -1
  230. package/src/shared/models/backend/well-known.ts +58 -0
  231. package/src/shared/models/base.ts +40 -26
  232. package/src/shared/models/errors.ts +82 -1
  233. package/src/shared/models/index.ts +3 -2
  234. package/src/shared/models/prisma.ts +36 -0
  235. package/src/shared/models/project/api-key.ts +37 -59
  236. package/src/shared/models/project/artifact.ts +16 -76
  237. package/src/shared/models/project/custom-status.ts +12 -0
  238. package/src/shared/models/project/index.ts +8 -7
  239. package/src/shared/models/project/lock.ts +10 -78
  240. package/src/shared/models/project/model.ts +19 -1
  241. package/src/shared/models/project/operation.ts +235 -99
  242. package/src/shared/models/project/page.ts +37 -48
  243. package/src/shared/models/project/secret.ts +29 -89
  244. package/src/shared/models/project/service-account.ts +12 -17
  245. package/src/shared/models/project/state.ts +100 -407
  246. package/src/shared/models/project/terminal.ts +75 -88
  247. package/src/shared/models/project/trigger.ts +13 -49
  248. package/src/shared/models/project/unlock-method.ts +20 -26
  249. package/src/shared/models/project/worker.ts +89 -90
  250. package/src/shared/resolvers/graph-resolver.ts +21 -0
  251. package/src/shared/resolvers/index.ts +1 -1
  252. package/src/shared/resolvers/input-hash.ts +24 -14
  253. package/src/shared/resolvers/input.ts +9 -2
  254. package/src/shared/resolvers/registry.ts +5 -4
  255. package/src/shared/resolvers/state.ts +12 -1
  256. package/src/shared/resolvers/validation.ts +7 -3
  257. package/src/shared/utils/index.ts +1 -2
  258. package/src/shared/utils/promise-tracker.ts +30 -3
  259. package/src/terminal/abstractions.ts +1 -1
  260. package/src/terminal/docker.ts +3 -3
  261. package/src/terminal/manager.ts +102 -118
  262. package/src/test-utils/database.ts +119 -0
  263. package/src/test-utils/index.ts +2 -0
  264. package/src/test-utils/services.ts +134 -0
  265. package/src/unlock/abstractions.ts +5 -23
  266. package/src/unlock/memory.ts +9 -14
  267. package/src/worker/abstractions.ts +7 -4
  268. package/src/worker/docker.ts +14 -19
  269. package/src/worker/manager.ts +366 -97
  270. package/dist/chunk-NAAIDR4U.js +0 -8499
  271. package/dist/chunk-NAAIDR4U.js.map +0 -1
  272. package/dist/chunk-Y7DXREVO.js +0 -1745
  273. package/dist/chunk-Y7DXREVO.js.map +0 -1
  274. package/dist/magic-string.es-5ABAC4JN.js +0 -1292
  275. package/dist/magic-string.es-5ABAC4JN.js.map +0 -1
  276. package/src/business/__traces__/secret/update-instance-secrets/create-and-delete-secrets-simultaneously.md +0 -356
  277. package/src/business/__traces__/secret/update-instance-secrets/create-new-secrets-for-instance.md +0 -274
  278. package/src/business/__traces__/secret/update-instance-secrets/delete-existing-secrets.md +0 -223
  279. package/src/business/__traces__/secret/update-instance-secrets/no-op-when-no-changes.md +0 -147
  280. package/src/business/__traces__/secret/update-instance-secrets/update-existing-secrets.md +0 -280
  281. package/src/business/__traces__/worker/update-unit-registrations/add-new-unit-registration-when-other-exists.md +0 -360
  282. package/src/business/__traces__/worker/update-unit-registrations/add-new-unit-registration.md +0 -215
  283. package/src/business/__traces__/worker/update-unit-registrations/create-multiple-workers-with-different-identities.md +0 -427
  284. package/src/business/__traces__/worker/update-unit-registrations/handle-nonexistent-registration-id-gracefully.md +0 -217
  285. package/src/business/__traces__/worker/update-unit-registrations/no-op-when-no-changes.md +0 -132
  286. package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-when-image-changes.md +0 -454
  287. package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-when-image-version-changes.md +0 -426
  288. package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-with-same-identity-reuses-service-account.md +0 -372
  289. package/src/business/__traces__/worker/update-unit-registrations/remove-one-of-multiple-unit-registrations.md +0 -383
  290. package/src/business/__traces__/worker/update-unit-registrations/remove-unit-registration.md +0 -245
  291. package/src/business/__traces__/worker/update-unit-registrations/update-existing-unit-registration-when-params-change.md +0 -174
  292. package/src/business/__traces__/worker/update-unit-registrations/update-params-and-image-simultaneously.md +0 -432
  293. package/src/business/__traces__/worker/update-unit-registrations/worker-with-multiple-registrations-not-deleted-when-one-removed.md +0 -220
  294. package/src/business/backend-unlock.ts +0 -10
  295. package/src/common/clock.ts +0 -18
  296. package/src/common/performance.ts +0 -44
  297. package/src/common/random.ts +0 -68
  298. package/src/common/test/index.ts +0 -2
  299. package/src/common/test/render.ts +0 -98
  300. package/src/common/test/tracer.ts +0 -359
  301. package/src/hotstate/abstractions.ts +0 -48
  302. package/src/hotstate/factory.ts +0 -17
  303. package/src/hotstate/index.ts +0 -3
  304. package/src/hotstate/manager.ts +0 -192
  305. package/src/hotstate/memory.ts +0 -100
  306. package/src/hotstate/validation.ts +0 -100
  307. package/src/lock/test.ts +0 -108
  308. package/src/project/abstractions.ts +0 -78
  309. package/src/project/evaluation.ts +0 -248
  310. package/src/project/factory.ts +0 -11
  311. package/src/project/index.ts +0 -3
  312. package/src/project/local.ts +0 -417
  313. package/src/pubsub/local.ts +0 -36
  314. package/src/pubsub/validation.ts +0 -33
  315. package/src/shared/utils/args.ts +0 -25
  316. package/src/state/abstractions.ts +0 -289
  317. package/src/state/encryption.ts +0 -98
  318. package/src/state/factory.ts +0 -20
  319. package/src/state/index.ts +0 -7
  320. package/src/state/local/backend.ts +0 -106
  321. package/src/state/local/collection.ts +0 -361
  322. package/src/state/local/index.ts +0 -2
  323. package/src/state/manager.ts +0 -890
  324. package/src/state/memory/backend.ts +0 -70
  325. package/src/state/memory/collection.ts +0 -270
  326. package/src/state/memory/index.ts +0 -2
  327. package/src/state/repository/index.ts +0 -2
  328. package/src/state/repository/repository.index.ts +0 -193
  329. package/src/state/repository/repository.ts +0 -507
  330. package/src/state/test.ts +0 -457
  331. /package/src/{state → database/local}/keyring.ts +0 -0
@@ -1,614 +1,391 @@
1
+ import type { UnitWorker } from "@highstate/contract"
2
+ import type { Worker, WorkerVersion } from "../database"
3
+ import type { PubSubManager } from "../pubsub"
1
4
  import type { WorkerManager } from "../worker"
2
- import { describe } from "vitest"
3
- import { getWorkerIdentity, type Worker, type WorkerUnitRegistration } from "../shared"
4
- import { lockFixtures } from "../lock"
5
- import { testBase } from "../common"
6
- import { stateFixtures } from "../state"
5
+ import { randomBytes } from "node:crypto"
6
+ import { createId } from "@paralleldrive/cuid2"
7
+ import { describe, vi } from "vitest"
8
+ import { extractDigestFromImage } from "../shared"
9
+ import { test } from "../test-utils"
7
10
  import { WorkerService } from "./worker"
8
11
 
9
- const test = testBase
10
- .extend(stateFixtures)
11
- .extend(lockFixtures)
12
- .extend<{ workerService: WorkerService }>({
13
- workerService: async ({ tracer, random, stateManager, lockManager }, use) => {
14
- const workerManager = tracer.createServiceMock<WorkerManager>("workerManager")
15
-
16
- const workerService = new WorkerService(
17
- stateManager,
18
- workerManager,
19
- lockManager,
20
- random,
21
- tracer.logger.child({ service: "WorkerService" }),
22
- )
23
-
24
- await use(workerService)
25
- },
26
- })
27
-
28
- const image1V1 = "ghcr.io/org/worker:v1"
29
- const image1V2 = "ghcr.io/org/worker:v2"
30
- const image2V1 = "ghcr.io/org/otherworker:v1"
31
-
32
- describe("updateUnitRegistrations", () => {
33
- test.concurrent("add new unit registration", async ({ tracer, workerService }) => {
34
- tracer.setPhase("act")
35
-
36
- await workerService.updateUnitRegistrations("test", "test", {}, [
37
- {
38
- name: "worker",
39
- image: image1V1,
40
- params: {},
41
- },
42
- ])
43
- })
44
-
45
- test.concurrent(
46
- "add new unit registration when other exists",
47
- async ({ tracer, stateManager, random, workerService }) => {
48
- tracer.setPhase("arrange")
49
-
50
- const existingWorker: Worker = {
51
- id: random.uuidv7(),
52
- meta: {},
53
- image: image2V1,
54
- apiKeyId: random.uuidv7(),
55
- failedStartAttempts: 5,
56
- status: "starting",
57
- identity: getWorkerIdentity(image2V1),
58
- serviceAccountId: random.uuidv7(),
59
- }
60
-
61
- const existingRegistration: WorkerUnitRegistration = {
62
- id: random.uuidv7(),
63
- meta: {},
64
- workerId: existingWorker.id,
65
- image: image2V1,
66
- instanceId: "test",
67
- params: {},
68
- }
69
-
70
- await stateManager.getWorkerRepository("test").putItem(existingWorker)
71
- await stateManager.getWorkerRegistrationRepository("test").putItem(existingRegistration)
72
-
73
- tracer.setPhase("act")
74
-
75
- await workerService.updateUnitRegistrations(
76
- "test",
77
- "test",
78
- {
79
- other: existingRegistration.id,
12
+ const workerTest = test.extend<{
13
+ workerService: WorkerService
14
+
15
+ createWorker(): Promise<Worker>
16
+
17
+ createWorkerVersion: (
18
+ worker: Worker,
19
+ overrides?: Pick<Partial<WorkerVersion>, "digest">,
20
+ ) => Promise<WorkerVersion>
21
+
22
+ createMockUnitWorker: (overrides?: Partial<UnitWorker>) => UnitWorker
23
+ }>({
24
+ workerService: async ({ database, logger }, use) => {
25
+ const workerManager = vi.mockObject({ syncWorkers: vi.fn() } as unknown as WorkerManager)
26
+
27
+ const workerService = new WorkerService(
28
+ database,
29
+ workerManager,
30
+ vi.mockObject({ subscribe: vi.fn(), publish: vi.fn() } as unknown as PubSubManager),
31
+ logger.child({ service: "WorkerService" }),
32
+ )
33
+
34
+ await use(workerService)
35
+ },
36
+
37
+ createWorker: async ({ projectDatabase }, use) => {
38
+ const createWorker = async () => {
39
+ return await projectDatabase.worker.create({
40
+ data: {
41
+ identity: `ghcr.io/org/${createId()}`,
42
+ serviceAccount: {
43
+ create: {
44
+ meta: {
45
+ title: "Test Worker Service Account",
46
+ },
47
+ },
48
+ },
80
49
  },
81
- [
82
- {
83
- name: "worker",
84
- image: image1V1,
85
- params: {},
50
+ })
51
+ }
52
+
53
+ await use(createWorker)
54
+ },
55
+
56
+ createWorkerVersion: async ({ projectDatabase }, use) => {
57
+ const createWorkerVersion = async (
58
+ worker: Worker,
59
+ overrides: Pick<Partial<WorkerVersion>, "digest"> = {},
60
+ ) => {
61
+ return await projectDatabase.workerVersion.create({
62
+ data: {
63
+ worker: { connect: worker },
64
+ digest: createId(),
65
+ meta: {
66
+ title: "Test Worker Version",
67
+ description: "Test worker version for testing purposes",
86
68
  },
87
- {
88
- // still here and uses the same image, so no changes are expected
89
- name: "other",
90
- image: image2V1,
91
- params: {},
69
+ apiKey: {
70
+ create: {
71
+ meta: {
72
+ title: "Test Worker API Key",
73
+ },
74
+ serviceAccountId: worker.serviceAccountId,
75
+ token: createId(),
76
+ },
92
77
  },
93
- ],
94
- )
95
- },
96
- )
97
-
98
- test.concurrent(
99
- "no-op when no changes",
100
- async ({ tracer, stateManager, random, workerService }) => {
101
- tracer.setPhase("arrange")
102
-
103
- const existingWorker: Worker = {
104
- id: random.uuidv7(),
105
- meta: {},
106
- image: image1V1,
107
- apiKeyId: random.uuidv7(),
108
- failedStartAttempts: 5,
109
- status: "starting",
110
- identity: getWorkerIdentity(image1V1),
111
- serviceAccountId: random.uuidv7(),
112
- }
113
-
114
- const existingRegistration: WorkerUnitRegistration = {
115
- id: random.uuidv7(),
116
- meta: {},
117
- workerId: existingWorker.id,
118
- image: image1V1,
119
- instanceId: "test",
120
- params: {},
121
- }
122
-
123
- await stateManager.getWorkerRepository("test").putItem(existingWorker)
124
- await stateManager.getWorkerRegistrationRepository("test").putItem(existingRegistration)
125
-
126
- tracer.setPhase("act")
127
-
128
- await workerService.updateUnitRegistrations(
129
- "test",
130
- "test",
131
- {
132
- worker: existingRegistration.id,
78
+ ...overrides,
133
79
  },
134
- [
135
- {
136
- name: "worker",
137
- image: image1V1,
138
- params: {},
139
- },
140
- ],
141
- )
142
- },
143
- )
80
+ })
81
+ }
82
+
83
+ await use(createWorkerVersion)
84
+ },
85
+
86
+ createMockUnitWorker: async ({}, use) => {
87
+ const createMockUnitWorker = (overrides: Partial<UnitWorker> = {}): UnitWorker => ({
88
+ name: "test-worker",
89
+ image: `ghcr.io/org/${createId()}@sha256:${randomBytes(32).toString("hex")}`,
90
+ params: { key: "value" },
91
+ ...overrides,
92
+ })
93
+
94
+ await use(createMockUnitWorker)
95
+ },
96
+ })
144
97
 
145
- test.concurrent(
146
- "update existing unit registration when params change",
147
- async ({ tracer, stateManager, random, workerService }) => {
148
- tracer.setPhase("arrange")
149
-
150
- const existingWorker: Worker = {
151
- id: random.uuidv7(),
152
- meta: {},
153
- image: image1V1,
154
- apiKeyId: random.uuidv7(),
155
- failedStartAttempts: 5,
156
- status: "starting",
157
- identity: getWorkerIdentity(image1V1),
158
- serviceAccountId: random.uuidv7(),
159
- }
160
-
161
- const existingRegistration: WorkerUnitRegistration = {
162
- id: random.uuidv7(),
163
- meta: {},
164
- workerId: existingWorker.id,
165
- image: image1V1,
166
- instanceId: "test",
167
- params: {},
168
- }
169
-
170
- await stateManager.getWorkerRepository("test").putItem(existingWorker)
171
- await stateManager.getWorkerRegistrationRepository("test").putItem(existingRegistration)
172
-
173
- tracer.setPhase("act")
174
-
175
- await workerService.updateUnitRegistrations(
176
- "test",
177
- "test",
178
- {
179
- worker: existingRegistration.id,
180
- },
181
- [
182
- {
183
- name: "worker",
184
- image: image1V1,
185
- params: { foo: "bar" }, // changed params
186
- },
187
- ],
188
- )
98
+ describe("updateUnitRegistrations", () => {
99
+ workerTest(
100
+ "creates new worker registrations for unit workers",
101
+ async ({
102
+ workerService,
103
+ project,
104
+ projectDatabase,
105
+ createInstanceState,
106
+ createMockUnitWorker,
107
+ expect,
108
+ }) => {
109
+ // arrange
110
+ const unitWorker = createMockUnitWorker()
111
+ const instance = await createInstanceState(project.id)
112
+
113
+ // act
114
+ await projectDatabase.$transaction(async tx => {
115
+ await workerService.updateUnitRegistrations(tx, project.id, instance.id, [unitWorker])
116
+ })
117
+
118
+ // assert
119
+ const registrations = await projectDatabase.workerUnitRegistration.findMany({
120
+ where: { stateId: instance.id },
121
+ })
122
+
123
+ // check that the registration was created with the correct data
124
+ expect(registrations).toHaveLength(1)
125
+ expect(registrations[0].name).toBe(unitWorker.name)
126
+ expect(registrations[0].params).toEqual(unitWorker.params)
127
+ expect(registrations[0].workerVersionId).toBeDefined()
128
+ expect(registrations[0].stateId).toBe(instance.id)
189
129
  },
190
130
  )
191
131
 
192
- test.concurrent(
193
- "recreate worker when image version changes",
194
- async ({ tracer, stateManager, random, workerService }) => {
195
- tracer.setPhase("arrange")
196
-
197
- const existingWorker: Worker = {
198
- id: random.uuidv7(),
199
- meta: {},
200
- image: image1V1,
201
- apiKeyId: random.uuidv7(),
202
- failedStartAttempts: 5,
203
- status: "starting",
204
- identity: getWorkerIdentity(image1V1),
205
- serviceAccountId: random.uuidv7(),
206
- }
207
-
208
- const existingRegistration: WorkerUnitRegistration = {
209
- id: random.uuidv7(),
210
- meta: {},
211
- workerId: existingWorker.id,
212
- image: image1V1,
213
- instanceId: "test",
214
- params: {},
215
- }
216
-
217
- await stateManager.getWorkerRepository("test").putItem(existingWorker)
218
- await stateManager.getWorkerRegistrationRepository("test").putItem(existingRegistration)
219
-
220
- tracer.setPhase("act")
221
-
222
- await workerService.updateUnitRegistrations(
223
- "test",
224
- "test",
225
- {
226
- worker: existingRegistration.id,
132
+ workerTest(
133
+ "updates existing worker registrations with new params",
134
+ async ({
135
+ workerService,
136
+ project,
137
+ projectDatabase,
138
+ createInstanceState,
139
+ createWorker,
140
+ createWorkerVersion,
141
+ createMockUnitWorker,
142
+ expect,
143
+ }) => {
144
+ // arrange
145
+ const unitWorker = createMockUnitWorker({ params: { key: "newValue" } })
146
+ const instance = await createInstanceState(project.id)
147
+
148
+ // create initial worker and version
149
+ const worker = await createWorker()
150
+ const workerVersion = await createWorkerVersion(worker, {
151
+ digest: extractDigestFromImage(unitWorker.image),
152
+ })
153
+
154
+ // create an initial registration
155
+ await projectDatabase.workerUnitRegistration.create({
156
+ data: {
157
+ stateId: instance.id,
158
+ name: unitWorker.name,
159
+ params: { key: "oldValue" },
160
+ workerVersionId: workerVersion.id,
227
161
  },
228
- [
229
- {
230
- name: "worker",
231
- image: image1V2, // changed image version
232
- params: {},
233
- },
234
- ],
235
- )
236
- },
237
- )
162
+ })
238
163
 
239
- test.concurrent(
240
- "recreate worker when image changes",
241
- async ({ tracer, stateManager, random, workerService }) => {
242
- tracer.setPhase("arrange")
243
-
244
- const existingWorker: Worker = {
245
- id: random.uuidv7(),
246
- meta: {},
247
- image: image1V1,
248
- apiKeyId: random.uuidv7(),
249
- failedStartAttempts: 5,
250
- status: "starting",
251
- identity: getWorkerIdentity(image1V1),
252
- serviceAccountId: random.uuidv7(),
253
- }
254
-
255
- const existingRegistration: WorkerUnitRegistration = {
256
- id: random.uuidv7(),
257
- meta: {},
258
- workerId: existingWorker.id,
259
- image: image1V1,
260
- instanceId: "test",
261
- params: {},
262
- }
263
-
264
- await stateManager.getWorkerRepository("test").putItem(existingWorker)
265
- await stateManager.getWorkerRegistrationRepository("test").putItem(existingRegistration)
266
-
267
- tracer.setPhase("act")
268
-
269
- await workerService.updateUnitRegistrations(
270
- "test",
271
- "test",
272
- {
273
- worker: existingRegistration.id,
274
- },
275
- [
276
- {
277
- name: "worker",
278
- image: image2V1, // changed image
279
- params: {},
280
- },
281
- ],
282
- )
283
- },
284
- )
164
+ // act
165
+ await projectDatabase.$transaction(async tx => {
166
+ await workerService.updateUnitRegistrations(tx, project.id, instance.id, [unitWorker])
167
+ })
285
168
 
286
- test.concurrent(
287
- "remove unit registration",
288
- async ({ tracer, stateManager, random, workerService }) => {
289
- tracer.setPhase("arrange")
290
-
291
- const existingWorker: Worker = {
292
- id: random.uuidv7(),
293
- meta: {},
294
- image: image1V1,
295
- apiKeyId: random.uuidv7(),
296
- failedStartAttempts: 5,
297
- status: "starting",
298
- identity: getWorkerIdentity(image1V1),
299
- serviceAccountId: random.uuidv7(),
300
- }
301
-
302
- const existingRegistration: WorkerUnitRegistration = {
303
- id: random.uuidv7(),
304
- meta: {},
305
- workerId: existingWorker.id,
306
- image: image1V1,
307
- instanceId: "test",
308
- params: {},
309
- }
310
-
311
- await stateManager.getWorkerRepository("test").putItem(existingWorker)
312
- await stateManager.getWorkerRegistrationRepository("test").putItem(existingRegistration)
313
-
314
- tracer.setPhase("act")
315
-
316
- // remove the unit worker by not including it in the list
317
- await workerService.updateUnitRegistrations(
318
- "test",
319
- "test",
320
- {
321
- worker: existingRegistration.id,
322
- },
323
- [], // empty list means remove all registrations
324
- )
325
- },
326
- )
169
+ // assert
170
+ const updatedRegistration = await projectDatabase.workerUnitRegistration.findFirst({
171
+ where: { stateId: instance.id, name: unitWorker.name },
172
+ })
327
173
 
328
- test.concurrent(
329
- "remove one of multiple unit registrations",
330
- async ({ tracer, stateManager, random, workerService }) => {
331
- tracer.setPhase("arrange")
332
-
333
- const existingWorker1: Worker = {
334
- id: random.uuidv7(),
335
- meta: {},
336
- image: image1V1,
337
- apiKeyId: random.uuidv7(),
338
- failedStartAttempts: 5,
339
- status: "starting",
340
- identity: getWorkerIdentity(image1V1),
341
- serviceAccountId: random.uuidv7(),
342
- }
343
-
344
- const existingWorker2: Worker = {
345
- id: random.uuidv7(),
346
- meta: {},
347
- image: image2V1,
348
- apiKeyId: random.uuidv7(),
349
- failedStartAttempts: 5,
350
- status: "starting",
351
- identity: getWorkerIdentity(image2V1),
352
- serviceAccountId: random.uuidv7(),
353
- }
354
-
355
- const existingRegistration1: WorkerUnitRegistration = {
356
- id: random.uuidv7(),
357
- meta: {},
358
- workerId: existingWorker1.id,
359
- image: image1V1,
360
- instanceId: "test",
361
- params: {},
362
- }
363
-
364
- const existingRegistration2: WorkerUnitRegistration = {
365
- id: random.uuidv7(),
366
- meta: {},
367
- workerId: existingWorker2.id,
368
- image: image2V1,
369
- instanceId: "test",
370
- params: {},
371
- }
372
-
373
- await stateManager.getWorkerRepository("test").putItem(existingWorker1)
374
- await stateManager.getWorkerRepository("test").putItem(existingWorker2)
375
- await stateManager.getWorkerRegistrationRepository("test").putItem(existingRegistration1)
376
- await stateManager.getWorkerRegistrationRepository("test").putItem(existingRegistration2)
377
-
378
- tracer.setPhase("act")
379
-
380
- // keep only worker1, remove worker2
381
- await workerService.updateUnitRegistrations(
382
- "test",
383
- "test",
384
- {
385
- worker1: existingRegistration1.id,
386
- worker2: existingRegistration2.id,
387
- },
388
- [
389
- {
390
- name: "worker1",
391
- image: image1V1,
392
- params: {},
393
- },
394
- // worker2 is removed by not being in the list
395
- ],
396
- )
174
+ expect(updatedRegistration).toBeDefined()
175
+ expect(updatedRegistration?.params).toEqual(unitWorker.params)
176
+ expect(updatedRegistration?.workerVersionId).toBe(workerVersion.id)
397
177
  },
398
178
  )
399
179
 
400
- test.concurrent(
401
- "recreate worker with same identity reuses service account",
402
- async ({ tracer, stateManager, random, workerService }) => {
403
- tracer.setPhase("arrange")
404
-
405
- const serviceAccountId = random.uuidv7()
406
-
407
- // worker with v1 image
408
- const existingWorker1: Worker = {
409
- id: random.uuidv7(),
410
- meta: {},
411
- image: image1V1,
412
- apiKeyId: random.uuidv7(),
413
- failedStartAttempts: 5,
414
- status: "starting",
415
- identity: getWorkerIdentity(image1V1),
416
- serviceAccountId,
417
- }
418
-
419
- // another worker with v2 image but same identity (already exists)
420
- const existingWorker2: Worker = {
421
- id: random.uuidv7(),
422
- meta: {},
423
- image: image1V2,
424
- apiKeyId: random.uuidv7(),
425
- failedStartAttempts: 5,
426
- status: "starting",
427
- identity: getWorkerIdentity(image1V2), // same as image1V1 identity
428
- serviceAccountId,
429
- }
430
-
431
- const existingRegistration: WorkerUnitRegistration = {
432
- id: random.uuidv7(),
433
- meta: {},
434
- workerId: existingWorker1.id,
435
- image: image1V1,
436
- instanceId: "test",
437
- params: {},
438
- }
439
-
440
- await stateManager.getWorkerRepository("test").putItem(existingWorker1)
441
- await stateManager.getWorkerRepository("test").putItem(existingWorker2)
442
- await stateManager.getWorkerRegistrationRepository("test").putItem(existingRegistration)
443
-
444
- tracer.setPhase("act")
445
-
446
- // request v2 image, should reuse existing worker2 and service account
447
- await workerService.updateUnitRegistrations(
448
- "test",
449
- "test",
450
- {
451
- worker: existingRegistration.id,
180
+ workerTest(
181
+ "creates new worker version while keeping existing one when both are in use",
182
+ async ({
183
+ workerService,
184
+ project,
185
+ projectDatabase,
186
+ createInstanceState,
187
+ createWorker,
188
+ createWorkerVersion,
189
+ createMockUnitWorker,
190
+ expect,
191
+ }) => {
192
+ // arrange
193
+ const oldDigest = randomBytes(32).toString("hex")
194
+ const newDigest = randomBytes(32).toString("hex")
195
+
196
+ const worker = await createWorker()
197
+ const oldWorkerVersion = await createWorkerVersion(worker, { digest: oldDigest })
198
+
199
+ // create two instances, both using the same worker version initially
200
+ const instance1 = await createInstanceState(project.id)
201
+ const instance2 = await createInstanceState(project.id)
202
+
203
+ // create registrations for both instances using the old version
204
+ await projectDatabase.workerUnitRegistration.create({
205
+ data: {
206
+ stateId: instance1.id,
207
+ name: "test-worker",
208
+ params: { key: "value1" },
209
+ workerVersionId: oldWorkerVersion.id,
452
210
  },
453
- [
454
- {
455
- name: "worker",
456
- image: image1V2, // worker2 already exists with this image
457
- params: {},
458
- },
459
- ],
460
- )
211
+ })
212
+
213
+ await projectDatabase.workerUnitRegistration.create({
214
+ data: {
215
+ stateId: instance2.id,
216
+ name: "test-worker",
217
+ params: { key: "value2" },
218
+ workerVersionId: oldWorkerVersion.id,
219
+ },
220
+ })
221
+
222
+ // act - update only instance1 to use new worker version
223
+ const newUnitWorker = createMockUnitWorker({
224
+ image: `${worker.identity}@sha256:${newDigest}`,
225
+ params: { key: "newValue" },
226
+ })
227
+
228
+ await projectDatabase.$transaction(async tx => {
229
+ await workerService.updateUnitRegistrations(tx, project.id, instance1.id, [newUnitWorker])
230
+ })
231
+
232
+ // assert
233
+ const instance1Registration = await projectDatabase.workerUnitRegistration.findFirst({
234
+ where: { stateId: instance1.id, name: "test-worker" },
235
+ include: { workerVersion: true },
236
+ })
237
+
238
+ const instance2Registration = await projectDatabase.workerUnitRegistration.findFirst({
239
+ where: { stateId: instance2.id, name: "test-worker" },
240
+ include: { workerVersion: true },
241
+ })
242
+
243
+ // verify instance1 now uses new version
244
+ expect(instance1Registration?.workerVersion.digest).toBe(newDigest)
245
+ expect(instance1Registration?.params).toEqual({ key: "newValue" })
246
+
247
+ // verify instance2 still uses old version
248
+ expect(instance2Registration?.workerVersion.digest).toBe(oldDigest)
249
+ expect(instance2Registration?.params).toEqual({ key: "value2" })
250
+
251
+ // verify both worker versions still exist (no cleanup since both are in use)
252
+ const allVersions = await projectDatabase.workerVersion.findMany({
253
+ where: { workerId: worker.id },
254
+ })
255
+ expect(allVersions).toHaveLength(2)
256
+ expect(allVersions.map(v => v.digest).sort()).toEqual([oldDigest, newDigest].sort())
461
257
  },
462
258
  )
463
259
 
464
- test.concurrent(
465
- "worker with multiple registrations not deleted when one removed",
466
- async ({ tracer, stateManager, random, workerService }) => {
467
- tracer.setPhase("arrange")
468
-
469
- const existingWorker: Worker = {
470
- id: random.uuidv7(),
471
- meta: {},
472
- image: image1V1,
473
- apiKeyId: random.uuidv7(),
474
- failedStartAttempts: 5,
475
- status: "starting",
476
- identity: getWorkerIdentity(image1V1),
477
- serviceAccountId: random.uuidv7(),
478
- }
479
-
480
- const existingRegistration1: WorkerUnitRegistration = {
481
- id: random.uuidv7(),
482
- meta: {},
483
- workerId: existingWorker.id,
484
- image: image1V1,
485
- instanceId: "test",
486
- params: {},
487
- }
488
-
489
- const existingRegistration2: WorkerUnitRegistration = {
490
- id: random.uuidv7(),
491
- meta: {},
492
- workerId: existingWorker.id,
493
- image: image1V1,
494
- instanceId: "test2", // different instance
495
- params: {},
496
- }
497
-
498
- await stateManager.getWorkerRepository("test").putItem(existingWorker)
499
- await stateManager.getWorkerRegistrationRepository("test").putItem(existingRegistration1)
500
- await stateManager.getWorkerRegistrationRepository("test").putItem(existingRegistration2)
501
-
502
- // add both registrations to the worker's index
503
- await stateManager
504
- .getWorkerRegistrationIndexRepository("test", existingWorker.id)
505
- .indexRepository.put(existingRegistration1.id, "")
506
- await stateManager
507
- .getWorkerRegistrationIndexRepository("test", existingWorker.id)
508
- .indexRepository.put(existingRegistration2.id, "")
509
-
510
- tracer.setPhase("act")
511
-
512
- // remove registration1, but registration2 still exists
513
- await workerService.updateUnitRegistrations(
514
- "test",
515
- "test",
516
- {
517
- worker1: existingRegistration1.id,
518
- // worker2 not included in existing registrations for this instance
260
+ workerTest(
261
+ "creates new worker version and deletes old unused one",
262
+ async ({
263
+ workerService,
264
+ project,
265
+ projectDatabase,
266
+ createInstanceState,
267
+ createWorker,
268
+ createWorkerVersion,
269
+ createMockUnitWorker,
270
+ expect,
271
+ }) => {
272
+ // arrange
273
+ const oldDigest = randomBytes(32).toString("hex")
274
+ const newDigest = randomBytes(32).toString("hex")
275
+
276
+ const worker = await createWorker()
277
+ const oldWorkerVersion = await createWorkerVersion(worker, { digest: oldDigest })
278
+
279
+ const instance = await createInstanceState(project.id)
280
+
281
+ // create registration using old version
282
+ await projectDatabase.workerUnitRegistration.create({
283
+ data: {
284
+ stateId: instance.id,
285
+ name: "test-worker",
286
+ params: { key: "oldValue" },
287
+ workerVersionId: oldWorkerVersion.id,
519
288
  },
520
- [], // remove all registrations for this instance
521
- )
289
+ })
290
+
291
+ // act - update to use new worker version
292
+ const newUnitWorker = createMockUnitWorker({
293
+ image: `${worker.identity}@sha256:${newDigest}`,
294
+ params: { key: "newValue" },
295
+ })
296
+
297
+ await projectDatabase.$transaction(async tx => {
298
+ await workerService.updateUnitRegistrations(tx, project.id, instance.id, [newUnitWorker])
299
+ })
300
+
301
+ // assert
302
+ const registration = await projectDatabase.workerUnitRegistration.findFirst({
303
+ where: { stateId: instance.id, name: "test-worker" },
304
+ include: { workerVersion: true },
305
+ })
306
+
307
+ // verify registration now uses new version
308
+ expect(registration?.workerVersion.digest).toBe(newDigest)
309
+ expect(registration?.params).toEqual({ key: "newValue" })
310
+
311
+ // verify old version was cleaned up
312
+ const oldVersion = await projectDatabase.workerVersion.findFirst({
313
+ where: { digest: oldDigest, workerId: worker.id },
314
+ })
315
+ expect(oldVersion).toBeNull()
316
+
317
+ // verify new version exists
318
+ const newVersion = await projectDatabase.workerVersion.findFirst({
319
+ where: { digest: newDigest, workerId: worker.id },
320
+ })
321
+ expect(newVersion).toBeDefined()
522
322
  },
523
323
  )
524
324
 
525
- test.concurrent(
526
- "update params and image simultaneously",
527
- async ({ tracer, stateManager, random, workerService }) => {
528
- tracer.setPhase("arrange")
529
-
530
- const existingWorker: Worker = {
531
- id: random.uuidv7(),
532
- meta: {},
533
- image: image1V1,
534
- apiKeyId: random.uuidv7(),
535
- failedStartAttempts: 5,
536
- status: "starting",
537
- identity: getWorkerIdentity(image1V1),
538
- serviceAccountId: random.uuidv7(),
539
- }
540
-
541
- const existingRegistration: WorkerUnitRegistration = {
542
- id: random.uuidv7(),
543
- meta: {},
544
- workerId: existingWorker.id,
545
- image: image1V1,
546
- instanceId: "test",
547
- params: { old: "value" },
548
- }
549
-
550
- await stateManager.getWorkerRepository("test").putItem(existingWorker)
551
- await stateManager.getWorkerRegistrationRepository("test").putItem(existingRegistration)
552
-
553
- tracer.setPhase("act")
554
-
555
- await workerService.updateUnitRegistrations(
556
- "test",
557
- "test",
558
- {
559
- worker: existingRegistration.id,
560
- },
561
- [
325
+ workerTest(
326
+ "removes all registrations and cleans up unused worker versions when unit workers list is empty",
327
+ async ({
328
+ workerService,
329
+ project,
330
+ projectDatabase,
331
+ createInstanceState,
332
+ createWorker,
333
+ createWorkerVersion,
334
+ expect,
335
+ }) => {
336
+ // arrange
337
+ const worker1 = await createWorker()
338
+ const worker2 = await createWorker()
339
+
340
+ const digest1 = randomBytes(32).toString("hex")
341
+ const digest2 = randomBytes(32).toString("hex")
342
+
343
+ const version1 = await createWorkerVersion(worker1, { digest: digest1 })
344
+ const version2 = await createWorkerVersion(worker2, { digest: digest2 })
345
+
346
+ const instance = await createInstanceState(project.id)
347
+
348
+ console.log(instance.id, version1.id, version2.id)
349
+
350
+ // create multiple registrations
351
+ await projectDatabase.workerUnitRegistration.createMany({
352
+ data: [
562
353
  {
563
- name: "worker",
564
- image: image1V2, // changed image
565
- params: { new: "value" }, // changed params
354
+ stateId: instance.id,
355
+ name: "worker1",
356
+ params: { key: "value1" },
357
+ workerVersionId: version1.id,
566
358
  },
567
- ],
568
- )
569
- },
570
- )
571
-
572
- test.concurrent(
573
- "handle nonexistent registration id gracefully",
574
- async ({ tracer, workerService }) => {
575
- tracer.setPhase("act")
576
-
577
- // provide a nonexistent registration ID
578
- await workerService.updateUnitRegistrations(
579
- "test",
580
- "test",
581
- {
582
- worker: "nonexistent-registration-id",
583
- },
584
- [
585
359
  {
586
- name: "worker",
587
- image: image1V1,
588
- params: {},
360
+ stateId: instance.id,
361
+ name: "worker2",
362
+ params: { key: "value2" },
363
+ workerVersionId: version2.id,
589
364
  },
590
365
  ],
591
- )
592
- },
593
- )
594
-
595
- test.concurrent(
596
- "create multiple workers with different identities",
597
- async ({ tracer, workerService }) => {
598
- tracer.setPhase("act")
599
-
600
- await workerService.updateUnitRegistrations("test", "test", {}, [
601
- {
602
- name: "worker1",
603
- image: image1V1,
604
- params: {},
605
- },
606
- {
607
- name: "worker2",
608
- image: image2V1, // different identity
609
- params: {},
366
+ })
367
+
368
+ // act - update with empty workers list
369
+ await projectDatabase.$transaction(async tx => {
370
+ await workerService.updateUnitRegistrations(tx, project.id, instance.id, [])
371
+ })
372
+
373
+ // assert
374
+ const remainingRegistrations = await projectDatabase.workerUnitRegistration.findMany({
375
+ where: { stateId: instance.id },
376
+ })
377
+ expect(remainingRegistrations).toHaveLength(0)
378
+
379
+ // verify unused worker versions were cleaned up
380
+ const remainingVersions = await projectDatabase.workerVersion.findMany({
381
+ where: {
382
+ OR: [
383
+ { digest: digest1, workerId: worker1.id },
384
+ { digest: digest2, workerId: worker2.id },
385
+ ],
610
386
  },
611
- ])
387
+ })
388
+ expect(remainingVersions).toHaveLength(0)
612
389
  },
613
390
  )
614
391
  })