@highstate/backend 0.18.0 → 0.20.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.
- package/dist/{chunk-JT4KWE3B.js → chunk-52MY2TCE.js} +348 -19
- package/dist/chunk-52MY2TCE.js.map +1 -0
- package/dist/{chunk-I7BWSAN6.js → chunk-UAWBPTDW.js} +3 -3
- package/dist/{chunk-I7BWSAN6.js.map → chunk-UAWBPTDW.js.map} +1 -1
- package/dist/highstate.manifest.json +4 -4
- package/dist/index.js +4159 -785
- package/dist/index.js.map +1 -1
- package/dist/library/worker/main.js +5 -2
- package/dist/library/worker/main.js.map +1 -1
- package/dist/shared/index.js +2 -2
- package/package.json +7 -7
- package/prisma/backend/_schema/object.prisma +12 -0
- package/prisma/backend/sqlite/migrations/20260222113554_add_object_tracking/migration.sql +7 -0
- package/prisma/project/artifact.prisma +3 -0
- package/prisma/project/entity.prisma +125 -0
- package/prisma/project/instance.prisma +6 -0
- package/prisma/project/migrations/20260301210131_add_entity_tracking/migration.sql +70 -0
- package/prisma/project/migrations/20260302212734_add_resource_hooks_flag/migration.sql +1 -0
- package/prisma/project/operation.prisma +3 -0
- package/src/business/artifact.test.ts +22 -2
- package/src/business/artifact.ts +7 -1
- package/src/business/entity-snapshot.test.ts +684 -0
- package/src/business/entity-snapshot.ts +904 -0
- package/src/business/evaluation.test.ts +56 -0
- package/src/business/evaluation.ts +102 -22
- package/src/business/global-search.test.ts +344 -0
- package/src/business/global-search.ts +902 -0
- package/src/business/index.ts +4 -0
- package/src/business/instance-lock.ts +58 -74
- package/src/business/instance-state.test.ts +15 -1
- package/src/business/instance-state.ts +37 -14
- package/src/business/object-ref-index.test.ts +140 -0
- package/src/business/object-ref-index.ts +193 -0
- package/src/business/operation.test.ts +15 -1
- package/src/business/operation.ts +4 -0
- package/src/business/project-model.ts +154 -13
- package/src/business/project-unlock.ts +25 -2
- package/src/business/project.ts +9 -0
- package/src/business/secret.test.ts +35 -2
- package/src/business/secret.ts +32 -9
- package/src/business/settings.ts +761 -0
- package/src/business/unit-output.test.ts +477 -0
- package/src/business/unit-output.ts +461 -0
- package/src/business/worker.ts +55 -4
- package/src/database/_generated/backend/postgresql/browser.ts +6 -0
- package/src/database/_generated/backend/postgresql/client.ts +6 -0
- package/src/database/_generated/backend/postgresql/internal/class.ts +23 -5
- package/src/database/_generated/backend/postgresql/internal/prismaNamespace.ts +89 -5
- package/src/database/_generated/backend/postgresql/internal/prismaNamespaceBrowser.ts +9 -0
- package/src/database/_generated/backend/postgresql/models/Object.ts +1076 -0
- package/src/database/_generated/backend/postgresql/models.ts +1 -0
- package/src/database/_generated/backend/sqlite/browser.ts +6 -0
- package/src/database/_generated/backend/sqlite/client.ts +6 -0
- package/src/database/_generated/backend/sqlite/internal/class.ts +23 -5
- package/src/database/_generated/backend/sqlite/internal/prismaNamespace.ts +89 -5
- package/src/database/_generated/backend/sqlite/internal/prismaNamespaceBrowser.ts +9 -0
- package/src/database/_generated/backend/sqlite/models/Object.ts +1074 -0
- package/src/database/_generated/backend/sqlite/models.ts +1 -0
- package/src/database/_generated/project/browser.ts +23 -0
- package/src/database/_generated/project/client.ts +23 -0
- package/src/database/_generated/project/commonInputTypes.ts +87 -53
- package/src/database/_generated/project/enums.ts +8 -0
- package/src/database/_generated/project/internal/class.ts +53 -5
- package/src/database/_generated/project/internal/prismaNamespace.ts +367 -13
- package/src/database/_generated/project/internal/prismaNamespaceBrowser.ts +48 -1
- package/src/database/_generated/project/models/Artifact.ts +199 -11
- package/src/database/_generated/project/models/Entity.ts +1274 -0
- package/src/database/_generated/project/models/EntitySnapshot.ts +2389 -0
- package/src/database/_generated/project/models/EntitySnapshotContent.ts +1260 -0
- package/src/database/_generated/project/models/EntitySnapshotReference.ts +1449 -0
- package/src/database/_generated/project/models/InstanceState.ts +361 -1
- package/src/database/_generated/project/models/Operation.ts +148 -3
- package/src/database/_generated/project/models/OperationLog.ts +0 -4
- package/src/database/_generated/project/models.ts +4 -0
- package/src/database/migration.ts +3 -0
- package/src/library/worker/evaluator.ts +7 -1
- package/src/orchestrator/manager.ts +7 -0
- package/src/orchestrator/operation-context.captured-outputs.test.ts +118 -0
- package/src/orchestrator/operation-context.ts +154 -16
- package/src/orchestrator/operation-plan.destroy.test.md +33 -12
- package/src/orchestrator/operation-plan.destroy.test.ts +140 -2
- package/src/orchestrator/operation-plan.fixtures.ts +2 -0
- package/src/orchestrator/operation-plan.md +4 -1
- package/src/orchestrator/operation-plan.ts +286 -92
- package/src/orchestrator/operation-plan.update.test.md +286 -11
- package/src/orchestrator/operation-plan.update.test.ts +656 -5
- package/src/orchestrator/operation-workset.ts +72 -22
- package/src/orchestrator/operation.cancel.test.ts +4 -0
- package/src/orchestrator/operation.composite.test.ts +341 -0
- package/src/orchestrator/operation.destroy.test.ts +4 -0
- package/src/orchestrator/operation.output-validation.failure.test.ts +124 -0
- package/src/orchestrator/operation.preview.test.ts +4 -0
- package/src/orchestrator/operation.refresh.test.ts +4 -0
- package/src/orchestrator/operation.test-utils.ts +52 -13
- package/src/orchestrator/operation.ts +228 -68
- package/src/orchestrator/operation.update.failure.test.ts +4 -0
- package/src/orchestrator/operation.update.skip.test.ts +110 -0
- package/src/orchestrator/operation.update.test.ts +4 -0
- package/src/orchestrator/plan-test-builder.ts +1 -0
- package/src/orchestrator/unit-input-values.test.ts +450 -0
- package/src/orchestrator/unit-input-values.ts +281 -0
- package/src/pubsub/manager.ts +3 -0
- package/src/runner/abstractions.ts +23 -54
- package/src/runner/local.ts +109 -85
- package/src/services.ts +52 -1
- package/src/shared/models/prisma.ts +1 -0
- package/src/shared/models/project/entity.ts +121 -0
- package/src/shared/models/project/index.ts +1 -0
- package/src/shared/models/project/operation.ts +61 -3
- package/src/shared/models/project/state.ts +10 -0
- package/src/shared/models/project/worker.ts +7 -0
- package/src/shared/resolvers/effective-output-type.test.ts +494 -0
- package/src/shared/resolvers/effective-output-type.ts +162 -0
- package/src/shared/resolvers/index.ts +1 -0
- package/src/shared/resolvers/input.ts +61 -9
- package/src/shared/utils/index.ts +1 -0
- package/src/shared/utils/stable-json.ts +41 -0
- package/src/terminal/manager.ts +6 -0
- package/src/worker/manager.ts +97 -1
- package/dist/chunk-JT4KWE3B.js.map +0 -1
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import type { InstanceId } from "@highstate/contract"
|
|
2
2
|
import type { InstanceStateService, UpdateOperationStateOptions } from "../business"
|
|
3
3
|
import type { InstanceOperationStatus } from "../database"
|
|
4
|
+
import type {
|
|
5
|
+
InstanceStatus,
|
|
6
|
+
OperationPhase,
|
|
7
|
+
OperationPhaseType,
|
|
8
|
+
OperationType,
|
|
9
|
+
ProjectOutput,
|
|
10
|
+
} from "../shared"
|
|
4
11
|
import type { OperationContext } from "./operation-context"
|
|
5
12
|
import { EventEmitter, on } from "node:events"
|
|
6
13
|
import { mapValues } from "remeda"
|
|
7
|
-
import {
|
|
8
|
-
type InstanceStatus,
|
|
9
|
-
isTransientInstanceOperationStatus,
|
|
10
|
-
type OperationPhase,
|
|
11
|
-
type OperationPhaseType,
|
|
12
|
-
type ProjectOutput,
|
|
13
|
-
} from "../shared"
|
|
14
14
|
|
|
15
15
|
type AbortControllerPair = {
|
|
16
16
|
abortController: AbortController
|
|
@@ -35,6 +35,7 @@ export class OperationWorkset {
|
|
|
35
35
|
constructor(
|
|
36
36
|
readonly project: ProjectOutput,
|
|
37
37
|
readonly operationId: string,
|
|
38
|
+
readonly operationType: OperationType,
|
|
38
39
|
readonly phases: OperationPhase[],
|
|
39
40
|
private readonly context: OperationContext,
|
|
40
41
|
private readonly instanceStateService: InstanceStateService,
|
|
@@ -117,16 +118,33 @@ export class OperationWorkset {
|
|
|
117
118
|
const patches = await this.instanceStateService.createOperationStates(
|
|
118
119
|
this.project.id,
|
|
119
120
|
Array.from(this.allAffectedInstanceIds).map(instanceId => {
|
|
120
|
-
const instance = this.context.getInstance(instanceId)
|
|
121
121
|
const state = this.context.getState(instanceId)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
inputs => inputs.map(input => input.input),
|
|
122
|
+
const hasDestroyPhase = this.phases.some(
|
|
123
|
+
phase =>
|
|
124
|
+
phase.type === "destroy" &&
|
|
125
|
+
phase.instances.some(instance => instance.id === instanceId),
|
|
127
126
|
)
|
|
128
127
|
|
|
129
|
-
|
|
128
|
+
// destroy plans may reference state-only instances that are no longer in the project model
|
|
129
|
+
const instance = hasDestroyPhase
|
|
130
|
+
? (this.context.tryGetInstance(instanceId) ??
|
|
131
|
+
this.context.tryGetInstanceForDestroy(instanceId))
|
|
132
|
+
: this.context.tryGetInstance(instanceId)
|
|
133
|
+
|
|
134
|
+
if (!instance) {
|
|
135
|
+
throw new Error(`Instance with ID ${instanceId} not found in the operation context`)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const serializedResolvedInputs =
|
|
139
|
+
this.context.getResolvedInputs(instance.id) != null
|
|
140
|
+
? this.context.serializeResolvedInputs(
|
|
141
|
+
mapValues(
|
|
142
|
+
//
|
|
143
|
+
this.context.getResolvedInputs(instance.id) ?? {},
|
|
144
|
+
inputs => inputs.map(input => input.input),
|
|
145
|
+
),
|
|
146
|
+
)
|
|
147
|
+
: (state.resolvedInputs ?? {})
|
|
130
148
|
|
|
131
149
|
return [
|
|
132
150
|
{
|
|
@@ -155,6 +173,10 @@ export class OperationWorkset {
|
|
|
155
173
|
async updateState(instanceId: InstanceId, options: UpdateOperationStateOptions): Promise<void> {
|
|
156
174
|
const state = this.context.getState(instanceId)
|
|
157
175
|
|
|
176
|
+
if (state.lastOperationState?.operationId !== this.operationId) {
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
|
|
158
180
|
const patch = await this.instanceStateService.updateOperationState(
|
|
159
181
|
this.project.id,
|
|
160
182
|
state.id,
|
|
@@ -164,9 +186,11 @@ export class OperationWorkset {
|
|
|
164
186
|
|
|
165
187
|
Object.assign(state, patch)
|
|
166
188
|
|
|
167
|
-
|
|
189
|
+
const parentInstanceId = this.getPhaseParentId(instanceId)
|
|
190
|
+
|
|
191
|
+
if (parentInstanceId && this.currentPhase !== "preview") {
|
|
168
192
|
// TODO: update all updates in single transaction
|
|
169
|
-
await this.recalculateCompositeInstanceState(
|
|
193
|
+
await this.recalculateCompositeInstanceState(parentInstanceId)
|
|
170
194
|
}
|
|
171
195
|
}
|
|
172
196
|
|
|
@@ -221,12 +245,6 @@ export class OperationWorkset {
|
|
|
221
245
|
|
|
222
246
|
await this.updateState(instanceId, {
|
|
223
247
|
operationState: {
|
|
224
|
-
status:
|
|
225
|
-
!state.lastOperationState?.status ||
|
|
226
|
-
// do not override final statuses
|
|
227
|
-
isTransientInstanceOperationStatus(state.lastOperationState.status)
|
|
228
|
-
? this.getTransientStatusByOperationPhase()
|
|
229
|
-
: state.lastOperationState.status,
|
|
230
248
|
currentResourceCount,
|
|
231
249
|
totalResourceCount: finalTotalResourceCount,
|
|
232
250
|
},
|
|
@@ -271,6 +289,7 @@ export class OperationWorkset {
|
|
|
271
289
|
|
|
272
290
|
getNextStableInstanceStatus(instanceId: InstanceId): InstanceStatus {
|
|
273
291
|
const state = this.context.getState(instanceId)
|
|
292
|
+
const modelInstance = this.context.tryGetInstance(instanceId)
|
|
274
293
|
|
|
275
294
|
switch (this.currentPhase) {
|
|
276
295
|
case "preview":
|
|
@@ -278,12 +297,43 @@ export class OperationWorkset {
|
|
|
278
297
|
case "update":
|
|
279
298
|
return "deployed"
|
|
280
299
|
case "destroy":
|
|
300
|
+
// update operations may include a destroy phase only for ghost cleanup
|
|
301
|
+
// keep composite containers deployed only while they still have unit descendants
|
|
302
|
+
if (this.operationType === "update" && modelInstance?.kind === "composite") {
|
|
303
|
+
return this.hasActiveUnitDescendant(instanceId) ? state.status : "undeployed"
|
|
304
|
+
}
|
|
305
|
+
|
|
281
306
|
return "undeployed"
|
|
282
307
|
case "refresh":
|
|
283
308
|
return state.status // do not change instance status when refreshing
|
|
284
309
|
}
|
|
285
310
|
}
|
|
286
311
|
|
|
312
|
+
private hasActiveUnitDescendant(instanceId: InstanceId): boolean {
|
|
313
|
+
const queue = [...this.context.getStateChildIds(instanceId)]
|
|
314
|
+
const visited = new Set<InstanceId>()
|
|
315
|
+
|
|
316
|
+
while (queue.length > 0) {
|
|
317
|
+
const childId = queue.shift()
|
|
318
|
+
if (!childId || visited.has(childId)) {
|
|
319
|
+
continue
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
visited.add(childId)
|
|
323
|
+
|
|
324
|
+
const childState = this.context.getState(childId)
|
|
325
|
+
if (childState.kind === "unit" && childState.status !== "undeployed") {
|
|
326
|
+
return true
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (childState.kind === "composite") {
|
|
330
|
+
queue.push(...this.context.getStateChildIds(childId))
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return false
|
|
335
|
+
}
|
|
336
|
+
|
|
287
337
|
setupAbortControllersForAllInstances(): void {
|
|
288
338
|
for (const instanceId of this.allAffectedInstanceIds) {
|
|
289
339
|
this.setupInstanceAbortControllers(instanceId)
|
|
@@ -20,6 +20,8 @@ describe("Operation - Cancel", () => {
|
|
|
20
20
|
instanceStateService,
|
|
21
21
|
projectModelService,
|
|
22
22
|
unitExtraService,
|
|
23
|
+
entitySnapshotService,
|
|
24
|
+
unitOutputService,
|
|
23
25
|
createUnit,
|
|
24
26
|
createDeployedUnitState,
|
|
25
27
|
createOperation,
|
|
@@ -86,6 +88,8 @@ describe("Operation - Cancel", () => {
|
|
|
86
88
|
instanceStateService,
|
|
87
89
|
projectModelService,
|
|
88
90
|
unitExtraService,
|
|
91
|
+
entitySnapshotService,
|
|
92
|
+
unitOutputService,
|
|
89
93
|
logger,
|
|
90
94
|
)
|
|
91
95
|
|
|
@@ -18,6 +18,8 @@ describe("Operation - Composite", () => {
|
|
|
18
18
|
instanceStateService,
|
|
19
19
|
projectModelService,
|
|
20
20
|
unitExtraService,
|
|
21
|
+
entitySnapshotService,
|
|
22
|
+
unitOutputService,
|
|
21
23
|
createComposite,
|
|
22
24
|
createUnit,
|
|
23
25
|
createDeployedUnitState,
|
|
@@ -79,6 +81,8 @@ describe("Operation - Composite", () => {
|
|
|
79
81
|
instanceStateService,
|
|
80
82
|
projectModelService,
|
|
81
83
|
unitExtraService,
|
|
84
|
+
entitySnapshotService,
|
|
85
|
+
unitOutputService,
|
|
82
86
|
logger,
|
|
83
87
|
)
|
|
84
88
|
|
|
@@ -120,4 +124,341 @@ describe("Operation - Composite", () => {
|
|
|
120
124
|
)
|
|
121
125
|
},
|
|
122
126
|
)
|
|
127
|
+
|
|
128
|
+
operationTest(
|
|
129
|
+
"recalculates using phase parent when state parent is stale and outside operation",
|
|
130
|
+
async ({
|
|
131
|
+
project,
|
|
132
|
+
logger,
|
|
133
|
+
runnerBackend,
|
|
134
|
+
libraryBackend,
|
|
135
|
+
artifactService,
|
|
136
|
+
instanceLockService,
|
|
137
|
+
operationService,
|
|
138
|
+
secretService,
|
|
139
|
+
instanceStateService,
|
|
140
|
+
projectModelService,
|
|
141
|
+
unitExtraService,
|
|
142
|
+
entitySnapshotService,
|
|
143
|
+
unitOutputService,
|
|
144
|
+
createComposite,
|
|
145
|
+
createUnit,
|
|
146
|
+
createDeployedUnitState,
|
|
147
|
+
createOperation,
|
|
148
|
+
createContext,
|
|
149
|
+
setupPersistenceMocks,
|
|
150
|
+
setupImmediateLocking,
|
|
151
|
+
expect,
|
|
152
|
+
}) => {
|
|
153
|
+
// arrange
|
|
154
|
+
const grandParent = createComposite("GrandParent")
|
|
155
|
+
const parent = {
|
|
156
|
+
...createComposite("Parent"),
|
|
157
|
+
parentId: grandParent.id,
|
|
158
|
+
}
|
|
159
|
+
const oldParent = createComposite("OldParent")
|
|
160
|
+
const child = {
|
|
161
|
+
...createUnit("Child"),
|
|
162
|
+
parentId: parent.id,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const grandParentState = createDeployedUnitState(grandParent)
|
|
166
|
+
const parentState = createDeployedUnitState(parent)
|
|
167
|
+
const oldParentState = createDeployedUnitState(oldParent)
|
|
168
|
+
const childState = createDeployedUnitState(child)
|
|
169
|
+
|
|
170
|
+
// simulate state/model drift: child state still points to old parent state
|
|
171
|
+
childState.parentInstanceId = oldParent.id
|
|
172
|
+
|
|
173
|
+
await createContext({
|
|
174
|
+
instances: [grandParent, parent, child],
|
|
175
|
+
states: [grandParentState, parentState, oldParentState, childState],
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
setupImmediateLocking()
|
|
179
|
+
setupPersistenceMocks({ instances: [grandParent, parent, child, oldParent] })
|
|
180
|
+
|
|
181
|
+
const inOperationStateIds = new Set([grandParent.id, parent.id, child.id])
|
|
182
|
+
const baseUpdateMock = instanceStateService.updateOperationState.getMockImplementation()
|
|
183
|
+
|
|
184
|
+
instanceStateService.updateOperationState.mockImplementation(
|
|
185
|
+
async (projectId, stateId, operationId, options) => {
|
|
186
|
+
if (!inOperationStateIds.has(stateId as typeof grandParent.id)) {
|
|
187
|
+
throw new Error(`No operation state row for stateId ${stateId}`)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!baseUpdateMock) {
|
|
191
|
+
throw new Error("updateOperationState base mock is not initialized")
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return await baseUpdateMock(projectId, stateId, operationId, options)
|
|
195
|
+
},
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
const operation = createOperation({
|
|
199
|
+
type: "update",
|
|
200
|
+
requestedInstanceIds: [child.id],
|
|
201
|
+
phases: [
|
|
202
|
+
{
|
|
203
|
+
type: "update",
|
|
204
|
+
instances: [
|
|
205
|
+
{ id: grandParent.id, message: "ancestor", parentId: undefined },
|
|
206
|
+
{ id: parent.id, message: "parent", parentId: grandParent.id },
|
|
207
|
+
{ id: child.id, message: "requested", parentId: parent.id },
|
|
208
|
+
],
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
const runtimeOperation = new RuntimeOperation(
|
|
214
|
+
project,
|
|
215
|
+
operation,
|
|
216
|
+
runnerBackend,
|
|
217
|
+
libraryBackend,
|
|
218
|
+
artifactService,
|
|
219
|
+
instanceLockService,
|
|
220
|
+
operationService,
|
|
221
|
+
secretService,
|
|
222
|
+
instanceStateService,
|
|
223
|
+
projectModelService,
|
|
224
|
+
unitExtraService,
|
|
225
|
+
entitySnapshotService,
|
|
226
|
+
unitOutputService,
|
|
227
|
+
logger,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
// act
|
|
231
|
+
await runtimeOperation.operateSafe()
|
|
232
|
+
|
|
233
|
+
// assert
|
|
234
|
+
const recalculatedOldParent = instanceStateService.updateOperationState.mock.calls.some(
|
|
235
|
+
([, stateId]) => stateId === oldParent.id,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
expect(recalculatedOldParent).toBe(false)
|
|
239
|
+
expect(operationService.markOperationFinished).toHaveBeenCalledWith(
|
|
240
|
+
project.id,
|
|
241
|
+
operation.id,
|
|
242
|
+
"completed",
|
|
243
|
+
)
|
|
244
|
+
},
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
operationTest(
|
|
248
|
+
"keeps composite parents deployed during ghost cleanup destroy phase in update operations",
|
|
249
|
+
async ({
|
|
250
|
+
project,
|
|
251
|
+
logger,
|
|
252
|
+
runnerBackend,
|
|
253
|
+
libraryBackend,
|
|
254
|
+
artifactService,
|
|
255
|
+
instanceLockService,
|
|
256
|
+
operationService,
|
|
257
|
+
secretService,
|
|
258
|
+
instanceStateService,
|
|
259
|
+
projectModelService,
|
|
260
|
+
unitExtraService,
|
|
261
|
+
entitySnapshotService,
|
|
262
|
+
unitOutputService,
|
|
263
|
+
createComposite,
|
|
264
|
+
createUnit,
|
|
265
|
+
createDeployedUnitState,
|
|
266
|
+
createOperation,
|
|
267
|
+
createContext,
|
|
268
|
+
setupPersistenceMocks,
|
|
269
|
+
setupImmediateLocking,
|
|
270
|
+
expect,
|
|
271
|
+
}) => {
|
|
272
|
+
// arrange
|
|
273
|
+
const grandParent = createComposite("GrandParent")
|
|
274
|
+
const parent = {
|
|
275
|
+
...createComposite("Parent"),
|
|
276
|
+
parentId: grandParent.id,
|
|
277
|
+
}
|
|
278
|
+
const child = {
|
|
279
|
+
...createUnit("Child"),
|
|
280
|
+
parentId: parent.id,
|
|
281
|
+
}
|
|
282
|
+
const ghostChild = {
|
|
283
|
+
...createUnit("GhostChild"),
|
|
284
|
+
parentId: parent.id,
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const grandParentState = createDeployedUnitState(grandParent)
|
|
288
|
+
const parentState = createDeployedUnitState(parent)
|
|
289
|
+
const childState = createDeployedUnitState(child)
|
|
290
|
+
const ghostChildState = createDeployedUnitState(ghostChild)
|
|
291
|
+
|
|
292
|
+
await createContext({
|
|
293
|
+
instances: [grandParent, parent, child],
|
|
294
|
+
states: [grandParentState, parentState, childState, ghostChildState],
|
|
295
|
+
})
|
|
296
|
+
setupImmediateLocking()
|
|
297
|
+
setupPersistenceMocks({ instances: [grandParent, parent, child, ghostChild] })
|
|
298
|
+
|
|
299
|
+
const operation = createOperation({
|
|
300
|
+
type: "update",
|
|
301
|
+
requestedInstanceIds: [parent.id],
|
|
302
|
+
phases: [
|
|
303
|
+
{
|
|
304
|
+
type: "update",
|
|
305
|
+
instances: [
|
|
306
|
+
{ id: grandParent.id, message: "ancestor", parentId: undefined },
|
|
307
|
+
{ id: parent.id, message: "requested", parentId: grandParent.id },
|
|
308
|
+
{ id: child.id, message: "child", parentId: parent.id },
|
|
309
|
+
],
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
type: "destroy",
|
|
313
|
+
instances: [
|
|
314
|
+
{ id: ghostChild.id, message: "ghost cleanup", parentId: parent.id },
|
|
315
|
+
{ id: parent.id, message: "parent of included child", parentId: grandParent.id },
|
|
316
|
+
{ id: grandParent.id, message: "parent of included child", parentId: undefined },
|
|
317
|
+
],
|
|
318
|
+
},
|
|
319
|
+
],
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
const runtimeOperation = new RuntimeOperation(
|
|
323
|
+
project,
|
|
324
|
+
operation,
|
|
325
|
+
runnerBackend,
|
|
326
|
+
libraryBackend,
|
|
327
|
+
artifactService,
|
|
328
|
+
instanceLockService,
|
|
329
|
+
operationService,
|
|
330
|
+
secretService,
|
|
331
|
+
instanceStateService,
|
|
332
|
+
projectModelService,
|
|
333
|
+
unitExtraService,
|
|
334
|
+
entitySnapshotService,
|
|
335
|
+
unitOutputService,
|
|
336
|
+
logger,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
// act
|
|
340
|
+
await runtimeOperation.operateSafe()
|
|
341
|
+
|
|
342
|
+
// assert
|
|
343
|
+
const parentStateUpdates = instanceStateService.updateOperationState.mock.calls
|
|
344
|
+
.filter(([, stateId]) => stateId === parent.id)
|
|
345
|
+
.map(([, , , options]) => options.instanceState?.status)
|
|
346
|
+
.filter((status): status is NonNullable<typeof status> => status != null)
|
|
347
|
+
|
|
348
|
+
const grandParentStateUpdates = instanceStateService.updateOperationState.mock.calls
|
|
349
|
+
.filter(([, stateId]) => stateId === grandParent.id)
|
|
350
|
+
.map(([, , , options]) => options.instanceState?.status)
|
|
351
|
+
.filter((status): status is NonNullable<typeof status> => status != null)
|
|
352
|
+
|
|
353
|
+
expect(parentStateUpdates).toContain("deployed")
|
|
354
|
+
expect(grandParentStateUpdates).toContain("deployed")
|
|
355
|
+
expect(parentStateUpdates).not.toContain("undeployed")
|
|
356
|
+
expect(grandParentStateUpdates).not.toContain("undeployed")
|
|
357
|
+
},
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
operationTest(
|
|
361
|
+
"marks composite parents undeployed after ghost cleanup when no unit descendants remain",
|
|
362
|
+
async ({
|
|
363
|
+
project,
|
|
364
|
+
logger,
|
|
365
|
+
runnerBackend,
|
|
366
|
+
libraryBackend,
|
|
367
|
+
artifactService,
|
|
368
|
+
instanceLockService,
|
|
369
|
+
operationService,
|
|
370
|
+
secretService,
|
|
371
|
+
instanceStateService,
|
|
372
|
+
projectModelService,
|
|
373
|
+
unitExtraService,
|
|
374
|
+
entitySnapshotService,
|
|
375
|
+
unitOutputService,
|
|
376
|
+
createComposite,
|
|
377
|
+
createUnit,
|
|
378
|
+
createDeployedUnitState,
|
|
379
|
+
createOperation,
|
|
380
|
+
createContext,
|
|
381
|
+
setupPersistenceMocks,
|
|
382
|
+
setupImmediateLocking,
|
|
383
|
+
expect,
|
|
384
|
+
}) => {
|
|
385
|
+
// arrange
|
|
386
|
+
const grandParent = createComposite("GrandParent")
|
|
387
|
+
const parent = {
|
|
388
|
+
...createComposite("Parent"),
|
|
389
|
+
parentId: grandParent.id,
|
|
390
|
+
}
|
|
391
|
+
const ghostChild = {
|
|
392
|
+
...createUnit("GhostChild"),
|
|
393
|
+
parentId: parent.id,
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const grandParentState = createDeployedUnitState(grandParent)
|
|
397
|
+
const parentState = createDeployedUnitState(parent)
|
|
398
|
+
const ghostChildState = createDeployedUnitState(ghostChild)
|
|
399
|
+
|
|
400
|
+
await createContext({
|
|
401
|
+
instances: [grandParent, parent],
|
|
402
|
+
states: [grandParentState, parentState, ghostChildState],
|
|
403
|
+
})
|
|
404
|
+
setupImmediateLocking()
|
|
405
|
+
setupPersistenceMocks({ instances: [grandParent, parent, ghostChild] })
|
|
406
|
+
|
|
407
|
+
const operation = createOperation({
|
|
408
|
+
type: "update",
|
|
409
|
+
requestedInstanceIds: [parent.id],
|
|
410
|
+
phases: [
|
|
411
|
+
{
|
|
412
|
+
type: "update",
|
|
413
|
+
instances: [
|
|
414
|
+
{ id: grandParent.id, message: "ancestor", parentId: undefined },
|
|
415
|
+
{ id: parent.id, message: "requested", parentId: grandParent.id },
|
|
416
|
+
],
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
type: "destroy",
|
|
420
|
+
instances: [
|
|
421
|
+
{ id: ghostChild.id, message: "ghost cleanup", parentId: parent.id },
|
|
422
|
+
{ id: parent.id, message: "parent of included child", parentId: grandParent.id },
|
|
423
|
+
{ id: grandParent.id, message: "parent of included child", parentId: undefined },
|
|
424
|
+
],
|
|
425
|
+
},
|
|
426
|
+
],
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
const runtimeOperation = new RuntimeOperation(
|
|
430
|
+
project,
|
|
431
|
+
operation,
|
|
432
|
+
runnerBackend,
|
|
433
|
+
libraryBackend,
|
|
434
|
+
artifactService,
|
|
435
|
+
instanceLockService,
|
|
436
|
+
operationService,
|
|
437
|
+
secretService,
|
|
438
|
+
instanceStateService,
|
|
439
|
+
projectModelService,
|
|
440
|
+
unitExtraService,
|
|
441
|
+
entitySnapshotService,
|
|
442
|
+
unitOutputService,
|
|
443
|
+
logger,
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
// act
|
|
447
|
+
await runtimeOperation.operateSafe()
|
|
448
|
+
|
|
449
|
+
// assert
|
|
450
|
+
const parentStateUpdates = instanceStateService.updateOperationState.mock.calls
|
|
451
|
+
.filter(([, stateId]) => stateId === parent.id)
|
|
452
|
+
.map(([, , , options]) => options.instanceState?.status)
|
|
453
|
+
.filter((status): status is NonNullable<typeof status> => status != null)
|
|
454
|
+
|
|
455
|
+
const grandParentStateUpdates = instanceStateService.updateOperationState.mock.calls
|
|
456
|
+
.filter(([, stateId]) => stateId === grandParent.id)
|
|
457
|
+
.map(([, , , options]) => options.instanceState?.status)
|
|
458
|
+
.filter((status): status is NonNullable<typeof status> => status != null)
|
|
459
|
+
|
|
460
|
+
expect(parentStateUpdates).toContain("undeployed")
|
|
461
|
+
expect(grandParentStateUpdates).toContain("undeployed")
|
|
462
|
+
},
|
|
463
|
+
)
|
|
123
464
|
})
|
|
@@ -18,6 +18,8 @@ describe("Operation - Destroy", () => {
|
|
|
18
18
|
instanceStateService,
|
|
19
19
|
projectModelService,
|
|
20
20
|
unitExtraService,
|
|
21
|
+
entitySnapshotService,
|
|
22
|
+
unitOutputService,
|
|
21
23
|
createUnit,
|
|
22
24
|
createDeployedUnitState,
|
|
23
25
|
createOperation,
|
|
@@ -59,6 +61,8 @@ describe("Operation - Destroy", () => {
|
|
|
59
61
|
instanceStateService,
|
|
60
62
|
projectModelService,
|
|
61
63
|
unitExtraService,
|
|
64
|
+
entitySnapshotService,
|
|
65
|
+
unitOutputService,
|
|
62
66
|
logger,
|
|
63
67
|
)
|
|
64
68
|
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe } from "vitest"
|
|
2
|
+
import { RuntimeOperation } from "./operation"
|
|
3
|
+
import { operationTest } from "./operation.test-utils"
|
|
4
|
+
|
|
5
|
+
describe("Operation - Output Validation Failure", () => {
|
|
6
|
+
operationTest(
|
|
7
|
+
"writes instance log and fails instance when unit outputs are invalid",
|
|
8
|
+
async ({
|
|
9
|
+
project,
|
|
10
|
+
logger,
|
|
11
|
+
runnerBackend,
|
|
12
|
+
runner,
|
|
13
|
+
libraryBackend,
|
|
14
|
+
artifactService,
|
|
15
|
+
instanceLockService,
|
|
16
|
+
operationService,
|
|
17
|
+
secretService,
|
|
18
|
+
instanceStateService,
|
|
19
|
+
projectModelService,
|
|
20
|
+
unitExtraService,
|
|
21
|
+
entitySnapshotService,
|
|
22
|
+
unitOutputService,
|
|
23
|
+
createUnit,
|
|
24
|
+
createDeployedUnitState,
|
|
25
|
+
createOperation,
|
|
26
|
+
createContext,
|
|
27
|
+
setupPersistenceMocks,
|
|
28
|
+
setupImmediateLocking,
|
|
29
|
+
expect,
|
|
30
|
+
}) => {
|
|
31
|
+
const unit = createUnit("A")
|
|
32
|
+
const state = createDeployedUnitState(unit)
|
|
33
|
+
|
|
34
|
+
await createContext({ instances: [unit], states: [state] })
|
|
35
|
+
setupImmediateLocking()
|
|
36
|
+
setupPersistenceMocks({ instances: [unit] })
|
|
37
|
+
|
|
38
|
+
unitOutputService.parseUnitOutputs.mockResolvedValue({
|
|
39
|
+
outputHash: null,
|
|
40
|
+
statusFields: null,
|
|
41
|
+
terminals: null,
|
|
42
|
+
pages: null,
|
|
43
|
+
triggers: null,
|
|
44
|
+
secrets: null,
|
|
45
|
+
workers: null,
|
|
46
|
+
exportedArtifactIds: null,
|
|
47
|
+
hasResourceHooks: false,
|
|
48
|
+
entitySnapshotError: "invalid entity schema",
|
|
49
|
+
entitySnapshotPayload: null,
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
runner.setAutoCompletion(false)
|
|
53
|
+
runner.emitCompletion(unit.id, {
|
|
54
|
+
operationType: "update",
|
|
55
|
+
rawOutputs: {
|
|
56
|
+
value: { value: { some: "output" } },
|
|
57
|
+
},
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const operation = createOperation({
|
|
61
|
+
type: "update",
|
|
62
|
+
requestedInstanceIds: [unit.id],
|
|
63
|
+
phases: [
|
|
64
|
+
{
|
|
65
|
+
type: "update",
|
|
66
|
+
instances: [{ id: unit.id, message: "requested", parentId: undefined }],
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const runtimeOperation = new RuntimeOperation(
|
|
72
|
+
project,
|
|
73
|
+
operation,
|
|
74
|
+
runnerBackend,
|
|
75
|
+
libraryBackend,
|
|
76
|
+
artifactService,
|
|
77
|
+
instanceLockService,
|
|
78
|
+
operationService,
|
|
79
|
+
secretService,
|
|
80
|
+
instanceStateService,
|
|
81
|
+
projectModelService,
|
|
82
|
+
unitExtraService,
|
|
83
|
+
entitySnapshotService,
|
|
84
|
+
unitOutputService,
|
|
85
|
+
logger,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
await runtimeOperation.operateSafe()
|
|
89
|
+
|
|
90
|
+
expect(operationService.appendLog).toHaveBeenCalledWith(
|
|
91
|
+
project.id,
|
|
92
|
+
operation.id,
|
|
93
|
+
state.id,
|
|
94
|
+
expect.stringContaining("Failed to parse unit outputs"),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
expect(instanceStateService.updateOperationState).toHaveBeenCalledWith(
|
|
98
|
+
project.id,
|
|
99
|
+
state.id,
|
|
100
|
+
operation.id,
|
|
101
|
+
expect.any(Object),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
const wasMarkedFailed = instanceStateService.updateOperationState.mock.calls.some(call => {
|
|
105
|
+
const patch = call[3] as unknown
|
|
106
|
+
if (!patch || typeof patch !== "object") {
|
|
107
|
+
return false
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const maybePatch = patch as {
|
|
111
|
+
operationState?: { status?: unknown }
|
|
112
|
+
instanceState?: { status?: unknown }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
maybePatch.operationState?.status === "failed" &&
|
|
117
|
+
maybePatch.instanceState?.status === "failed"
|
|
118
|
+
)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
expect(wasMarkedFailed).toBe(true)
|
|
122
|
+
},
|
|
123
|
+
)
|
|
124
|
+
})
|
|
@@ -18,6 +18,8 @@ describe("Operation - Preview", () => {
|
|
|
18
18
|
instanceStateService,
|
|
19
19
|
projectModelService,
|
|
20
20
|
unitExtraService,
|
|
21
|
+
entitySnapshotService,
|
|
22
|
+
unitOutputService,
|
|
21
23
|
createUnit,
|
|
22
24
|
createDeployedUnitState,
|
|
23
25
|
createOperation,
|
|
@@ -59,6 +61,8 @@ describe("Operation - Preview", () => {
|
|
|
59
61
|
instanceStateService,
|
|
60
62
|
projectModelService,
|
|
61
63
|
unitExtraService,
|
|
64
|
+
entitySnapshotService,
|
|
65
|
+
unitOutputService,
|
|
62
66
|
logger,
|
|
63
67
|
)
|
|
64
68
|
|