ultravisor 1.0.3 → 1.0.8

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 (248) hide show
  1. package/.claude/launch.json +11 -0
  2. package/.claude/ultravisor-dev-config.json +3 -0
  3. package/card-audit.json +1545 -0
  4. package/docs/_sidebar.md +17 -0
  5. package/docs/css/docuserve.css +73 -0
  6. package/docs/features/beacon-authentication.md +209 -0
  7. package/docs/features/beacon-providers.md +1299 -0
  8. package/docs/features/beacons.md +530 -0
  9. package/docs/features/capabilities.md +193 -0
  10. package/docs/features/llm-model-setup.md +505 -0
  11. package/docs/features/llm.md +262 -0
  12. package/docs/features/tasks-content-system.md +110 -0
  13. package/docs/features/tasks-data-transform.md +233 -0
  14. package/docs/features/tasks-extension.md +36 -0
  15. package/docs/features/tasks-file-system.md +203 -0
  16. package/docs/features/tasks-flow-control.md +125 -0
  17. package/docs/features/tasks-http-client.md +119 -0
  18. package/docs/features/tasks-llm.md +118 -0
  19. package/docs/features/tasks-meadow-api.md +163 -0
  20. package/docs/features/tasks-user-interaction.md +49 -0
  21. package/docs/features/tasks.md +14 -0
  22. package/docs/index.html +1 -1
  23. package/docs/retold-catalog.json +1 -1
  24. package/docs/retold-keyword-index.json +17374 -5
  25. package/operation-library/api-data-pipeline.json +143 -0
  26. package/operation-library/beacon-remote-command.json +72 -0
  27. package/operation-library/conditional-file-backup.json +143 -0
  28. package/operation-library/config-processor.json +136 -0
  29. package/operation-library/csv-data-analysis.json +228 -0
  30. package/operation-library/csv-to-json.json +160 -0
  31. package/operation-library/directory-inventory.json +128 -0
  32. package/operation-library/expression-calculator.json +114 -0
  33. package/operation-library/file-search-replace.json +175 -0
  34. package/operation-library/git-status-report.json +368 -0
  35. package/operation-library/health-check-ping.json +329 -0
  36. package/operation-library/json-config-merge.json +313 -0
  37. package/operation-library/llm-embedding-tool-use.json +314 -0
  38. package/operation-library/llm-multi-turn-analysis.json +322 -0
  39. package/operation-library/llm-summarize.json +264 -0
  40. package/operation-library/log-line-counter.json +185 -0
  41. package/operation-library/meadow-crud-lifecycle.json +264 -0
  42. package/operation-library/npm-project-validator.json +193 -0
  43. package/operation-library/rest-api-orchestrator.json +218 -0
  44. package/operation-library/shell-system-info.json +137 -0
  45. package/operation-library/simple-echo.json +95 -0
  46. package/operation-library/template-processor.json +299 -0
  47. package/operation-library/text-sanitizer.json +176 -0
  48. package/package.json +12 -8
  49. package/source/Ultravisor.cjs +30 -3
  50. package/source/beacon/Ultravisor-Beacon-CLI.cjs +143 -0
  51. package/source/beacon/Ultravisor-Beacon-CapabilityProvider.cjs +129 -0
  52. package/source/beacon/Ultravisor-Beacon-Client.cjs +568 -0
  53. package/source/beacon/Ultravisor-Beacon-Executor.cjs +500 -0
  54. package/source/beacon/Ultravisor-Beacon-ProviderRegistry.cjs +330 -0
  55. package/source/beacon/providers/Ultravisor-Beacon-Provider-FileSystem.cjs +331 -0
  56. package/source/beacon/providers/Ultravisor-Beacon-Provider-LLM.cjs +966 -0
  57. package/source/beacon/providers/Ultravisor-Beacon-Provider-Shell.cjs +95 -0
  58. package/source/cli/Ultravisor-CLIProgram.cjs +44 -22
  59. package/source/cli/commands/Ultravisor-Command-SingleOperation.cjs +29 -18
  60. package/source/cli/commands/Ultravisor-Command-SingleTask.cjs +62 -19
  61. package/source/cli/commands/Ultravisor-Command-UpdateTask.cjs +27 -15
  62. package/source/config/Ultravisor-Default-Command-Configuration.cjs +12 -3
  63. package/source/services/Ultravisor-Beacon-Coordinator.cjs +966 -0
  64. package/source/services/Ultravisor-ExecutionEngine.cjs +1093 -0
  65. package/source/services/Ultravisor-ExecutionManifest.cjs +629 -0
  66. package/source/services/Ultravisor-Hypervisor-State.cjs +270 -97
  67. package/source/services/Ultravisor-Hypervisor.cjs +185 -75
  68. package/source/services/Ultravisor-Schedule-Persistence-Base.cjs +45 -0
  69. package/source/services/Ultravisor-StateManager.cjs +253 -0
  70. package/source/services/Ultravisor-TaskTypeRegistry.cjs +213 -0
  71. package/source/services/persistence/Ultravisor-Schedule-Persistence-JSONFile.cjs +140 -0
  72. package/source/services/tasks/Ultravisor-BuiltIn-TaskConfigs.cjs +19 -0
  73. package/source/services/tasks/Ultravisor-TaskHelper-BeaconDispatch.cjs +107 -0
  74. package/source/services/tasks/Ultravisor-TaskType-Base.cjs +125 -0
  75. package/source/services/tasks/content-system/Ultravisor-TaskConfigs-ContentSystem.cjs +90 -0
  76. package/source/services/tasks/content-system/definitions/content-create-folder.json +24 -0
  77. package/source/services/tasks/content-system/definitions/content-list-files.json +27 -0
  78. package/source/services/tasks/content-system/definitions/content-read-file.json +26 -0
  79. package/source/services/tasks/content-system/definitions/content-save-file.json +26 -0
  80. package/source/services/tasks/data-transform/Ultravisor-TaskConfigs-DataTransform.cjs +577 -0
  81. package/source/services/tasks/data-transform/Ultravisor-TaskType-ReplaceString.cjs +80 -0
  82. package/source/services/tasks/data-transform/Ultravisor-TaskType-SetValues.cjs +74 -0
  83. package/source/services/tasks/data-transform/Ultravisor-TaskType-StringAppender.cjs +90 -0
  84. package/source/services/tasks/data-transform/definitions/comprehension-intersect.json +24 -0
  85. package/source/services/tasks/data-transform/definitions/csv-transform.json +24 -0
  86. package/source/services/tasks/data-transform/definitions/expression-solver.json +20 -0
  87. package/source/services/tasks/data-transform/definitions/histogram.json +23 -0
  88. package/source/services/tasks/data-transform/definitions/parse-csv.json +27 -0
  89. package/source/services/tasks/data-transform/definitions/replace-string.json +27 -0
  90. package/source/services/tasks/data-transform/definitions/set-values.json +17 -0
  91. package/source/services/tasks/data-transform/definitions/string-appender.json +22 -0
  92. package/source/services/tasks/data-transform/definitions/template-string.json +20 -0
  93. package/source/services/tasks/extension/Ultravisor-TaskConfigs-Extension.cjs +87 -0
  94. package/source/services/tasks/extension/definitions/beacon-dispatch.json +32 -0
  95. package/source/services/tasks/file-system/Ultravisor-TaskConfigs-FileSystem.cjs +509 -0
  96. package/source/services/tasks/file-system/Ultravisor-TaskType-ReadFile.cjs +96 -0
  97. package/source/services/tasks/file-system/Ultravisor-TaskType-ReadFileBuffered.cjs +123 -0
  98. package/source/services/tasks/file-system/Ultravisor-TaskType-WriteFile.cjs +106 -0
  99. package/source/services/tasks/file-system/definitions/copy-file.json +27 -0
  100. package/source/services/tasks/file-system/definitions/list-files.json +27 -0
  101. package/source/services/tasks/file-system/definitions/read-file-buffered.json +31 -0
  102. package/source/services/tasks/file-system/definitions/read-file.json +26 -0
  103. package/source/services/tasks/file-system/definitions/read-json.json +23 -0
  104. package/source/services/tasks/file-system/definitions/write-file.json +29 -0
  105. package/source/services/tasks/file-system/definitions/write-json.json +30 -0
  106. package/source/services/tasks/flow-control/Ultravisor-TaskConfigs-FlowControl.cjs +353 -0
  107. package/source/services/tasks/flow-control/Ultravisor-TaskType-IfConditional.cjs +125 -0
  108. package/source/services/tasks/flow-control/Ultravisor-TaskType-LaunchOperation.cjs +186 -0
  109. package/source/services/tasks/flow-control/Ultravisor-TaskType-SplitExecute.cjs +164 -0
  110. package/source/services/tasks/flow-control/definitions/if-conditional.json +25 -0
  111. package/source/services/tasks/flow-control/definitions/launch-operation.json +27 -0
  112. package/source/services/tasks/flow-control/definitions/split-execute.json +32 -0
  113. package/source/services/tasks/http-client/Ultravisor-TaskConfigs-HttpClient.cjs +281 -0
  114. package/source/services/tasks/http-client/definitions/get-json.json +26 -0
  115. package/source/services/tasks/http-client/definitions/get-text.json +26 -0
  116. package/source/services/tasks/http-client/definitions/rest-request.json +32 -0
  117. package/source/services/tasks/http-client/definitions/send-json.json +28 -0
  118. package/source/services/tasks/llm/Ultravisor-TaskConfigs-LLM.cjs +605 -0
  119. package/source/services/tasks/llm/definitions/llm-chat-completion.json +46 -0
  120. package/source/services/tasks/llm/definitions/llm-embedding.json +31 -0
  121. package/source/services/tasks/llm/definitions/llm-tool-use.json +42 -0
  122. package/source/services/tasks/meadow-api/Ultravisor-TaskConfigs-MeadowApi.cjs +341 -0
  123. package/source/services/tasks/meadow-api/definitions/meadow-count.json +26 -0
  124. package/source/services/tasks/meadow-api/definitions/meadow-create.json +26 -0
  125. package/source/services/tasks/meadow-api/definitions/meadow-delete.json +23 -0
  126. package/source/services/tasks/meadow-api/definitions/meadow-read.json +26 -0
  127. package/source/services/tasks/meadow-api/definitions/meadow-reads.json +29 -0
  128. package/source/services/tasks/meadow-api/definitions/meadow-update.json +26 -0
  129. package/source/services/tasks/shell/Ultravisor-TaskConfigs-Shell.cjs +63 -0
  130. package/source/services/tasks/shell/definitions/command.json +29 -0
  131. package/source/services/tasks/user-interaction/Ultravisor-TaskConfigs-UserInteraction.cjs +64 -0
  132. package/source/services/tasks/user-interaction/Ultravisor-TaskType-ErrorMessage.cjs +55 -0
  133. package/source/services/tasks/user-interaction/Ultravisor-TaskType-ValueInput.cjs +47 -0
  134. package/source/services/tasks/user-interaction/definitions/error-message.json +18 -0
  135. package/source/services/tasks/user-interaction/definitions/value-input.json +23 -0
  136. package/source/web_server/Ultravisor-API-Server.cjs +849 -124
  137. package/test/Ultravisor_browser_tests.js +2226 -0
  138. package/test/Ultravisor_operation_library_tests.js +894 -0
  139. package/test/Ultravisor_tests.js +4946 -5044
  140. package/test/workflows/test-content-concatenate-all.json +339 -0
  141. package/test/workflows/test-content-full-pipeline.json +159 -0
  142. package/test/workflows/test-content-list-files.json +76 -0
  143. package/test/workflows/test-content-save-and-read.json +106 -0
  144. package/webinterface/css/ultravisor-themes.css +668 -0
  145. package/webinterface/css/ultravisor.css +37 -14
  146. package/webinterface/docs/card-help/beacon-dispatch.md +30 -0
  147. package/webinterface/docs/card-help/command.md +27 -0
  148. package/webinterface/docs/card-help/comprehension-intersect.md +24 -0
  149. package/webinterface/docs/card-help/content-create-folder.md +22 -0
  150. package/webinterface/docs/card-help/content-list-files.md +25 -0
  151. package/webinterface/docs/card-help/content-read-file.md +24 -0
  152. package/webinterface/docs/card-help/content-save-file.md +24 -0
  153. package/webinterface/docs/card-help/copy-file.md +25 -0
  154. package/webinterface/docs/card-help/csv-transform.md +24 -0
  155. package/webinterface/docs/card-help/end.md +11 -0
  156. package/webinterface/docs/card-help/error-message.md +16 -0
  157. package/webinterface/docs/card-help/expression-solver.md +21 -0
  158. package/webinterface/docs/card-help/get-json.md +24 -0
  159. package/webinterface/docs/card-help/get-text.md +24 -0
  160. package/webinterface/docs/card-help/histogram.md +23 -0
  161. package/webinterface/docs/card-help/if-conditional.md +23 -0
  162. package/webinterface/docs/card-help/launch-operation.md +25 -0
  163. package/webinterface/docs/card-help/list-files.md +25 -0
  164. package/webinterface/docs/card-help/llm-chat-completion.md +43 -0
  165. package/webinterface/docs/card-help/llm-embedding.md +24 -0
  166. package/webinterface/docs/card-help/llm-tool-use.md +39 -0
  167. package/webinterface/docs/card-help/meadow-count.md +24 -0
  168. package/webinterface/docs/card-help/meadow-create.md +24 -0
  169. package/webinterface/docs/card-help/meadow-delete.md +19 -0
  170. package/webinterface/docs/card-help/meadow-read.md +24 -0
  171. package/webinterface/docs/card-help/meadow-reads.md +27 -0
  172. package/webinterface/docs/card-help/meadow-update.md +24 -0
  173. package/webinterface/docs/card-help/parse-csv.md +27 -0
  174. package/webinterface/docs/card-help/read-file-buffered.md +29 -0
  175. package/webinterface/docs/card-help/read-file.md +24 -0
  176. package/webinterface/docs/card-help/read-json.md +21 -0
  177. package/webinterface/docs/card-help/replace-string.md +25 -0
  178. package/webinterface/docs/card-help/rest-request.md +30 -0
  179. package/webinterface/docs/card-help/send-json.md +26 -0
  180. package/webinterface/docs/card-help/set-values.md +16 -0
  181. package/webinterface/docs/card-help/split-execute.md +35 -0
  182. package/webinterface/docs/card-help/start.md +11 -0
  183. package/webinterface/docs/card-help/string-appender.md +22 -0
  184. package/webinterface/docs/card-help/template-string.md +21 -0
  185. package/webinterface/docs/card-help/value-input.md +24 -0
  186. package/webinterface/docs/card-help/write-file.md +27 -0
  187. package/webinterface/docs/card-help/write-json.md +28 -0
  188. package/webinterface/html/index.html +5 -0
  189. package/webinterface/package.json +13 -4
  190. package/webinterface/source/Pict-Application-Ultravisor.js +564 -70
  191. package/webinterface/source/Ultravisor-TimingUtils.js +99 -0
  192. package/webinterface/source/card-help-content.js +46 -0
  193. package/webinterface/source/cards/Ultravisor-BuiltIn-CardConfigs.js +174 -0
  194. package/webinterface/source/cards/Ultravisor-CardConfigGenerator.js +431 -0
  195. package/webinterface/source/data/ExampleFlow-CSVPipeline.js +231 -0
  196. package/webinterface/source/data/ExampleFlow-FileProcessor.js +315 -0
  197. package/webinterface/source/data/ExampleFlow-MeadowPipeline.js +328 -0
  198. package/webinterface/source/panels/Ultravisor-CardSettingsPanel.js +902 -0
  199. package/webinterface/source/providers/PictRouter-Ultravisor-Configuration.json +20 -8
  200. package/webinterface/source/views/PictView-Ultravisor-BeaconList.js +817 -0
  201. package/webinterface/source/views/PictView-Ultravisor-BottomBar.js +5 -5
  202. package/webinterface/source/views/PictView-Ultravisor-Dashboard.js +25 -25
  203. package/webinterface/source/views/PictView-Ultravisor-Documentation.js +856 -0
  204. package/webinterface/source/views/PictView-Ultravisor-FlowEditor.js +777 -0
  205. package/webinterface/source/views/PictView-Ultravisor-ManifestList.js +321 -58
  206. package/webinterface/source/views/PictView-Ultravisor-OperationEdit.js +36 -91
  207. package/webinterface/source/views/PictView-Ultravisor-OperationList.js +388 -16
  208. package/webinterface/source/views/PictView-Ultravisor-PendingInput.js +314 -0
  209. package/webinterface/source/views/PictView-Ultravisor-Schedule.js +521 -65
  210. package/webinterface/source/views/PictView-Ultravisor-TimingView.js +333 -71
  211. package/webinterface/source/views/PictView-Ultravisor-TopBar.js +257 -21
  212. package/webinterface/theme-sampler.html +645 -0
  213. package/debug/Harness.js +0 -6
  214. package/source/services/Ultravisor-Operation-Manifest.cjs +0 -160
  215. package/source/services/Ultravisor-Operation.cjs +0 -200
  216. package/source/services/Ultravisor-Task.cjs +0 -349
  217. package/source/services/events/Ultravisor-Hypervisor-Event-Solver.cjs +0 -11
  218. package/source/services/tasks/Ultravisor-Task-Base.cjs +0 -264
  219. package/source/services/tasks/Ultravisor-Task-CollectValues.cjs +0 -188
  220. package/source/services/tasks/Ultravisor-Task-Command.cjs +0 -65
  221. package/source/services/tasks/Ultravisor-Task-CommandEach.cjs +0 -190
  222. package/source/services/tasks/Ultravisor-Task-Conditional.cjs +0 -104
  223. package/source/services/tasks/Ultravisor-Task-DateWindow.cjs +0 -72
  224. package/source/services/tasks/Ultravisor-Task-GeneratePagedOperation.cjs +0 -336
  225. package/source/services/tasks/Ultravisor-Task-LaunchOperation.cjs +0 -143
  226. package/source/services/tasks/Ultravisor-Task-LaunchTask.cjs +0 -146
  227. package/source/services/tasks/Ultravisor-Task-LineMatch.cjs +0 -158
  228. package/source/services/tasks/Ultravisor-Task-Request.cjs +0 -56
  229. package/source/services/tasks/Ultravisor-Task-Solver.cjs +0 -89
  230. package/source/services/tasks/Ultravisor-Task-TemplateString.cjs +0 -93
  231. package/source/services/tasks/rest/Ultravisor-Task-GetBinary.cjs +0 -127
  232. package/source/services/tasks/rest/Ultravisor-Task-GetJSON.cjs +0 -119
  233. package/source/services/tasks/rest/Ultravisor-Task-GetText.cjs +0 -109
  234. package/source/services/tasks/rest/Ultravisor-Task-GetXML.cjs +0 -112
  235. package/source/services/tasks/rest/Ultravisor-Task-RestRequest.cjs +0 -499
  236. package/source/services/tasks/rest/Ultravisor-Task-SendJSON.cjs +0 -150
  237. package/source/services/tasks/stagingfiles/Ultravisor-Task-CopyFile.cjs +0 -110
  238. package/source/services/tasks/stagingfiles/Ultravisor-Task-ListFiles.cjs +0 -89
  239. package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadBinary.cjs +0 -87
  240. package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadJSON.cjs +0 -67
  241. package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadText.cjs +0 -66
  242. package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadXML.cjs +0 -69
  243. package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteBinary.cjs +0 -95
  244. package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteJSON.cjs +0 -96
  245. package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteText.cjs +0 -99
  246. package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteXML.cjs +0 -102
  247. package/webinterface/source/views/PictView-Ultravisor-TaskEdit.js +0 -220
  248. package/webinterface/source/views/PictView-Ultravisor-TaskList.js +0 -248
@@ -0,0 +1,894 @@
1
+ /**
2
+ * Ultravisor -- Operation Library Visual Tests
3
+ *
4
+ * Loads each operation from the operation-library/ folder, imports it,
5
+ * opens it in the Flow Editor, executes it via the API, and takes
6
+ * screenshots at each major point:
7
+ * 1. Flow loaded in editor (shows nodes, ports, and connections)
8
+ * 2. Execution result (shows status, task outputs, logs)
9
+ * 3. Manifest/timing view after execution
10
+ *
11
+ * Operations that require user input (value-input) are tested with
12
+ * the pause/resume flow. Operations requiring external services
13
+ * (LLM, Beacon, HTTP endpoints, Meadow) are loaded and rendered
14
+ * but skipped for execution.
15
+ *
16
+ * Requires: puppeteer (dev dependency)
17
+ * Requires: webinterface/dist/ to be pre-built (npx quack build)
18
+ */
19
+ const libAssert = require('assert');
20
+ const libPath = require('path');
21
+ const libFs = require('fs');
22
+ const libPuppeteer = require('puppeteer');
23
+ const libPict = require('pict');
24
+
25
+ // Services
26
+ const libServiceHypervisor = require('../source/services/Ultravisor-Hypervisor.cjs');
27
+ const libServiceHypervisorState = require('../source/services/Ultravisor-Hypervisor-State.cjs');
28
+ const libServiceHypervisorEventBase = require('../source/services/Ultravisor-Hypervisor-Event-Base.cjs');
29
+ const libServiceHypervisorEventCron = require('../source/services/events/Ultravisor-Hypervisor-Event-Cron.cjs');
30
+ const libServiceTaskTypeRegistry = require('../source/services/Ultravisor-TaskTypeRegistry.cjs');
31
+ const libServiceStateManager = require('../source/services/Ultravisor-StateManager.cjs');
32
+ const libServiceExecutionEngine = require('../source/services/Ultravisor-ExecutionEngine.cjs');
33
+ const libServiceExecutionManifest = require('../source/services/Ultravisor-ExecutionManifest.cjs');
34
+ const libWebServerAPIServer = require('../source/web_server/Ultravisor-API-Server.cjs');
35
+
36
+ // ── Module-scope state ──────────────────────────────────
37
+ let _Browser = null;
38
+ let _Page = null;
39
+ let _Fable = null;
40
+ let _BaseURL = '';
41
+ let _ScreenshotDir = '';
42
+ let _ScreenshotIndex = 0;
43
+ let _ConsoleErrors = [];
44
+ let _StagingDir = '';
45
+ let _OperationLibraryPath = '';
46
+ let _TestResults =
47
+ {
48
+ timestamp: new Date().toISOString(),
49
+ totalTests: 0,
50
+ passed: 0,
51
+ failed: 0,
52
+ screenshots: [],
53
+ operations: [],
54
+ duration: ''
55
+ };
56
+ let _StartTime = Date.now();
57
+
58
+ // ── Operations that need external services (skip execution) ──
59
+ const _SkipExecutionTypes =
60
+ [
61
+ 'llm-chat-completion', 'llm-embedding', 'llm-tool-use',
62
+ 'beacon-dispatch',
63
+ 'get-json', 'get-text', 'send-json', 'rest-request',
64
+ 'meadow-read', 'meadow-reads', 'meadow-create', 'meadow-update', 'meadow-delete', 'meadow-count'
65
+ ];
66
+
67
+ // Operations that use value-input (need pause/resume handling)
68
+ const _InteractiveOperations =
69
+ [
70
+ 'expression-calculator.json',
71
+ 'file-search-replace.json',
72
+ 'text-sanitizer.json'
73
+ ];
74
+
75
+ // Test input values for interactive operations
76
+ const _InteractiveInputValues =
77
+ {
78
+ 'expression-calculator.json': '2+3*4',
79
+ 'file-search-replace.json': 'test-input.txt',
80
+ 'text-sanitizer.json': 'test-sanitize.txt'
81
+ };
82
+
83
+ // ── Helpers ─────────────────────────────────────────────
84
+
85
+ function takeScreenshot(pName)
86
+ {
87
+ _ScreenshotIndex++;
88
+ let tmpPadded = String(_ScreenshotIndex).padStart(3, '0');
89
+ let tmpFilename = tmpPadded + '-' + pName + '.png';
90
+ _TestResults.screenshots.push(tmpFilename);
91
+ return _Page.screenshot(
92
+ {
93
+ path: libPath.join(_ScreenshotDir, tmpFilename),
94
+ fullPage: false
95
+ });
96
+ }
97
+
98
+ async function settle(pMs)
99
+ {
100
+ await _Page.evaluate((pDelay) => new Promise((r) => setTimeout(r, pDelay)), pMs || 500);
101
+ }
102
+
103
+ async function waitForAppReady()
104
+ {
105
+ await _Page.waitForSelector('#Ultravisor-Application-Container', { timeout: 15000 });
106
+ try
107
+ {
108
+ await _Page.waitForFunction(
109
+ () =>
110
+ {
111
+ return (typeof(window.Pict) !== 'undefined') || (typeof(window._Pict) !== 'undefined');
112
+ },
113
+ { timeout: 10000 }
114
+ );
115
+ }
116
+ catch (pErr)
117
+ {
118
+ console.log(' [Warning] Pict global not found after 10s; proceeding without it.');
119
+ }
120
+ await settle(2000);
121
+ }
122
+
123
+ async function navigateToRoute(pRoute)
124
+ {
125
+ await _Page.evaluate((pHash) =>
126
+ {
127
+ window.location.hash = pHash;
128
+ }, pRoute);
129
+ await settle(800);
130
+ }
131
+
132
+ async function apiGet(pPath)
133
+ {
134
+ return _Page.evaluate(async (pBaseURL, pPath) =>
135
+ {
136
+ let tmpRes = await fetch(pBaseURL + pPath);
137
+ return { status: tmpRes.status, body: await tmpRes.json() };
138
+ }, _BaseURL, pPath);
139
+ }
140
+
141
+ async function apiPost(pPath, pBody)
142
+ {
143
+ return _Page.evaluate(async (pBaseURL, pPath, pBody) =>
144
+ {
145
+ let tmpRes = await fetch(pBaseURL + pPath,
146
+ {
147
+ method: 'POST',
148
+ headers: { 'Content-Type': 'application/json' },
149
+ body: JSON.stringify(pBody)
150
+ });
151
+ return { status: tmpRes.status, body: await tmpRes.json() };
152
+ }, _BaseURL, pPath, pBody);
153
+ }
154
+
155
+ async function apiCreateOperation(pOperationData)
156
+ {
157
+ return _Page.evaluate(async (pBaseURL, pData) =>
158
+ {
159
+ let tmpRes = await fetch(pBaseURL + '/Operation',
160
+ {
161
+ method: 'POST',
162
+ headers: { 'Content-Type': 'application/json' },
163
+ body: JSON.stringify(pData)
164
+ });
165
+ return tmpRes.json();
166
+ }, _BaseURL, pOperationData);
167
+ }
168
+
169
+ async function apiExecuteOperation(pHash)
170
+ {
171
+ return _Page.evaluate(async (pBaseURL, pHash) =>
172
+ {
173
+ let tmpRes = await fetch(pBaseURL + '/Operation/' + pHash + '/Execute');
174
+ return { status: tmpRes.status, body: await tmpRes.json() };
175
+ }, _BaseURL, pHash);
176
+ }
177
+
178
+ /**
179
+ * Check if an operation's graph uses any task types that require external services.
180
+ */
181
+ function operationNeedsExternalServices(pOperationDef)
182
+ {
183
+ if (!pOperationDef || !pOperationDef.Graph || !Array.isArray(pOperationDef.Graph.Nodes))
184
+ {
185
+ return false;
186
+ }
187
+
188
+ for (let i = 0; i < pOperationDef.Graph.Nodes.length; i++)
189
+ {
190
+ let tmpType = pOperationDef.Graph.Nodes[i].Type;
191
+ if (_SkipExecutionTypes.indexOf(tmpType) >= 0)
192
+ {
193
+ return true;
194
+ }
195
+ }
196
+
197
+ return false;
198
+ }
199
+
200
+ /**
201
+ * Check if an operation uses value-input nodes.
202
+ */
203
+ function operationHasValueInput(pOperationDef)
204
+ {
205
+ if (!pOperationDef || !pOperationDef.Graph || !Array.isArray(pOperationDef.Graph.Nodes))
206
+ {
207
+ return false;
208
+ }
209
+
210
+ for (let i = 0; i < pOperationDef.Graph.Nodes.length; i++)
211
+ {
212
+ if (pOperationDef.Graph.Nodes[i].Type === 'value-input')
213
+ {
214
+ return true;
215
+ }
216
+ }
217
+
218
+ return false;
219
+ }
220
+
221
+ /**
222
+ * Get the first value-input node hash from an operation.
223
+ */
224
+ function getValueInputNodeHash(pOperationDef)
225
+ {
226
+ if (!pOperationDef || !pOperationDef.Graph || !Array.isArray(pOperationDef.Graph.Nodes))
227
+ {
228
+ return null;
229
+ }
230
+
231
+ for (let i = 0; i < pOperationDef.Graph.Nodes.length; i++)
232
+ {
233
+ if (pOperationDef.Graph.Nodes[i].Type === 'value-input')
234
+ {
235
+ return pOperationDef.Graph.Nodes[i].Hash;
236
+ }
237
+ }
238
+
239
+ return null;
240
+ }
241
+
242
+ /**
243
+ * Count event ports, state ports, and connections in an operation graph.
244
+ */
245
+ function analyzeGraphPorts(pOperationDef)
246
+ {
247
+ let tmpResult =
248
+ {
249
+ NodeCount: 0,
250
+ ConnectionCount: 0,
251
+ EventPorts: 0,
252
+ StatePorts: 0,
253
+ EventConnections: 0,
254
+ StateConnections: 0
255
+ };
256
+
257
+ if (!pOperationDef || !pOperationDef.Graph)
258
+ {
259
+ return tmpResult;
260
+ }
261
+
262
+ let tmpNodes = pOperationDef.Graph.Nodes || [];
263
+ let tmpConnections = pOperationDef.Graph.Connections || [];
264
+
265
+ tmpResult.NodeCount = tmpNodes.length;
266
+ tmpResult.ConnectionCount = tmpConnections.length;
267
+
268
+ for (let i = 0; i < tmpNodes.length; i++)
269
+ {
270
+ let tmpPorts = tmpNodes[i].Ports || [];
271
+ for (let j = 0; j < tmpPorts.length; j++)
272
+ {
273
+ let tmpPort = tmpPorts[j];
274
+ let tmpHash = tmpPort.Hash || '';
275
+ let tmpSide = tmpPort.Side || '';
276
+
277
+ if (tmpHash.includes('-so-') || tmpHash.includes('-si-') ||
278
+ tmpSide === 'right-top' || tmpSide === 'left-top')
279
+ {
280
+ tmpResult.StatePorts++;
281
+ }
282
+ else
283
+ {
284
+ tmpResult.EventPorts++;
285
+ }
286
+ }
287
+ }
288
+
289
+ for (let i = 0; i < tmpConnections.length; i++)
290
+ {
291
+ let tmpConn = tmpConnections[i];
292
+ let tmpSourceHash = tmpConn.SourcePortHash || '';
293
+ let tmpTargetHash = tmpConn.TargetPortHash || '';
294
+
295
+ if (tmpConn.ConnectionType === 'State' ||
296
+ tmpSourceHash.includes('-so-') || tmpTargetHash.includes('-si-'))
297
+ {
298
+ tmpResult.StateConnections++;
299
+ }
300
+ else
301
+ {
302
+ tmpResult.EventConnections++;
303
+ }
304
+ }
305
+
306
+ return tmpResult;
307
+ }
308
+
309
+
310
+ // ── Test Suite ──────────────────────────────────────────
311
+
312
+ suite
313
+ (
314
+ 'Operation Library Visual Tests',
315
+ function ()
316
+ {
317
+ this.timeout(600000);
318
+
319
+ suiteSetup
320
+ (
321
+ function (fDone)
322
+ {
323
+ this.timeout(60000);
324
+
325
+ _ScreenshotDir = libPath.join(__dirname, '..', 'debug', 'dist', 'operation_library_test_output');
326
+ _StagingDir = libPath.resolve(__dirname, '..', '.test_staging_oplib');
327
+ _OperationLibraryPath = libPath.resolve(__dirname, '..', 'operation-library');
328
+
329
+ // Clean and create output directory
330
+ if (libFs.existsSync(_ScreenshotDir))
331
+ {
332
+ libFs.rmSync(_ScreenshotDir, { recursive: true, force: true });
333
+ }
334
+ libFs.mkdirSync(_ScreenshotDir, { recursive: true });
335
+
336
+ // Ensure staging directory exists
337
+ libFs.mkdirSync(_StagingDir, { recursive: true });
338
+
339
+ // Verify webinterface dist exists
340
+ let tmpWebInterfacePath = libPath.resolve(__dirname, '..', 'webinterface', 'dist');
341
+ if (!libFs.existsSync(libPath.join(tmpWebInterfacePath, 'index.html')))
342
+ {
343
+ return fDone(new Error('webinterface/dist/index.html not found. Run "npx quack build" first.'));
344
+ }
345
+
346
+ // Create test fixture files for interactive operations
347
+ libFs.writeFileSync(libPath.join(_StagingDir, 'test-input.txt'), 'Hello John, this is a test.\nJohn likes testing.\n', 'utf8');
348
+ libFs.writeFileSync(libPath.join(_StagingDir, 'test-sanitize.txt'), 'Hello\tworld\r\nDouble spaces here\n', 'utf8');
349
+
350
+ // Create Pict instance with random port
351
+ let tmpPort = 10000 + Math.floor(Math.random() * 50000);
352
+ _Fable = new libPict(
353
+ {
354
+ Product: 'Ultravisor-OpLibTest',
355
+ APIServerPort: tmpPort,
356
+ LogLevel: 5
357
+ });
358
+
359
+ _Fable.ProgramConfiguration =
360
+ {
361
+ UltravisorWebInterfacePath: tmpWebInterfacePath,
362
+ UltravisorStagingRoot: _StagingDir,
363
+ UltravisorOperationLibraryPath: _OperationLibraryPath
364
+ };
365
+
366
+ if (typeof(_Fable.gatherProgramConfiguration) !== 'function')
367
+ {
368
+ _Fable.gatherProgramConfiguration = function ()
369
+ {
370
+ return {
371
+ GatherPhases:
372
+ [
373
+ { Phase: 'Default Program Configuration' },
374
+ { Phase: 'Test Configuration', Path: libPath.join(_StagingDir, '.ultravisor.json') }
375
+ ],
376
+ ConfigurationOutcome: {}
377
+ };
378
+ };
379
+ }
380
+
381
+ // Register all services
382
+ _Fable.addAndInstantiateServiceTypeIfNotExists('UltravisorHypervisor', libServiceHypervisor);
383
+ _Fable.addAndInstantiateServiceTypeIfNotExists('UltravisorHypervisorState', libServiceHypervisorState);
384
+ _Fable.addAndInstantiateServiceTypeIfNotExists('UltravisorHypervisorEventBase', libServiceHypervisorEventBase);
385
+ _Fable.addAndInstantiateServiceTypeIfNotExists('UltravisorHypervisorEventCron', libServiceHypervisorEventCron);
386
+ _Fable.addAndInstantiateServiceTypeIfNotExists('UltravisorTaskTypeRegistry', libServiceTaskTypeRegistry);
387
+ _Fable.addAndInstantiateServiceTypeIfNotExists('UltravisorStateManager', libServiceStateManager);
388
+ _Fable.addAndInstantiateServiceTypeIfNotExists('UltravisorExecutionEngine', libServiceExecutionEngine);
389
+ _Fable.addAndInstantiateServiceTypeIfNotExists('UltravisorExecutionManifest', libServiceExecutionManifest);
390
+
391
+ let tmpRegistry = Object.values(_Fable.servicesMap['UltravisorTaskTypeRegistry'])[0];
392
+ if (tmpRegistry)
393
+ {
394
+ tmpRegistry.registerBuiltInTaskTypes();
395
+ }
396
+
397
+ // Register and start API server
398
+ _Fable.addAndInstantiateServiceTypeIfNotExists('UltravisorAPIServer', libWebServerAPIServer);
399
+ let tmpAPIServer = Object.values(_Fable.servicesMap['UltravisorAPIServer'])[0];
400
+
401
+ tmpAPIServer.start(function (pError)
402
+ {
403
+ if (pError)
404
+ {
405
+ return fDone(pError);
406
+ }
407
+
408
+ _BaseURL = 'http://localhost:' + tmpPort;
409
+
410
+ libPuppeteer.launch(
411
+ {
412
+ headless: true,
413
+ args: ['--no-sandbox', '--disable-setuid-sandbox']
414
+ }).then(function (pBrowser)
415
+ {
416
+ _Browser = pBrowser;
417
+ return _Browser.newPage();
418
+ }).then(function (pPage)
419
+ {
420
+ _Page = pPage;
421
+ return _Page.setViewport({ width: 1440, height: 900 });
422
+ }).then(function ()
423
+ {
424
+ _Page.on('console', function (pMsg)
425
+ {
426
+ if (pMsg.type() === 'error')
427
+ {
428
+ console.log(' [Browser Console Error]', pMsg.text());
429
+ }
430
+ });
431
+ _Page.on('pageerror', function (pError)
432
+ {
433
+ _ConsoleErrors.push(pError.message);
434
+ console.log(' [Browser Error]', pError.message);
435
+ });
436
+ _Page.on('requestfailed', function (pRequest)
437
+ {
438
+ console.log(' [Request Failed]', pRequest.url(), pRequest.failure().errorText);
439
+ });
440
+
441
+ return _Page.goto(_BaseURL + '/index.html', { waitUntil: 'networkidle2', timeout: 30000 });
442
+ }).then(function ()
443
+ {
444
+ return waitForAppReady();
445
+ }).then(function ()
446
+ {
447
+ fDone();
448
+ }).catch(function (pErr)
449
+ {
450
+ fDone(pErr);
451
+ });
452
+ });
453
+ }
454
+ );
455
+
456
+ suiteTeardown
457
+ (
458
+ function (fDone)
459
+ {
460
+ this.timeout(15000);
461
+
462
+ _TestResults.failed = _TestResults.totalTests - _TestResults.passed;
463
+ _TestResults.duration = ((Date.now() - _StartTime) / 1000).toFixed(1) + 's';
464
+
465
+ try
466
+ {
467
+ libFs.writeFileSync(
468
+ libPath.join(_ScreenshotDir, 'test-results.json'),
469
+ JSON.stringify(_TestResults, null, '\t'),
470
+ 'utf8'
471
+ );
472
+ }
473
+ catch (pErr)
474
+ {
475
+ console.log(' [Warning] Failed to write test-results.json:', pErr.message);
476
+ }
477
+
478
+ let tmpClosePromise = Promise.resolve();
479
+
480
+ if (_Browser)
481
+ {
482
+ tmpClosePromise = _Browser.close();
483
+ }
484
+
485
+ tmpClosePromise.then(function ()
486
+ {
487
+ if (_Fable)
488
+ {
489
+ let tmpAPIServer = Object.values(_Fable.servicesMap['UltravisorAPIServer'] || {})[0];
490
+ if (tmpAPIServer && tmpAPIServer._Orator)
491
+ {
492
+ return tmpAPIServer._Orator.stopService(function ()
493
+ {
494
+ if (libFs.existsSync(_StagingDir))
495
+ {
496
+ libFs.rmSync(_StagingDir, { recursive: true, force: true });
497
+ }
498
+ return fDone();
499
+ });
500
+ }
501
+ }
502
+ return fDone();
503
+ }).catch(function ()
504
+ {
505
+ return fDone();
506
+ });
507
+ }
508
+ );
509
+
510
+ setup(function ()
511
+ {
512
+ _TestResults.totalTests++;
513
+ });
514
+
515
+ // ════════════════════════════════════════════════
516
+ // Operation Library API
517
+ // ════════════════════════════════════════════════
518
+ suite
519
+ (
520
+ 'Operation Library API',
521
+ function ()
522
+ {
523
+ test
524
+ (
525
+ 'GET /OperationLibrary lists all operations',
526
+ async function ()
527
+ {
528
+ this.timeout(10000);
529
+
530
+ let tmpResponse = await apiGet('/OperationLibrary');
531
+
532
+ libAssert.strictEqual(tmpResponse.status, 200, 'Should return 200');
533
+ libAssert.ok(Array.isArray(tmpResponse.body), 'Should return an array');
534
+ libAssert.ok(tmpResponse.body.length >= 15, 'Should have at least 15 library operations (got ' + tmpResponse.body.length + ')');
535
+
536
+ console.log(' Library operations:', tmpResponse.body.length);
537
+ for (let i = 0; i < tmpResponse.body.length; i++)
538
+ {
539
+ console.log(' -', tmpResponse.body[i].FileName, ':', tmpResponse.body[i].Name);
540
+ }
541
+
542
+ _TestResults.passed++;
543
+ }
544
+ );
545
+ }
546
+ );
547
+
548
+ // ════════════════════════════════════════════════
549
+ // Per-Operation Tests
550
+ // ════════════════════════════════════════════════
551
+
552
+ // Get the list of operation files
553
+ let tmpOperationFiles = [];
554
+ try
555
+ {
556
+ tmpOperationFiles = libFs.readdirSync(
557
+ libPath.resolve(__dirname, '..', 'operation-library')
558
+ ).filter(function (pF) { return pF.endsWith('.json'); }).sort();
559
+ }
560
+ catch (pErr)
561
+ {
562
+ console.log(' [Warning] Could not read operation-library/:', pErr.message);
563
+ }
564
+
565
+ for (let tmpFileIndex = 0; tmpFileIndex < tmpOperationFiles.length; tmpFileIndex++)
566
+ {
567
+ // Use an IIFE to capture the filename for each iteration
568
+ (function (pFileName)
569
+ {
570
+ let tmpOperationDef = null;
571
+ let tmpImportedOpHash = '';
572
+ let tmpSlug = pFileName.replace('.json', '');
573
+
574
+ suite
575
+ (
576
+ 'Operation: ' + tmpSlug,
577
+ function ()
578
+ {
579
+ // ── Step 1: Load the operation from the library ──
580
+ test
581
+ (
582
+ 'load ' + tmpSlug + ' from library',
583
+ async function ()
584
+ {
585
+ this.timeout(15000);
586
+
587
+ let tmpResponse = await apiGet('/OperationLibrary/' + pFileName);
588
+ libAssert.strictEqual(tmpResponse.status, 200, 'Should load ' + pFileName);
589
+
590
+ tmpOperationDef = tmpResponse.body;
591
+ libAssert.ok(tmpOperationDef.Graph, 'Should have a Graph');
592
+ libAssert.ok(Array.isArray(tmpOperationDef.Graph.Nodes), 'Should have Nodes');
593
+ libAssert.ok(Array.isArray(tmpOperationDef.Graph.Connections), 'Should have Connections');
594
+
595
+ // Analyze port structure
596
+ let tmpAnalysis = analyzeGraphPorts(tmpOperationDef);
597
+
598
+ let tmpOpRecord =
599
+ {
600
+ FileName: pFileName,
601
+ Name: tmpOperationDef.Name || tmpSlug,
602
+ Nodes: tmpAnalysis.NodeCount,
603
+ Connections: tmpAnalysis.ConnectionCount,
604
+ EventPorts: tmpAnalysis.EventPorts,
605
+ StatePorts: tmpAnalysis.StatePorts,
606
+ EventConnections: tmpAnalysis.EventConnections,
607
+ StateConnections: tmpAnalysis.StateConnections,
608
+ NeedsExternalServices: operationNeedsExternalServices(tmpOperationDef),
609
+ HasValueInput: operationHasValueInput(tmpOperationDef),
610
+ ExecutionStatus: 'not-attempted'
611
+ };
612
+ _TestResults.operations.push(tmpOpRecord);
613
+
614
+ console.log(' ' + tmpOperationDef.Name +
615
+ ': ' + tmpAnalysis.NodeCount + ' nodes, ' +
616
+ tmpAnalysis.EventPorts + ' event ports, ' +
617
+ tmpAnalysis.StatePorts + ' state ports, ' +
618
+ tmpAnalysis.EventConnections + ' event conn, ' +
619
+ tmpAnalysis.StateConnections + ' state conn');
620
+
621
+ _TestResults.passed++;
622
+ }
623
+ );
624
+
625
+ // ── Step 2: Import as a persisted operation ──
626
+ test
627
+ (
628
+ 'import ' + tmpSlug + ' as operation',
629
+ async function ()
630
+ {
631
+ this.timeout(15000);
632
+
633
+ libAssert.ok(tmpOperationDef, 'Operation definition should be loaded');
634
+
635
+ let tmpResponse = await apiCreateOperation(tmpOperationDef);
636
+ libAssert.ok(tmpResponse.Hash, 'Should get an operation hash');
637
+
638
+ tmpImportedOpHash = tmpResponse.Hash;
639
+ console.log(' Imported as:', tmpImportedOpHash);
640
+
641
+ _TestResults.passed++;
642
+ }
643
+ );
644
+
645
+ // ── Step 3: Open in Flow Editor and screenshot ──
646
+ test
647
+ (
648
+ 'render ' + tmpSlug + ' in flow editor',
649
+ async function ()
650
+ {
651
+ this.timeout(20000);
652
+
653
+ libAssert.ok(tmpImportedOpHash, 'Operation should be imported');
654
+
655
+ // Navigate to Flow Editor with operation hash
656
+ await navigateToRoute('#/FlowEditor/' + tmpImportedOpHash);
657
+ await settle(2000);
658
+
659
+ await takeScreenshot('flow-' + tmpSlug);
660
+
661
+ _TestResults.passed++;
662
+ }
663
+ );
664
+
665
+ // ── Step 4: Execute the operation ──
666
+ test
667
+ (
668
+ 'execute ' + tmpSlug,
669
+ async function ()
670
+ {
671
+ this.timeout(30000);
672
+
673
+ libAssert.ok(tmpImportedOpHash, 'Operation should be imported');
674
+
675
+ let tmpOpRecord = _TestResults.operations.find(
676
+ function (pR) { return pR.FileName === pFileName; });
677
+
678
+ // Skip execution for operations needing external services
679
+ if (operationNeedsExternalServices(tmpOperationDef))
680
+ {
681
+ console.log(' SKIPPED: requires external services');
682
+ if (tmpOpRecord) { tmpOpRecord.ExecutionStatus = 'skipped-external'; }
683
+ _TestResults.passed++;
684
+ return;
685
+ }
686
+
687
+ // Handle interactive operations (value-input)
688
+ if (operationHasValueInput(tmpOperationDef))
689
+ {
690
+ // Execute — should pause at value-input
691
+ let tmpResult = await apiExecuteOperation(tmpImportedOpHash);
692
+
693
+ await takeScreenshot('exec-paused-' + tmpSlug);
694
+
695
+ if (tmpResult.body.Status === 'WaitingForInput')
696
+ {
697
+ console.log(' Paused at value-input');
698
+
699
+ let tmpNodeHash = getValueInputNodeHash(tmpOperationDef);
700
+ let tmpInputValue = _InteractiveInputValues[pFileName] || 'test-value';
701
+
702
+ // Submit input to resume
703
+ let tmpResumeResult = await apiPost(
704
+ '/PendingInput/' + tmpResult.body.Hash,
705
+ { NodeHash: tmpNodeHash, Value: tmpInputValue }
706
+ );
707
+
708
+ console.log(' Resumed with value:', JSON.stringify(tmpInputValue),
709
+ '-> Status:', tmpResumeResult.body.Status);
710
+
711
+ if (tmpOpRecord)
712
+ {
713
+ tmpOpRecord.ExecutionStatus = tmpResumeResult.body.Status || 'unknown';
714
+ }
715
+
716
+ await takeScreenshot('exec-resumed-' + tmpSlug);
717
+ }
718
+ else
719
+ {
720
+ console.log(' Execution status:', tmpResult.body.Status,
721
+ '(expected WaitingForInput)');
722
+ if (tmpOpRecord) { tmpOpRecord.ExecutionStatus = tmpResult.body.Status; }
723
+ }
724
+
725
+ _TestResults.passed++;
726
+ return;
727
+ }
728
+
729
+ // Standard execution (non-interactive, no external deps)
730
+ let tmpResult = await apiExecuteOperation(tmpImportedOpHash);
731
+
732
+ libAssert.strictEqual(tmpResult.status, 200, 'Should return 200');
733
+
734
+ let tmpStatus = tmpResult.body.Status || 'unknown';
735
+ let tmpLogCount = tmpResult.body.Log ? tmpResult.body.Log.length : 0;
736
+ let tmpErrorCount = tmpResult.body.Errors ? tmpResult.body.Errors.length : 0;
737
+ let tmpOutputNodes = tmpResult.body.TaskOutputs
738
+ ? Object.keys(tmpResult.body.TaskOutputs) : [];
739
+
740
+ console.log(' Status:', tmpStatus,
741
+ '| Log:', tmpLogCount,
742
+ '| Errors:', tmpErrorCount,
743
+ '| Outputs:', tmpOutputNodes.join(', '));
744
+
745
+ if (tmpOpRecord) { tmpOpRecord.ExecutionStatus = tmpStatus; }
746
+
747
+ // Log task outputs summary
748
+ if (tmpResult.body.TaskOutputs)
749
+ {
750
+ let tmpOutputKeys = Object.keys(tmpResult.body.TaskOutputs);
751
+ for (let k = 0; k < tmpOutputKeys.length; k++)
752
+ {
753
+ let tmpNodeOutputs = tmpResult.body.TaskOutputs[tmpOutputKeys[k]];
754
+ let tmpOutputFields = Object.keys(tmpNodeOutputs || {});
755
+ if (tmpOutputFields.length > 0)
756
+ {
757
+ console.log(' ' + tmpOutputKeys[k] + ':',
758
+ tmpOutputFields.map(function (pF)
759
+ {
760
+ let tmpVal = tmpNodeOutputs[pF];
761
+ if (typeof(tmpVal) === 'string' && tmpVal.length > 60)
762
+ {
763
+ return pF + '=' + JSON.stringify(tmpVal.substring(0, 60) + '...');
764
+ }
765
+ return pF + '=' + JSON.stringify(tmpVal);
766
+ }).join(', '));
767
+ }
768
+ }
769
+ }
770
+
771
+ await takeScreenshot('exec-result-' + tmpSlug);
772
+
773
+ _TestResults.passed++;
774
+ }
775
+ );
776
+
777
+ // ── Step 5: Check manifest entry ──
778
+ test
779
+ (
780
+ 'verify manifest for ' + tmpSlug,
781
+ async function ()
782
+ {
783
+ this.timeout(10000);
784
+
785
+ let tmpOpRecord = _TestResults.operations.find(
786
+ function (pR) { return pR.FileName === pFileName; });
787
+
788
+ // Skip for operations we didn't execute
789
+ if (tmpOpRecord && (tmpOpRecord.ExecutionStatus === 'skipped-external' ||
790
+ tmpOpRecord.ExecutionStatus === 'not-attempted'))
791
+ {
792
+ console.log(' SKIPPED: no execution to verify');
793
+ _TestResults.passed++;
794
+ return;
795
+ }
796
+
797
+ let tmpManifests = await apiGet('/Manifest');
798
+ libAssert.strictEqual(tmpManifests.status, 200, 'Should return 200');
799
+
800
+ console.log(' Total manifests:', tmpManifests.body.length);
801
+
802
+ _TestResults.passed++;
803
+ }
804
+ );
805
+ }
806
+ );
807
+ })(tmpOperationFiles[tmpFileIndex]);
808
+ }
809
+
810
+ // ════════════════════════════════════════════════
811
+ // Summary
812
+ // ════════════════════════════════════════════════
813
+ suite
814
+ (
815
+ 'Summary',
816
+ function ()
817
+ {
818
+ test
819
+ (
820
+ 'all operations summary',
821
+ function ()
822
+ {
823
+ console.log('\n ═══════════════════════════════════════════');
824
+ console.log(' OPERATION LIBRARY TEST SUMMARY');
825
+ console.log(' ═══════════════════════════════════════════');
826
+
827
+ let tmpCompleted = 0;
828
+ let tmpSkipped = 0;
829
+ let tmpFailed = 0;
830
+ let tmpWaiting = 0;
831
+
832
+ for (let i = 0; i < _TestResults.operations.length; i++)
833
+ {
834
+ let tmpOp = _TestResults.operations[i];
835
+ let tmpIcon = '?';
836
+
837
+ if (tmpOp.ExecutionStatus === 'Complete') { tmpIcon = 'OK'; tmpCompleted++; }
838
+ else if (tmpOp.ExecutionStatus.startsWith('skipped')) { tmpIcon = 'SKIP'; tmpSkipped++; }
839
+ else if (tmpOp.ExecutionStatus === 'WaitingForInput') { tmpIcon = 'WAIT'; tmpWaiting++; }
840
+ else if (tmpOp.ExecutionStatus === 'Error' || tmpOp.ExecutionStatus === 'Failed') { tmpIcon = 'FAIL'; tmpFailed++; }
841
+ else { tmpIcon = tmpOp.ExecutionStatus.substring(0, 4).toUpperCase(); }
842
+
843
+ console.log(' [' + tmpIcon + '] ' + tmpOp.Name +
844
+ ' | EP:' + tmpOp.EventPorts + ' SP:' + tmpOp.StatePorts +
845
+ ' | EC:' + tmpOp.EventConnections + ' SC:' + tmpOp.StateConnections);
846
+ }
847
+
848
+ console.log(' ───────────────────────────────────────────');
849
+ console.log(' Complete:', tmpCompleted,
850
+ '| Skipped:', tmpSkipped,
851
+ '| Waiting:', tmpWaiting,
852
+ '| Failed:', tmpFailed,
853
+ '| Total:', _TestResults.operations.length);
854
+ console.log(' Screenshots:', _TestResults.screenshots.length);
855
+ console.log(' ═══════════════════════════════════════════\n');
856
+
857
+ _TestResults.passed++;
858
+ }
859
+ );
860
+
861
+ test
862
+ (
863
+ 'no critical console errors during tests',
864
+ function ()
865
+ {
866
+ let tmpCriticalErrors = _ConsoleErrors.filter(function (pMsg)
867
+ {
868
+ if (pMsg.includes('fetch') || pMsg.includes('Failed to load')
869
+ || pMsg.includes('NetworkError') || pMsg.includes('net::'))
870
+ {
871
+ return false;
872
+ }
873
+ return true;
874
+ });
875
+
876
+ if (tmpCriticalErrors.length > 0)
877
+ {
878
+ console.log(' Critical errors:', tmpCriticalErrors.length);
879
+ for (let i = 0; i < tmpCriticalErrors.length; i++)
880
+ {
881
+ console.log(' -', tmpCriticalErrors[i]);
882
+ }
883
+ }
884
+
885
+ libAssert.strictEqual(tmpCriticalErrors.length, 0,
886
+ 'Should have no critical console errors (found ' + tmpCriticalErrors.length + ')');
887
+
888
+ _TestResults.passed++;
889
+ }
890
+ );
891
+ }
892
+ );
893
+ }
894
+ );