@highstate/backend 0.9.16 → 0.9.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (302) hide show
  1. package/dist/chunk-5WVU2AK4.js +1535 -0
  2. package/dist/chunk-5WVU2AK4.js.map +1 -0
  3. package/dist/chunk-I7BWSAN6.js +49 -0
  4. package/dist/chunk-I7BWSAN6.js.map +1 -0
  5. package/dist/{chunk-RCB4AFGD.js → chunk-VB4YL327.js} +51 -71
  6. package/dist/chunk-VB4YL327.js.map +1 -0
  7. package/dist/database/local/prisma.config.js +26 -0
  8. package/dist/database/local/prisma.config.js.map +1 -0
  9. package/dist/highstate.manifest.json +5 -4
  10. package/dist/index.js +7676 -6634
  11. package/dist/index.js.map +1 -1
  12. package/dist/library/package-resolution-worker.js +8 -6
  13. package/dist/library/package-resolution-worker.js.map +1 -1
  14. package/dist/library/worker/main.js +63 -58
  15. package/dist/library/worker/main.js.map +1 -1
  16. package/dist/shared/index.js +3 -216
  17. package/dist/shared/index.js.map +1 -1
  18. package/package.json +23 -11
  19. package/prisma/backend/_schema/layout.prisma +7 -0
  20. package/prisma/backend/_schema/library.prisma +17 -0
  21. package/prisma/backend/_schema/project.prisma +101 -0
  22. package/prisma/backend/_schema/pulumi.prisma +17 -0
  23. package/prisma/backend/postgresql/main.prisma +17 -0
  24. package/prisma/backend/sqlite/main.prisma +17 -0
  25. package/prisma/backend/sqlite/migrations/20250817070609_initiial/migration.sql +34 -0
  26. package/prisma/backend/sqlite/migrations/20250817104948_add_fields/migration.sql +59 -0
  27. package/prisma/backend/sqlite/migrations/20250818082732_add_models/migration.sql +41 -0
  28. package/prisma/backend/sqlite/migrations/20250818083106_a/migration.sql +19 -0
  29. package/prisma/backend/sqlite/migrations/20250818101945_hi/migration.sql +1 -0
  30. package/prisma/backend/sqlite/migrations/20250819082315_a/migration.sql +5 -0
  31. package/prisma/backend/sqlite/migrations/migration_lock.toml +3 -0
  32. package/prisma/project/api-key.prisma +27 -0
  33. package/prisma/project/artifact.prisma +52 -0
  34. package/prisma/project/custom-status.prisma +46 -0
  35. package/prisma/project/evaluation.prisma +35 -0
  36. package/prisma/project/instance.prisma +160 -0
  37. package/prisma/project/layout.prisma +23 -0
  38. package/prisma/project/lock.prisma +18 -0
  39. package/prisma/project/main.prisma +17 -0
  40. package/prisma/project/migrations/20250816081310_initial/migration.sql +300 -0
  41. package/prisma/project/migrations/20250816082523_test/migration.sql +72 -0
  42. package/prisma/project/migrations/20250818065643_update/migration.sql +42 -0
  43. package/prisma/project/migrations/20250818070758_a/migration.sql +8 -0
  44. package/prisma/project/migrations/20250818070913_a/migration.sql +8 -0
  45. package/prisma/project/migrations/20250818082720_add_motels/migration.sql +11 -0
  46. package/prisma/project/migrations/20250818112523_hello/migration.sql +35 -0
  47. package/prisma/project/migrations/20250819082305_a/migration.sql +14 -0
  48. package/prisma/project/migrations/20250819165004_add_missing_fields/migration.sql +216 -0
  49. package/prisma/project/migrations/20250819171309_a/migration.sql +22 -0
  50. package/prisma/project/migrations/20250820113949_a/migration.sql +66 -0
  51. package/prisma/project/migrations/20250820144256_b/migration.sql +31 -0
  52. package/prisma/project/migrations/20250820145547_a/migration.sql +24 -0
  53. package/prisma/project/migrations/20250820182517_b/migration.sql +2 -0
  54. package/prisma/project/migrations/20250821172324_a/migration.sql +2 -0
  55. package/prisma/project/migrations/20250822081339_a/migration.sql +219 -0
  56. package/prisma/project/migrations/20250822083742_b/migration.sql +1 -0
  57. package/prisma/project/migrations/20250822105134_boom/migration.sql +1 -0
  58. package/prisma/project/migrations/20250822141028_b/migration.sql +1 -0
  59. package/prisma/project/migrations/20250822142342_b/migration.sql +16 -0
  60. package/prisma/project/migrations/20250824072720_a/migration.sql +1 -0
  61. package/prisma/project/migrations/20250824093656_b/migration.sql +21 -0
  62. package/prisma/project/migrations/20250825082518_a/migration.sql +1 -0
  63. package/prisma/project/migrations/20250825085343_b/migration.sql +1 -0
  64. package/prisma/project/migrations/20250825091312_a/migration.sql +1 -0
  65. package/prisma/project/migrations/20250903095431_hi/migration.sql +44 -0
  66. package/prisma/project/migrations/20250903174255_a/migration.sql +24 -0
  67. package/prisma/project/migrations/20250908095205_hi/migration.sql +18 -0
  68. package/prisma/project/migrations/20250909155857_hi/migration.sql +15 -0
  69. package/prisma/project/migrations/migration_lock.toml +3 -0
  70. package/prisma/project/model.prisma +37 -0
  71. package/prisma/project/operation.prisma +148 -0
  72. package/prisma/project/page.prisma +41 -0
  73. package/prisma/project/secret.prisma +42 -0
  74. package/prisma/project/service-account.prisma +36 -0
  75. package/prisma/project/terminal.prisma +90 -0
  76. package/prisma/project/trigger.prisma +31 -0
  77. package/prisma/project/unlock-method.prisma +32 -0
  78. package/prisma/project/worker.prisma +138 -0
  79. package/src/artifact/abstractions.ts +13 -13
  80. package/src/artifact/encryption.ts +31 -15
  81. package/src/artifact/factory.ts +7 -10
  82. package/src/artifact/local.ts +33 -50
  83. package/src/business/api-key.ts +24 -36
  84. package/src/business/artifact.test.ts +978 -0
  85. package/src/business/artifact.ts +136 -215
  86. package/src/business/evaluation.ts +328 -0
  87. package/src/business/index.ts +5 -1
  88. package/src/business/instance-lock.test.ts +1060 -0
  89. package/src/business/instance-lock.ts +387 -77
  90. package/src/business/instance-state.test.ts +735 -0
  91. package/src/business/instance-state.ts +604 -217
  92. package/src/business/operation.test.ts +439 -0
  93. package/src/business/operation.ts +174 -208
  94. package/src/business/project-model.ts +258 -0
  95. package/src/business/project-unlock.ts +172 -112
  96. package/src/business/project.ts +407 -0
  97. package/src/business/secret.test.ts +513 -0
  98. package/src/business/secret.ts +194 -131
  99. package/src/business/settings.test.ts +695 -0
  100. package/src/business/settings.ts +855 -0
  101. package/src/business/terminal-session.ts +90 -0
  102. package/src/business/unit-extra.test.ts +539 -0
  103. package/src/business/unit-extra.ts +160 -0
  104. package/src/business/worker.test.ts +391 -0
  105. package/src/business/worker.ts +250 -114
  106. package/src/common/codebase.ts +65 -0
  107. package/src/common/index.ts +3 -2
  108. package/src/common/logger.ts +5 -0
  109. package/src/common/utils.ts +4 -3
  110. package/src/config.ts +15 -12
  111. package/src/database/_generated/backend/postgresql/client.ts +72 -0
  112. package/src/database/_generated/backend/postgresql/commonInputTypes.ts +350 -0
  113. package/src/database/_generated/backend/postgresql/enums.ts +13 -0
  114. package/src/database/_generated/backend/postgresql/internal/class.ts +320 -0
  115. package/src/database/_generated/backend/postgresql/internal/prismaNamespace.ts +1238 -0
  116. package/src/database/_generated/backend/postgresql/models/Library.ts +1263 -0
  117. package/src/database/_generated/backend/postgresql/models/Project.ts +2175 -0
  118. package/src/database/_generated/backend/postgresql/models/ProjectModelStorage.ts +1263 -0
  119. package/src/database/_generated/backend/postgresql/models/ProjectSpace.ts +1602 -0
  120. package/src/database/_generated/backend/postgresql/models/PulumiBackend.ts +1263 -0
  121. package/src/database/_generated/backend/postgresql/models/UserWorkspaseLayout.ts +1065 -0
  122. package/src/database/_generated/backend/postgresql/models.ts +16 -0
  123. package/src/database/_generated/backend/postgresql/pjtg.ts +182 -0
  124. package/src/database/_generated/backend/sqlite/client.ts +72 -0
  125. package/src/database/_generated/backend/sqlite/commonInputTypes.ts +331 -0
  126. package/src/database/_generated/backend/sqlite/enums.ts +13 -0
  127. package/src/database/_generated/backend/sqlite/internal/class.ts +318 -0
  128. package/src/database/_generated/backend/sqlite/internal/prismaNamespace.ts +1207 -0
  129. package/src/database/_generated/backend/sqlite/models/Library.ts +1261 -0
  130. package/src/database/_generated/backend/sqlite/models/Project.ts +2169 -0
  131. package/src/database/_generated/backend/sqlite/models/ProjectModelStorage.ts +1261 -0
  132. package/src/database/_generated/backend/sqlite/models/ProjectSpace.ts +1599 -0
  133. package/src/database/_generated/backend/sqlite/models/PulumiBackend.ts +1261 -0
  134. package/src/database/_generated/backend/sqlite/models/UserWorkspaseLayout.ts +1063 -0
  135. package/src/database/_generated/backend/sqlite/models.ts +16 -0
  136. package/src/database/_generated/backend/sqlite/pjtg.ts +182 -0
  137. package/src/database/_generated/project/client.ts +204 -0
  138. package/src/database/_generated/project/commonInputTypes.ts +827 -0
  139. package/src/database/_generated/project/enums.ts +104 -0
  140. package/src/database/_generated/project/internal/class.ts +479 -0
  141. package/src/database/_generated/project/internal/prismaNamespace.ts +2974 -0
  142. package/src/database/_generated/project/models/ApiKey.ts +1506 -0
  143. package/src/database/_generated/project/models/Artifact.ts +2051 -0
  144. package/src/database/_generated/project/models/HubModel.ts +1125 -0
  145. package/src/database/_generated/project/models/InstanceCustomStatus.ts +1713 -0
  146. package/src/database/_generated/project/models/InstanceEvaluationState.ts +1312 -0
  147. package/src/database/_generated/project/models/InstanceLock.ts +1268 -0
  148. package/src/database/_generated/project/models/InstanceModel.ts +1125 -0
  149. package/src/database/_generated/project/models/InstanceOperationState.ts +1707 -0
  150. package/src/database/_generated/project/models/InstanceState.ts +4613 -0
  151. package/src/database/_generated/project/models/Operation.ts +1647 -0
  152. package/src/database/_generated/project/models/OperationLog.ts +1455 -0
  153. package/src/database/_generated/project/models/Page.ts +1838 -0
  154. package/src/database/_generated/project/models/Secret.ts +1692 -0
  155. package/src/database/_generated/project/models/ServiceAccount.ts +2165 -0
  156. package/src/database/_generated/project/models/Terminal.ts +2038 -0
  157. package/src/database/_generated/project/models/TerminalSession.ts +1454 -0
  158. package/src/database/_generated/project/models/TerminalSessionLog.ts +1280 -0
  159. package/src/database/_generated/project/models/Trigger.ts +1430 -0
  160. package/src/database/_generated/project/models/UnlockMethod.ts +1220 -0
  161. package/src/database/_generated/project/models/UserCompositeViewport.ts +1280 -0
  162. package/src/database/_generated/project/models/UserProjectViewport.ts +1059 -0
  163. package/src/database/_generated/project/models/Worker.ts +1459 -0
  164. package/src/database/_generated/project/models/WorkerUnitRegistration.ts +1524 -0
  165. package/src/database/_generated/project/models/WorkerVersion.ts +1974 -0
  166. package/src/database/_generated/project/models/WorkerVersionLog.ts +1318 -0
  167. package/src/database/_generated/project/models.ts +35 -0
  168. package/src/database/_generated/project/pjtg.ts +182 -0
  169. package/src/database/abstractions.ts +19 -0
  170. package/src/database/factory.ts +37 -0
  171. package/src/database/index.ts +6 -0
  172. package/src/database/local/backend.ts +134 -0
  173. package/src/database/local/index.ts +3 -0
  174. package/src/database/local/meta.ts +46 -0
  175. package/src/database/local/prisma.config.ts +25 -0
  176. package/src/database/local/project.ts +39 -0
  177. package/src/database/manager.ts +181 -0
  178. package/src/database/migrate.ts +35 -0
  179. package/src/database/prisma.ts +56 -0
  180. package/src/database/well-known.ts +38 -0
  181. package/src/index.ts +4 -4
  182. package/src/library/abstractions.ts +21 -14
  183. package/src/library/factory.ts +1 -1
  184. package/src/library/local.ts +86 -38
  185. package/src/library/package-resolution-worker.ts +1 -1
  186. package/src/library/worker/evaluator.ts +61 -48
  187. package/src/library/worker/loader.lite.ts +14 -1
  188. package/src/library/worker/main.ts +9 -16
  189. package/src/library/worker/protocol.ts +0 -12
  190. package/src/lock/manager.ts +12 -7
  191. package/src/orchestrator/manager.ts +198 -131
  192. package/src/orchestrator/operation-context.ts +357 -0
  193. package/src/orchestrator/operation-plan.destroy.test.md +357 -0
  194. package/src/orchestrator/operation-plan.destroy.test.ts +775 -0
  195. package/src/orchestrator/operation-plan.fixtures.ts +213 -0
  196. package/src/orchestrator/operation-plan.md +198 -0
  197. package/src/orchestrator/operation-plan.refresh.test.md +199 -0
  198. package/src/orchestrator/operation-plan.refresh.test.ts +367 -0
  199. package/src/orchestrator/operation-plan.ts +709 -0
  200. package/src/orchestrator/operation-plan.update.test.md +485 -0
  201. package/src/orchestrator/operation-plan.update.test.ts +1066 -0
  202. package/src/orchestrator/operation-workset.ts +235 -583
  203. package/src/orchestrator/operation.ts +446 -904
  204. package/src/orchestrator/plan-test-builder.ts +267 -0
  205. package/src/project-model/abstractions.ts +118 -0
  206. package/src/project-model/backends/codebase.ts +365 -0
  207. package/src/project-model/backends/database.ts +440 -0
  208. package/src/project-model/errors.ts +81 -0
  209. package/src/project-model/factory.ts +24 -0
  210. package/src/project-model/index.ts +4 -0
  211. package/src/project-model/utils.test.ts +544 -0
  212. package/src/project-model/utils.ts +242 -0
  213. package/src/pubsub/abstractions.ts +10 -1
  214. package/src/pubsub/factory.ts +4 -4
  215. package/src/pubsub/index.ts +1 -0
  216. package/src/pubsub/manager.ts +49 -25
  217. package/src/pubsub/memory.ts +31 -0
  218. package/src/runner/abstractions.ts +38 -26
  219. package/src/runner/artifact-env.ts +17 -6
  220. package/src/runner/factory.ts +6 -6
  221. package/src/runner/force-abort.ts +3 -6
  222. package/src/runner/local.ts +79 -72
  223. package/src/runner/pulumi.ts +26 -63
  224. package/src/services.ts +214 -103
  225. package/src/shared/models/backend/index.ts +3 -1
  226. package/src/shared/models/backend/library.ts +12 -4
  227. package/src/shared/models/backend/project.ts +43 -23
  228. package/src/shared/models/backend/pulumi.ts +14 -0
  229. package/src/shared/models/backend/unlock-method.ts +1 -1
  230. package/src/shared/models/backend/well-known.ts +58 -0
  231. package/src/shared/models/base.ts +40 -109
  232. package/src/shared/models/errors.ts +82 -1
  233. package/src/shared/models/index.ts +3 -2
  234. package/src/shared/models/prisma.ts +36 -0
  235. package/src/shared/models/project/api-key.ts +37 -56
  236. package/src/shared/models/project/artifact.ts +15 -105
  237. package/src/shared/models/project/custom-status.ts +12 -0
  238. package/src/shared/models/project/index.ts +9 -9
  239. package/src/shared/models/project/lock.ts +10 -78
  240. package/src/shared/models/project/model.ts +32 -0
  241. package/src/shared/models/project/operation.ts +222 -99
  242. package/src/shared/models/project/page.ts +37 -48
  243. package/src/shared/models/project/secret.ts +29 -103
  244. package/src/shared/models/project/service-account.ts +12 -17
  245. package/src/shared/models/project/state.ts +100 -390
  246. package/src/shared/models/project/terminal.ts +75 -89
  247. package/src/shared/models/project/trigger.ts +13 -49
  248. package/src/shared/models/project/unlock-method.ts +21 -20
  249. package/src/shared/models/project/worker.ts +89 -88
  250. package/src/shared/resolvers/graph-resolver.ts +62 -26
  251. package/src/shared/resolvers/index.ts +1 -1
  252. package/src/shared/resolvers/input-hash.ts +24 -14
  253. package/src/shared/resolvers/input.ts +48 -6
  254. package/src/shared/resolvers/registry.ts +5 -4
  255. package/src/shared/resolvers/state.ts +12 -1
  256. package/src/shared/resolvers/validation.ts +29 -9
  257. package/src/shared/utils/index.ts +1 -1
  258. package/src/shared/utils/promise-tracker.ts +30 -3
  259. package/src/terminal/abstractions.ts +1 -1
  260. package/src/terminal/docker.ts +3 -3
  261. package/src/terminal/manager.ts +102 -118
  262. package/src/test-utils/database.ts +119 -0
  263. package/src/test-utils/index.ts +2 -0
  264. package/src/test-utils/services.ts +134 -0
  265. package/src/unlock/abstractions.ts +31 -0
  266. package/src/unlock/index.ts +2 -0
  267. package/src/unlock/memory.ts +27 -0
  268. package/src/worker/abstractions.ts +7 -4
  269. package/src/worker/docker.ts +14 -19
  270. package/src/worker/manager.ts +376 -79
  271. package/dist/chunk-RCB4AFGD.js.map +0 -1
  272. package/dist/chunk-WHALQHEZ.js +0 -2017
  273. package/dist/chunk-WHALQHEZ.js.map +0 -1
  274. package/src/business/backend-unlock.ts +0 -10
  275. package/src/common/performance.ts +0 -44
  276. package/src/hotstate/abstractions.ts +0 -48
  277. package/src/hotstate/factory.ts +0 -17
  278. package/src/hotstate/index.ts +0 -3
  279. package/src/hotstate/manager.ts +0 -192
  280. package/src/hotstate/memory.ts +0 -100
  281. package/src/hotstate/validation.ts +0 -101
  282. package/src/project/abstractions.ts +0 -102
  283. package/src/project/factory.ts +0 -11
  284. package/src/project/index.ts +0 -3
  285. package/src/project/local.ts +0 -469
  286. package/src/project/manager.ts +0 -574
  287. package/src/pubsub/local.ts +0 -36
  288. package/src/pubsub/validation.ts +0 -33
  289. package/src/shared/models/project/component.ts +0 -45
  290. package/src/shared/models/project/instance.ts +0 -74
  291. package/src/state/abstractions.ts +0 -450
  292. package/src/state/encryption.ts +0 -59
  293. package/src/state/factory.ts +0 -20
  294. package/src/state/index.ts +0 -6
  295. package/src/state/local/backend.ts +0 -299
  296. package/src/state/local/collection.ts +0 -342
  297. package/src/state/local/index.ts +0 -2
  298. package/src/state/manager.ts +0 -819
  299. package/src/state/repository/index.ts +0 -2
  300. package/src/state/repository/repository.index.ts +0 -193
  301. package/src/state/repository/repository.ts +0 -458
  302. /package/src/{state → database/local}/keyring.ts +0 -0
@@ -0,0 +1,1066 @@
1
+ import { describe } from "vitest"
2
+ import { createOperationPlan } from "./operation-plan"
3
+ import { operationPlanTest } from "./operation-plan.fixtures"
4
+
5
+ describe("OperationPlan - Update Operations", () => {
6
+ operationPlanTest(
7
+ "1. should include out-of-date dependencies in linear chain",
8
+ async ({ testBuilder, expect }) => {
9
+ // arrange
10
+ const { context, operation } = await testBuilder()
11
+ .unit("A")
12
+ .unit("B")
13
+ .unit("C")
14
+ .depends("C", "B")
15
+ .depends("B", "A")
16
+ .states({ A: "upToDate", B: "changed", C: "upToDate" })
17
+ .request("update", "C")
18
+ .build()
19
+
20
+ // act
21
+ const plan = createOperationPlan(
22
+ context,
23
+ operation.type,
24
+ operation.requestedInstanceIds,
25
+ operation.options,
26
+ )
27
+
28
+ // assert
29
+ expect(plan).toMatchInlineSnapshot(`
30
+ [
31
+ {
32
+ "instances": [
33
+ {
34
+ "id": "component.v1:B",
35
+ "message": "changed and required by "component.v1:C"",
36
+ "parentId": undefined,
37
+ },
38
+ {
39
+ "id": "component.v1:C",
40
+ "message": "explicitly requested",
41
+ "parentId": undefined,
42
+ },
43
+ ],
44
+ "type": "update",
45
+ },
46
+ ]
47
+ `)
48
+ },
49
+ )
50
+
51
+ operationPlanTest(
52
+ "2. should not propagate beyond compositional inclusion",
53
+ async ({ testBuilder, expect }) => {
54
+ // arrange
55
+ const { context, operation } = await testBuilder()
56
+ .composite("GrandParent")
57
+ .composite("Parent")
58
+ .unit("A")
59
+ .unit("B")
60
+ .unit("C")
61
+ .children("GrandParent", "Parent", "C")
62
+ .children("Parent", "A")
63
+ .depends("B", "A")
64
+ .states({
65
+ GrandParent: "upToDate",
66
+ Parent: "upToDate",
67
+ A: "changed",
68
+ B: "upToDate",
69
+ C: "changed",
70
+ })
71
+ .request("update", "B")
72
+ .build()
73
+
74
+ // act
75
+ const plan = createOperationPlan(
76
+ context,
77
+ operation.type,
78
+ operation.requestedInstanceIds,
79
+ operation.options,
80
+ )
81
+
82
+ // assert
83
+ expect(plan).toMatchInlineSnapshot(`
84
+ [
85
+ {
86
+ "instances": [
87
+ {
88
+ "id": "component.v1:A",
89
+ "message": "changed and required by "component.v1:B"",
90
+ "parentId": "composite.v1:Parent",
91
+ },
92
+ {
93
+ "id": "component.v1:B",
94
+ "message": "explicitly requested",
95
+ "parentId": undefined,
96
+ },
97
+ {
98
+ "id": "composite.v1:Parent",
99
+ "message": "parent of included child "component.v1:A"",
100
+ "parentId": "composite.v1:GrandParent",
101
+ },
102
+ ],
103
+ "type": "update",
104
+ },
105
+ ]
106
+ `)
107
+ },
108
+ )
109
+
110
+ operationPlanTest(
111
+ "3. should force all dependencies when flag enabled",
112
+ async ({ testBuilder, expect }) => {
113
+ // arrange
114
+ const { context, operation } = await testBuilder()
115
+ .unit("A")
116
+ .unit("B")
117
+ .unit("C")
118
+ .depends("C", "B")
119
+ .depends("B", "A")
120
+ .states({ A: "upToDate", B: "upToDate", C: "upToDate" })
121
+ .options({ forceUpdateDependencies: true })
122
+ .request("update", "C")
123
+ .build()
124
+
125
+ // act
126
+ const plan = createOperationPlan(
127
+ context,
128
+ operation.type,
129
+ operation.requestedInstanceIds,
130
+ operation.options,
131
+ )
132
+
133
+ // assert
134
+ expect(plan).toMatchInlineSnapshot(`
135
+ [
136
+ {
137
+ "instances": [
138
+ {
139
+ "id": "component.v1:A",
140
+ "message": "required by "component.v1:B" (forced by options)",
141
+ "parentId": undefined,
142
+ },
143
+ {
144
+ "id": "component.v1:B",
145
+ "message": "required by "component.v1:C" (forced by options)",
146
+ "parentId": undefined,
147
+ },
148
+ {
149
+ "id": "component.v1:C",
150
+ "message": "explicitly requested",
151
+ "parentId": undefined,
152
+ },
153
+ ],
154
+ "type": "update",
155
+ },
156
+ ]
157
+ `)
158
+ },
159
+ )
160
+
161
+ operationPlanTest(
162
+ "4. should include outdated children of substantive composite",
163
+ async ({ testBuilder, expect }) => {
164
+ // arrange
165
+ const { context, operation } = await testBuilder()
166
+ .composite("Parent")
167
+ .unit("Child1")
168
+ .unit("Child2")
169
+ .unit("Child3")
170
+ .children("Parent", "Child1", "Child2", "Child3")
171
+ .states({
172
+ Parent: "upToDate",
173
+ Child1: "changed",
174
+ Child2: "undeployed",
175
+ Child3: "upToDate",
176
+ })
177
+ .request("update", "Parent")
178
+ .build()
179
+
180
+ // act
181
+ const plan = createOperationPlan(
182
+ context,
183
+ operation.type,
184
+ operation.requestedInstanceIds,
185
+ operation.options,
186
+ )
187
+
188
+ // assert
189
+ expect(plan).toMatchInlineSnapshot(`
190
+ [
191
+ {
192
+ "instances": [
193
+ {
194
+ "id": "composite.v1:Parent",
195
+ "message": "explicitly requested",
196
+ "parentId": undefined,
197
+ },
198
+ {
199
+ "id": "component.v1:Child1",
200
+ "message": "changed and child of included parent",
201
+ "parentId": "composite.v1:Parent",
202
+ },
203
+ {
204
+ "id": "component.v1:Child2",
205
+ "message": "undeployed and child of included parent",
206
+ "parentId": "composite.v1:Parent",
207
+ },
208
+ ],
209
+ "type": "update",
210
+ },
211
+ ]
212
+ `)
213
+ },
214
+ )
215
+
216
+ operationPlanTest(
217
+ "5. should cleanup ghost children during composite update",
218
+ async ({ testBuilder, expect }) => {
219
+ // arrange
220
+ const { context, operation } = await testBuilder()
221
+ .composite("Parent")
222
+ .unit("Child1")
223
+ .unit("GhostChild")
224
+ .children("Parent", "Child1", "GhostChild")
225
+ .states({
226
+ Parent: "upToDate",
227
+ Child1: "upToDate",
228
+ GhostChild: "ghost",
229
+ })
230
+ .request("update", "Parent")
231
+ .build()
232
+
233
+ // act
234
+ const plan = createOperationPlan(
235
+ context,
236
+ operation.type,
237
+ operation.requestedInstanceIds,
238
+ operation.options,
239
+ )
240
+
241
+ // assert
242
+ expect(plan).toMatchInlineSnapshot(`
243
+ [
244
+ {
245
+ "instances": [
246
+ {
247
+ "id": "composite.v1:Parent",
248
+ "message": "explicitly requested",
249
+ "parentId": undefined,
250
+ },
251
+ {
252
+ "id": "component.v1:GhostChild",
253
+ "message": "included in operation",
254
+ "parentId": "composite.v1:Parent",
255
+ },
256
+ ],
257
+ "type": "destroy",
258
+ },
259
+ ]
260
+ `)
261
+ },
262
+ )
263
+
264
+ operationPlanTest(
265
+ "6. should handle complex nested hierarchy correctly",
266
+ async ({ testBuilder, expect }) => {
267
+ // arrange
268
+ const { context, operation } = await testBuilder()
269
+ .composite("GrandParent")
270
+ .composite("Parent1")
271
+ .composite("Parent2")
272
+ .unit("Child1")
273
+ .unit("Child2")
274
+ .unit("Child3")
275
+ .children("GrandParent", "Parent1", "Parent2")
276
+ .children("Parent1", "Child1", "Child2")
277
+ .children("Parent2", "Child3")
278
+ .depends("Child1", "Child3")
279
+ .states({
280
+ GrandParent: "upToDate",
281
+ Parent1: "upToDate",
282
+ Parent2: "upToDate",
283
+ Child1: "changed",
284
+ Child2: "upToDate",
285
+ Child3: "upToDate",
286
+ })
287
+ .request("update", "GrandParent")
288
+ .build()
289
+
290
+ // act
291
+ const plan = createOperationPlan(
292
+ context,
293
+ operation.type,
294
+ operation.requestedInstanceIds,
295
+ operation.options,
296
+ )
297
+
298
+ // assert
299
+ expect(plan).toMatchInlineSnapshot(`
300
+ [
301
+ {
302
+ "instances": [
303
+ {
304
+ "id": "composite.v1:GrandParent",
305
+ "message": "explicitly requested",
306
+ "parentId": undefined,
307
+ },
308
+ {
309
+ "id": "component.v1:Child1",
310
+ "message": "changed and child of included parent",
311
+ "parentId": "composite.v1:Parent1",
312
+ },
313
+ {
314
+ "id": "composite.v1:Parent1",
315
+ "message": "parent of included child "component.v1:Child1"",
316
+ "parentId": "composite.v1:GrandParent",
317
+ },
318
+ ],
319
+ "type": "update",
320
+ },
321
+ ]
322
+ `)
323
+ },
324
+ )
325
+
326
+ operationPlanTest(
327
+ "7. should not include siblings when child explicitly requested (isolated update)",
328
+ async ({ testBuilder, expect }) => {
329
+ // arrange
330
+ const { context, operation } = await testBuilder()
331
+ .composite("Parent")
332
+ .unit("Child1")
333
+ .unit("Child2")
334
+ .unit("Child3")
335
+ .children("Parent", "Child1", "Child2", "Child3")
336
+ .states({
337
+ Parent: "upToDate",
338
+ Child1: "upToDate",
339
+ Child2: "undeployed",
340
+ Child3: "upToDate",
341
+ })
342
+ .request("update", "Child1")
343
+ .build()
344
+
345
+ // act
346
+ const plan = createOperationPlan(
347
+ context,
348
+ operation.type,
349
+ operation.requestedInstanceIds,
350
+ operation.options,
351
+ )
352
+
353
+ // assert
354
+ expect(plan).toMatchInlineSnapshot(`
355
+ [
356
+ {
357
+ "instances": [
358
+ {
359
+ "id": "component.v1:Child1",
360
+ "message": "explicitly requested",
361
+ "parentId": "composite.v1:Parent",
362
+ },
363
+ {
364
+ "id": "composite.v1:Parent",
365
+ "message": "parent of included child "component.v1:Child1"",
366
+ "parentId": undefined,
367
+ },
368
+ ],
369
+ "type": "update",
370
+ },
371
+ ]
372
+ `)
373
+ },
374
+ )
375
+
376
+ operationPlanTest(
377
+ "8. should force all children when flag enabled",
378
+ async ({ testBuilder, expect }) => {
379
+ // arrange
380
+ const { context, operation } = await testBuilder()
381
+ .composite("Parent")
382
+ .unit("Child1")
383
+ .unit("Child2")
384
+ .unit("Child3")
385
+ .children("Parent", "Child1", "Child2", "Child3")
386
+ .states({
387
+ Parent: "upToDate",
388
+ Child1: "changed",
389
+ Child2: "undeployed",
390
+ Child3: "upToDate",
391
+ })
392
+ .options({ forceUpdateChildren: true })
393
+ .request("update", "Parent")
394
+ .build()
395
+
396
+ // act
397
+ const plan = createOperationPlan(
398
+ context,
399
+ operation.type,
400
+ operation.requestedInstanceIds,
401
+ operation.options,
402
+ )
403
+
404
+ // assert
405
+ expect(plan).toMatchInlineSnapshot(`
406
+ [
407
+ {
408
+ "instances": [
409
+ {
410
+ "id": "composite.v1:Parent",
411
+ "message": "explicitly requested",
412
+ "parentId": undefined,
413
+ },
414
+ {
415
+ "id": "component.v1:Child1",
416
+ "message": "child of included parent (forced by options)",
417
+ "parentId": "composite.v1:Parent",
418
+ },
419
+ {
420
+ "id": "component.v1:Child2",
421
+ "message": "child of included parent (forced by options)",
422
+ "parentId": "composite.v1:Parent",
423
+ },
424
+ {
425
+ "id": "component.v1:Child3",
426
+ "message": "child of included parent (forced by options)",
427
+ "parentId": "composite.v1:Parent",
428
+ },
429
+ ],
430
+ "type": "update",
431
+ },
432
+ ]
433
+ `)
434
+ },
435
+ )
436
+
437
+ operationPlanTest(
438
+ "9. should include instances with error status for recovery",
439
+ async ({ testBuilder, expect }) => {
440
+ // arrange
441
+ const { context, operation } = await testBuilder()
442
+ .unit("A")
443
+ .unit("B")
444
+ .unit("C")
445
+ .depends("C", "B")
446
+ .depends("B", "A")
447
+ .states({ A: "upToDate", B: "error", C: "upToDate" })
448
+ .request("update", "C")
449
+ .build()
450
+
451
+ // act
452
+ const plan = createOperationPlan(
453
+ context,
454
+ operation.type,
455
+ operation.requestedInstanceIds,
456
+ operation.options,
457
+ )
458
+
459
+ // assert
460
+ expect(plan).toMatchInlineSnapshot(`
461
+ [
462
+ {
463
+ "instances": [
464
+ {
465
+ "id": "component.v1:B",
466
+ "message": "failed and required by "component.v1:C"",
467
+ "parentId": undefined,
468
+ },
469
+ {
470
+ "id": "component.v1:C",
471
+ "message": "explicitly requested",
472
+ "parentId": undefined,
473
+ },
474
+ ],
475
+ "type": "update",
476
+ },
477
+ ]
478
+ `)
479
+ },
480
+ )
481
+
482
+ operationPlanTest(
483
+ "10. should handle dependencies crossing composite boundaries",
484
+ async ({ testBuilder, expect }) => {
485
+ // arrange
486
+ const { context, operation } = await testBuilder()
487
+ .composite("CompositeA")
488
+ .composite("CompositeB")
489
+ .unit("ChildA")
490
+ .unit("ChildB")
491
+ .children("CompositeA", "ChildA")
492
+ .children("CompositeB", "ChildB")
493
+ .depends("ChildB", "ChildA")
494
+ .states({
495
+ CompositeA: "upToDate",
496
+ CompositeB: "upToDate",
497
+ ChildA: "changed",
498
+ ChildB: "upToDate",
499
+ })
500
+ .request("update", "ChildB")
501
+ .build()
502
+
503
+ // act
504
+ const plan = createOperationPlan(
505
+ context,
506
+ operation.type,
507
+ operation.requestedInstanceIds,
508
+ operation.options,
509
+ )
510
+
511
+ // assert
512
+ expect(plan).toMatchInlineSnapshot(`
513
+ [
514
+ {
515
+ "instances": [
516
+ {
517
+ "id": "component.v1:ChildA",
518
+ "message": "changed and required by "component.v1:ChildB"",
519
+ "parentId": "composite.v1:CompositeA",
520
+ },
521
+ {
522
+ "id": "component.v1:ChildB",
523
+ "message": "explicitly requested",
524
+ "parentId": "composite.v1:CompositeB",
525
+ },
526
+ {
527
+ "id": "composite.v1:CompositeB",
528
+ "message": "parent of included child "component.v1:ChildB"",
529
+ "parentId": undefined,
530
+ },
531
+ {
532
+ "id": "composite.v1:CompositeA",
533
+ "message": "parent of included child "component.v1:ChildA"",
534
+ "parentId": undefined,
535
+ },
536
+ ],
537
+ "type": "update",
538
+ },
539
+ ]
540
+ `)
541
+ },
542
+ )
543
+
544
+ operationPlanTest(
545
+ "11. should not include unrelated instances even if they depend on updated instance",
546
+ async ({ testBuilder, expect }) => {
547
+ // arrange
548
+ const { context, operation } = await testBuilder()
549
+ .composite("Parent")
550
+ .unit("Child1")
551
+ .unit("Child2")
552
+ .unit("ExternalX")
553
+ .children("Parent", "Child1", "Child2")
554
+ .depends("Child1", "ExternalX")
555
+ .states({
556
+ Parent: "upToDate",
557
+ Child1: "changed",
558
+ Child2: "upToDate",
559
+ ExternalX: "changed",
560
+ })
561
+ .request("update", "ExternalX")
562
+ .build()
563
+
564
+ // act
565
+ const plan = createOperationPlan(
566
+ context,
567
+ operation.type,
568
+ operation.requestedInstanceIds,
569
+ operation.options,
570
+ )
571
+
572
+ // assert
573
+ expect(plan).toMatchInlineSnapshot(`
574
+ [
575
+ {
576
+ "instances": [
577
+ {
578
+ "id": "component.v1:ExternalX",
579
+ "message": "explicitly requested",
580
+ "parentId": undefined,
581
+ },
582
+ ],
583
+ "type": "update",
584
+ },
585
+ ]
586
+ `)
587
+ },
588
+ )
589
+
590
+ operationPlanTest(
591
+ "12. should handle multiple explicit requests with overlapping dependencies",
592
+ async ({ testBuilder, expect }) => {
593
+ // arrange
594
+ const { context, operation } = await testBuilder()
595
+ .unit("A")
596
+ .unit("B")
597
+ .unit("C")
598
+ .depends("C", "B")
599
+ .depends("B", "A")
600
+ .states({ A: "upToDate", B: "upToDate", C: "upToDate" })
601
+ .request("update", "A", "C")
602
+ .build()
603
+
604
+ // act
605
+ const plan = createOperationPlan(
606
+ context,
607
+ operation.type,
608
+ operation.requestedInstanceIds,
609
+ operation.options,
610
+ )
611
+
612
+ // assert
613
+ expect(plan).toMatchInlineSnapshot(`
614
+ [
615
+ {
616
+ "instances": [
617
+ {
618
+ "id": "component.v1:A",
619
+ "message": "explicitly requested",
620
+ "parentId": undefined,
621
+ },
622
+ {
623
+ "id": "component.v1:C",
624
+ "message": "explicitly requested",
625
+ "parentId": undefined,
626
+ },
627
+ ],
628
+ "type": "update",
629
+ },
630
+ ]
631
+ `)
632
+ },
633
+ )
634
+
635
+ operationPlanTest(
636
+ "13. should isolate boundaries in deep composite hierarchies",
637
+ async ({ testBuilder, expect }) => {
638
+ // arrange
639
+ const { context, operation } = await testBuilder()
640
+ .composite("GreatGrandParent")
641
+ .composite("GrandParent")
642
+ .composite("Parent")
643
+ .unit("Child")
644
+ .unit("Uncle")
645
+ .unit("GreatUncle")
646
+ .children("GreatGrandParent", "GrandParent", "GreatUncle")
647
+ .children("GrandParent", "Parent", "Uncle")
648
+ .children("Parent", "Child")
649
+ .states({
650
+ GreatGrandParent: "upToDate",
651
+ GrandParent: "upToDate",
652
+ Parent: "upToDate",
653
+ Child: "upToDate",
654
+ Uncle: "upToDate",
655
+ GreatUncle: "upToDate",
656
+ })
657
+ .request("update", "Child")
658
+ .build()
659
+
660
+ // act
661
+ const plan = createOperationPlan(
662
+ context,
663
+ operation.type,
664
+ operation.requestedInstanceIds,
665
+ operation.options,
666
+ )
667
+
668
+ // assert
669
+ expect(plan).toMatchInlineSnapshot(`
670
+ [
671
+ {
672
+ "instances": [
673
+ {
674
+ "id": "component.v1:Child",
675
+ "message": "explicitly requested",
676
+ "parentId": "composite.v1:Parent",
677
+ },
678
+ {
679
+ "id": "composite.v1:Parent",
680
+ "message": "parent of included child "component.v1:Child"",
681
+ "parentId": "composite.v1:GrandParent",
682
+ },
683
+ ],
684
+ "type": "update",
685
+ },
686
+ ]
687
+ `)
688
+ },
689
+ )
690
+
691
+ operationPlanTest(
692
+ "14. should handle both force flags enabled together",
693
+ async ({ testBuilder, expect }) => {
694
+ // arrange
695
+ const { context, operation } = await testBuilder()
696
+ .unit("A")
697
+ .unit("B")
698
+ .unit("C")
699
+ .composite("Parent")
700
+ .unit("Child1")
701
+ .unit("Child2")
702
+ .depends("C", "B")
703
+ .depends("B", "A")
704
+ .depends("C", "Child1")
705
+ .children("Parent", "Child1", "Child2")
706
+ .states({
707
+ A: "upToDate",
708
+ B: "upToDate",
709
+ C: "upToDate",
710
+ Parent: "upToDate",
711
+ Child1: "upToDate",
712
+ Child2: "upToDate",
713
+ })
714
+ .options({
715
+ forceUpdateDependencies: true,
716
+ forceUpdateChildren: true,
717
+ })
718
+ .request("update", "C")
719
+ .build()
720
+
721
+ // act
722
+ const plan = createOperationPlan(
723
+ context,
724
+ operation.type,
725
+ operation.requestedInstanceIds,
726
+ operation.options,
727
+ )
728
+
729
+ // assert
730
+ expect(plan).toMatchInlineSnapshot(`
731
+ [
732
+ {
733
+ "instances": [
734
+ {
735
+ "id": "component.v1:A",
736
+ "message": "required by "component.v1:B" (forced by options)",
737
+ "parentId": undefined,
738
+ },
739
+ {
740
+ "id": "component.v1:B",
741
+ "message": "required by "component.v1:C" (forced by options)",
742
+ "parentId": undefined,
743
+ },
744
+ {
745
+ "id": "component.v1:Child1",
746
+ "message": "required by "component.v1:C" (forced by options)",
747
+ "parentId": "composite.v1:Parent",
748
+ },
749
+ {
750
+ "id": "component.v1:C",
751
+ "message": "explicitly requested",
752
+ "parentId": undefined,
753
+ },
754
+ {
755
+ "id": "composite.v1:Parent",
756
+ "message": "parent of included child "component.v1:Child1"",
757
+ "parentId": undefined,
758
+ },
759
+ {
760
+ "id": "component.v1:Child2",
761
+ "message": "child of included parent (forced by options)",
762
+ "parentId": "composite.v1:Parent",
763
+ },
764
+ ],
765
+ "type": "update",
766
+ },
767
+ ]
768
+ `)
769
+ },
770
+ )
771
+
772
+ operationPlanTest(
773
+ "15. should handle diamond dependency correctly",
774
+ async ({ testBuilder, expect }) => {
775
+ // arrange
776
+ const { context, operation } = await testBuilder()
777
+ .unit("A")
778
+ .unit("B")
779
+ .unit("C")
780
+ .unit("D")
781
+ .depends("D", "B")
782
+ .depends("D", "C")
783
+ .depends("B", "A")
784
+ .depends("C", "A")
785
+ .states({ A: "upToDate", B: "changed", C: "changed", D: "upToDate" })
786
+ .request("update", "D")
787
+ .build()
788
+
789
+ // act
790
+ const plan = createOperationPlan(
791
+ context,
792
+ operation.type,
793
+ operation.requestedInstanceIds,
794
+ operation.options,
795
+ )
796
+
797
+ // assert
798
+ expect(plan).toMatchInlineSnapshot(`
799
+ [
800
+ {
801
+ "instances": [
802
+ {
803
+ "id": "component.v1:B",
804
+ "message": "changed and required by "component.v1:D"",
805
+ "parentId": undefined,
806
+ },
807
+ {
808
+ "id": "component.v1:C",
809
+ "message": "changed and required by "component.v1:D"",
810
+ "parentId": undefined,
811
+ },
812
+ {
813
+ "id": "component.v1:D",
814
+ "message": "explicitly requested",
815
+ "parentId": undefined,
816
+ },
817
+ ],
818
+ "type": "update",
819
+ },
820
+ ]
821
+ `)
822
+ },
823
+ )
824
+
825
+ operationPlanTest(
826
+ "16. should handle composite with both ghost and real children",
827
+ async ({ testBuilder, expect }) => {
828
+ // arrange
829
+ const { context, operation } = await testBuilder()
830
+ .composite("Parent")
831
+ .unit("Child1")
832
+ .unit("Child2")
833
+ .unit("GhostChild")
834
+ .children("Parent", "Child1", "Child2", "GhostChild")
835
+ .states({
836
+ Parent: "upToDate",
837
+ Child1: "changed",
838
+ Child2: "upToDate",
839
+ GhostChild: "ghost",
840
+ })
841
+ .request("update", "Parent")
842
+ .build()
843
+
844
+ // act
845
+ const plan = createOperationPlan(
846
+ context,
847
+ operation.type,
848
+ operation.requestedInstanceIds,
849
+ operation.options,
850
+ )
851
+
852
+ // assert
853
+ expect(plan).toMatchInlineSnapshot(`
854
+ [
855
+ {
856
+ "instances": [
857
+ {
858
+ "id": "composite.v1:Parent",
859
+ "message": "explicitly requested",
860
+ "parentId": undefined,
861
+ },
862
+ {
863
+ "id": "component.v1:Child1",
864
+ "message": "changed and child of included parent",
865
+ "parentId": "composite.v1:Parent",
866
+ },
867
+ ],
868
+ "type": "update",
869
+ },
870
+ {
871
+ "instances": [
872
+ {
873
+ "id": "composite.v1:Parent",
874
+ "message": "explicitly requested",
875
+ "parentId": undefined,
876
+ },
877
+ {
878
+ "id": "component.v1:GhostChild",
879
+ "message": "included in operation",
880
+ "parentId": "composite.v1:Parent",
881
+ },
882
+ ],
883
+ "type": "destroy",
884
+ },
885
+ ]
886
+ `)
887
+ },
888
+ )
889
+
890
+ operationPlanTest(
891
+ "17. should include dependency chain and force siblings when partial update disabled",
892
+ async ({ testBuilder, expect }) => {
893
+ // arrange
894
+ const { context, operation } = await testBuilder()
895
+ .composite("Parent")
896
+ .unit("Child1")
897
+ .unit("Child2")
898
+ .unit("ExternalX")
899
+ .children("Parent", "Child1", "Child2")
900
+ .depends("ExternalX", "Child1")
901
+ .states({
902
+ Parent: "upToDate",
903
+ Child1: "changed",
904
+ Child2: "changed",
905
+ ExternalX: "upToDate",
906
+ })
907
+ .request("update", "ExternalX")
908
+ .build()
909
+
910
+ // act
911
+ const plan = createOperationPlan(
912
+ context,
913
+ operation.type,
914
+ operation.requestedInstanceIds,
915
+ operation.options,
916
+ )
917
+
918
+ // assert
919
+ expect(plan).toMatchInlineSnapshot(`
920
+ [
921
+ {
922
+ "instances": [
923
+ {
924
+ "id": "component.v1:Child1",
925
+ "message": "changed and required by "component.v1:ExternalX"",
926
+ "parentId": "composite.v1:Parent",
927
+ },
928
+ {
929
+ "id": "component.v1:ExternalX",
930
+ "message": "explicitly requested",
931
+ "parentId": undefined,
932
+ },
933
+ {
934
+ "id": "composite.v1:Parent",
935
+ "message": "parent of included child "component.v1:Child1"",
936
+ "parentId": undefined,
937
+ },
938
+ {
939
+ "id": "component.v1:Child2",
940
+ "message": "changed and child of included parent",
941
+ "parentId": "composite.v1:Parent",
942
+ },
943
+ ],
944
+ "type": "update",
945
+ },
946
+ ]
947
+ `)
948
+ },
949
+ )
950
+
951
+ operationPlanTest(
952
+ "18. should include dependency chain without forcing siblings when partial update enabled",
953
+ async ({ testBuilder, expect }) => {
954
+ // arrange
955
+ const { context, operation } = await testBuilder()
956
+ .composite("Parent")
957
+ .unit("Child1")
958
+ .unit("Child2")
959
+ .unit("ExternalX")
960
+ .children("Parent", "Child1", "Child2")
961
+ .depends("ExternalX", "Child1")
962
+ .states({
963
+ Parent: "upToDate",
964
+ Child1: "changed",
965
+ Child2: "changed",
966
+ ExternalX: "upToDate",
967
+ })
968
+ .options({ allowPartialCompositeInstanceUpdate: true })
969
+ .request("update", "ExternalX")
970
+ .build()
971
+
972
+ // act
973
+ const plan = createOperationPlan(
974
+ context,
975
+ operation.type,
976
+ operation.requestedInstanceIds,
977
+ operation.options,
978
+ )
979
+
980
+ // assert
981
+ expect(plan).toMatchInlineSnapshot(`
982
+ [
983
+ {
984
+ "instances": [
985
+ {
986
+ "id": "component.v1:Child1",
987
+ "message": "changed and required by "component.v1:ExternalX"",
988
+ "parentId": "composite.v1:Parent",
989
+ },
990
+ {
991
+ "id": "component.v1:ExternalX",
992
+ "message": "explicitly requested",
993
+ "parentId": undefined,
994
+ },
995
+ {
996
+ "id": "composite.v1:Parent",
997
+ "message": "parent of included child "component.v1:Child1"",
998
+ "parentId": undefined,
999
+ },
1000
+ ],
1001
+ "type": "update",
1002
+ },
1003
+ ]
1004
+ `)
1005
+ },
1006
+ )
1007
+
1008
+ operationPlanTest(
1009
+ "19. should include child dependencies when child explicitly requested",
1010
+ async ({ testBuilder, expect }) => {
1011
+ // arrange
1012
+ const { context, operation } = await testBuilder()
1013
+ .composite("Parent")
1014
+ .unit("Child1")
1015
+ .unit("Child2")
1016
+ .unit("Child3")
1017
+ .unit("Child4")
1018
+ .children("Parent", "Child1", "Child2", "Child3", "Child4")
1019
+ .depends("Child2", "Child1")
1020
+ .depends("Child3", "Child4")
1021
+ .states({
1022
+ Parent: "upToDate",
1023
+ Child1: "changed",
1024
+ Child2: "changed",
1025
+ Child3: "changed",
1026
+ Child4: "changed",
1027
+ })
1028
+ .request("update", "Child2")
1029
+ .build()
1030
+
1031
+ // act
1032
+ const plan = createOperationPlan(
1033
+ context,
1034
+ operation.type,
1035
+ operation.requestedInstanceIds,
1036
+ operation.options,
1037
+ )
1038
+
1039
+ // assert
1040
+ expect(plan).toMatchInlineSnapshot(`
1041
+ [
1042
+ {
1043
+ "instances": [
1044
+ {
1045
+ "id": "component.v1:Child1",
1046
+ "message": "changed and required by "component.v1:Child2"",
1047
+ "parentId": "composite.v1:Parent",
1048
+ },
1049
+ {
1050
+ "id": "component.v1:Child2",
1051
+ "message": "explicitly requested",
1052
+ "parentId": "composite.v1:Parent",
1053
+ },
1054
+ {
1055
+ "id": "composite.v1:Parent",
1056
+ "message": "parent of included child "component.v1:Child2"",
1057
+ "parentId": undefined,
1058
+ },
1059
+ ],
1060
+ "type": "update",
1061
+ },
1062
+ ]
1063
+ `)
1064
+ },
1065
+ )
1066
+ })