@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.
Files changed (120) hide show
  1. package/dist/{chunk-JT4KWE3B.js → chunk-52MY2TCE.js} +348 -19
  2. package/dist/chunk-52MY2TCE.js.map +1 -0
  3. package/dist/{chunk-I7BWSAN6.js → chunk-UAWBPTDW.js} +3 -3
  4. package/dist/{chunk-I7BWSAN6.js.map → chunk-UAWBPTDW.js.map} +1 -1
  5. package/dist/highstate.manifest.json +4 -4
  6. package/dist/index.js +4159 -785
  7. package/dist/index.js.map +1 -1
  8. package/dist/library/worker/main.js +5 -2
  9. package/dist/library/worker/main.js.map +1 -1
  10. package/dist/shared/index.js +2 -2
  11. package/package.json +7 -7
  12. package/prisma/backend/_schema/object.prisma +12 -0
  13. package/prisma/backend/sqlite/migrations/20260222113554_add_object_tracking/migration.sql +7 -0
  14. package/prisma/project/artifact.prisma +3 -0
  15. package/prisma/project/entity.prisma +125 -0
  16. package/prisma/project/instance.prisma +6 -0
  17. package/prisma/project/migrations/20260301210131_add_entity_tracking/migration.sql +70 -0
  18. package/prisma/project/migrations/20260302212734_add_resource_hooks_flag/migration.sql +1 -0
  19. package/prisma/project/operation.prisma +3 -0
  20. package/src/business/artifact.test.ts +22 -2
  21. package/src/business/artifact.ts +7 -1
  22. package/src/business/entity-snapshot.test.ts +684 -0
  23. package/src/business/entity-snapshot.ts +904 -0
  24. package/src/business/evaluation.test.ts +56 -0
  25. package/src/business/evaluation.ts +102 -22
  26. package/src/business/global-search.test.ts +344 -0
  27. package/src/business/global-search.ts +902 -0
  28. package/src/business/index.ts +4 -0
  29. package/src/business/instance-lock.ts +58 -74
  30. package/src/business/instance-state.test.ts +15 -1
  31. package/src/business/instance-state.ts +37 -14
  32. package/src/business/object-ref-index.test.ts +140 -0
  33. package/src/business/object-ref-index.ts +193 -0
  34. package/src/business/operation.test.ts +15 -1
  35. package/src/business/operation.ts +4 -0
  36. package/src/business/project-model.ts +154 -13
  37. package/src/business/project-unlock.ts +25 -2
  38. package/src/business/project.ts +9 -0
  39. package/src/business/secret.test.ts +35 -2
  40. package/src/business/secret.ts +32 -9
  41. package/src/business/settings.ts +761 -0
  42. package/src/business/unit-output.test.ts +477 -0
  43. package/src/business/unit-output.ts +461 -0
  44. package/src/business/worker.ts +55 -4
  45. package/src/database/_generated/backend/postgresql/browser.ts +6 -0
  46. package/src/database/_generated/backend/postgresql/client.ts +6 -0
  47. package/src/database/_generated/backend/postgresql/internal/class.ts +23 -5
  48. package/src/database/_generated/backend/postgresql/internal/prismaNamespace.ts +89 -5
  49. package/src/database/_generated/backend/postgresql/internal/prismaNamespaceBrowser.ts +9 -0
  50. package/src/database/_generated/backend/postgresql/models/Object.ts +1076 -0
  51. package/src/database/_generated/backend/postgresql/models.ts +1 -0
  52. package/src/database/_generated/backend/sqlite/browser.ts +6 -0
  53. package/src/database/_generated/backend/sqlite/client.ts +6 -0
  54. package/src/database/_generated/backend/sqlite/internal/class.ts +23 -5
  55. package/src/database/_generated/backend/sqlite/internal/prismaNamespace.ts +89 -5
  56. package/src/database/_generated/backend/sqlite/internal/prismaNamespaceBrowser.ts +9 -0
  57. package/src/database/_generated/backend/sqlite/models/Object.ts +1074 -0
  58. package/src/database/_generated/backend/sqlite/models.ts +1 -0
  59. package/src/database/_generated/project/browser.ts +23 -0
  60. package/src/database/_generated/project/client.ts +23 -0
  61. package/src/database/_generated/project/commonInputTypes.ts +87 -53
  62. package/src/database/_generated/project/enums.ts +8 -0
  63. package/src/database/_generated/project/internal/class.ts +53 -5
  64. package/src/database/_generated/project/internal/prismaNamespace.ts +367 -13
  65. package/src/database/_generated/project/internal/prismaNamespaceBrowser.ts +48 -1
  66. package/src/database/_generated/project/models/Artifact.ts +199 -11
  67. package/src/database/_generated/project/models/Entity.ts +1274 -0
  68. package/src/database/_generated/project/models/EntitySnapshot.ts +2389 -0
  69. package/src/database/_generated/project/models/EntitySnapshotContent.ts +1260 -0
  70. package/src/database/_generated/project/models/EntitySnapshotReference.ts +1449 -0
  71. package/src/database/_generated/project/models/InstanceState.ts +361 -1
  72. package/src/database/_generated/project/models/Operation.ts +148 -3
  73. package/src/database/_generated/project/models/OperationLog.ts +0 -4
  74. package/src/database/_generated/project/models.ts +4 -0
  75. package/src/database/migration.ts +3 -0
  76. package/src/library/worker/evaluator.ts +7 -1
  77. package/src/orchestrator/manager.ts +7 -0
  78. package/src/orchestrator/operation-context.captured-outputs.test.ts +118 -0
  79. package/src/orchestrator/operation-context.ts +154 -16
  80. package/src/orchestrator/operation-plan.destroy.test.md +33 -12
  81. package/src/orchestrator/operation-plan.destroy.test.ts +140 -2
  82. package/src/orchestrator/operation-plan.fixtures.ts +2 -0
  83. package/src/orchestrator/operation-plan.md +4 -1
  84. package/src/orchestrator/operation-plan.ts +286 -92
  85. package/src/orchestrator/operation-plan.update.test.md +286 -11
  86. package/src/orchestrator/operation-plan.update.test.ts +656 -5
  87. package/src/orchestrator/operation-workset.ts +72 -22
  88. package/src/orchestrator/operation.cancel.test.ts +4 -0
  89. package/src/orchestrator/operation.composite.test.ts +341 -0
  90. package/src/orchestrator/operation.destroy.test.ts +4 -0
  91. package/src/orchestrator/operation.output-validation.failure.test.ts +124 -0
  92. package/src/orchestrator/operation.preview.test.ts +4 -0
  93. package/src/orchestrator/operation.refresh.test.ts +4 -0
  94. package/src/orchestrator/operation.test-utils.ts +52 -13
  95. package/src/orchestrator/operation.ts +228 -68
  96. package/src/orchestrator/operation.update.failure.test.ts +4 -0
  97. package/src/orchestrator/operation.update.skip.test.ts +110 -0
  98. package/src/orchestrator/operation.update.test.ts +4 -0
  99. package/src/orchestrator/plan-test-builder.ts +1 -0
  100. package/src/orchestrator/unit-input-values.test.ts +450 -0
  101. package/src/orchestrator/unit-input-values.ts +281 -0
  102. package/src/pubsub/manager.ts +3 -0
  103. package/src/runner/abstractions.ts +23 -54
  104. package/src/runner/local.ts +109 -85
  105. package/src/services.ts +52 -1
  106. package/src/shared/models/prisma.ts +1 -0
  107. package/src/shared/models/project/entity.ts +121 -0
  108. package/src/shared/models/project/index.ts +1 -0
  109. package/src/shared/models/project/operation.ts +61 -3
  110. package/src/shared/models/project/state.ts +10 -0
  111. package/src/shared/models/project/worker.ts +7 -0
  112. package/src/shared/resolvers/effective-output-type.test.ts +494 -0
  113. package/src/shared/resolvers/effective-output-type.ts +162 -0
  114. package/src/shared/resolvers/index.ts +1 -0
  115. package/src/shared/resolvers/input.ts +61 -9
  116. package/src/shared/utils/index.ts +1 -0
  117. package/src/shared/utils/stable-json.ts +41 -0
  118. package/src/terminal/manager.ts +6 -0
  119. package/src/worker/manager.ts +97 -1
  120. package/dist/chunk-JT4KWE3B.js.map +0 -1
@@ -17,6 +17,8 @@ describe("RuntimeOperation - Update Short-Circuit", () => {
17
17
  instanceStateService,
18
18
  projectModelService,
19
19
  unitExtraService,
20
+ entitySnapshotService,
21
+ unitOutputService,
20
22
  createUnit,
21
23
  createDeployedUnitState,
22
24
  createContext,
@@ -62,6 +64,8 @@ describe("RuntimeOperation - Update Short-Circuit", () => {
62
64
  instanceStateService,
63
65
  projectModelService,
64
66
  unitExtraService,
67
+ entitySnapshotService,
68
+ unitOutputService,
65
69
  logger,
66
70
  )
67
71
 
@@ -83,6 +87,7 @@ describe("RuntimeOperation - Update Short-Circuit", () => {
83
87
  const skipOptions = skipCall?.[3]
84
88
  expect(skipOptions?.instanceState).toEqual({
85
89
  inputHash: expected.inputHash,
90
+ parentId: null,
86
91
  })
87
92
 
88
93
  expect(operationService.markOperationFinished).toHaveBeenCalledWith(
@@ -92,4 +97,109 @@ describe("RuntimeOperation - Update Short-Circuit", () => {
92
97
  )
93
98
  },
94
99
  )
100
+
101
+ operationTest(
102
+ "updates parentId during short-circuit when unit moved between composites",
103
+ async ({
104
+ project,
105
+ logger,
106
+ runnerBackend,
107
+ libraryBackend,
108
+ artifactService,
109
+ instanceLockService,
110
+ operationService,
111
+ secretService,
112
+ instanceStateService,
113
+ projectModelService,
114
+ unitExtraService,
115
+ entitySnapshotService,
116
+ unitOutputService,
117
+ createComposite,
118
+ createUnit,
119
+ createDeployedUnitState,
120
+ createContext,
121
+ createOperation,
122
+ setupImmediateLocking,
123
+ setupPersistenceMocks,
124
+ expect,
125
+ }) => {
126
+ // arrange
127
+ const oldParent = createComposite("OldParent")
128
+ const newParent = createComposite("NewParent")
129
+ const unit = {
130
+ ...createUnit("A"),
131
+ parentId: newParent.id,
132
+ }
133
+
134
+ const oldParentState = createDeployedUnitState(oldParent)
135
+ const newParentState = createDeployedUnitState(newParent)
136
+ const state = createDeployedUnitState(unit)
137
+ state.parentInstanceId = oldParent.id
138
+
139
+ const context = await createContext({
140
+ instances: [oldParent, newParent, unit],
141
+ states: [oldParentState, newParentState, state],
142
+ })
143
+
144
+ const expected = await context.getUpToDateInputHashOutput(unit)
145
+ state.selfHash = expected.selfHash
146
+ state.dependencyOutputHash = expected.dependencyOutputHash
147
+ instanceStateService.getInstanceStates.mockResolvedValue([
148
+ oldParentState,
149
+ newParentState,
150
+ state,
151
+ ])
152
+
153
+ setupImmediateLocking()
154
+ setupPersistenceMocks({ instances: [oldParent, newParent, unit] })
155
+
156
+ const operation = createOperation({
157
+ type: "update",
158
+ requestedInstanceIds: [],
159
+ phases: [
160
+ {
161
+ type: "update",
162
+ instances: [
163
+ { id: newParent.id, message: "parent", parentId: undefined },
164
+ { id: unit.id, message: "explicitly requested", parentId: newParent.id },
165
+ ],
166
+ },
167
+ ],
168
+ })
169
+
170
+ const runtimeOperation = new RuntimeOperation(
171
+ project,
172
+ operation,
173
+ runnerBackend,
174
+ libraryBackend,
175
+ artifactService,
176
+ instanceLockService,
177
+ operationService,
178
+ secretService,
179
+ instanceStateService,
180
+ projectModelService,
181
+ unitExtraService,
182
+ entitySnapshotService,
183
+ unitOutputService,
184
+ logger,
185
+ )
186
+
187
+ // act
188
+ await runtimeOperation.operateSafe()
189
+
190
+ // assert
191
+ expect(runnerBackend.update).not.toHaveBeenCalled()
192
+
193
+ const skipCall = instanceStateService.updateOperationState.mock.calls.find(
194
+ ([, stateId, , options]) =>
195
+ stateId === unit.id && options.operationState?.status === "skipped",
196
+ )
197
+
198
+ expect(skipCall).toBeDefined()
199
+ expect(skipCall?.[3].instanceState).toEqual({
200
+ inputHash: expected.inputHash,
201
+ parentId: newParent.id,
202
+ })
203
+ },
204
+ )
95
205
  })
@@ -19,6 +19,8 @@ describe("Operation - Update", () => {
19
19
  instanceStateService,
20
20
  projectModelService,
21
21
  unitExtraService,
22
+ entitySnapshotService,
23
+ unitOutputService,
22
24
  createUnit,
23
25
  createDeployedUnitState,
24
26
  createOperation,
@@ -90,6 +92,8 @@ describe("Operation - Update", () => {
90
92
  instanceStateService,
91
93
  projectModelService,
92
94
  unitExtraService,
95
+ entitySnapshotService,
96
+ unitOutputService,
93
97
  logger,
94
98
  )
95
99
 
@@ -101,6 +101,7 @@ export class PlanTestBuilder {
101
101
  status: "undeployed",
102
102
  source: stateType === "ghost" ? "virtual" : "resident",
103
103
  kind: instance.kind,
104
+ hasResourceHooks: false,
104
105
  parentId: null,
105
106
  parentInstanceId: instance.parentId ?? null,
106
107
  selfHash: null,
@@ -0,0 +1,450 @@
1
+ import type { VersionedName } from "@highstate/contract"
2
+ import type { ResolvedInstanceInput } from "../shared"
3
+ import { describe, expect, test } from "vitest"
4
+ import { resolveUnitInputValues } from "./unit-input-values"
5
+
6
+ describe("resolveUnitInputValues", () => {
7
+ test("returns captured values when input type matches output type", () => {
8
+ const dependencyInstanceId = "component.v1:dep"
9
+ const dependencyInstanceType = "component.v1" satisfies VersionedName
10
+
11
+ const library = {
12
+ components: {
13
+ "component.v1": {
14
+ outputs: {
15
+ out: { type: "parent.v1" },
16
+ },
17
+ },
18
+ },
19
+ entities: {
20
+ "parent.v1": {
21
+ type: "parent.v1",
22
+ },
23
+ },
24
+ }
25
+
26
+ const resolved: ResolvedInstanceInput = {
27
+ input: {
28
+ instanceId: dependencyInstanceId,
29
+ output: "out",
30
+ } as never,
31
+ type: "parent.v1",
32
+ }
33
+
34
+ const values = resolveUnitInputValues({
35
+ library: library as never,
36
+ inputName: "dep",
37
+ resolvedInput: resolved,
38
+ dependencyInstanceType,
39
+ captured: [{ ok: true, value: { a: 1 } }],
40
+ })
41
+
42
+ expect(values).toEqual([
43
+ {
44
+ value: { a: 1 },
45
+ source: { instanceId: dependencyInstanceId, output: "out" },
46
+ },
47
+ ])
48
+ })
49
+
50
+ test("extracts inclusion field when input type differs", () => {
51
+ const dependencyInstanceId = "component.v1:dep"
52
+ const dependencyInstanceType = "component.v1" satisfies VersionedName
53
+
54
+ const library = {
55
+ components: {
56
+ "component.v1": {
57
+ outputs: {
58
+ out: { type: "parent.v1" },
59
+ },
60
+ },
61
+ },
62
+ entities: {
63
+ "parent.v1": {
64
+ type: "parent.v1",
65
+ inclusions: [{ type: "child.v1", field: "child" }],
66
+ },
67
+ },
68
+ }
69
+
70
+ const resolved: ResolvedInstanceInput = {
71
+ input: {
72
+ instanceId: dependencyInstanceId,
73
+ output: "out",
74
+ } as never,
75
+ type: "child.v1",
76
+ }
77
+
78
+ const values = resolveUnitInputValues({
79
+ library: library as never,
80
+ inputName: "dep",
81
+ resolvedInput: resolved,
82
+ dependencyInstanceType,
83
+ captured: [{ ok: true, value: { child: { v: 42 }, other: "x" } }],
84
+ })
85
+
86
+ expect(values).toEqual([
87
+ {
88
+ value: { v: 42 },
89
+ source: { instanceId: dependencyInstanceId, output: "out" },
90
+ },
91
+ ])
92
+ })
93
+
94
+ test("uses included entity from reconstructed parent value", () => {
95
+ const dependencyInstanceId = "component.v1:dep"
96
+ const dependencyInstanceType = "component.v1" satisfies VersionedName
97
+
98
+ const library = {
99
+ components: {
100
+ "component.v1": {
101
+ outputs: {
102
+ out: { type: "parent.v1" },
103
+ },
104
+ },
105
+ },
106
+ entities: {
107
+ "parent.v1": {
108
+ type: "parent.v1",
109
+ inclusions: [{ type: "child.v1", field: "child" }],
110
+ },
111
+ "child.v1": {
112
+ type: "child.v1",
113
+ },
114
+ },
115
+ }
116
+
117
+ const resolved: ResolvedInstanceInput = {
118
+ input: {
119
+ instanceId: dependencyInstanceId,
120
+ output: "out",
121
+ } as never,
122
+ type: "child.v1",
123
+ }
124
+
125
+ const captured = [
126
+ {
127
+ ok: true as const,
128
+ value: {
129
+ $meta: { type: "parent.v1", identity: "p1" },
130
+ child: { $meta: { type: "child.v1", identity: "c1" }, v: 41 },
131
+ },
132
+ },
133
+ {
134
+ ok: true as const,
135
+ value: {
136
+ $meta: { type: "child.v1", identity: "c1", title: "Child" },
137
+ v: 42,
138
+ },
139
+ },
140
+ ]
141
+
142
+ const values = resolveUnitInputValues({
143
+ library: library as never,
144
+ inputName: "dep",
145
+ resolvedInput: resolved,
146
+ dependencyInstanceType,
147
+ captured,
148
+ })
149
+
150
+ expect(values).toEqual([
151
+ {
152
+ value: { $meta: { type: "child.v1", identity: "c1" }, v: 41 },
153
+ source: { instanceId: dependencyInstanceId, output: "out" },
154
+ },
155
+ ])
156
+ })
157
+
158
+ test("throws when no matching inclusion exists", () => {
159
+ const dependencyInstanceId = "component.v1:dep"
160
+ const dependencyInstanceType = "component.v1" satisfies VersionedName
161
+
162
+ const library = {
163
+ components: {
164
+ "component.v1": {
165
+ outputs: {
166
+ out: { type: "parent.v1" },
167
+ },
168
+ },
169
+ },
170
+ entities: {
171
+ "parent.v1": {
172
+ type: "parent.v1",
173
+ inclusions: [{ type: "other.v1", field: "x" }],
174
+ },
175
+ },
176
+ }
177
+
178
+ const resolved: ResolvedInstanceInput = {
179
+ input: {
180
+ instanceId: dependencyInstanceId,
181
+ output: "out",
182
+ } as never,
183
+ type: "child.v1",
184
+ }
185
+
186
+ expect(() =>
187
+ resolveUnitInputValues({
188
+ library: library as never,
189
+ inputName: "dep",
190
+ resolvedInput: resolved,
191
+ dependencyInstanceType,
192
+ captured: [{ ok: true, value: { child: { v: 1 } } }],
193
+ }),
194
+ ).toThrow(/no matching inclusion found/i)
195
+ })
196
+
197
+ test("returns captured values when effective output type is forwarded subtype", () => {
198
+ const dependencyInstanceId = "component.v1:dep"
199
+ const dependencyInstanceType = "component.v1" satisfies VersionedName
200
+
201
+ const library = {
202
+ components: {
203
+ "component.v1": {
204
+ outputs: {
205
+ out: { type: "parent.v1" },
206
+ },
207
+ },
208
+ },
209
+ entities: {
210
+ "parent.v1": {
211
+ type: "parent.v1",
212
+ },
213
+ "child.v1": {
214
+ type: "child.v1",
215
+ },
216
+ },
217
+ }
218
+
219
+ const resolved: ResolvedInstanceInput = {
220
+ input: {
221
+ instanceId: dependencyInstanceId,
222
+ output: "out",
223
+ } as never,
224
+ type: "child.v1",
225
+ }
226
+
227
+ const values = resolveUnitInputValues({
228
+ library: library as never,
229
+ inputName: "dep",
230
+ resolvedInput: resolved,
231
+ dependencyInstanceType,
232
+ captured: [{ ok: true, value: { v: 42 } }],
233
+ effectiveOutputType: "child.v1",
234
+ effectiveRootOutputType: "child.v1",
235
+ })
236
+
237
+ expect(values).toEqual([
238
+ {
239
+ value: { v: 42 },
240
+ source: { instanceId: dependencyInstanceId, output: "out" },
241
+ },
242
+ ])
243
+ })
244
+
245
+ test("throws when captured value is an error", () => {
246
+ const dependencyInstanceId = "component.v1:dep"
247
+ const dependencyInstanceType = "component.v1" satisfies VersionedName
248
+
249
+ const library = {
250
+ components: {
251
+ "component.v1": {
252
+ outputs: {
253
+ out: { type: "parent.v1" },
254
+ },
255
+ },
256
+ },
257
+ entities: {
258
+ "parent.v1": {
259
+ type: "parent.v1",
260
+ },
261
+ },
262
+ }
263
+
264
+ const resolved: ResolvedInstanceInput = {
265
+ input: {
266
+ instanceId: dependencyInstanceId,
267
+ output: "out",
268
+ } as never,
269
+ type: "parent.v1",
270
+ }
271
+
272
+ expect(() =>
273
+ resolveUnitInputValues({
274
+ library: library as never,
275
+ inputName: "dep",
276
+ resolvedInput: resolved,
277
+ dependencyInstanceType,
278
+ captured: [
279
+ {
280
+ ok: false,
281
+ error: {
282
+ message: 'Missing required inclusion "child"',
283
+ snapshotId: "snap_1",
284
+ },
285
+ },
286
+ ],
287
+ }),
288
+ ).toThrow(/failed to reconstruct/i)
289
+ })
290
+
291
+ test("extracts values using explicit nested inclusion path", () => {
292
+ const dependencyInstanceId = "component.v1:dep"
293
+ const dependencyInstanceType = "component.v1" satisfies VersionedName
294
+
295
+ const library = {
296
+ components: {
297
+ "component.v1": {
298
+ outputs: {
299
+ out: { type: "network.v1" },
300
+ },
301
+ },
302
+ },
303
+ entities: {
304
+ "network.v1": {
305
+ type: "network.v1",
306
+ inclusions: [{ type: "peer.v1", field: "peer", multiple: false }],
307
+ },
308
+ "peer.v1": {
309
+ type: "peer.v1",
310
+ inclusions: [{ type: "endpoint.v1", field: "endpoints", multiple: true }],
311
+ },
312
+ "endpoint.v1": {
313
+ type: "endpoint.v1",
314
+ },
315
+ },
316
+ }
317
+
318
+ const resolved: ResolvedInstanceInput = {
319
+ input: {
320
+ instanceId: dependencyInstanceId,
321
+ output: "out",
322
+ path: "peer.endpoints",
323
+ } as never,
324
+ type: "endpoint.v1",
325
+ }
326
+
327
+ const values = resolveUnitInputValues({
328
+ library: library as never,
329
+ inputName: "dep",
330
+ resolvedInput: resolved,
331
+ dependencyInstanceType,
332
+ captured: [
333
+ {
334
+ ok: true,
335
+ value: {
336
+ peer: {
337
+ endpoints: [{ host: "a.example" }, { host: "b.example" }],
338
+ },
339
+ },
340
+ },
341
+ ],
342
+ })
343
+
344
+ expect(values).toEqual([
345
+ {
346
+ value: { host: "a.example" },
347
+ source: {
348
+ instanceId: dependencyInstanceId,
349
+ output: "out",
350
+ path: "peer.endpoints",
351
+ },
352
+ },
353
+ {
354
+ value: { host: "b.example" },
355
+ source: {
356
+ instanceId: dependencyInstanceId,
357
+ output: "out",
358
+ path: "peer.endpoints",
359
+ },
360
+ },
361
+ ])
362
+ })
363
+
364
+ test("throws when explicit path contains bracket syntax", () => {
365
+ const dependencyInstanceId = "component.v1:dep"
366
+ const dependencyInstanceType = "component.v1" satisfies VersionedName
367
+
368
+ const library = {
369
+ components: {
370
+ "component.v1": {
371
+ outputs: {
372
+ out: { type: "peer.v1" },
373
+ },
374
+ },
375
+ },
376
+ entities: {
377
+ "peer.v1": {
378
+ type: "peer.v1",
379
+ inclusions: [{ type: "endpoint.v1", field: "endpoints", multiple: true }],
380
+ },
381
+ "endpoint.v1": {
382
+ type: "endpoint.v1",
383
+ },
384
+ },
385
+ }
386
+
387
+ const resolved: ResolvedInstanceInput = {
388
+ input: {
389
+ instanceId: dependencyInstanceId,
390
+ output: "out",
391
+ path: "endpoints[*]",
392
+ } as never,
393
+ type: "endpoint.v1",
394
+ }
395
+
396
+ expect(() =>
397
+ resolveUnitInputValues({
398
+ library: library as never,
399
+ inputName: "dep",
400
+ resolvedInput: resolved,
401
+ dependencyInstanceType,
402
+ captured: [{ ok: true, value: { endpoints: [{ host: "a.example" }] } }],
403
+ }),
404
+ ).toThrow(/invalid input path segment/i)
405
+ })
406
+
407
+ test("applies explicit path even when output type matches input type", () => {
408
+ const dependencyInstanceId = "component.v1:dep"
409
+ const dependencyInstanceType = "component.v1" satisfies VersionedName
410
+
411
+ const library = {
412
+ components: {
413
+ "component.v1": {
414
+ outputs: {
415
+ out: { type: "network.v1" },
416
+ },
417
+ },
418
+ },
419
+ entities: {
420
+ "network.v1": {
421
+ type: "network.v1",
422
+ inclusions: [{ type: "peer.v1", field: "peer", multiple: false }],
423
+ },
424
+ "peer.v1": {
425
+ type: "peer.v1",
426
+ },
427
+ },
428
+ }
429
+
430
+ const resolved: ResolvedInstanceInput = {
431
+ input: {
432
+ instanceId: dependencyInstanceId,
433
+ output: "out",
434
+ path: "peer",
435
+ } as never,
436
+ // same as output type on purpose: explicit path should still be applied first
437
+ type: "network.v1",
438
+ }
439
+
440
+ expect(() =>
441
+ resolveUnitInputValues({
442
+ library: library as never,
443
+ inputName: "dep",
444
+ resolvedInput: resolved,
445
+ dependencyInstanceType,
446
+ captured: [{ ok: true, value: { peer: { id: "p1" } } }],
447
+ }),
448
+ ).toThrow(/resolved path type is "peer\.v1"/i)
449
+ })
450
+ })