@highstate/backend 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/dist/{chunk-4JUMOKLV.js → chunk-JT4KWE3B.js} +10 -8
  2. package/dist/chunk-JT4KWE3B.js.map +1 -0
  3. package/dist/{chunk-VB4YL327.js → chunk-X2WG3WGL.js} +9 -2
  4. package/dist/chunk-X2WG3WGL.js.map +1 -0
  5. package/dist/highstate.manifest.json +4 -5
  6. package/dist/index.js +256 -287
  7. package/dist/index.js.map +1 -1
  8. package/dist/library/worker/main.js +1 -1
  9. package/dist/shared/index.js +1 -1
  10. package/package.json +9 -10
  11. package/prisma/backend/postgresql/main.prisma +0 -2
  12. package/prisma/backend/sqlite/main.prisma +0 -2
  13. package/prisma/project/instance.prisma +12 -0
  14. package/prisma/project/main.prisma +0 -1
  15. package/prisma/project/migrations/20260123000000_add_instance_state_self_hash/migration.sql +2 -0
  16. package/src/business/instance-state.test.ts +1 -0
  17. package/src/business/instance-state.ts +3 -0
  18. package/src/business/project.ts +1 -2
  19. package/src/common/utils.ts +9 -0
  20. package/src/database/_generated/backend/postgresql/browser.ts +54 -0
  21. package/src/database/_generated/backend/postgresql/client.ts +7 -8
  22. package/src/database/_generated/backend/postgresql/commonInputTypes.ts +3 -2
  23. package/src/database/_generated/backend/postgresql/enums.ts +3 -1
  24. package/src/database/_generated/backend/postgresql/internal/class.ts +24 -71
  25. package/src/database/_generated/backend/postgresql/internal/prismaNamespace.ts +41 -43
  26. package/src/database/_generated/backend/postgresql/internal/prismaNamespaceBrowser.ts +191 -0
  27. package/src/database/_generated/backend/postgresql/models/BackendUnlockMethod.ts +12 -11
  28. package/src/database/_generated/backend/postgresql/models/Library.ts +29 -28
  29. package/src/database/_generated/backend/postgresql/models/Project.ts +69 -68
  30. package/src/database/_generated/backend/postgresql/models/ProjectModelStorage.ts +29 -28
  31. package/src/database/_generated/backend/postgresql/models/ProjectSpace.ts +26 -25
  32. package/src/database/_generated/backend/postgresql/models/PulumiBackend.ts +29 -28
  33. package/src/database/_generated/backend/postgresql/models/UserWorkspaceLayout.ts +12 -11
  34. package/src/database/_generated/backend/postgresql/models.ts +2 -1
  35. package/src/database/_generated/backend/postgresql/pjtg.ts +1 -0
  36. package/src/database/_generated/backend/sqlite/browser.ts +54 -0
  37. package/src/database/_generated/backend/sqlite/client.ts +7 -8
  38. package/src/database/_generated/backend/sqlite/commonInputTypes.ts +3 -2
  39. package/src/database/_generated/backend/sqlite/enums.ts +3 -1
  40. package/src/database/_generated/backend/sqlite/internal/class.ts +24 -71
  41. package/src/database/_generated/backend/sqlite/internal/prismaNamespace.ts +41 -43
  42. package/src/database/_generated/backend/sqlite/internal/prismaNamespaceBrowser.ts +188 -0
  43. package/src/database/_generated/backend/sqlite/models/BackendUnlockMethod.ts +12 -11
  44. package/src/database/_generated/backend/sqlite/models/Library.ts +29 -28
  45. package/src/database/_generated/backend/sqlite/models/Project.ts +69 -68
  46. package/src/database/_generated/backend/sqlite/models/ProjectModelStorage.ts +29 -28
  47. package/src/database/_generated/backend/sqlite/models/ProjectSpace.ts +26 -25
  48. package/src/database/_generated/backend/sqlite/models/PulumiBackend.ts +29 -28
  49. package/src/database/_generated/backend/sqlite/models/UserWorkspaceLayout.ts +12 -11
  50. package/src/database/_generated/backend/sqlite/models.ts +2 -1
  51. package/src/database/_generated/backend/sqlite/pjtg.ts +1 -0
  52. package/src/database/_generated/project/browser.ts +1 -0
  53. package/src/database/_generated/project/client.ts +4 -5
  54. package/src/database/_generated/project/commonInputTypes.ts +1 -0
  55. package/src/database/_generated/project/enums.ts +1 -0
  56. package/src/database/_generated/project/internal/class.ts +21 -63
  57. package/src/database/_generated/project/internal/prismaNamespace.ts +41 -36
  58. package/src/database/_generated/project/internal/prismaNamespaceBrowser.ts +10 -6
  59. package/src/database/_generated/project/models/ApiKey.ts +1 -0
  60. package/src/database/_generated/project/models/Artifact.ts +1 -0
  61. package/src/database/_generated/project/models/HubModel.ts +1 -0
  62. package/src/database/_generated/project/models/InstanceCustomStatus.ts +1 -0
  63. package/src/database/_generated/project/models/InstanceEvaluationState.ts +1 -0
  64. package/src/database/_generated/project/models/InstanceLock.ts +1 -0
  65. package/src/database/_generated/project/models/InstanceModel.ts +1 -0
  66. package/src/database/_generated/project/models/InstanceOperationState.ts +1 -0
  67. package/src/database/_generated/project/models/InstanceState.ts +108 -1
  68. package/src/database/_generated/project/models/Operation.ts +1 -0
  69. package/src/database/_generated/project/models/OperationLog.ts +1 -0
  70. package/src/database/_generated/project/models/Page.ts +1 -0
  71. package/src/database/_generated/project/models/Secret.ts +1 -0
  72. package/src/database/_generated/project/models/ServiceAccount.ts +1 -0
  73. package/src/database/_generated/project/models/Terminal.ts +1 -0
  74. package/src/database/_generated/project/models/TerminalSession.ts +1 -0
  75. package/src/database/_generated/project/models/TerminalSessionLog.ts +1 -0
  76. package/src/database/_generated/project/models/Trigger.ts +1 -0
  77. package/src/database/_generated/project/models/UnlockMethod.ts +1 -0
  78. package/src/database/_generated/project/models/UserCompositeViewport.ts +1 -0
  79. package/src/database/_generated/project/models/UserProjectViewport.ts +1 -0
  80. package/src/database/_generated/project/models/Worker.ts +1 -0
  81. package/src/database/_generated/project/models/WorkerUnitRegistration.ts +1 -0
  82. package/src/database/_generated/project/models/WorkerVersion.ts +1 -0
  83. package/src/database/_generated/project/models/WorkerVersionLog.ts +1 -0
  84. package/src/database/_generated/project/models.ts +1 -0
  85. package/src/database/abstractions.ts +1 -7
  86. package/src/database/index.ts +1 -0
  87. package/src/database/local/backend.ts +19 -30
  88. package/src/database/local/project.ts +4 -9
  89. package/src/database/manager.ts +28 -34
  90. package/src/database/migration.ts +126 -0
  91. package/src/orchestrator/operation.cancel.test.ts +112 -0
  92. package/src/orchestrator/operation.composite.test.ts +123 -0
  93. package/src/orchestrator/operation.destroy.test.ts +77 -0
  94. package/src/orchestrator/operation.preview.test.ts +95 -0
  95. package/src/orchestrator/operation.refresh.test.ts +77 -0
  96. package/src/orchestrator/operation.test-utils.ts +646 -0
  97. package/src/orchestrator/operation.ts +91 -3
  98. package/src/orchestrator/operation.update.failure.test.ts +88 -0
  99. package/src/orchestrator/operation.update.skip.test.ts +95 -0
  100. package/src/orchestrator/operation.update.test.ts +117 -0
  101. package/src/orchestrator/plan-test-builder.ts +1 -0
  102. package/src/runner/abstractions.ts +5 -0
  103. package/src/runner/local.ts +1 -0
  104. package/src/shared/resolvers/input-hash.ts +10 -6
  105. package/src/terminal/manager.ts +0 -3
  106. package/src/test-utils/database.ts +28 -14
  107. package/dist/chunk-4JUMOKLV.js.map +0 -1
  108. package/dist/chunk-VB4YL327.js.map +0 -1
  109. package/dist/database/local/prisma.config.js +0 -26
  110. package/dist/database/local/prisma.config.js.map +0 -1
  111. package/src/database/local/prisma.config.ts +0 -25
  112. package/src/database/migrate.ts +0 -35
@@ -0,0 +1,646 @@
1
+ import type { InstanceId, InstanceModel, VersionedName } from "@highstate/contract"
2
+ import type { ArtifactService } from "../artifact"
3
+ import type {
4
+ InstanceLockService,
5
+ InstanceStateService,
6
+ OperationService,
7
+ ProjectModelService,
8
+ SecretService,
9
+ UnitExtraService,
10
+ } from "../business"
11
+ import type { Operation } from "../database"
12
+ import type { LibraryBackend } from "../library"
13
+ import type {
14
+ OperationType,
15
+ RunnerBackend,
16
+ UnitDestroyOptions,
17
+ UnitOptions,
18
+ UnitStateUpdate,
19
+ UnitUpdateOptions,
20
+ } from "../runner"
21
+ import type {
22
+ InstanceOperationStatus,
23
+ InstanceState,
24
+ LibraryModel,
25
+ OperationOptions,
26
+ OperationPhase,
27
+ } from "../shared"
28
+ import { defineComponent, defineEntity, defineUnit, getInstanceId, z } from "@highstate/contract"
29
+ import { createId } from "@paralleldrive/cuid2"
30
+ import { type MockedObject, vi } from "vitest"
31
+ import { test } from "../test-utils"
32
+ import { OperationContext } from "./operation-context"
33
+
34
+ export type Deferred<T> = {
35
+ promise: Promise<T>
36
+ resolve: (value: T) => void
37
+ reject: (error: unknown) => void
38
+ }
39
+
40
+ export function createDeferred<T>(): Deferred<T> {
41
+ let resolve!: (value: T) => void
42
+ let reject!: (error: unknown) => void
43
+
44
+ const promise = new Promise<T>((res, rej) => {
45
+ resolve = res
46
+ reject = rej
47
+ })
48
+
49
+ return { promise, resolve, reject }
50
+ }
51
+
52
+ type AsyncQueue<T> = {
53
+ push: (value: T) => void
54
+ shift: () => Promise<T>
55
+ }
56
+
57
+ function createAsyncQueue<T>(): AsyncQueue<T> {
58
+ const values: T[] = []
59
+ const waiters: Array<(value: T) => void> = []
60
+
61
+ const push = (value: T) => {
62
+ const waiter = waiters.shift()
63
+ if (waiter) {
64
+ waiter(value)
65
+ return
66
+ }
67
+
68
+ values.push(value)
69
+ }
70
+
71
+ const shift = async (): Promise<T> => {
72
+ const existing = values.shift()
73
+ if (existing !== undefined) {
74
+ return existing
75
+ }
76
+
77
+ const deferred = createDeferred<T>()
78
+ waiters.push(deferred.resolve)
79
+ return await deferred.promise
80
+ }
81
+
82
+ return { push, shift }
83
+ }
84
+
85
+ export type RunnerTestController = {
86
+ setAutoCompletion: (enabled: boolean) => void
87
+
88
+ setUpdateImpl: (impl: (options: UnitUpdateOptions) => Promise<void>) => void
89
+ setPreviewImpl: (impl: (options: UnitUpdateOptions) => Promise<void>) => void
90
+ setRefreshImpl: (impl: (options: UnitOptions) => Promise<void>) => void
91
+ setDestroyImpl: (impl: (options: UnitDestroyOptions) => Promise<void>) => void
92
+
93
+ emitProgress: (stateId: string, update: { current?: number; total?: number }) => void
94
+ emitMessage: (stateId: string, message: string) => void
95
+ emitError: (stateId: string, message: string) => void
96
+ emitCompletion: (
97
+ stateId: string,
98
+ update?: Omit<
99
+ Extract<UnitStateUpdate, { type: "completion" }>,
100
+ "type" | "unitId" | "operationType"
101
+ > & {
102
+ operationType?: OperationType
103
+ },
104
+ ) => void
105
+ }
106
+
107
+ export const operationTest = test.extend<{
108
+ runnerBackend: MockedObject<RunnerBackend>
109
+ runner: RunnerTestController
110
+ libraryBackend: MockedObject<LibraryBackend>
111
+ artifactService: MockedObject<ArtifactService>
112
+ instanceLockService: MockedObject<InstanceLockService>
113
+ operationService: MockedObject<OperationService>
114
+ secretService: MockedObject<SecretService>
115
+ instanceStateService: MockedObject<InstanceStateService>
116
+ projectModelService: MockedObject<ProjectModelService>
117
+ unitExtraService: MockedObject<UnitExtraService>
118
+
119
+ createMockLibrary: () => LibraryModel
120
+ createUnit: (name: string, type?: VersionedName) => InstanceModel
121
+ createComposite: (name: string, type?: VersionedName) => InstanceModel
122
+ createDeployedUnitState: (instance: InstanceModel) => InstanceState
123
+ createOperation: (input: {
124
+ type: Operation["type"]
125
+ requestedInstanceIds: InstanceId[]
126
+ phases: OperationPhase[]
127
+ options?: Partial<OperationOptions>
128
+ }) => Operation
129
+ createContext: (input: {
130
+ instances: InstanceModel[]
131
+ states: InstanceState[]
132
+ library?: LibraryModel
133
+ }) => Promise<OperationContext>
134
+ setupPersistenceMocks: (input: { instances: InstanceModel[] }) => void
135
+ setupImmediateLocking: () => void
136
+ }>({
137
+ runnerBackend: async ({}, use) => {
138
+ let autoCompletionEnabled = true
139
+
140
+ const lastOperationTypeByStateId = new Map<string, OperationType>()
141
+ const updatesByStateId = new Map<string, AsyncQueue<UnitStateUpdate>>()
142
+
143
+ const getQueue = (stateId: string): AsyncQueue<UnitStateUpdate> => {
144
+ const existing = updatesByStateId.get(stateId)
145
+ if (existing) {
146
+ return existing
147
+ }
148
+
149
+ const created = createAsyncQueue<UnitStateUpdate>()
150
+ updatesByStateId.set(stateId, created)
151
+ return created
152
+ }
153
+
154
+ let updateImpl: (options: UnitUpdateOptions) => Promise<void> = async () => {}
155
+ let previewImpl: (options: UnitUpdateOptions) => Promise<void> = async () => {}
156
+ let refreshImpl: (options: UnitOptions) => Promise<void> = async () => {}
157
+ let destroyImpl: (options: UnitDestroyOptions) => Promise<void> = async () => {}
158
+
159
+ const runner: RunnerTestController = {
160
+ setAutoCompletion: enabled => {
161
+ autoCompletionEnabled = enabled
162
+ },
163
+ setUpdateImpl: impl => {
164
+ updateImpl = impl
165
+ },
166
+ setPreviewImpl: impl => {
167
+ previewImpl = impl
168
+ },
169
+ setRefreshImpl: impl => {
170
+ refreshImpl = impl
171
+ },
172
+ setDestroyImpl: impl => {
173
+ destroyImpl = impl
174
+ },
175
+ emitProgress: (stateId, update) => {
176
+ getQueue(stateId).push({
177
+ type: "progress",
178
+ unitId: stateId as unknown as InstanceId,
179
+ currentResourceCount: update.current,
180
+ totalResourceCount: update.total,
181
+ })
182
+ },
183
+ emitMessage: (stateId, message) => {
184
+ getQueue(stateId).push({
185
+ type: "message",
186
+ unitId: stateId as unknown as InstanceId,
187
+ message,
188
+ })
189
+ },
190
+ emitError: (stateId, message) => {
191
+ getQueue(stateId).push({
192
+ type: "error",
193
+ unitId: stateId as unknown as InstanceId,
194
+ message,
195
+ })
196
+ },
197
+ emitCompletion: (stateId, update = {}) => {
198
+ const operationType =
199
+ update.operationType ?? lastOperationTypeByStateId.get(stateId) ?? "update"
200
+
201
+ getQueue(stateId).push({
202
+ type: "completion",
203
+ unitId: stateId as unknown as InstanceId,
204
+ operationType,
205
+ outputHash: update.outputHash ?? null,
206
+ statusFields: update.statusFields ?? null,
207
+ terminals: update.terminals ?? null,
208
+ pages: update.pages ?? null,
209
+ triggers: update.triggers ?? null,
210
+ secrets: update.secrets ?? null,
211
+ workers: update.workers ?? null,
212
+ exportedArtifactIds: update.exportedArtifactIds ?? null,
213
+ })
214
+ },
215
+ }
216
+
217
+ const runnerBackend = vi.mockObject({
218
+ update: vi.fn().mockImplementation(async (options: UnitUpdateOptions) => {
219
+ lastOperationTypeByStateId.set(options.stateId, "update")
220
+ await updateImpl(options)
221
+
222
+ if (autoCompletionEnabled) {
223
+ runner.emitCompletion(options.stateId, { operationType: "update" })
224
+ }
225
+ }),
226
+ preview: vi.fn().mockImplementation(async (options: UnitUpdateOptions) => {
227
+ lastOperationTypeByStateId.set(options.stateId, "update")
228
+ await previewImpl(options)
229
+
230
+ if (autoCompletionEnabled) {
231
+ runner.emitCompletion(options.stateId, { operationType: "update" })
232
+ }
233
+ }),
234
+ refresh: vi.fn().mockImplementation(async (options: UnitOptions) => {
235
+ lastOperationTypeByStateId.set(options.stateId, "refresh")
236
+ await refreshImpl(options)
237
+
238
+ if (autoCompletionEnabled) {
239
+ runner.emitCompletion(options.stateId, { operationType: "refresh" })
240
+ }
241
+ }),
242
+ destroy: vi.fn().mockImplementation(async (options: UnitDestroyOptions) => {
243
+ lastOperationTypeByStateId.set(options.stateId, "destroy")
244
+ await destroyImpl(options)
245
+
246
+ if (autoCompletionEnabled) {
247
+ runner.emitCompletion(options.stateId, { operationType: "destroy" })
248
+ }
249
+ }),
250
+ watch: vi.fn().mockImplementation((options: UnitOptions) => {
251
+ const queue = getQueue(options.stateId)
252
+
253
+ return (async function* () {
254
+ while (true) {
255
+ const update = await queue.shift()
256
+ yield update
257
+
258
+ if (update.type === "completion") {
259
+ return
260
+ }
261
+ }
262
+ })()
263
+ }),
264
+ } as unknown as RunnerBackend)
265
+
266
+ Object.defineProperty(runnerBackend, "__testController", {
267
+ value: runner,
268
+ enumerable: false,
269
+ })
270
+
271
+ await use(runnerBackend)
272
+ },
273
+
274
+ runner: async ({ runnerBackend }, use) => {
275
+ const controller = (runnerBackend as unknown as { __testController?: RunnerTestController })
276
+ .__testController
277
+
278
+ if (!controller) {
279
+ throw new Error("runner test controller was not initialized")
280
+ }
281
+
282
+ await use(controller)
283
+ },
284
+
285
+ libraryBackend: async ({}, use) => {
286
+ const libraryBackend = vi.mockObject({
287
+ loadLibrary: vi.fn(),
288
+ getResolvedUnitSources: vi.fn(),
289
+ } as unknown as LibraryBackend)
290
+
291
+ await use(libraryBackend)
292
+ },
293
+
294
+ artifactService: async ({}, use) => {
295
+ const artifactService = vi.mockObject({
296
+ getArtifactsByIds: vi.fn().mockResolvedValue([]),
297
+ } as unknown as ArtifactService)
298
+
299
+ await use(artifactService)
300
+ },
301
+
302
+ instanceLockService: async ({}, use) => {
303
+ const instanceLockService = vi.mockObject({
304
+ lockInstances: vi.fn(),
305
+ unlockInstances: vi.fn().mockResolvedValue(undefined),
306
+ unlockInstancesUnconditionally: vi.fn().mockResolvedValue(undefined),
307
+ tryLockInstances: vi.fn(),
308
+ } as unknown as InstanceLockService)
309
+
310
+ await use(instanceLockService)
311
+ },
312
+
313
+ operationService: async ({}, use) => {
314
+ const operationService = vi.mockObject({
315
+ updateOperation: vi.fn().mockResolvedValue({} as Operation),
316
+ markOperationFinished: vi.fn().mockResolvedValue({} as Operation),
317
+ appendLog: vi.fn().mockResolvedValue({} as Operation),
318
+ } as unknown as OperationService)
319
+
320
+ await use(operationService)
321
+ },
322
+
323
+ secretService: async ({}, use) => {
324
+ const secretService = vi.mockObject({
325
+ getInstanceSecretValues: vi.fn().mockResolvedValue({}),
326
+ } as unknown as SecretService)
327
+
328
+ await use(secretService)
329
+ },
330
+
331
+ instanceStateService: async ({}, use) => {
332
+ const instanceStateService = vi.mockObject({
333
+ getInstanceStates: vi.fn(),
334
+ createOperationStates: vi.fn(),
335
+ updateOperationState: vi.fn(),
336
+ updateOperationProgress: vi.fn(),
337
+ publishGhostInstanceDeletion: vi.fn(),
338
+ } as unknown as InstanceStateService)
339
+
340
+ await use(instanceStateService)
341
+ },
342
+
343
+ projectModelService: async ({}, use) => {
344
+ const projectModelService = vi.mockObject({
345
+ getProjectModel: vi.fn(),
346
+ } as unknown as ProjectModelService)
347
+
348
+ await use(projectModelService)
349
+ },
350
+
351
+ unitExtraService: async ({}, use) => {
352
+ const unitExtraService = vi.mockObject({
353
+ getInstanceTriggers: vi.fn().mockResolvedValue([]),
354
+ processUnitPages: vi.fn().mockResolvedValue([]),
355
+ processUnitTerminals: vi.fn().mockResolvedValue([]),
356
+ processUnitTriggers: vi.fn().mockResolvedValue([]),
357
+ pruneInstanceArtifacts: vi.fn().mockResolvedValue(undefined),
358
+ } as unknown as UnitExtraService)
359
+
360
+ await use(unitExtraService)
361
+ },
362
+
363
+ createMockLibrary: async ({}, use) => {
364
+ const createMockLibrary = (): LibraryModel => {
365
+ const testEntity = defineEntity({
366
+ type: "test.entity.v1",
367
+ schema: z.object({
368
+ value: z.string(),
369
+ }),
370
+ })
371
+
372
+ const testUnit = defineUnit({
373
+ type: "component.v1",
374
+ inputs: {
375
+ dependency: testEntity,
376
+ },
377
+ outputs: {
378
+ value: testEntity,
379
+ },
380
+ source: {
381
+ package: "@test/units",
382
+ path: "test-unit",
383
+ },
384
+ })
385
+
386
+ const testComposite = defineComponent({
387
+ type: "composite.v1",
388
+ create: () => {},
389
+ })
390
+
391
+ return {
392
+ components: {
393
+ "component.v1": testUnit.model,
394
+ "composite.v1": testComposite.model,
395
+ },
396
+ entities: {
397
+ "test.entity.v1": testEntity.model,
398
+ },
399
+ }
400
+ }
401
+
402
+ await use(createMockLibrary)
403
+ },
404
+
405
+ createUnit: async ({}, use) => {
406
+ const createUnit = (name: string, type: VersionedName = "component.v1"): InstanceModel => {
407
+ return {
408
+ id: getInstanceId(type, name),
409
+ name,
410
+ type,
411
+ kind: "unit",
412
+ parentId: undefined,
413
+ inputs: {},
414
+ args: {},
415
+ outputs: {},
416
+ }
417
+ }
418
+
419
+ await use(createUnit)
420
+ },
421
+
422
+ createComposite: async ({}, use) => {
423
+ const createComposite = (name: string, type: VersionedName = "composite.v1"): InstanceModel => {
424
+ return {
425
+ id: getInstanceId(type, name),
426
+ name,
427
+ type,
428
+ kind: "composite",
429
+ parentId: undefined,
430
+ inputs: {},
431
+ args: {},
432
+ outputs: {},
433
+ }
434
+ }
435
+
436
+ await use(createComposite)
437
+ },
438
+
439
+ createDeployedUnitState: async ({}, use) => {
440
+ const createDeployedUnitState = (instance: InstanceModel): InstanceState => {
441
+ return {
442
+ id: instance.id,
443
+ instanceId: instance.id,
444
+ status: "deployed",
445
+ source: "resident",
446
+ kind: instance.kind,
447
+ parentId: null,
448
+ parentInstanceId: instance.parentId ?? null,
449
+ selfHash: null,
450
+ inputHash: 12345,
451
+ outputHash: 12345,
452
+ dependencyOutputHash: 0,
453
+ statusFields: null,
454
+ exportedArtifactIds: null,
455
+ inputHashNonce: null,
456
+ currentResourceCount: null,
457
+ model: null,
458
+ resolvedInputs: null,
459
+ lastOperationState: {
460
+ operationId: "test-op",
461
+ stateId: instance.id,
462
+ status: "updated" as InstanceOperationStatus,
463
+ currentResourceCount: null,
464
+ totalResourceCount: null,
465
+ model: instance,
466
+ resolvedInputs: {},
467
+ startedAt: null,
468
+ finishedAt: null,
469
+ },
470
+ evaluationState: {} as InstanceState["evaluationState"],
471
+ }
472
+ }
473
+
474
+ await use(createDeployedUnitState)
475
+ },
476
+
477
+ createOperation: async ({}, use) => {
478
+ const createOperation = (input: {
479
+ type: Operation["type"]
480
+ requestedInstanceIds: InstanceId[]
481
+ phases: OperationPhase[]
482
+ options?: Partial<OperationOptions>
483
+ }): Operation => {
484
+ return {
485
+ id: createId(),
486
+ meta: {
487
+ title: "Test Operation",
488
+ description: "Orchestrator runtime test",
489
+ },
490
+ type: input.type,
491
+ status: "pending",
492
+ options: {
493
+ forceUpdateDependencies: false,
494
+ ignoreDependencies: false,
495
+ forceUpdateChildren: false,
496
+ destroyDependentInstances: true,
497
+ invokeDestroyTriggers: true,
498
+ deleteUnreachableResources: false,
499
+ forceDeleteState: false,
500
+ allowPartialCompositeInstanceUpdate: false,
501
+ allowPartialCompositeInstanceDestruction: false,
502
+ refresh: false,
503
+ ...input.options,
504
+ },
505
+ phases: input.phases,
506
+ requestedInstanceIds: input.requestedInstanceIds,
507
+ startedAt: new Date(),
508
+ updatedAt: new Date(),
509
+ finishedAt: null,
510
+ }
511
+ }
512
+
513
+ await use(createOperation)
514
+ },
515
+
516
+ createContext: async (
517
+ {
518
+ project,
519
+ logger,
520
+ libraryBackend,
521
+ instanceStateService,
522
+ projectModelService,
523
+ createMockLibrary,
524
+ },
525
+ use,
526
+ ) => {
527
+ const createContext = async (input: {
528
+ instances: InstanceModel[]
529
+ states: InstanceState[]
530
+ library?: LibraryModel
531
+ }): Promise<OperationContext> => {
532
+ const library = input.library ?? createMockLibrary()
533
+
534
+ libraryBackend.loadLibrary.mockResolvedValue(library)
535
+ libraryBackend.getResolvedUnitSources.mockResolvedValue([
536
+ {
537
+ unitType: "component.v1",
538
+ sourceHash: 12345,
539
+ projectPath: "test",
540
+ allowedDependencies: [],
541
+ },
542
+ {
543
+ unitType: "composite.v1",
544
+ sourceHash: 12345,
545
+ projectPath: "test",
546
+ allowedDependencies: [],
547
+ },
548
+ ])
549
+
550
+ projectModelService.getProjectModel.mockResolvedValue([
551
+ {
552
+ instances: input.instances,
553
+ virtualInstances: [],
554
+ hubs: [],
555
+ ghostInstances: [],
556
+ },
557
+ project,
558
+ ])
559
+
560
+ instanceStateService.getInstanceStates.mockResolvedValue(input.states)
561
+
562
+ return await OperationContext.load(
563
+ project.id,
564
+ libraryBackend,
565
+ instanceStateService,
566
+ projectModelService,
567
+ logger,
568
+ )
569
+ }
570
+
571
+ await use(createContext)
572
+ },
573
+
574
+ setupPersistenceMocks: async ({ instanceStateService }, use) => {
575
+ const setupPersistenceMocks = (input: { instances: InstanceModel[] }) => {
576
+ const instanceByStateId = new Map<string, InstanceModel>(
577
+ input.instances.map(instance => [instance.id, instance]),
578
+ )
579
+
580
+ instanceStateService.createOperationStates.mockImplementation(async (_projectId, tuples) => {
581
+ return tuples.map(([opState, instancePatch]) => {
582
+ return {
583
+ instanceId: opState.model.id,
584
+ ...instancePatch,
585
+ lastOperationState: {
586
+ operationId: opState.operationId,
587
+ stateId: opState.stateId,
588
+ status: opState.status,
589
+ currentResourceCount: null,
590
+ totalResourceCount: null,
591
+ model: opState.model,
592
+ resolvedInputs: opState.resolvedInputs,
593
+ startedAt: null,
594
+ finishedAt: null,
595
+ },
596
+ }
597
+ })
598
+ })
599
+
600
+ instanceStateService.updateOperationState.mockImplementation(
601
+ async (_projectId, stateId, operationId, options) => {
602
+ const status =
603
+ typeof options.operationState?.status === "string"
604
+ ? options.operationState.status
605
+ : "pending"
606
+
607
+ const instance = instanceByStateId.get(stateId) ?? input.instances[0]
608
+ if (!instance) {
609
+ throw new Error("no instances provided to setupPersistenceMocks")
610
+ }
611
+
612
+ return {
613
+ instanceId: instance.id,
614
+ ...options.instanceState,
615
+ lastOperationState: {
616
+ operationId,
617
+ stateId,
618
+ status,
619
+ currentResourceCount: null,
620
+ totalResourceCount: null,
621
+ model: instance,
622
+ resolvedInputs: {},
623
+ startedAt: null,
624
+ finishedAt: null,
625
+ },
626
+ }
627
+ },
628
+ )
629
+ }
630
+
631
+ await use(setupPersistenceMocks)
632
+ },
633
+
634
+ setupImmediateLocking: async ({ instanceLockService }, use) => {
635
+ const setupImmediateLocking = () => {
636
+ instanceLockService.lockInstances.mockImplementation(
637
+ async (_projectId, stateIds, _meta, action) => {
638
+ await action?.(undefined as never, stateIds)
639
+ return ["test", stateIds]
640
+ },
641
+ )
642
+ }
643
+
644
+ await use(setupImmediateLocking)
645
+ },
646
+ })