@highstate/backend 0.19.1 → 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-V2NILDHS.js → chunk-52MY2TCE.js} +347 -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 +59 -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-V2NILDHS.js.map +0 -1
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { Logger } from "pino"
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
CapturedEntitySnapshotValue,
|
|
4
|
+
EntitySnapshotService,
|
|
5
|
+
InstanceStateService,
|
|
6
|
+
ProjectModelService,
|
|
7
|
+
} from "../business"
|
|
3
8
|
import type { LibraryBackend } from "../library"
|
|
4
9
|
import {
|
|
5
10
|
type ComponentModel,
|
|
@@ -7,6 +12,8 @@ import {
|
|
|
7
12
|
type InstanceInput,
|
|
8
13
|
type InstanceModel,
|
|
9
14
|
isUnitModel,
|
|
15
|
+
parseInstanceId,
|
|
16
|
+
type VersionedName,
|
|
10
17
|
} from "@highstate/contract"
|
|
11
18
|
import { BetterLock } from "better-lock"
|
|
12
19
|
import { mapValues, unique } from "remeda"
|
|
@@ -24,6 +31,8 @@ import {
|
|
|
24
31
|
type StableInstanceInput,
|
|
25
32
|
} from "../shared"
|
|
26
33
|
|
|
34
|
+
type RawPulumiOutputs = Record<string, { value: unknown; secret?: boolean }>
|
|
35
|
+
|
|
27
36
|
export class OperationContext {
|
|
28
37
|
private readonly instanceMap = new Map<InstanceId, InstanceModel>()
|
|
29
38
|
private readonly instanceChildrenMap = new Map<InstanceId, InstanceModel[]>()
|
|
@@ -48,6 +57,8 @@ export class OperationContext {
|
|
|
48
57
|
Record<InstanceId, ResolvedInstanceInput[]>
|
|
49
58
|
>()
|
|
50
59
|
|
|
60
|
+
private readonly capturedOutputValueMap = new Map<string, CapturedEntitySnapshotValue[]>()
|
|
61
|
+
|
|
51
62
|
private constructor(
|
|
52
63
|
public readonly project: ProjectOutput,
|
|
53
64
|
public readonly library: LibraryModel,
|
|
@@ -55,7 +66,7 @@ export class OperationContext {
|
|
|
55
66
|
) {}
|
|
56
67
|
|
|
57
68
|
public getInstance(instanceId: InstanceId): InstanceModel {
|
|
58
|
-
const instance = this.
|
|
69
|
+
const instance = this.tryGetInstance(instanceId)
|
|
59
70
|
if (!instance) {
|
|
60
71
|
throw new Error(`Instance with ID ${instanceId} not found in the operation context`)
|
|
61
72
|
}
|
|
@@ -63,6 +74,42 @@ export class OperationContext {
|
|
|
63
74
|
return instance
|
|
64
75
|
}
|
|
65
76
|
|
|
77
|
+
public tryGetInstance(instanceId: InstanceId): InstanceModel | undefined {
|
|
78
|
+
return this.instanceMap.get(instanceId)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
public tryGetInstanceForDestroy(instanceId: InstanceId): InstanceModel | undefined {
|
|
82
|
+
const modelInstance = this.tryGetInstance(instanceId)
|
|
83
|
+
if (modelInstance) {
|
|
84
|
+
return modelInstance
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const state = this.stateMap.get(instanceId)
|
|
88
|
+
if (!state) {
|
|
89
|
+
return undefined
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const stateModel = state.model ?? state.lastOperationState?.model
|
|
93
|
+
if (stateModel) {
|
|
94
|
+
return stateModel
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const [type, name] = parseInstanceId(instanceId)
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
id: instanceId,
|
|
101
|
+
name,
|
|
102
|
+
type,
|
|
103
|
+
kind: state.kind,
|
|
104
|
+
parentId: state.parentInstanceId ?? undefined,
|
|
105
|
+
inputs: {},
|
|
106
|
+
args: {},
|
|
107
|
+
outputs: {},
|
|
108
|
+
resolvedInputs: {},
|
|
109
|
+
resolvedOutputs: {},
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
66
113
|
public isGhostInstance(instanceId: InstanceId): boolean {
|
|
67
114
|
return this.ghostInstanceIds.has(instanceId)
|
|
68
115
|
}
|
|
@@ -85,6 +132,56 @@ export class OperationContext {
|
|
|
85
132
|
return this.resolvedInstanceInputs.get(instanceId)
|
|
86
133
|
}
|
|
87
134
|
|
|
135
|
+
public getCapturedOutputValues(
|
|
136
|
+
instanceId: InstanceId,
|
|
137
|
+
output: string,
|
|
138
|
+
): CapturedEntitySnapshotValue[] {
|
|
139
|
+
const key = `${instanceId}:${output}`
|
|
140
|
+
return this.capturedOutputValueMap.get(key) ?? []
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
public updateCapturedOutputValuesFromUnitOutputs(options: {
|
|
144
|
+
instanceId: InstanceId
|
|
145
|
+
instanceType: VersionedName
|
|
146
|
+
outputs: RawPulumiOutputs
|
|
147
|
+
}): void {
|
|
148
|
+
const component = this.library.components[options.instanceType]
|
|
149
|
+
if (!component) {
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
for (const [outputName, outputSpec] of Object.entries(component.outputs ?? {})) {
|
|
154
|
+
const raw = options.outputs[outputName]?.value
|
|
155
|
+
|
|
156
|
+
if (raw === undefined || raw === null) {
|
|
157
|
+
this.capturedOutputValueMap.set(`${options.instanceId}:${outputName}`, [])
|
|
158
|
+
continue
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const items = outputSpec.multiple ? raw : [raw]
|
|
162
|
+
if (outputSpec.multiple && !Array.isArray(raw)) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`Output "${outputName}" for instance "${options.instanceId}" must be an array`,
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const values = (items as unknown[]).map(item => {
|
|
169
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
`Output "${outputName}" for instance "${options.instanceId}" must contain objects`,
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
ok: true,
|
|
177
|
+
value: item as Record<string, unknown>,
|
|
178
|
+
} satisfies CapturedEntitySnapshotValue
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
this.capturedOutputValueMap.set(`${options.instanceId}:${outputName}`, values)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
88
185
|
public setState(state: InstanceState): void {
|
|
89
186
|
this.stateMap.set(state.instanceId, state)
|
|
90
187
|
this.stateIdMap.set(state.id, state.instanceId)
|
|
@@ -127,6 +224,7 @@ export class OperationContext {
|
|
|
127
224
|
return {
|
|
128
225
|
stateId: state.id,
|
|
129
226
|
output: input.output,
|
|
227
|
+
path: input.path,
|
|
130
228
|
}
|
|
131
229
|
}),
|
|
132
230
|
)
|
|
@@ -247,22 +345,49 @@ export class OperationContext {
|
|
|
247
345
|
}
|
|
248
346
|
}
|
|
249
347
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
348
|
+
private async captureEntitySnapshotsAtOperationStart(options: {
|
|
349
|
+
projectId: string
|
|
350
|
+
entitySnapshotService: EntitySnapshotService
|
|
351
|
+
}): Promise<void> {
|
|
352
|
+
const keys: {
|
|
353
|
+
stateId: string
|
|
354
|
+
output: string
|
|
355
|
+
instanceId: InstanceId
|
|
356
|
+
operationId?: string
|
|
357
|
+
}[] = []
|
|
358
|
+
|
|
359
|
+
for (const [instanceId, inputs] of this.resolvedInstanceInputs.entries()) {
|
|
360
|
+
for (const inputGroup of Object.values(inputs ?? {})) {
|
|
361
|
+
for (const input of inputGroup) {
|
|
362
|
+
if (input.input.instanceId === instanceId) {
|
|
363
|
+
continue
|
|
364
|
+
}
|
|
259
365
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
366
|
+
const dependencyState = this.getState(input.input.instanceId)
|
|
367
|
+
keys.push({
|
|
368
|
+
stateId: dependencyState.id,
|
|
369
|
+
output: input.input.output,
|
|
370
|
+
instanceId: input.input.instanceId,
|
|
371
|
+
operationId: dependencyState.lastOperationState?.operationId,
|
|
372
|
+
})
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (keys.length === 0) {
|
|
378
|
+
return
|
|
263
379
|
}
|
|
264
380
|
|
|
265
|
-
|
|
381
|
+
const captured = await options.entitySnapshotService.reconstructLatestExportedOutputValues(
|
|
382
|
+
options.projectId,
|
|
383
|
+
keys.map(k => ({ stateId: k.stateId, output: k.output, operationId: k.operationId })),
|
|
384
|
+
this.library,
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
for (const key of keys) {
|
|
388
|
+
const snapshotValues = captured.get(`${key.stateId}:${key.output}`) ?? []
|
|
389
|
+
this.capturedOutputValueMap.set(`${key.instanceId}:${key.output}`, snapshotValues)
|
|
390
|
+
}
|
|
266
391
|
}
|
|
267
392
|
|
|
268
393
|
getUnfinishedOperationStates(): InstanceState[] {
|
|
@@ -281,6 +406,7 @@ export class OperationContext {
|
|
|
281
406
|
libraryBackend: LibraryBackend,
|
|
282
407
|
instanceStateService: InstanceStateService,
|
|
283
408
|
projectModelService: ProjectModelService,
|
|
409
|
+
entitySnapshotService: EntitySnapshotService | undefined,
|
|
284
410
|
logger: Logger,
|
|
285
411
|
): Promise<OperationContext> {
|
|
286
412
|
const [{ instances, virtualInstances, hubs, ghostInstances }, project] =
|
|
@@ -348,11 +474,16 @@ export class OperationContext {
|
|
|
348
474
|
kind: "instance",
|
|
349
475
|
instance,
|
|
350
476
|
component: library.components[instance.type],
|
|
477
|
+
entities: library.entities,
|
|
351
478
|
})
|
|
352
479
|
}
|
|
353
480
|
|
|
354
481
|
for (const hub of hubs) {
|
|
355
|
-
context.inputResolverNodes.set(`hub:${hub.id}`, {
|
|
482
|
+
context.inputResolverNodes.set(`hub:${hub.id}`, {
|
|
483
|
+
kind: "hub",
|
|
484
|
+
hub,
|
|
485
|
+
entities: library.entities,
|
|
486
|
+
})
|
|
356
487
|
}
|
|
357
488
|
|
|
358
489
|
context.inputResolver = new InputResolver(context.inputResolverNodes, logger)
|
|
@@ -388,6 +519,13 @@ export class OperationContext {
|
|
|
388
519
|
|
|
389
520
|
await context.inputHashResolver.process()
|
|
390
521
|
|
|
522
|
+
if (entitySnapshotService) {
|
|
523
|
+
await context.captureEntitySnapshotsAtOperationStart({
|
|
524
|
+
projectId,
|
|
525
|
+
entitySnapshotService,
|
|
526
|
+
})
|
|
527
|
+
}
|
|
528
|
+
|
|
391
529
|
return context
|
|
392
530
|
}
|
|
393
531
|
}
|
|
@@ -77,9 +77,9 @@ graph RL
|
|
|
77
77
|
|
|
78
78
|
**Destroy Phase**: `C`, `B`
|
|
79
79
|
|
|
80
|
-
### Example 4:
|
|
80
|
+
### Example 4: Full Ancestor Chain for Compositional Inclusion
|
|
81
81
|
|
|
82
|
-
**Test**: `should
|
|
82
|
+
**Test**: `should include full ancestor chain for compositional inclusion`
|
|
83
83
|
|
|
84
84
|
```mermaid
|
|
85
85
|
graph RL
|
|
@@ -99,10 +99,10 @@ graph RL
|
|
|
99
99
|
1. `A` explicitly requested;
|
|
100
100
|
2. `B` depends on `A`, cascade enabled → `B` included (dependent of `A`);
|
|
101
101
|
3. `A` is child of `Parent` → `Parent` included (compositional);
|
|
102
|
-
4. `Parent` is child of `GrandParent` → `GrandParent`
|
|
103
|
-
5. `C` is sibling of `Parent
|
|
102
|
+
4. `Parent` is child of `GrandParent` → `GrandParent` included as ancestor;
|
|
103
|
+
5. `C` is sibling of `Parent`, but ancestor inclusion does not auto-include siblings → `C` NOT included.
|
|
104
104
|
|
|
105
|
-
**Destroy Phase**: `B`, `A`, `Parent`
|
|
105
|
+
**Destroy Phase**: `B`, `A`, `Parent`, `GrandParent`
|
|
106
106
|
|
|
107
107
|
### Example 5: Substantive Composite with Mixed Child States
|
|
108
108
|
|
|
@@ -150,9 +150,9 @@ graph RL
|
|
|
150
150
|
1. `Child1` explicitly requested;
|
|
151
151
|
2. No instances depend on `Child1` (Child1 depends on Child3, not vice versa);
|
|
152
152
|
3. `Child1` is child of `Parent1` → `Parent1` included (compositional);
|
|
153
|
-
4. `Parent1` is child of `GrandParent` → `GrandParent`
|
|
153
|
+
4. `Parent1` is child of `GrandParent` → `GrandParent` included as ancestor.
|
|
154
154
|
|
|
155
|
-
**Destroy Phase**: `Child1`, `Parent1`
|
|
155
|
+
**Destroy Phase**: `Child1`, `Parent1`, `GrandParent`
|
|
156
156
|
|
|
157
157
|
### Example 7: Request Child with Isolated Destroy
|
|
158
158
|
|
|
@@ -250,9 +250,9 @@ graph RL
|
|
|
250
250
|
|
|
251
251
|
**Destroy Phase**: `C`, `B`, `A`
|
|
252
252
|
|
|
253
|
-
### Example 11: Deep Nesting with
|
|
253
|
+
### Example 11: Deep Nesting with Ancestor Chain Inclusion
|
|
254
254
|
|
|
255
|
-
**Test**: `should
|
|
255
|
+
**Test**: `should include deep ancestor chain without ancestor siblings`
|
|
256
256
|
|
|
257
257
|
```mermaid
|
|
258
258
|
graph RL
|
|
@@ -272,10 +272,11 @@ graph RL
|
|
|
272
272
|
1. `Child` explicitly requested;
|
|
273
273
|
2. No instances depend on `Child`;
|
|
274
274
|
3. `Child` is child of `Parent` → `Parent` included (compositional);
|
|
275
|
-
4. `Parent` is child of `GrandParent` → `GrandParent`
|
|
276
|
-
5. `
|
|
275
|
+
4. `Parent` is child of `GrandParent` → `GrandParent` included as ancestor;
|
|
276
|
+
5. `GrandParent` is child of `GreatGrandParent` → `GreatGrandParent` included as ancestor;
|
|
277
|
+
6. `Uncle` and `GreatUncle` are ancestor siblings and are not auto-included.
|
|
277
278
|
|
|
278
|
-
**Destroy Phase**: `Child`, `Parent`
|
|
279
|
+
**Destroy Phase**: `Child`, `Parent`, `GrandParent`, `GreatGrandParent`
|
|
279
280
|
|
|
280
281
|
### Example 12: Diamond Dependency Pattern
|
|
281
282
|
|
|
@@ -355,3 +356,23 @@ graph RL
|
|
|
355
356
|
4. `Child2` is sibling of `Child1` in substantive composite, partial destruction enabled → `Child2` excluded.
|
|
356
357
|
|
|
357
358
|
**Destroy Phase**: `Child1`, `Parent`, `ExternalX`
|
|
359
|
+
|
|
360
|
+
### Example 15: State-Only Dependent Missing from Model
|
|
361
|
+
|
|
362
|
+
**Test**: `should include state-only dependents when model instance is missing`
|
|
363
|
+
|
|
364
|
+
```mermaid
|
|
365
|
+
graph RL
|
|
366
|
+
Requested["requested 🚀"]
|
|
367
|
+
Dependent["dependent ✅ (state-only)"]
|
|
368
|
+
|
|
369
|
+
Dependent --> Requested
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
**Decision Steps**:
|
|
373
|
+
|
|
374
|
+
1. `requested` explicitly requested;
|
|
375
|
+
2. `dependent` is not present in current project model, but exists in state with a dependency on `requested`;
|
|
376
|
+
3. destroy cascade uses state relationships, so `dependent` is included as dependent of destroyed `requested`.
|
|
377
|
+
|
|
378
|
+
**Destroy Phase**: `dependent`, `requested`
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { InstanceState } from "../shared"
|
|
2
|
+
import { getInstanceId } from "@highstate/contract"
|
|
1
3
|
import { describe } from "vitest"
|
|
2
4
|
import { createOperationPlan } from "./operation-plan"
|
|
3
5
|
import { operationPlanTest } from "./operation-plan.fixtures"
|
|
@@ -140,7 +142,7 @@ describe("OperationPlan - Destroy Operations", () => {
|
|
|
140
142
|
)
|
|
141
143
|
|
|
142
144
|
operationPlanTest(
|
|
143
|
-
"4. should
|
|
145
|
+
"4. should include full ancestor chain for compositional inclusion",
|
|
144
146
|
async ({ testBuilder, expect }) => {
|
|
145
147
|
// arrange
|
|
146
148
|
const { context, operation } = await testBuilder()
|
|
@@ -190,6 +192,11 @@ describe("OperationPlan - Destroy Operations", () => {
|
|
|
190
192
|
"message": "parent of included child "component.v1:A"",
|
|
191
193
|
"parentId": "composite.v1:GrandParent",
|
|
192
194
|
},
|
|
195
|
+
{
|
|
196
|
+
"id": "composite.v1:GrandParent",
|
|
197
|
+
"message": "parent of included child "composite.v1:Parent"",
|
|
198
|
+
"parentId": undefined,
|
|
199
|
+
},
|
|
193
200
|
],
|
|
194
201
|
"type": "destroy",
|
|
195
202
|
},
|
|
@@ -307,6 +314,11 @@ describe("OperationPlan - Destroy Operations", () => {
|
|
|
307
314
|
"message": "parent of included child "component.v1:Child1"",
|
|
308
315
|
"parentId": "composite.v1:GrandParent",
|
|
309
316
|
},
|
|
317
|
+
{
|
|
318
|
+
"id": "composite.v1:GrandParent",
|
|
319
|
+
"message": "parent of included child "composite.v1:Parent1"",
|
|
320
|
+
"parentId": undefined,
|
|
321
|
+
},
|
|
310
322
|
],
|
|
311
323
|
"type": "destroy",
|
|
312
324
|
},
|
|
@@ -542,7 +554,7 @@ describe("OperationPlan - Destroy Operations", () => {
|
|
|
542
554
|
)
|
|
543
555
|
|
|
544
556
|
operationPlanTest(
|
|
545
|
-
"11. should
|
|
557
|
+
"11. should include deep ancestor chain without ancestor siblings",
|
|
546
558
|
async ({ testBuilder, expect }) => {
|
|
547
559
|
// arrange
|
|
548
560
|
const { context, operation } = await testBuilder()
|
|
@@ -589,6 +601,16 @@ describe("OperationPlan - Destroy Operations", () => {
|
|
|
589
601
|
"message": "parent of included child "component.v1:Child"",
|
|
590
602
|
"parentId": "composite.v1:GrandParent",
|
|
591
603
|
},
|
|
604
|
+
{
|
|
605
|
+
"id": "composite.v1:GrandParent",
|
|
606
|
+
"message": "parent of included child "composite.v1:Parent"",
|
|
607
|
+
"parentId": "composite.v1:GreatGrandParent",
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
"id": "composite.v1:GreatGrandParent",
|
|
611
|
+
"message": "parent of included child "composite.v1:GrandParent"",
|
|
612
|
+
"parentId": undefined,
|
|
613
|
+
},
|
|
592
614
|
],
|
|
593
615
|
"type": "destroy",
|
|
594
616
|
},
|
|
@@ -772,4 +794,120 @@ describe("OperationPlan - Destroy Operations", () => {
|
|
|
772
794
|
`)
|
|
773
795
|
},
|
|
774
796
|
)
|
|
797
|
+
|
|
798
|
+
operationPlanTest(
|
|
799
|
+
"15. should include state-only dependents when model instance is missing",
|
|
800
|
+
async ({ createContext, createTestOperation, expect }) => {
|
|
801
|
+
// arrange
|
|
802
|
+
const requestedId = getInstanceId("component.v1", "requested")
|
|
803
|
+
const dependentId = getInstanceId("component.v1", "dependent")
|
|
804
|
+
|
|
805
|
+
const context = await createContext(
|
|
806
|
+
[
|
|
807
|
+
{
|
|
808
|
+
id: requestedId,
|
|
809
|
+
name: "requested",
|
|
810
|
+
type: "component.v1",
|
|
811
|
+
kind: "unit",
|
|
812
|
+
parentId: undefined,
|
|
813
|
+
inputs: {},
|
|
814
|
+
args: {},
|
|
815
|
+
outputs: {},
|
|
816
|
+
resolvedInputs: {},
|
|
817
|
+
resolvedOutputs: {},
|
|
818
|
+
},
|
|
819
|
+
],
|
|
820
|
+
[
|
|
821
|
+
{
|
|
822
|
+
id: requestedId,
|
|
823
|
+
instanceId: requestedId,
|
|
824
|
+
status: "deployed",
|
|
825
|
+
source: "resident",
|
|
826
|
+
kind: "unit",
|
|
827
|
+
hasResourceHooks: false,
|
|
828
|
+
parentId: null,
|
|
829
|
+
parentInstanceId: null,
|
|
830
|
+
selfHash: null,
|
|
831
|
+
inputHash: null,
|
|
832
|
+
outputHash: null,
|
|
833
|
+
dependencyOutputHash: null,
|
|
834
|
+
statusFields: null,
|
|
835
|
+
exportedArtifactIds: null,
|
|
836
|
+
inputHashNonce: null,
|
|
837
|
+
currentResourceCount: null,
|
|
838
|
+
model: null,
|
|
839
|
+
resolvedInputs: null,
|
|
840
|
+
lastOperationState: undefined,
|
|
841
|
+
evaluationState: {} as InstanceState["evaluationState"],
|
|
842
|
+
},
|
|
843
|
+
{
|
|
844
|
+
id: dependentId,
|
|
845
|
+
instanceId: dependentId,
|
|
846
|
+
status: "deployed",
|
|
847
|
+
source: "resident",
|
|
848
|
+
kind: "unit",
|
|
849
|
+
hasResourceHooks: false,
|
|
850
|
+
parentId: null,
|
|
851
|
+
parentInstanceId: null,
|
|
852
|
+
selfHash: null,
|
|
853
|
+
inputHash: null,
|
|
854
|
+
outputHash: null,
|
|
855
|
+
dependencyOutputHash: null,
|
|
856
|
+
statusFields: null,
|
|
857
|
+
exportedArtifactIds: null,
|
|
858
|
+
inputHashNonce: null,
|
|
859
|
+
currentResourceCount: null,
|
|
860
|
+
model: {
|
|
861
|
+
id: dependentId,
|
|
862
|
+
name: "dependent",
|
|
863
|
+
type: "component.v1",
|
|
864
|
+
kind: "unit",
|
|
865
|
+
parentId: undefined,
|
|
866
|
+
inputs: {},
|
|
867
|
+
args: {},
|
|
868
|
+
outputs: {},
|
|
869
|
+
resolvedInputs: {},
|
|
870
|
+
resolvedOutputs: {},
|
|
871
|
+
},
|
|
872
|
+
resolvedInputs: {
|
|
873
|
+
dependency: [{ stateId: requestedId, output: "default" }],
|
|
874
|
+
},
|
|
875
|
+
lastOperationState: undefined,
|
|
876
|
+
evaluationState: {} as InstanceState["evaluationState"],
|
|
877
|
+
},
|
|
878
|
+
],
|
|
879
|
+
)
|
|
880
|
+
|
|
881
|
+
const operation = createTestOperation("destroy", [requestedId])
|
|
882
|
+
|
|
883
|
+
// act
|
|
884
|
+
const plan = createOperationPlan(
|
|
885
|
+
context,
|
|
886
|
+
operation.type,
|
|
887
|
+
operation.requestedInstanceIds,
|
|
888
|
+
operation.options,
|
|
889
|
+
)
|
|
890
|
+
|
|
891
|
+
// assert
|
|
892
|
+
expect(plan).toMatchInlineSnapshot(`
|
|
893
|
+
[
|
|
894
|
+
{
|
|
895
|
+
"instances": [
|
|
896
|
+
{
|
|
897
|
+
"id": "component.v1:dependent",
|
|
898
|
+
"message": "dependent of destroyed "component.v1:requested"",
|
|
899
|
+
"parentId": undefined,
|
|
900
|
+
},
|
|
901
|
+
{
|
|
902
|
+
"id": "component.v1:requested",
|
|
903
|
+
"message": "explicitly requested",
|
|
904
|
+
"parentId": undefined,
|
|
905
|
+
},
|
|
906
|
+
],
|
|
907
|
+
"type": "destroy",
|
|
908
|
+
},
|
|
909
|
+
]
|
|
910
|
+
`)
|
|
911
|
+
},
|
|
912
|
+
)
|
|
775
913
|
})
|
|
@@ -85,6 +85,7 @@ export const operationPlanTest = test.extend<{
|
|
|
85
85
|
options: {
|
|
86
86
|
forceUpdateDependencies: false,
|
|
87
87
|
ignoreDependencies: false,
|
|
88
|
+
ignoreChangedDependencies: false,
|
|
88
89
|
forceUpdateChildren: false,
|
|
89
90
|
destroyDependentInstances: true,
|
|
90
91
|
invokeDestroyTriggers: true,
|
|
@@ -197,6 +198,7 @@ export const operationPlanTest = test.extend<{
|
|
|
197
198
|
libraryBackend,
|
|
198
199
|
instanceStateService,
|
|
199
200
|
projectModelService,
|
|
201
|
+
undefined,
|
|
200
202
|
logger,
|
|
201
203
|
)
|
|
202
204
|
}
|
|
@@ -41,10 +41,12 @@ Different operation types are assembled from phases as follows:
|
|
|
41
41
|
- Then an `update phase` containing the same instances to be recreated
|
|
42
42
|
|
|
43
43
|
**Preview Operation:**
|
|
44
|
+
|
|
44
45
|
- Single `update phase` calculated using update phase rules but not executed
|
|
45
46
|
- **Restriction**: Only allowed for "edge" instances (instances that depend on others but no instances depend on them)
|
|
46
47
|
|
|
47
48
|
**Refresh Operation:**
|
|
49
|
+
|
|
48
50
|
- Single `refresh phase` calculated using update phase rules for state refresh
|
|
49
51
|
- **Key difference**: No destroy phase is created, even if ghost cleanup would normally occur
|
|
50
52
|
|
|
@@ -108,7 +110,8 @@ Since composite instances cannot be directly depended upon, message conflicts ar
|
|
|
108
110
|
### Propagation Rules
|
|
109
111
|
|
|
110
112
|
- **Substantive composite inclusions** trigger further dependency resolution and parent propagation
|
|
111
|
-
- **Compositional composite inclusions**
|
|
113
|
+
- **Compositional composite inclusions** still propagate upward through the full parent chain
|
|
114
|
+
- **Ancestor siblings are not auto-included** unless they are independently required by other rules
|
|
112
115
|
|
|
113
116
|
### Ghost Cleanup Rules
|
|
114
117
|
|