@highstate/backend 0.9.16 → 0.9.19

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 (302) hide show
  1. package/dist/chunk-5WVU2AK4.js +1535 -0
  2. package/dist/chunk-5WVU2AK4.js.map +1 -0
  3. package/dist/chunk-I7BWSAN6.js +49 -0
  4. package/dist/chunk-I7BWSAN6.js.map +1 -0
  5. package/dist/{chunk-RCB4AFGD.js → chunk-VB4YL327.js} +51 -71
  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 +5 -4
  10. package/dist/index.js +7676 -6634
  11. package/dist/index.js.map +1 -1
  12. package/dist/library/package-resolution-worker.js +8 -6
  13. package/dist/library/package-resolution-worker.js.map +1 -1
  14. package/dist/library/worker/main.js +63 -58
  15. package/dist/library/worker/main.js.map +1 -1
  16. package/dist/shared/index.js +3 -216
  17. package/dist/shared/index.js.map +1 -1
  18. package/package.json +23 -11
  19. package/prisma/backend/_schema/layout.prisma +7 -0
  20. package/prisma/backend/_schema/library.prisma +17 -0
  21. package/prisma/backend/_schema/project.prisma +101 -0
  22. package/prisma/backend/_schema/pulumi.prisma +17 -0
  23. package/prisma/backend/postgresql/main.prisma +17 -0
  24. package/prisma/backend/sqlite/main.prisma +17 -0
  25. package/prisma/backend/sqlite/migrations/20250817070609_initiial/migration.sql +34 -0
  26. package/prisma/backend/sqlite/migrations/20250817104948_add_fields/migration.sql +59 -0
  27. package/prisma/backend/sqlite/migrations/20250818082732_add_models/migration.sql +41 -0
  28. package/prisma/backend/sqlite/migrations/20250818083106_a/migration.sql +19 -0
  29. package/prisma/backend/sqlite/migrations/20250818101945_hi/migration.sql +1 -0
  30. package/prisma/backend/sqlite/migrations/20250819082315_a/migration.sql +5 -0
  31. package/prisma/backend/sqlite/migrations/migration_lock.toml +3 -0
  32. package/prisma/project/api-key.prisma +27 -0
  33. package/prisma/project/artifact.prisma +52 -0
  34. package/prisma/project/custom-status.prisma +46 -0
  35. package/prisma/project/evaluation.prisma +35 -0
  36. package/prisma/project/instance.prisma +160 -0
  37. package/prisma/project/layout.prisma +23 -0
  38. package/prisma/project/lock.prisma +18 -0
  39. package/prisma/project/main.prisma +17 -0
  40. package/prisma/project/migrations/20250816081310_initial/migration.sql +300 -0
  41. package/prisma/project/migrations/20250816082523_test/migration.sql +72 -0
  42. package/prisma/project/migrations/20250818065643_update/migration.sql +42 -0
  43. package/prisma/project/migrations/20250818070758_a/migration.sql +8 -0
  44. package/prisma/project/migrations/20250818070913_a/migration.sql +8 -0
  45. package/prisma/project/migrations/20250818082720_add_motels/migration.sql +11 -0
  46. package/prisma/project/migrations/20250818112523_hello/migration.sql +35 -0
  47. package/prisma/project/migrations/20250819082305_a/migration.sql +14 -0
  48. package/prisma/project/migrations/20250819165004_add_missing_fields/migration.sql +216 -0
  49. package/prisma/project/migrations/20250819171309_a/migration.sql +22 -0
  50. package/prisma/project/migrations/20250820113949_a/migration.sql +66 -0
  51. package/prisma/project/migrations/20250820144256_b/migration.sql +31 -0
  52. package/prisma/project/migrations/20250820145547_a/migration.sql +24 -0
  53. package/prisma/project/migrations/20250820182517_b/migration.sql +2 -0
  54. package/prisma/project/migrations/20250821172324_a/migration.sql +2 -0
  55. package/prisma/project/migrations/20250822081339_a/migration.sql +219 -0
  56. package/prisma/project/migrations/20250822083742_b/migration.sql +1 -0
  57. package/prisma/project/migrations/20250822105134_boom/migration.sql +1 -0
  58. package/prisma/project/migrations/20250822141028_b/migration.sql +1 -0
  59. package/prisma/project/migrations/20250822142342_b/migration.sql +16 -0
  60. package/prisma/project/migrations/20250824072720_a/migration.sql +1 -0
  61. package/prisma/project/migrations/20250824093656_b/migration.sql +21 -0
  62. package/prisma/project/migrations/20250825082518_a/migration.sql +1 -0
  63. package/prisma/project/migrations/20250825085343_b/migration.sql +1 -0
  64. package/prisma/project/migrations/20250825091312_a/migration.sql +1 -0
  65. package/prisma/project/migrations/20250903095431_hi/migration.sql +44 -0
  66. package/prisma/project/migrations/20250903174255_a/migration.sql +24 -0
  67. package/prisma/project/migrations/20250908095205_hi/migration.sql +18 -0
  68. package/prisma/project/migrations/20250909155857_hi/migration.sql +15 -0
  69. package/prisma/project/migrations/migration_lock.toml +3 -0
  70. package/prisma/project/model.prisma +37 -0
  71. package/prisma/project/operation.prisma +148 -0
  72. package/prisma/project/page.prisma +41 -0
  73. package/prisma/project/secret.prisma +42 -0
  74. package/prisma/project/service-account.prisma +36 -0
  75. package/prisma/project/terminal.prisma +90 -0
  76. package/prisma/project/trigger.prisma +31 -0
  77. package/prisma/project/unlock-method.prisma +32 -0
  78. package/prisma/project/worker.prisma +138 -0
  79. package/src/artifact/abstractions.ts +13 -13
  80. package/src/artifact/encryption.ts +31 -15
  81. package/src/artifact/factory.ts +7 -10
  82. package/src/artifact/local.ts +33 -50
  83. package/src/business/api-key.ts +24 -36
  84. package/src/business/artifact.test.ts +978 -0
  85. package/src/business/artifact.ts +136 -215
  86. package/src/business/evaluation.ts +328 -0
  87. package/src/business/index.ts +5 -1
  88. package/src/business/instance-lock.test.ts +1060 -0
  89. package/src/business/instance-lock.ts +387 -77
  90. package/src/business/instance-state.test.ts +735 -0
  91. package/src/business/instance-state.ts +604 -217
  92. package/src/business/operation.test.ts +439 -0
  93. package/src/business/operation.ts +174 -208
  94. package/src/business/project-model.ts +258 -0
  95. package/src/business/project-unlock.ts +172 -112
  96. package/src/business/project.ts +407 -0
  97. package/src/business/secret.test.ts +513 -0
  98. package/src/business/secret.ts +194 -131
  99. package/src/business/settings.test.ts +695 -0
  100. package/src/business/settings.ts +855 -0
  101. package/src/business/terminal-session.ts +90 -0
  102. package/src/business/unit-extra.test.ts +539 -0
  103. package/src/business/unit-extra.ts +160 -0
  104. package/src/business/worker.test.ts +391 -0
  105. package/src/business/worker.ts +250 -114
  106. package/src/common/codebase.ts +65 -0
  107. package/src/common/index.ts +3 -2
  108. package/src/common/logger.ts +5 -0
  109. package/src/common/utils.ts +4 -3
  110. package/src/config.ts +15 -12
  111. package/src/database/_generated/backend/postgresql/client.ts +72 -0
  112. package/src/database/_generated/backend/postgresql/commonInputTypes.ts +350 -0
  113. package/src/database/_generated/backend/postgresql/enums.ts +13 -0
  114. package/src/database/_generated/backend/postgresql/internal/class.ts +320 -0
  115. package/src/database/_generated/backend/postgresql/internal/prismaNamespace.ts +1238 -0
  116. package/src/database/_generated/backend/postgresql/models/Library.ts +1263 -0
  117. package/src/database/_generated/backend/postgresql/models/Project.ts +2175 -0
  118. package/src/database/_generated/backend/postgresql/models/ProjectModelStorage.ts +1263 -0
  119. package/src/database/_generated/backend/postgresql/models/ProjectSpace.ts +1602 -0
  120. package/src/database/_generated/backend/postgresql/models/PulumiBackend.ts +1263 -0
  121. package/src/database/_generated/backend/postgresql/models/UserWorkspaseLayout.ts +1065 -0
  122. package/src/database/_generated/backend/postgresql/models.ts +16 -0
  123. package/src/database/_generated/backend/postgresql/pjtg.ts +182 -0
  124. package/src/database/_generated/backend/sqlite/client.ts +72 -0
  125. package/src/database/_generated/backend/sqlite/commonInputTypes.ts +331 -0
  126. package/src/database/_generated/backend/sqlite/enums.ts +13 -0
  127. package/src/database/_generated/backend/sqlite/internal/class.ts +318 -0
  128. package/src/database/_generated/backend/sqlite/internal/prismaNamespace.ts +1207 -0
  129. package/src/database/_generated/backend/sqlite/models/Library.ts +1261 -0
  130. package/src/database/_generated/backend/sqlite/models/Project.ts +2169 -0
  131. package/src/database/_generated/backend/sqlite/models/ProjectModelStorage.ts +1261 -0
  132. package/src/database/_generated/backend/sqlite/models/ProjectSpace.ts +1599 -0
  133. package/src/database/_generated/backend/sqlite/models/PulumiBackend.ts +1261 -0
  134. package/src/database/_generated/backend/sqlite/models/UserWorkspaseLayout.ts +1063 -0
  135. package/src/database/_generated/backend/sqlite/models.ts +16 -0
  136. package/src/database/_generated/backend/sqlite/pjtg.ts +182 -0
  137. package/src/database/_generated/project/client.ts +204 -0
  138. package/src/database/_generated/project/commonInputTypes.ts +827 -0
  139. package/src/database/_generated/project/enums.ts +104 -0
  140. package/src/database/_generated/project/internal/class.ts +479 -0
  141. package/src/database/_generated/project/internal/prismaNamespace.ts +2974 -0
  142. package/src/database/_generated/project/models/ApiKey.ts +1506 -0
  143. package/src/database/_generated/project/models/Artifact.ts +2051 -0
  144. package/src/database/_generated/project/models/HubModel.ts +1125 -0
  145. package/src/database/_generated/project/models/InstanceCustomStatus.ts +1713 -0
  146. package/src/database/_generated/project/models/InstanceEvaluationState.ts +1312 -0
  147. package/src/database/_generated/project/models/InstanceLock.ts +1268 -0
  148. package/src/database/_generated/project/models/InstanceModel.ts +1125 -0
  149. package/src/database/_generated/project/models/InstanceOperationState.ts +1707 -0
  150. package/src/database/_generated/project/models/InstanceState.ts +4613 -0
  151. package/src/database/_generated/project/models/Operation.ts +1647 -0
  152. package/src/database/_generated/project/models/OperationLog.ts +1455 -0
  153. package/src/database/_generated/project/models/Page.ts +1838 -0
  154. package/src/database/_generated/project/models/Secret.ts +1692 -0
  155. package/src/database/_generated/project/models/ServiceAccount.ts +2165 -0
  156. package/src/database/_generated/project/models/Terminal.ts +2038 -0
  157. package/src/database/_generated/project/models/TerminalSession.ts +1454 -0
  158. package/src/database/_generated/project/models/TerminalSessionLog.ts +1280 -0
  159. package/src/database/_generated/project/models/Trigger.ts +1430 -0
  160. package/src/database/_generated/project/models/UnlockMethod.ts +1220 -0
  161. package/src/database/_generated/project/models/UserCompositeViewport.ts +1280 -0
  162. package/src/database/_generated/project/models/UserProjectViewport.ts +1059 -0
  163. package/src/database/_generated/project/models/Worker.ts +1459 -0
  164. package/src/database/_generated/project/models/WorkerUnitRegistration.ts +1524 -0
  165. package/src/database/_generated/project/models/WorkerVersion.ts +1974 -0
  166. package/src/database/_generated/project/models/WorkerVersionLog.ts +1318 -0
  167. package/src/database/_generated/project/models.ts +35 -0
  168. package/src/database/_generated/project/pjtg.ts +182 -0
  169. package/src/database/abstractions.ts +19 -0
  170. package/src/database/factory.ts +37 -0
  171. package/src/database/index.ts +6 -0
  172. package/src/database/local/backend.ts +134 -0
  173. package/src/database/local/index.ts +3 -0
  174. package/src/database/local/meta.ts +46 -0
  175. package/src/database/local/prisma.config.ts +25 -0
  176. package/src/database/local/project.ts +39 -0
  177. package/src/database/manager.ts +181 -0
  178. package/src/database/migrate.ts +35 -0
  179. package/src/database/prisma.ts +56 -0
  180. package/src/database/well-known.ts +38 -0
  181. package/src/index.ts +4 -4
  182. package/src/library/abstractions.ts +21 -14
  183. package/src/library/factory.ts +1 -1
  184. package/src/library/local.ts +86 -38
  185. package/src/library/package-resolution-worker.ts +1 -1
  186. package/src/library/worker/evaluator.ts +61 -48
  187. package/src/library/worker/loader.lite.ts +14 -1
  188. package/src/library/worker/main.ts +9 -16
  189. package/src/library/worker/protocol.ts +0 -12
  190. package/src/lock/manager.ts +12 -7
  191. package/src/orchestrator/manager.ts +198 -131
  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 +235 -583
  203. package/src/orchestrator/operation.ts +446 -904
  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 +49 -25
  217. package/src/pubsub/memory.ts +31 -0
  218. package/src/runner/abstractions.ts +38 -26
  219. package/src/runner/artifact-env.ts +17 -6
  220. package/src/runner/factory.ts +6 -6
  221. package/src/runner/force-abort.ts +3 -6
  222. package/src/runner/local.ts +79 -72
  223. package/src/runner/pulumi.ts +26 -63
  224. package/src/services.ts +214 -103
  225. package/src/shared/models/backend/index.ts +3 -1
  226. package/src/shared/models/backend/library.ts +12 -4
  227. package/src/shared/models/backend/project.ts +43 -23
  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 -109
  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 -56
  236. package/src/shared/models/project/artifact.ts +15 -105
  237. package/src/shared/models/project/custom-status.ts +12 -0
  238. package/src/shared/models/project/index.ts +9 -9
  239. package/src/shared/models/project/lock.ts +10 -78
  240. package/src/shared/models/project/model.ts +32 -0
  241. package/src/shared/models/project/operation.ts +222 -99
  242. package/src/shared/models/project/page.ts +37 -48
  243. package/src/shared/models/project/secret.ts +29 -103
  244. package/src/shared/models/project/service-account.ts +12 -17
  245. package/src/shared/models/project/state.ts +100 -390
  246. package/src/shared/models/project/terminal.ts +75 -89
  247. package/src/shared/models/project/trigger.ts +13 -49
  248. package/src/shared/models/project/unlock-method.ts +21 -20
  249. package/src/shared/models/project/worker.ts +89 -88
  250. package/src/shared/resolvers/graph-resolver.ts +62 -26
  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 +48 -6
  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 +29 -9
  257. package/src/shared/utils/index.ts +1 -1
  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 +31 -0
  266. package/src/unlock/index.ts +2 -0
  267. package/src/unlock/memory.ts +27 -0
  268. package/src/worker/abstractions.ts +7 -4
  269. package/src/worker/docker.ts +14 -19
  270. package/src/worker/manager.ts +376 -79
  271. package/dist/chunk-RCB4AFGD.js.map +0 -1
  272. package/dist/chunk-WHALQHEZ.js +0 -2017
  273. package/dist/chunk-WHALQHEZ.js.map +0 -1
  274. package/src/business/backend-unlock.ts +0 -10
  275. package/src/common/performance.ts +0 -44
  276. package/src/hotstate/abstractions.ts +0 -48
  277. package/src/hotstate/factory.ts +0 -17
  278. package/src/hotstate/index.ts +0 -3
  279. package/src/hotstate/manager.ts +0 -192
  280. package/src/hotstate/memory.ts +0 -100
  281. package/src/hotstate/validation.ts +0 -101
  282. package/src/project/abstractions.ts +0 -102
  283. package/src/project/factory.ts +0 -11
  284. package/src/project/index.ts +0 -3
  285. package/src/project/local.ts +0 -469
  286. package/src/project/manager.ts +0 -574
  287. package/src/pubsub/local.ts +0 -36
  288. package/src/pubsub/validation.ts +0 -33
  289. package/src/shared/models/project/component.ts +0 -45
  290. package/src/shared/models/project/instance.ts +0 -74
  291. package/src/state/abstractions.ts +0 -450
  292. package/src/state/encryption.ts +0 -59
  293. package/src/state/factory.ts +0 -20
  294. package/src/state/index.ts +0 -6
  295. package/src/state/local/backend.ts +0 -299
  296. package/src/state/local/collection.ts +0 -342
  297. package/src/state/local/index.ts +0 -2
  298. package/src/state/manager.ts +0 -819
  299. package/src/state/repository/index.ts +0 -2
  300. package/src/state/repository/repository.index.ts +0 -193
  301. package/src/state/repository/repository.ts +0 -458
  302. /package/src/{state → database/local}/keyring.ts +0 -0
@@ -0,0 +1,735 @@
1
+ import type { InstanceId, InstanceModel } from "@highstate/contract"
2
+ import type { ArtifactService } from "../artifact"
3
+ import type { PubSubManager } from "../pubsub"
4
+ import type { RunnerBackend } from "../runner"
5
+ import type { ProjectService } from "./project"
6
+ import type { SecretService } from "./secret"
7
+ import type { UnitExtraService } from "./unit-extra"
8
+ import type { WorkerService } from "./worker"
9
+ import { parseInstanceId } from "@highstate/contract"
10
+ import { createId } from "@paralleldrive/cuid2"
11
+ import { describe, type MockedObject, vi } from "vitest"
12
+ import { InstanceLockedError, InstanceStateNotFoundError } from "../shared"
13
+ import { test } from "../test-utils"
14
+ import { InstanceStateService } from "./instance-state"
15
+
16
+ const instanceStateTest = test.extend<{
17
+ pubsubManager: MockedObject<PubSubManager>
18
+ projectService: MockedObject<ProjectService>
19
+ runnerBackend: MockedObject<RunnerBackend>
20
+ workerService: MockedObject<WorkerService>
21
+ artifactService: MockedObject<ArtifactService>
22
+ unitExtraService: MockedObject<UnitExtraService>
23
+ secretService: MockedObject<SecretService>
24
+ instanceStateService: InstanceStateService
25
+ }>({
26
+ pubsubManager: async ({}, use) => {
27
+ const pubsubManager = vi.mockObject({
28
+ publish: vi.fn().mockResolvedValue(undefined),
29
+ subscribe: vi.fn(),
30
+ publishMany: vi.fn().mockResolvedValue(undefined),
31
+ } as unknown as PubSubManager)
32
+
33
+ await use(pubsubManager)
34
+ },
35
+
36
+ projectService: async ({}, use) => {
37
+ const projectService = vi.mockObject({
38
+ getProjectOrThrow: vi.fn(),
39
+ } as unknown as ProjectService)
40
+
41
+ await use(projectService)
42
+ },
43
+
44
+ runnerBackend: async ({}, use) => {
45
+ const runnerBackend = vi.mockObject({
46
+ deleteState: vi.fn().mockResolvedValue(undefined),
47
+ } as unknown as RunnerBackend)
48
+
49
+ await use(runnerBackend)
50
+ },
51
+
52
+ workerService: async ({}, use) => {
53
+ const workerService = vi.mockObject({
54
+ cleanupWorkerUsageAndSync: vi.fn().mockResolvedValue(undefined),
55
+ } as unknown as WorkerService)
56
+
57
+ await use(workerService)
58
+ },
59
+
60
+ artifactService: async ({}, use) => {
61
+ const artifactService = vi.mockObject({
62
+ collectGarbage: vi.fn().mockResolvedValue(undefined),
63
+ } as unknown as ArtifactService)
64
+
65
+ await use(artifactService)
66
+ },
67
+
68
+ unitExtraService: async ({}, use) => {
69
+ const unitExtraService = vi.mockObject({} as unknown as UnitExtraService)
70
+ await use(unitExtraService)
71
+ },
72
+
73
+ secretService: async ({}, use) => {
74
+ const secretService = vi.mockObject({} as unknown as SecretService)
75
+ await use(secretService)
76
+ },
77
+
78
+ instanceStateService: async (
79
+ {
80
+ database,
81
+ pubsubManager,
82
+ runnerBackend,
83
+ workerService,
84
+ artifactService,
85
+ unitExtraService,
86
+ secretService,
87
+ logger,
88
+ },
89
+ use,
90
+ ) => {
91
+ const service = new InstanceStateService(
92
+ database,
93
+ pubsubManager,
94
+ runnerBackend,
95
+ workerService,
96
+ artifactService,
97
+ unitExtraService,
98
+ secretService,
99
+ logger.child({ service: "InstanceStateService" }),
100
+ )
101
+
102
+ await use(service)
103
+ },
104
+ })
105
+
106
+ describe("getInstanceStates", () => {
107
+ instanceStateTest(
108
+ "successfully retrieves instance states",
109
+ async ({ instanceStateService, project, createInstanceState, expect }) => {
110
+ // arrange
111
+ const instanceState1 = await createInstanceState(project.id)
112
+ const instanceState2 = await createInstanceState(project.id)
113
+
114
+ // act
115
+ const instanceStates = await instanceStateService.getInstanceStates(project.id)
116
+
117
+ // assert
118
+ expect(instanceStates).toHaveLength(2)
119
+ expect(instanceStates.map(i => i.id)).toEqual([instanceState1.id, instanceState2.id])
120
+ expect(instanceStates[0].secretNames).toEqual([])
121
+ expect(instanceStates[0].lastOperationState).toBeUndefined()
122
+ },
123
+ )
124
+
125
+ instanceStateTest(
126
+ "includes evaluation states when requested",
127
+ async ({ instanceStateService, projectDatabase, project, createInstanceState, expect }) => {
128
+ // arrange
129
+ const instanceState = await createInstanceState(project.id)
130
+
131
+ await projectDatabase.instanceEvaluationState.create({
132
+ data: {
133
+ stateId: instanceState.id,
134
+ status: "evaluated",
135
+ model: {
136
+ id: "test.v1:example",
137
+ kind: "unit",
138
+ type: "test.v1",
139
+ name: "example",
140
+ args: {},
141
+ inputs: {},
142
+ },
143
+ },
144
+ })
145
+
146
+ // act
147
+ const instanceStates = await instanceStateService.getInstanceStates(project.id, {
148
+ includeEvaluationState: true,
149
+ })
150
+
151
+ // assert
152
+ const instanceWithEvaluation = instanceStates.find(state => state.id === instanceState.id)
153
+ expect(instanceWithEvaluation).toBeDefined()
154
+ expect(instanceWithEvaluation?.evaluationState).toBeDefined()
155
+ expect(instanceWithEvaluation?.evaluationState?.status).toBe("evaluated")
156
+ },
157
+ )
158
+ })
159
+
160
+ describe("forgetInstanceState", () => {
161
+ instanceStateTest(
162
+ "successfully deletes instance with no operation state",
163
+ async ({
164
+ instanceStateService,
165
+ projectDatabase,
166
+ project,
167
+ createInstanceState,
168
+ projectService,
169
+ pubsubManager,
170
+ workerService,
171
+ artifactService,
172
+ expect,
173
+ }) => {
174
+ // arrange
175
+ const instanceState = await createInstanceState(project.id)
176
+ projectService.getProjectOrThrow.mockResolvedValue(project)
177
+
178
+ // act
179
+ await instanceStateService.forgetInstanceState(
180
+ project.id,
181
+ instanceState.instanceId as InstanceId,
182
+ )
183
+
184
+ // assert
185
+ const updatedState = await projectDatabase.instanceState.findUnique({
186
+ where: { id: instanceState.id },
187
+ })
188
+ expect(updatedState?.status).toBe("undeployed")
189
+ expect(updatedState?.statusFields).toBeNull()
190
+ expect(updatedState?.inputHash).toBeNull()
191
+ expect(updatedState?.outputHash).toBeNull()
192
+ expect(updatedState?.dependencyOutputHash).toBeNull()
193
+
194
+ // verify side effects were called
195
+ expect(workerService.cleanupWorkerUsageAndSync).toHaveBeenCalledWith(project.id)
196
+ expect(artifactService.collectGarbage).toHaveBeenCalledWith(project.id)
197
+ expect(pubsubManager.publish).toHaveBeenCalledWith(
198
+ ["instance-state", project.id],
199
+ expect.objectContaining({
200
+ type: "patched",
201
+ stateId: instanceState.id,
202
+ patch: expect.objectContaining({
203
+ status: "undeployed",
204
+ }),
205
+ }),
206
+ )
207
+ },
208
+ )
209
+
210
+ instanceStateTest(
211
+ "successfully deletes instance with destroyed operation state",
212
+ async ({
213
+ instanceStateService,
214
+ projectDatabase,
215
+ project,
216
+ createInstanceState,
217
+ projectService,
218
+ expect,
219
+ }) => {
220
+ // arrange
221
+ const instanceState = await createInstanceState(project.id)
222
+
223
+ // create operation with destroyed status
224
+ const operationId = createId()
225
+ await projectDatabase.operation.create({
226
+ data: {
227
+ id: operationId,
228
+ meta: { title: "Test Operation" },
229
+ type: "destroy",
230
+ options: {},
231
+ requestedInstanceIds: [instanceState.instanceId],
232
+ startedAt: new Date(),
233
+ },
234
+ })
235
+
236
+ await projectDatabase.instanceOperationState.create({
237
+ data: {
238
+ operationId,
239
+ stateId: instanceState.id,
240
+ status: "updated",
241
+ model: {} as InstanceModel,
242
+ resolvedInputs: {},
243
+ },
244
+ })
245
+
246
+ projectService.getProjectOrThrow.mockResolvedValue(project)
247
+
248
+ // act
249
+ await instanceStateService.forgetInstanceState(
250
+ project.id,
251
+ instanceState.instanceId as InstanceId,
252
+ )
253
+
254
+ // assert
255
+ const updatedState = await projectDatabase.instanceState.findUnique({
256
+ where: { id: instanceState.id },
257
+ })
258
+ expect(updatedState?.status).toBe("undeployed")
259
+ },
260
+ )
261
+
262
+ instanceStateTest(
263
+ "successfully deletes instance with active operation state",
264
+ async ({
265
+ instanceStateService,
266
+ projectDatabase,
267
+ project,
268
+ createInstanceState,
269
+ projectService,
270
+ expect,
271
+ }) => {
272
+ // arrange
273
+ const instanceState = await createInstanceState(project.id)
274
+
275
+ // create operation with pending status
276
+ const operationId = createId()
277
+ await projectDatabase.operation.create({
278
+ data: {
279
+ id: operationId,
280
+ meta: { title: "Test Operation" },
281
+ type: "update",
282
+ options: {},
283
+ requestedInstanceIds: [instanceState.instanceId],
284
+ startedAt: new Date(),
285
+ },
286
+ })
287
+
288
+ await projectDatabase.instanceOperationState.create({
289
+ data: {
290
+ operationId,
291
+ stateId: instanceState.id,
292
+ status: "updating",
293
+ model: {} as InstanceModel,
294
+ resolvedInputs: {},
295
+ },
296
+ })
297
+
298
+ projectService.getProjectOrThrow.mockResolvedValue(project)
299
+
300
+ // act
301
+ await instanceStateService.forgetInstanceState(
302
+ project.id,
303
+ instanceState.instanceId as InstanceId,
304
+ )
305
+
306
+ // assert
307
+ const updatedState = await projectDatabase.instanceState.findUnique({
308
+ where: { id: instanceState.id },
309
+ })
310
+ expect(updatedState?.status).toBe("undeployed")
311
+ },
312
+ )
313
+
314
+ instanceStateTest(
315
+ "throws error when instance has active locks",
316
+ async ({
317
+ instanceStateService,
318
+ projectDatabase,
319
+ project,
320
+ createInstanceState,
321
+ projectService,
322
+ expect,
323
+ }) => {
324
+ // arrange
325
+ const instanceState = await createInstanceState(project.id)
326
+
327
+ await projectDatabase.instanceLock.create({
328
+ data: {
329
+ stateId: instanceState.id,
330
+ token: "test-lock-token",
331
+ meta: { title: "Test Lock" },
332
+ },
333
+ })
334
+
335
+ projectService.getProjectOrThrow.mockResolvedValue(project)
336
+
337
+ // act & assert
338
+ await expect(
339
+ instanceStateService.forgetInstanceState(
340
+ project.id,
341
+ instanceState.instanceId as InstanceId,
342
+ ),
343
+ ).rejects.toThrow(InstanceLockedError)
344
+ },
345
+ )
346
+
347
+ instanceStateTest(
348
+ "handles terminal data when clearTerminalData=true",
349
+ async ({
350
+ instanceStateService,
351
+ projectDatabase,
352
+ project,
353
+ createInstanceState,
354
+ projectService,
355
+ expect,
356
+ }) => {
357
+ // arrange
358
+ const instanceState = await createInstanceState(project.id)
359
+
360
+ // create service account for terminal
361
+ const serviceAccount = await projectDatabase.serviceAccount.create({
362
+ data: {
363
+ id: createId(),
364
+ meta: { title: "Test Service Account" },
365
+ },
366
+ })
367
+
368
+ await projectDatabase.terminal.create({
369
+ data: {
370
+ id: createId(),
371
+ stateId: instanceState.id,
372
+ name: "test-terminal",
373
+ serviceAccountId: serviceAccount.id,
374
+ meta: { title: "Test Terminal" },
375
+ status: "active",
376
+ spec: { image: "test:latest" },
377
+ },
378
+ })
379
+
380
+ projectService.getProjectOrThrow.mockResolvedValue(project)
381
+
382
+ // act
383
+ await instanceStateService.forgetInstanceState(
384
+ project.id,
385
+ instanceState.instanceId as InstanceId,
386
+ {
387
+ clearTerminalData: true,
388
+ },
389
+ )
390
+
391
+ // assert
392
+ const terminals = await projectDatabase.terminal.findMany({
393
+ where: { stateId: instanceState.id },
394
+ })
395
+ expect(terminals).toHaveLength(0)
396
+ },
397
+ )
398
+
399
+ instanceStateTest(
400
+ "marks terminals as unavailable when clearTerminalData=false",
401
+ async ({
402
+ instanceStateService,
403
+ projectDatabase,
404
+ project,
405
+ createInstanceState,
406
+ projectService,
407
+ expect,
408
+ }) => {
409
+ // arrange
410
+ const instanceState = await createInstanceState(project.id)
411
+
412
+ // create service account for terminal
413
+ const serviceAccount = await projectDatabase.serviceAccount.create({
414
+ data: {
415
+ id: createId(),
416
+ meta: { title: "Test Service Account" },
417
+ },
418
+ })
419
+
420
+ const terminalId = createId()
421
+ await projectDatabase.terminal.create({
422
+ data: {
423
+ id: terminalId,
424
+ stateId: instanceState.id,
425
+ name: "test-terminal",
426
+ serviceAccountId: serviceAccount.id,
427
+ meta: { title: "Test Terminal" },
428
+ status: "active",
429
+ spec: { image: "test:latest" },
430
+ },
431
+ })
432
+
433
+ projectService.getProjectOrThrow.mockResolvedValue(project)
434
+
435
+ // act
436
+ await instanceStateService.forgetInstanceState(
437
+ project.id,
438
+ instanceState.instanceId as InstanceId,
439
+ {
440
+ clearTerminalData: false,
441
+ },
442
+ )
443
+
444
+ // assert
445
+ const terminal = await projectDatabase.terminal.findUnique({
446
+ where: { id: terminalId },
447
+ })
448
+ expect(terminal?.status).toBe("unavailable")
449
+ },
450
+ )
451
+
452
+ instanceStateTest(
453
+ "deletes secrets when deleteSecrets=true",
454
+ async ({
455
+ instanceStateService,
456
+ projectDatabase,
457
+ project,
458
+ createInstanceState,
459
+ projectService,
460
+ expect,
461
+ }) => {
462
+ // arrange
463
+ const instanceState = await createInstanceState(project.id)
464
+
465
+ await projectDatabase.secret.create({
466
+ data: {
467
+ id: createId(),
468
+ stateId: instanceState.id,
469
+ name: "test-secret",
470
+ meta: { title: "Test Secret" },
471
+ content: "secret-value",
472
+ },
473
+ })
474
+
475
+ projectService.getProjectOrThrow.mockResolvedValue(project)
476
+
477
+ // act
478
+ await instanceStateService.forgetInstanceState(
479
+ project.id,
480
+ instanceState.instanceId as InstanceId,
481
+ {
482
+ deleteSecrets: true,
483
+ },
484
+ )
485
+
486
+ // assert
487
+ const secrets = await projectDatabase.secret.findMany({
488
+ where: { stateId: instanceState.id },
489
+ })
490
+ expect(secrets).toHaveLength(0)
491
+ },
492
+ )
493
+
494
+ instanceStateTest(
495
+ "keeps secrets when deleteSecrets=false",
496
+ async ({
497
+ instanceStateService,
498
+ projectDatabase,
499
+ project,
500
+ createInstanceState,
501
+ projectService,
502
+ expect,
503
+ }) => {
504
+ // arrange
505
+ const instanceState = await createInstanceState(project.id)
506
+
507
+ const secretId = createId()
508
+ await projectDatabase.secret.create({
509
+ data: {
510
+ id: secretId,
511
+ stateId: instanceState.id,
512
+ name: "test-secret",
513
+ meta: { title: "Test Secret" },
514
+ content: "secret-value",
515
+ },
516
+ })
517
+
518
+ projectService.getProjectOrThrow.mockResolvedValue(project)
519
+
520
+ // act
521
+ await instanceStateService.forgetInstanceState(
522
+ project.id,
523
+ instanceState.instanceId as InstanceId,
524
+ {
525
+ deleteSecrets: false,
526
+ },
527
+ )
528
+
529
+ // assert
530
+ const secret = await projectDatabase.secret.findUnique({
531
+ where: { id: secretId },
532
+ })
533
+ expect(secret).toBeDefined()
534
+ expect(secret?.stateId).toBe(instanceState.id)
535
+ },
536
+ )
537
+
538
+ instanceStateTest(
539
+ "recursively deletes child instances",
540
+ async ({
541
+ instanceStateService,
542
+ projectDatabase,
543
+ project,
544
+ createInstanceState,
545
+ projectService,
546
+ pubsubManager,
547
+ expect,
548
+ }) => {
549
+ // arrange
550
+ const parentInstance = await createInstanceState(project.id, "composite.v1", "composite")
551
+ const childInstance = await createInstanceState(project.id)
552
+
553
+ // set both instances to deployed status and establish parent-child relationship
554
+ await projectDatabase.instanceState.update({
555
+ where: { id: parentInstance.id },
556
+ data: { status: "deployed" },
557
+ })
558
+
559
+ await projectDatabase.instanceState.update({
560
+ where: { id: childInstance.id },
561
+ data: { parentId: parentInstance.id, status: "deployed" },
562
+ })
563
+
564
+ projectService.getProjectOrThrow.mockResolvedValue(project)
565
+
566
+ // act
567
+ await instanceStateService.forgetInstanceState(
568
+ project.id,
569
+ parentInstance.instanceId as InstanceId,
570
+ )
571
+
572
+ // assert
573
+ const updatedParent = await projectDatabase.instanceState.findUnique({
574
+ where: { id: parentInstance.id },
575
+ })
576
+ const updatedChild = await projectDatabase.instanceState.findUnique({
577
+ where: { id: childInstance.id },
578
+ })
579
+
580
+ expect(updatedParent?.status).toBe("undeployed")
581
+ expect(updatedChild?.status).toBe("undeployed")
582
+
583
+ // verify both instances got state update events
584
+ expect(pubsubManager.publish).toHaveBeenCalledTimes(2)
585
+ },
586
+ )
587
+
588
+ instanceStateTest(
589
+ "calls Pulumi deleteState for unit instances",
590
+ async ({
591
+ instanceStateService,
592
+ project,
593
+ createInstanceState,
594
+ projectService,
595
+ runnerBackend,
596
+ expect,
597
+ }) => {
598
+ // arrange
599
+ const instanceState = await createInstanceState(project.id, "server.v1", "unit")
600
+ projectService.getProjectOrThrow.mockResolvedValue(project)
601
+
602
+ // act
603
+ await instanceStateService.forgetInstanceState(
604
+ project.id,
605
+ instanceState.instanceId as InstanceId,
606
+ )
607
+
608
+ // assert
609
+ const [instanceType, instanceName] = parseInstanceId(instanceState.instanceId as InstanceId)
610
+ expect(runnerBackend.deleteState).toHaveBeenCalledWith({
611
+ projectId: project.id,
612
+ stateId: instanceState.id,
613
+ libraryId: project.libraryId,
614
+ instanceName,
615
+ instanceType,
616
+ })
617
+ },
618
+ )
619
+
620
+ instanceStateTest(
621
+ "throws error for missing instance",
622
+ async ({ instanceStateService, project, projectService, expect }) => {
623
+ // arrange
624
+ const nonexistentInstanceId: InstanceId = "server.v1:nonexistent"
625
+ projectService.getProjectOrThrow.mockResolvedValue(project)
626
+
627
+ // act & assert
628
+ await expect(
629
+ instanceStateService.forgetInstanceState(project.id, nonexistentInstanceId),
630
+ ).rejects.toThrow(InstanceStateNotFoundError)
631
+ },
632
+ )
633
+ })
634
+
635
+ describe("replaceCustomStatus", () => {
636
+ instanceStateTest(
637
+ "successfully replaces custom status",
638
+ async ({ instanceStateService, projectDatabase, project, createInstanceState, expect }) => {
639
+ // arrange
640
+ const instanceState = await createInstanceState(project.id)
641
+ const serviceAccountId = createId()
642
+
643
+ // create service account first
644
+ await projectDatabase.serviceAccount.create({
645
+ data: {
646
+ id: serviceAccountId,
647
+ meta: { title: "Test Service Account" },
648
+ },
649
+ })
650
+
651
+ const status = {
652
+ instanceId: instanceState.instanceId,
653
+ name: "health",
654
+ meta: { title: "Health Status" },
655
+ value: "healthy",
656
+ title: "Health Status",
657
+ message: "Service is running normally",
658
+ order: 10,
659
+ }
660
+
661
+ // act
662
+ await instanceStateService.updateCustomStatus(
663
+ project.id,
664
+ instanceState.id,
665
+ serviceAccountId,
666
+ status,
667
+ )
668
+
669
+ // assert
670
+ const customStatus = await projectDatabase.instanceCustomStatus.findUnique({
671
+ where: {
672
+ stateId_serviceAccountId_name: {
673
+ stateId: instanceState.id,
674
+ serviceAccountId,
675
+ name: status.name,
676
+ },
677
+ },
678
+ })
679
+
680
+ expect(customStatus).toBeDefined()
681
+ expect(customStatus?.value).toBe("healthy")
682
+ expect(customStatus?.message).toBe("Service is running normally")
683
+ expect(customStatus?.order).toBe(10)
684
+ },
685
+ )
686
+ })
687
+
688
+ describe("removeCustomStatus", () => {
689
+ instanceStateTest(
690
+ "successfully removes custom status",
691
+ async ({ instanceStateService, projectDatabase, project, createInstanceState, expect }) => {
692
+ // arrange
693
+ const instanceState = await createInstanceState(project.id)
694
+
695
+ // create service account first
696
+ const serviceAccount = await projectDatabase.serviceAccount.create({
697
+ data: {
698
+ meta: { title: "Test Service Account" },
699
+ },
700
+ })
701
+
702
+ await projectDatabase.instanceCustomStatus.create({
703
+ data: {
704
+ stateId: instanceState.id,
705
+ serviceAccountId: serviceAccount.id,
706
+ name: "health",
707
+ meta: { title: "Health Status" },
708
+ value: "healthy",
709
+ order: 10,
710
+ },
711
+ })
712
+
713
+ // act
714
+ await instanceStateService.removeCustomStatus(
715
+ project.id,
716
+ instanceState.id,
717
+ serviceAccount.id,
718
+ "health",
719
+ )
720
+
721
+ // assert
722
+ const customStatus = await projectDatabase.instanceCustomStatus.findUnique({
723
+ where: {
724
+ stateId_serviceAccountId_name: {
725
+ stateId: instanceState.id,
726
+ serviceAccountId: serviceAccount.id,
727
+ name: "health",
728
+ },
729
+ },
730
+ })
731
+
732
+ expect(customStatus).toBeNull()
733
+ },
734
+ )
735
+ })