miravo 0.6.0 → 0.6.2

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 (187) hide show
  1. package/LICENSE +199 -0
  2. package/README.md +29 -59
  3. package/bin/miravo.js +84 -0
  4. package/package.json +21 -37
  5. package/console-dist/assets/activity-DsWeRq4h.js +0 -1
  6. package/console-dist/assets/api-4AqKvsm9.js +0 -1
  7. package/console-dist/assets/arrow-right-Ea_UhTNi.js +0 -1
  8. package/console-dist/assets/arrow-up-down-DFSjzzl2.js +0 -1
  9. package/console-dist/assets/blueprint-card-classes-DBFCDy0M.js +0 -1
  10. package/console-dist/assets/box-C1WaoOMF.js +0 -1
  11. package/console-dist/assets/card-BjeP32PN.js +0 -1
  12. package/console-dist/assets/catalog-BJzyzbLR.js +0 -1
  13. package/console-dist/assets/catalog-DKjEm-c4.js +0 -1
  14. package/console-dist/assets/catalog-template-icons-BrUeHaCY.js +0 -1
  15. package/console-dist/assets/checkbox-DZlJXTE4.js +0 -1
  16. package/console-dist/assets/chunk-OE4NN4TA-CEjpfhS6.js +0 -3
  17. package/console-dist/assets/combobox-BGnuCYxO.js +0 -1
  18. package/console-dist/assets/control-CPhvwx9o.js +0 -1
  19. package/console-dist/assets/control-Cg2up9NA.js +0 -1
  20. package/console-dist/assets/copy-button-WkbNkMcd.js +0 -1
  21. package/console-dist/assets/createLucideIcon-Bwxw859K.js +0 -1
  22. package/console-dist/assets/deploy-tab-JeWDwPoZ.js +0 -1
  23. package/console-dist/assets/dist-BhbDML0a.js +0 -1
  24. package/console-dist/assets/dist-Cy8XTNFT.js +0 -2
  25. package/console-dist/assets/download-DQ6NH16U.js +0 -1
  26. package/console-dist/assets/engine-store-CrLnz692.js +0 -1
  27. package/console-dist/assets/events-CXm29Ok4.js +0 -1
  28. package/console-dist/assets/events-D01IxhnP.js +0 -1
  29. package/console-dist/assets/fira-code-cyrillic-ext-wght-normal-DhYMMuQd.woff2 +0 -0
  30. package/console-dist/assets/fira-code-cyrillic-wght-normal-Y3u8pIsh.woff2 +0 -0
  31. package/console-dist/assets/fira-code-greek-ext-wght-normal-wWus70Ix.woff2 +0 -0
  32. package/console-dist/assets/fira-code-greek-wght-normal-B2SviObF.woff2 +0 -0
  33. package/console-dist/assets/fira-code-latin-ext-wght-normal-Dvfvaomy.woff2 +0 -0
  34. package/console-dist/assets/fira-code-latin-wght-normal-CHoedHDv.woff2 +0 -0
  35. package/console-dist/assets/fira-code-symbols2-wght-normal-CE6EOz_n.woff2 +0 -0
  36. package/console-dist/assets/fleet-CfxbKXt9.js +0 -1
  37. package/console-dist/assets/fleet-lotMGVqe.js +0 -1
  38. package/console-dist/assets/format-kXTonpDJ.js +0 -1
  39. package/console-dist/assets/gauge-Ci8zNZlk.js +0 -1
  40. package/console-dist/assets/get-error-message-3Q5Q-2G5.js +0 -1
  41. package/console-dist/assets/index-CVPKSDvJ.js +0 -12
  42. package/console-dist/assets/index-DJkgDeun.css +0 -2
  43. package/console-dist/assets/input-BWEQFptx.js +0 -1
  44. package/console-dist/assets/instance-detail-C5IVwuh3.js +0 -1
  45. package/console-dist/assets/karla-latin-ext-wght-normal-Dq0rrjgs.woff2 +0 -0
  46. package/console-dist/assets/karla-latin-wght-normal-C3-ma4ov.woff2 +0 -0
  47. package/console-dist/assets/label-uephX5ZG.js +0 -1
  48. package/console-dist/assets/layers-CUsLQ1fq.js +0 -1
  49. package/console-dist/assets/login-C3RJli0m.js +0 -126
  50. package/console-dist/assets/models-B_UioWQ5.js +0 -1
  51. package/console-dist/assets/models-_IPlPiZ7.js +0 -1
  52. package/console-dist/assets/not-found-B8gAm08S.js +0 -1
  53. package/console-dist/assets/platform-targets-R3T-4eJ7.js +0 -1
  54. package/console-dist/assets/platforms-BWh0BQ3Y.js +0 -1
  55. package/console-dist/assets/platforms-DStNbtVc.js +0 -1
  56. package/console-dist/assets/proxy-CsnYqnPp.js +0 -9
  57. package/console-dist/assets/query-keys-BvtE7BMs.js +0 -1
  58. package/console-dist/assets/radio-CoHegOv5.js +0 -1
  59. package/console-dist/assets/rolldown-runtime-COnpUsM8.js +0 -1
  60. package/console-dist/assets/schibsted-grotesk-latin-ext-wght-normal-hsMS0n0O.woff2 +0 -0
  61. package/console-dist/assets/schibsted-grotesk-latin-wght-normal-Bb8VGrTG.woff2 +0 -0
  62. package/console-dist/assets/select-CeO4vP0G.js +0 -1
  63. package/console-dist/assets/select-display-items-CIzY8953.js +0 -1
  64. package/console-dist/assets/server-D9Kfn8fQ.js +0 -1
  65. package/console-dist/assets/shallow-6lGjQhtJ.js +0 -1
  66. package/console-dist/assets/tags-CiXOIHtY.js +0 -4
  67. package/console-dist/assets/tags-FQCXIxrP.js +0 -1
  68. package/console-dist/assets/tooltip-HGLF366Q.js +0 -1
  69. package/console-dist/assets/transition-nav-link-Df4Rk6vV.js +0 -1
  70. package/console-dist/assets/transition-types-CPlGtM8w.js +0 -1
  71. package/console-dist/assets/triangle-alert-adNk_sea.js +0 -1
  72. package/console-dist/assets/use-platform-stream-DGbK1k2L.js +0 -2
  73. package/console-dist/assets/use-transition-navigate-CCpcWXB0.js +0 -2
  74. package/console-dist/assets/validation-Blvud9_O.js +0 -1
  75. package/console-dist/assets/vendor-charts-CAZdaPlq.js +0 -60
  76. package/console-dist/assets/vendor-table-wd8JuMmT.js +0 -4
  77. package/console-dist/assets/vendor-ui-ByahnaY0.js +0 -1
  78. package/console-dist/favicon.svg +0 -37
  79. package/console-dist/index.html +0 -82
  80. package/console-dist/miravo-logo.png +0 -0
  81. package/src/__tests__/integration/executable-methods-runner.ts +0 -262
  82. package/src/__tests__/integration/executable-methods.integration.ts +0 -2348
  83. package/src/__tests__/integration/opcua-concurrent-clients.integration.ts +0 -314
  84. package/src/commands/adapter.ts +0 -249
  85. package/src/commands/apply.test.ts +0 -62
  86. package/src/commands/apply.ts +0 -216
  87. package/src/commands/cleanup.test.ts +0 -34
  88. package/src/commands/cleanup.ts +0 -201
  89. package/src/commands/cli-test-setup.ts +0 -91
  90. package/src/commands/export.test.ts +0 -51
  91. package/src/commands/export.ts +0 -404
  92. package/src/commands/fork.ts +0 -219
  93. package/src/commands/inject.test.ts +0 -57
  94. package/src/commands/inject.ts +0 -212
  95. package/src/commands/load.ts +0 -167
  96. package/src/commands/logs.ts +0 -173
  97. package/src/commands/models.test.ts +0 -162
  98. package/src/commands/models.ts +0 -338
  99. package/src/commands/pause.ts +0 -67
  100. package/src/commands/platforms.test.ts +0 -186
  101. package/src/commands/platforms.ts +0 -371
  102. package/src/commands/ps.ts +0 -116
  103. package/src/commands/remove.ts +0 -38
  104. package/src/commands/reset.ts +0 -44
  105. package/src/commands/rm.ts +0 -97
  106. package/src/commands/spawn.ts +0 -45
  107. package/src/commands/speed.ts +0 -40
  108. package/src/commands/start/adapter-config.ts +0 -157
  109. package/src/commands/start/catalog-le-dt.test.ts +0 -50
  110. package/src/commands/start/catalog-le-dt.ts +0 -187
  111. package/src/commands/start/catalog-model-schema.ts +0 -89
  112. package/src/commands/start/catalog-template-detail.ts +0 -54
  113. package/src/commands/start/daemon.ts +0 -255
  114. package/src/commands/start/display.ts +0 -150
  115. package/src/commands/start/engine-setup.ts +0 -247
  116. package/src/commands/start/events.ts +0 -126
  117. package/src/commands/start/index.ts +0 -897
  118. package/src/commands/start/protocol-plans.ts +0 -239
  119. package/src/commands/start/template-loader.ts +0 -197
  120. package/src/commands/start/template-resolver.ts +0 -185
  121. package/src/commands/start/types.ts +0 -73
  122. package/src/commands/start.test.ts +0 -480
  123. package/src/commands/status.test.ts +0 -20
  124. package/src/commands/status.ts +0 -470
  125. package/src/commands/stop.test.ts +0 -84
  126. package/src/commands/stop.ts +0 -169
  127. package/src/commands/templates.test.ts +0 -58
  128. package/src/commands/templates.ts +0 -277
  129. package/src/index.ts +0 -166
  130. package/src/lib/admin-auth.ts +0 -157
  131. package/src/lib/admin-client.test.ts +0 -51
  132. package/src/lib/admin-client.ts +0 -326
  133. package/src/lib/admin-platform-routes.ts +0 -1017
  134. package/src/lib/admin-platform-stream.test.ts +0 -434
  135. package/src/lib/admin-platform-stream.ts +0 -788
  136. package/src/lib/admin-responses.ts +0 -211
  137. package/src/lib/admin-server.test.ts +0 -781
  138. package/src/lib/admin-server.ts +0 -797
  139. package/src/lib/auth-store.ts +0 -87
  140. package/src/lib/daemon-registry.test.ts +0 -429
  141. package/src/lib/daemon-registry.ts +0 -343
  142. package/src/lib/le/le-apply.test.ts +0 -591
  143. package/src/lib/le/le-apply.ts +0 -832
  144. package/src/lib/le/le-auth.ts +0 -89
  145. package/src/lib/le/le-cleanup.test.ts +0 -168
  146. package/src/lib/le/le-cleanup.ts +0 -399
  147. package/src/lib/le/le-cli-helpers.ts +0 -77
  148. package/src/lib/le/le-client.ts +0 -184
  149. package/src/lib/le/le-concurrency.test.ts +0 -157
  150. package/src/lib/le/le-concurrency.ts +0 -78
  151. package/src/lib/le/le-connection-test.ts +0 -42
  152. package/src/lib/le/le-devicehub-properties.test.ts +0 -53
  153. package/src/lib/le/le-devicehub-properties.ts +0 -33
  154. package/src/lib/le/le-devicehub.ts +0 -213
  155. package/src/lib/le/le-digital-twins.ts +0 -285
  156. package/src/lib/le/le-drivers.ts +0 -23
  157. package/src/lib/le/le-endpoint.ts +0 -40
  158. package/src/lib/le/le-environments.test.ts +0 -73
  159. package/src/lib/le/le-environments.ts +0 -156
  160. package/src/lib/le/le-markers.ts +0 -87
  161. package/src/lib/le/le-plan.test.ts +0 -26
  162. package/src/lib/le/le-plan.ts +0 -109
  163. package/src/lib/le/le-platform-operations.test.ts +0 -198
  164. package/src/lib/le/le-platform-operations.ts +0 -340
  165. package/src/lib/le/le-static-model-schema.ts +0 -78
  166. package/src/lib/le/le-static-planner.test.ts +0 -255
  167. package/src/lib/le/le-static-planner.ts +0 -442
  168. package/src/lib/le/le-template-exporter.test.ts +0 -527
  169. package/src/lib/le/le-template-exporter.ts +0 -459
  170. package/src/lib/le/le-value-types.ts +0 -23
  171. package/src/lib/mqtt-client.ts +0 -115
  172. package/src/lib/parse.test.ts +0 -123
  173. package/src/lib/parse.ts +0 -87
  174. package/src/lib/resolve-simulation.test.ts +0 -118
  175. package/src/lib/resolve-simulation.ts +0 -181
  176. package/src/lib/session-store.ts +0 -110
  177. package/src/lib/shutdown.test.ts +0 -562
  178. package/src/lib/shutdown.ts +0 -621
  179. package/src/lib/template-category.test.ts +0 -71
  180. package/src/lib/template-category.ts +0 -72
  181. package/src/lib/template-locator.ts +0 -55
  182. package/src/lib/template-utils.ts +0 -45
  183. package/src/lib/ws-hub.ts +0 -281
  184. package/src/logger.test.ts +0 -172
  185. package/src/logger.ts +0 -402
  186. package/src/output.test.ts +0 -64
  187. package/src/output.ts +0 -160
@@ -1,434 +0,0 @@
1
- import { afterEach, describe, expect, mock, spyOn, test } from "bun:test";
2
- import type { EventLog } from "@miravo/core";
3
- import * as adminPlatformRoutes from "./admin-platform-routes.ts";
4
- import { handleApplyStream, handleCleanupStream } from "./admin-platform-stream.ts";
5
- import * as leApply from "./le/le-apply.ts";
6
- import * as leCleanup from "./le/le-cleanup.ts";
7
- import { LEClient } from "./le/le-client.ts";
8
- import type { LEDeviceHub } from "./le/le-devicehub.ts";
9
- import type { LEDigitalTwins } from "./le/le-digital-twins.ts";
10
- import type { LEEnvironment } from "./le/le-environments.ts";
11
- import * as leEnvironments from "./le/le-environments.ts";
12
- import type { ApplyPlan, PlanEntry } from "./le/le-plan.ts";
13
- import type { PlatformOperationRecord } from "./le/le-platform-operations.ts";
14
- import * as leOperations from "./le/le-platform-operations.ts";
15
-
16
- type ApplyContext = {
17
- readonly req: {
18
- readonly environmentId: string;
19
- readonly template?: string;
20
- readonly model?: string;
21
- readonly area?: string;
22
- readonly line?: string;
23
- readonly instance?: string;
24
- readonly ifUnmanaged?: "fail" | "skip";
25
- readonly scope?: readonly string[];
26
- };
27
- readonly env: LEEnvironment;
28
- readonly templateId: string;
29
- readonly hub: LEDeviceHub;
30
- readonly dt: LEDigitalTwins;
31
- readonly desired: {
32
- readonly devices: readonly unknown[];
33
- readonly models: readonly unknown[];
34
- readonly instances: readonly unknown[];
35
- };
36
- };
37
-
38
- type TrackerSummary = {
39
- readonly successCount: number;
40
- readonly failedCount: number;
41
- readonly blockedCount: number;
42
- readonly progressCount: number;
43
- readonly error?: string | undefined;
44
- };
45
-
46
- type ParsedSSEEvent = {
47
- readonly event: string;
48
- readonly data: Record<string, unknown>;
49
- };
50
-
51
- const APPLY_PLAN_ATTEMPTS = [
52
- createPlan("le-host", "miravo-host", [
53
- { action: "create", resourceType: "device", name: "dev-1" },
54
- { action: "create", resourceType: "device", name: "dev-2" },
55
- ]),
56
- createPlan("le-host", "miravo-host", [
57
- { action: "create", resourceType: "device", name: "dev-2" },
58
- ]),
59
- ] as const;
60
-
61
- const CLEANUP_PLAN_ATTEMPTS = [
62
- createPlan("le-host", undefined, [
63
- { action: "delete", resourceType: "device", name: "dev-1" },
64
- { action: "delete", resourceType: "device", name: "dev-2" },
65
- ]),
66
- createPlan("le-host", undefined, [{ action: "delete", resourceType: "device", name: "dev-2" }]),
67
- ] as const;
68
-
69
- afterEach(() => {
70
- mock.restore();
71
- });
72
-
73
- describe("admin-platform-stream retry accounting", () => {
74
- test("apply complete persists cumulative counts across retries", async () => {
75
- let applyBuildCallCount = 0;
76
- let applyExecuteCallCount = 0;
77
- const appendedOperations: Array<PlatformOperationRecord & TrackerSummary> = [];
78
-
79
- using resolveApplyContextSpy = spyOn(
80
- adminPlatformRoutes,
81
- "resolveApplyContext",
82
- ).mockResolvedValue({
83
- req: {
84
- environmentId: "env-1",
85
- template: "smart-factory",
86
- ifUnmanaged: "fail",
87
- scope: [],
88
- },
89
- env: {
90
- id: "env-1",
91
- platformId: leEnvironments.DEFAULT_PLATFORM_ID,
92
- label: "Detroit",
93
- hostname: "le-host",
94
- apiToken: "token",
95
- sourceHostname: "miravo-host",
96
- },
97
- templateId: "smart-factory",
98
- hub: {} as unknown as LEDeviceHub,
99
- dt: {} as unknown as LEDigitalTwins,
100
- desired: { devices: [], models: [], instances: [] },
101
- } satisfies ApplyContext);
102
- using buildApplyPlanSpy = spyOn(leApply, "buildApplyPlan").mockImplementation(async () => {
103
- const plan = latestAttemptPlan(APPLY_PLAN_ATTEMPTS, applyBuildCallCount);
104
- applyBuildCallCount++;
105
- return { plan, snapshot: {} as never };
106
- });
107
- using executePlanSpy = spyOn(leApply, "executePlan").mockImplementation(
108
- async (
109
- _hub,
110
- _dt,
111
- plan,
112
- _desired,
113
- opts: { onResourceComplete?: ((progress: leApply.ResourceProgress) => void) | undefined },
114
- ) => {
115
- applyExecuteCallCount++;
116
- if (applyExecuteCallCount === 1) {
117
- opts.onResourceComplete?.({
118
- phase: "device",
119
- resourceType: "device",
120
- name: "dev-1",
121
- action: "create",
122
- status: "success",
123
- index: 1,
124
- of: 2,
125
- });
126
- opts.onResourceComplete?.({
127
- phase: "device",
128
- resourceType: "device",
129
- name: "dev-2",
130
- action: "create",
131
- status: "failed",
132
- error: "transient failure",
133
- index: 2,
134
- of: 2,
135
- });
136
- return {
137
- plan,
138
- applied: 1,
139
- failed: 1,
140
- failedEntries: [],
141
- error: "transient failure",
142
- };
143
- }
144
-
145
- opts.onResourceComplete?.({
146
- phase: "device",
147
- resourceType: "device",
148
- name: "dev-2",
149
- action: "create",
150
- status: "success",
151
- index: 1,
152
- of: 1,
153
- });
154
- return {
155
- plan,
156
- applied: 1,
157
- failed: 0,
158
- failedEntries: [],
159
- };
160
- },
161
- );
162
- using createTrackerSpy = spyOn(
163
- leOperations,
164
- "createPlatformOperationTracker",
165
- ).mockImplementation(() => {
166
- let progressCount = 0;
167
- return {
168
- onResourceComplete() {
169
- progressCount++;
170
- },
171
- finalize(input) {
172
- return buildOperationRecord(input, progressCount);
173
- },
174
- };
175
- });
176
- using appendOperationSpy = spyOn(leOperations, "appendPlatformOperation").mockImplementation(
177
- async (record) => {
178
- appendedOperations.push(record as PlatformOperationRecord & TrackerSummary);
179
- },
180
- );
181
- using sleepSpy = spyOn(Bun, "sleep").mockResolvedValue(undefined);
182
-
183
- const pushedEvents: unknown[] = [];
184
- const eventLog = {
185
- pushExternal(event: unknown) {
186
- pushedEvents.push(event);
187
- },
188
- } as unknown as EventLog;
189
-
190
- // Regression: retry completion must retain successes + failures from earlier attempts.
191
- const response = handleApplyStream({}, eventLog);
192
- const events = await readSSE(response);
193
- const complete = events.find((event) => event.event === "complete");
194
-
195
- expect(resolveApplyContextSpy).toHaveBeenCalledTimes(1);
196
- expect(buildApplyPlanSpy).toHaveBeenCalledTimes(2);
197
- expect(executePlanSpy).toHaveBeenCalledTimes(2);
198
- expect(createTrackerSpy).toHaveBeenCalledTimes(1);
199
- expect(appendOperationSpy).toHaveBeenCalledTimes(1);
200
- expect(sleepSpy).toHaveBeenCalledTimes(1);
201
- expect(complete?.data).toEqual(
202
- expect.objectContaining({
203
- applied: 2,
204
- failed: 1,
205
- }),
206
- );
207
- expect(pushedEvents).toHaveLength(4);
208
- expect(pushedEvents.at(-1)).toEqual(
209
- expect.objectContaining({
210
- type: "platform:operation",
211
- operationType: "apply",
212
- applied: 2,
213
- failed: 1,
214
- }),
215
- );
216
- expect(appendedOperations).toEqual([
217
- expect.objectContaining({
218
- successCount: 2,
219
- failedCount: 1,
220
- blockedCount: 0,
221
- progressCount: 3,
222
- }),
223
- ]);
224
- });
225
-
226
- test("cleanup complete persists cumulative counts across retries", async () => {
227
- let cleanupBuildCallCount = 0;
228
- let cleanupExecuteCallCount = 0;
229
- const appendedOperations: Array<PlatformOperationRecord & TrackerSummary> = [];
230
-
231
- using getEnvironmentSpy = spyOn(leEnvironments, "getEnvironment").mockResolvedValue({
232
- id: "env-1",
233
- platformId: leEnvironments.DEFAULT_PLATFORM_ID,
234
- label: "Detroit",
235
- hostname: "le-host",
236
- apiToken: "token",
237
- sourceHostname: "miravo-host",
238
- });
239
- using touchEnvironmentSpy = spyOn(leEnvironments, "touchEnvironment").mockResolvedValue();
240
- using checkAuthSpy = spyOn(LEClient.prototype, "checkAuth").mockResolvedValue();
241
- using buildCleanupPlanSpy = spyOn(leCleanup, "buildCleanupPlan").mockImplementation(
242
- async () => {
243
- const plan = latestAttemptPlan(CLEANUP_PLAN_ATTEMPTS, cleanupBuildCallCount);
244
- cleanupBuildCallCount++;
245
- return { plan, snapshot: {} as never };
246
- },
247
- );
248
- using executeCleanupSpy = spyOn(leCleanup, "executeCleanup").mockImplementation(
249
- async (
250
- _hub,
251
- _dt,
252
- planResult,
253
- opts: { onResourceComplete?: ((progress: leApply.ResourceProgress) => void) | undefined },
254
- ) => {
255
- cleanupExecuteCallCount++;
256
- if (cleanupExecuteCallCount === 1) {
257
- opts.onResourceComplete?.({
258
- phase: "device",
259
- resourceType: "device",
260
- name: "dev-1",
261
- action: "delete",
262
- status: "success",
263
- index: 1,
264
- of: 2,
265
- });
266
- opts.onResourceComplete?.({
267
- phase: "device",
268
- resourceType: "device",
269
- name: "dev-2",
270
- action: "delete",
271
- status: "failed",
272
- error: "transient failure",
273
- index: 2,
274
- of: 2,
275
- });
276
- return {
277
- plan: planResult.plan,
278
- deleted: 1,
279
- failed: 1,
280
- failedEntries: [],
281
- error: "transient failure",
282
- };
283
- }
284
-
285
- opts.onResourceComplete?.({
286
- phase: "device",
287
- resourceType: "device",
288
- name: "dev-2",
289
- action: "delete",
290
- status: "success",
291
- index: 1,
292
- of: 1,
293
- });
294
- return {
295
- plan: planResult.plan,
296
- deleted: 1,
297
- failed: 0,
298
- failedEntries: [],
299
- };
300
- },
301
- );
302
- using createTrackerSpy = spyOn(
303
- leOperations,
304
- "createPlatformOperationTracker",
305
- ).mockImplementation(() => {
306
- let progressCount = 0;
307
- return {
308
- onResourceComplete() {
309
- progressCount++;
310
- },
311
- finalize(input) {
312
- return buildOperationRecord(input, progressCount);
313
- },
314
- };
315
- });
316
- using appendOperationSpy = spyOn(leOperations, "appendPlatformOperation").mockImplementation(
317
- async (record) => {
318
- appendedOperations.push(record as PlatformOperationRecord & TrackerSummary);
319
- },
320
- );
321
- using sleepSpy = spyOn(Bun, "sleep").mockResolvedValue(undefined);
322
-
323
- const pushedEvents: unknown[] = [];
324
- const eventLog = {
325
- pushExternal(event: unknown) {
326
- pushedEvents.push(event);
327
- },
328
- } as unknown as EventLog;
329
-
330
- // Regression: cleanup completion must retain deletions + failures from earlier attempts.
331
- const response = handleCleanupStream({ environmentId: "env-1", allManaged: true }, eventLog);
332
- const events = await readSSE(response);
333
- const complete = events.find((event) => event.event === "complete");
334
-
335
- expect(getEnvironmentSpy).toHaveBeenCalledTimes(1);
336
- expect(touchEnvironmentSpy).toHaveBeenCalledTimes(1);
337
- expect(checkAuthSpy).toHaveBeenCalledTimes(1);
338
- expect(buildCleanupPlanSpy).toHaveBeenCalledTimes(2);
339
- expect(executeCleanupSpy).toHaveBeenCalledTimes(2);
340
- expect(createTrackerSpy).toHaveBeenCalledTimes(1);
341
- expect(appendOperationSpy).toHaveBeenCalledTimes(1);
342
- expect(sleepSpy).toHaveBeenCalledTimes(1);
343
- expect(complete?.data).toEqual(
344
- expect.objectContaining({
345
- deleted: 2,
346
- failed: 1,
347
- }),
348
- );
349
- expect(pushedEvents).toHaveLength(4);
350
- expect(pushedEvents.at(-1)).toEqual(
351
- expect.objectContaining({
352
- type: "platform:operation",
353
- operationType: "cleanup",
354
- applied: 2,
355
- failed: 1,
356
- }),
357
- );
358
- expect(appendedOperations).toEqual([
359
- expect.objectContaining({
360
- successCount: 2,
361
- failedCount: 1,
362
- blockedCount: 0,
363
- progressCount: 3,
364
- }),
365
- ]);
366
- });
367
- });
368
-
369
- function createPlan(
370
- targetHost: string,
371
- sourceHostname: string | undefined,
372
- entries: readonly Pick<PlanEntry, "action" | "resourceType" | "name">[],
373
- ): ApplyPlan {
374
- return {
375
- sourceMode: sourceHostname ? "static" : "managed",
376
- targetHost,
377
- sourceHostname,
378
- scope: [],
379
- filters: {},
380
- entries,
381
- };
382
- }
383
-
384
- function latestAttemptPlan<T>(attempts: readonly T[], index: number): T {
385
- const plan = attempts[index] ?? attempts.at(-1);
386
- if (plan !== undefined) return plan;
387
- throw new Error("Expected at least one retry plan.");
388
- }
389
-
390
- function buildOperationRecord(
391
- input: Parameters<ReturnType<typeof leOperations.createPlatformOperationTracker>["finalize"]>[0],
392
- progressCount: number,
393
- ): PlatformOperationRecord & TrackerSummary {
394
- return {
395
- schemaVersion: 1,
396
- operationId: "op-1",
397
- operationType: "apply",
398
- status: input.failedCount > 0 ? "partial" : "success",
399
- templateId: "smart-factory",
400
- environmentId: "env-1",
401
- environmentLabel: "Detroit",
402
- hostname: "le-host",
403
- scope: [],
404
- filters: {},
405
- successCount: input.successCount,
406
- failedCount: input.failedCount,
407
- blockedCount: input.blockedCount ?? 0,
408
- startedAt: 0,
409
- completedAt: 0,
410
- durationMs: 0,
411
- resourceCounts: {
412
- device: { create: 0, update: 0, replace: 0, delete: 0, failed: 0, total: 0 },
413
- model: { create: 0, update: 0, replace: 0, delete: 0, failed: 0, total: 0 },
414
- instance: { create: 0, update: 0, replace: 0, delete: 0, failed: 0, total: 0 },
415
- "param-sync": { create: 0, update: 0, replace: 0, delete: 0, failed: 0, total: 0 },
416
- },
417
- progressCount,
418
- ...(input.error ? { error: input.error } : {}),
419
- };
420
- }
421
-
422
- async function readSSE(response: Response): Promise<readonly ParsedSSEEvent[]> {
423
- const text = await response.text();
424
- return text
425
- .trim()
426
- .split("\n\n")
427
- .filter((chunk) => chunk.length > 0)
428
- .map((chunk) => {
429
- const lines = chunk.split("\n");
430
- const event = lines.find((line) => line.startsWith("event: "))?.slice(7) ?? "";
431
- const data = lines.find((line) => line.startsWith("data: "))?.slice(6) ?? "{}";
432
- return { event, data: JSON.parse(data) as Record<string, unknown> };
433
- });
434
- }