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.
- package/.claude/launch.json +11 -0
- package/.claude/ultravisor-dev-config.json +3 -0
- package/card-audit.json +1545 -0
- package/docs/_sidebar.md +17 -0
- package/docs/css/docuserve.css +73 -0
- package/docs/features/beacon-authentication.md +209 -0
- package/docs/features/beacon-providers.md +1299 -0
- package/docs/features/beacons.md +530 -0
- package/docs/features/capabilities.md +193 -0
- package/docs/features/llm-model-setup.md +505 -0
- package/docs/features/llm.md +262 -0
- package/docs/features/tasks-content-system.md +110 -0
- package/docs/features/tasks-data-transform.md +233 -0
- package/docs/features/tasks-extension.md +36 -0
- package/docs/features/tasks-file-system.md +203 -0
- package/docs/features/tasks-flow-control.md +125 -0
- package/docs/features/tasks-http-client.md +119 -0
- package/docs/features/tasks-llm.md +118 -0
- package/docs/features/tasks-meadow-api.md +163 -0
- package/docs/features/tasks-user-interaction.md +49 -0
- package/docs/features/tasks.md +14 -0
- package/docs/index.html +1 -1
- package/docs/retold-catalog.json +1 -1
- package/docs/retold-keyword-index.json +17374 -5
- package/operation-library/api-data-pipeline.json +143 -0
- package/operation-library/beacon-remote-command.json +72 -0
- package/operation-library/conditional-file-backup.json +143 -0
- package/operation-library/config-processor.json +136 -0
- package/operation-library/csv-data-analysis.json +228 -0
- package/operation-library/csv-to-json.json +160 -0
- package/operation-library/directory-inventory.json +128 -0
- package/operation-library/expression-calculator.json +114 -0
- package/operation-library/file-search-replace.json +175 -0
- package/operation-library/git-status-report.json +368 -0
- package/operation-library/health-check-ping.json +329 -0
- package/operation-library/json-config-merge.json +313 -0
- package/operation-library/llm-embedding-tool-use.json +314 -0
- package/operation-library/llm-multi-turn-analysis.json +322 -0
- package/operation-library/llm-summarize.json +264 -0
- package/operation-library/log-line-counter.json +185 -0
- package/operation-library/meadow-crud-lifecycle.json +264 -0
- package/operation-library/npm-project-validator.json +193 -0
- package/operation-library/rest-api-orchestrator.json +218 -0
- package/operation-library/shell-system-info.json +137 -0
- package/operation-library/simple-echo.json +95 -0
- package/operation-library/template-processor.json +299 -0
- package/operation-library/text-sanitizer.json +176 -0
- package/package.json +12 -8
- package/source/Ultravisor.cjs +30 -3
- package/source/beacon/Ultravisor-Beacon-CLI.cjs +143 -0
- package/source/beacon/Ultravisor-Beacon-CapabilityProvider.cjs +129 -0
- package/source/beacon/Ultravisor-Beacon-Client.cjs +568 -0
- package/source/beacon/Ultravisor-Beacon-Executor.cjs +500 -0
- package/source/beacon/Ultravisor-Beacon-ProviderRegistry.cjs +330 -0
- package/source/beacon/providers/Ultravisor-Beacon-Provider-FileSystem.cjs +331 -0
- package/source/beacon/providers/Ultravisor-Beacon-Provider-LLM.cjs +966 -0
- package/source/beacon/providers/Ultravisor-Beacon-Provider-Shell.cjs +95 -0
- package/source/cli/Ultravisor-CLIProgram.cjs +44 -22
- package/source/cli/commands/Ultravisor-Command-SingleOperation.cjs +29 -18
- package/source/cli/commands/Ultravisor-Command-SingleTask.cjs +62 -19
- package/source/cli/commands/Ultravisor-Command-UpdateTask.cjs +27 -15
- package/source/config/Ultravisor-Default-Command-Configuration.cjs +12 -3
- package/source/services/Ultravisor-Beacon-Coordinator.cjs +966 -0
- package/source/services/Ultravisor-ExecutionEngine.cjs +1093 -0
- package/source/services/Ultravisor-ExecutionManifest.cjs +629 -0
- package/source/services/Ultravisor-Hypervisor-State.cjs +270 -97
- package/source/services/Ultravisor-Hypervisor.cjs +185 -75
- package/source/services/Ultravisor-Schedule-Persistence-Base.cjs +45 -0
- package/source/services/Ultravisor-StateManager.cjs +253 -0
- package/source/services/Ultravisor-TaskTypeRegistry.cjs +213 -0
- package/source/services/persistence/Ultravisor-Schedule-Persistence-JSONFile.cjs +140 -0
- package/source/services/tasks/Ultravisor-BuiltIn-TaskConfigs.cjs +19 -0
- package/source/services/tasks/Ultravisor-TaskHelper-BeaconDispatch.cjs +107 -0
- package/source/services/tasks/Ultravisor-TaskType-Base.cjs +125 -0
- package/source/services/tasks/content-system/Ultravisor-TaskConfigs-ContentSystem.cjs +90 -0
- package/source/services/tasks/content-system/definitions/content-create-folder.json +24 -0
- package/source/services/tasks/content-system/definitions/content-list-files.json +27 -0
- package/source/services/tasks/content-system/definitions/content-read-file.json +26 -0
- package/source/services/tasks/content-system/definitions/content-save-file.json +26 -0
- package/source/services/tasks/data-transform/Ultravisor-TaskConfigs-DataTransform.cjs +577 -0
- package/source/services/tasks/data-transform/Ultravisor-TaskType-ReplaceString.cjs +80 -0
- package/source/services/tasks/data-transform/Ultravisor-TaskType-SetValues.cjs +74 -0
- package/source/services/tasks/data-transform/Ultravisor-TaskType-StringAppender.cjs +90 -0
- package/source/services/tasks/data-transform/definitions/comprehension-intersect.json +24 -0
- package/source/services/tasks/data-transform/definitions/csv-transform.json +24 -0
- package/source/services/tasks/data-transform/definitions/expression-solver.json +20 -0
- package/source/services/tasks/data-transform/definitions/histogram.json +23 -0
- package/source/services/tasks/data-transform/definitions/parse-csv.json +27 -0
- package/source/services/tasks/data-transform/definitions/replace-string.json +27 -0
- package/source/services/tasks/data-transform/definitions/set-values.json +17 -0
- package/source/services/tasks/data-transform/definitions/string-appender.json +22 -0
- package/source/services/tasks/data-transform/definitions/template-string.json +20 -0
- package/source/services/tasks/extension/Ultravisor-TaskConfigs-Extension.cjs +87 -0
- package/source/services/tasks/extension/definitions/beacon-dispatch.json +32 -0
- package/source/services/tasks/file-system/Ultravisor-TaskConfigs-FileSystem.cjs +509 -0
- package/source/services/tasks/file-system/Ultravisor-TaskType-ReadFile.cjs +96 -0
- package/source/services/tasks/file-system/Ultravisor-TaskType-ReadFileBuffered.cjs +123 -0
- package/source/services/tasks/file-system/Ultravisor-TaskType-WriteFile.cjs +106 -0
- package/source/services/tasks/file-system/definitions/copy-file.json +27 -0
- package/source/services/tasks/file-system/definitions/list-files.json +27 -0
- package/source/services/tasks/file-system/definitions/read-file-buffered.json +31 -0
- package/source/services/tasks/file-system/definitions/read-file.json +26 -0
- package/source/services/tasks/file-system/definitions/read-json.json +23 -0
- package/source/services/tasks/file-system/definitions/write-file.json +29 -0
- package/source/services/tasks/file-system/definitions/write-json.json +30 -0
- package/source/services/tasks/flow-control/Ultravisor-TaskConfigs-FlowControl.cjs +353 -0
- package/source/services/tasks/flow-control/Ultravisor-TaskType-IfConditional.cjs +125 -0
- package/source/services/tasks/flow-control/Ultravisor-TaskType-LaunchOperation.cjs +186 -0
- package/source/services/tasks/flow-control/Ultravisor-TaskType-SplitExecute.cjs +164 -0
- package/source/services/tasks/flow-control/definitions/if-conditional.json +25 -0
- package/source/services/tasks/flow-control/definitions/launch-operation.json +27 -0
- package/source/services/tasks/flow-control/definitions/split-execute.json +32 -0
- package/source/services/tasks/http-client/Ultravisor-TaskConfigs-HttpClient.cjs +281 -0
- package/source/services/tasks/http-client/definitions/get-json.json +26 -0
- package/source/services/tasks/http-client/definitions/get-text.json +26 -0
- package/source/services/tasks/http-client/definitions/rest-request.json +32 -0
- package/source/services/tasks/http-client/definitions/send-json.json +28 -0
- package/source/services/tasks/llm/Ultravisor-TaskConfigs-LLM.cjs +605 -0
- package/source/services/tasks/llm/definitions/llm-chat-completion.json +46 -0
- package/source/services/tasks/llm/definitions/llm-embedding.json +31 -0
- package/source/services/tasks/llm/definitions/llm-tool-use.json +42 -0
- package/source/services/tasks/meadow-api/Ultravisor-TaskConfigs-MeadowApi.cjs +341 -0
- package/source/services/tasks/meadow-api/definitions/meadow-count.json +26 -0
- package/source/services/tasks/meadow-api/definitions/meadow-create.json +26 -0
- package/source/services/tasks/meadow-api/definitions/meadow-delete.json +23 -0
- package/source/services/tasks/meadow-api/definitions/meadow-read.json +26 -0
- package/source/services/tasks/meadow-api/definitions/meadow-reads.json +29 -0
- package/source/services/tasks/meadow-api/definitions/meadow-update.json +26 -0
- package/source/services/tasks/shell/Ultravisor-TaskConfigs-Shell.cjs +63 -0
- package/source/services/tasks/shell/definitions/command.json +29 -0
- package/source/services/tasks/user-interaction/Ultravisor-TaskConfigs-UserInteraction.cjs +64 -0
- package/source/services/tasks/user-interaction/Ultravisor-TaskType-ErrorMessage.cjs +55 -0
- package/source/services/tasks/user-interaction/Ultravisor-TaskType-ValueInput.cjs +47 -0
- package/source/services/tasks/user-interaction/definitions/error-message.json +18 -0
- package/source/services/tasks/user-interaction/definitions/value-input.json +23 -0
- package/source/web_server/Ultravisor-API-Server.cjs +849 -124
- package/test/Ultravisor_browser_tests.js +2226 -0
- package/test/Ultravisor_operation_library_tests.js +894 -0
- package/test/Ultravisor_tests.js +4946 -5044
- package/test/workflows/test-content-concatenate-all.json +339 -0
- package/test/workflows/test-content-full-pipeline.json +159 -0
- package/test/workflows/test-content-list-files.json +76 -0
- package/test/workflows/test-content-save-and-read.json +106 -0
- package/webinterface/css/ultravisor-themes.css +668 -0
- package/webinterface/css/ultravisor.css +37 -14
- package/webinterface/docs/card-help/beacon-dispatch.md +30 -0
- package/webinterface/docs/card-help/command.md +27 -0
- package/webinterface/docs/card-help/comprehension-intersect.md +24 -0
- package/webinterface/docs/card-help/content-create-folder.md +22 -0
- package/webinterface/docs/card-help/content-list-files.md +25 -0
- package/webinterface/docs/card-help/content-read-file.md +24 -0
- package/webinterface/docs/card-help/content-save-file.md +24 -0
- package/webinterface/docs/card-help/copy-file.md +25 -0
- package/webinterface/docs/card-help/csv-transform.md +24 -0
- package/webinterface/docs/card-help/end.md +11 -0
- package/webinterface/docs/card-help/error-message.md +16 -0
- package/webinterface/docs/card-help/expression-solver.md +21 -0
- package/webinterface/docs/card-help/get-json.md +24 -0
- package/webinterface/docs/card-help/get-text.md +24 -0
- package/webinterface/docs/card-help/histogram.md +23 -0
- package/webinterface/docs/card-help/if-conditional.md +23 -0
- package/webinterface/docs/card-help/launch-operation.md +25 -0
- package/webinterface/docs/card-help/list-files.md +25 -0
- package/webinterface/docs/card-help/llm-chat-completion.md +43 -0
- package/webinterface/docs/card-help/llm-embedding.md +24 -0
- package/webinterface/docs/card-help/llm-tool-use.md +39 -0
- package/webinterface/docs/card-help/meadow-count.md +24 -0
- package/webinterface/docs/card-help/meadow-create.md +24 -0
- package/webinterface/docs/card-help/meadow-delete.md +19 -0
- package/webinterface/docs/card-help/meadow-read.md +24 -0
- package/webinterface/docs/card-help/meadow-reads.md +27 -0
- package/webinterface/docs/card-help/meadow-update.md +24 -0
- package/webinterface/docs/card-help/parse-csv.md +27 -0
- package/webinterface/docs/card-help/read-file-buffered.md +29 -0
- package/webinterface/docs/card-help/read-file.md +24 -0
- package/webinterface/docs/card-help/read-json.md +21 -0
- package/webinterface/docs/card-help/replace-string.md +25 -0
- package/webinterface/docs/card-help/rest-request.md +30 -0
- package/webinterface/docs/card-help/send-json.md +26 -0
- package/webinterface/docs/card-help/set-values.md +16 -0
- package/webinterface/docs/card-help/split-execute.md +35 -0
- package/webinterface/docs/card-help/start.md +11 -0
- package/webinterface/docs/card-help/string-appender.md +22 -0
- package/webinterface/docs/card-help/template-string.md +21 -0
- package/webinterface/docs/card-help/value-input.md +24 -0
- package/webinterface/docs/card-help/write-file.md +27 -0
- package/webinterface/docs/card-help/write-json.md +28 -0
- package/webinterface/html/index.html +5 -0
- package/webinterface/package.json +13 -4
- package/webinterface/source/Pict-Application-Ultravisor.js +564 -70
- package/webinterface/source/Ultravisor-TimingUtils.js +99 -0
- package/webinterface/source/card-help-content.js +46 -0
- package/webinterface/source/cards/Ultravisor-BuiltIn-CardConfigs.js +174 -0
- package/webinterface/source/cards/Ultravisor-CardConfigGenerator.js +431 -0
- package/webinterface/source/data/ExampleFlow-CSVPipeline.js +231 -0
- package/webinterface/source/data/ExampleFlow-FileProcessor.js +315 -0
- package/webinterface/source/data/ExampleFlow-MeadowPipeline.js +328 -0
- package/webinterface/source/panels/Ultravisor-CardSettingsPanel.js +902 -0
- package/webinterface/source/providers/PictRouter-Ultravisor-Configuration.json +20 -8
- package/webinterface/source/views/PictView-Ultravisor-BeaconList.js +817 -0
- package/webinterface/source/views/PictView-Ultravisor-BottomBar.js +5 -5
- package/webinterface/source/views/PictView-Ultravisor-Dashboard.js +25 -25
- package/webinterface/source/views/PictView-Ultravisor-Documentation.js +856 -0
- package/webinterface/source/views/PictView-Ultravisor-FlowEditor.js +777 -0
- package/webinterface/source/views/PictView-Ultravisor-ManifestList.js +321 -58
- package/webinterface/source/views/PictView-Ultravisor-OperationEdit.js +36 -91
- package/webinterface/source/views/PictView-Ultravisor-OperationList.js +388 -16
- package/webinterface/source/views/PictView-Ultravisor-PendingInput.js +314 -0
- package/webinterface/source/views/PictView-Ultravisor-Schedule.js +521 -65
- package/webinterface/source/views/PictView-Ultravisor-TimingView.js +333 -71
- package/webinterface/source/views/PictView-Ultravisor-TopBar.js +257 -21
- package/webinterface/theme-sampler.html +645 -0
- package/debug/Harness.js +0 -6
- package/source/services/Ultravisor-Operation-Manifest.cjs +0 -160
- package/source/services/Ultravisor-Operation.cjs +0 -200
- package/source/services/Ultravisor-Task.cjs +0 -349
- package/source/services/events/Ultravisor-Hypervisor-Event-Solver.cjs +0 -11
- package/source/services/tasks/Ultravisor-Task-Base.cjs +0 -264
- package/source/services/tasks/Ultravisor-Task-CollectValues.cjs +0 -188
- package/source/services/tasks/Ultravisor-Task-Command.cjs +0 -65
- package/source/services/tasks/Ultravisor-Task-CommandEach.cjs +0 -190
- package/source/services/tasks/Ultravisor-Task-Conditional.cjs +0 -104
- package/source/services/tasks/Ultravisor-Task-DateWindow.cjs +0 -72
- package/source/services/tasks/Ultravisor-Task-GeneratePagedOperation.cjs +0 -336
- package/source/services/tasks/Ultravisor-Task-LaunchOperation.cjs +0 -143
- package/source/services/tasks/Ultravisor-Task-LaunchTask.cjs +0 -146
- package/source/services/tasks/Ultravisor-Task-LineMatch.cjs +0 -158
- package/source/services/tasks/Ultravisor-Task-Request.cjs +0 -56
- package/source/services/tasks/Ultravisor-Task-Solver.cjs +0 -89
- package/source/services/tasks/Ultravisor-Task-TemplateString.cjs +0 -93
- package/source/services/tasks/rest/Ultravisor-Task-GetBinary.cjs +0 -127
- package/source/services/tasks/rest/Ultravisor-Task-GetJSON.cjs +0 -119
- package/source/services/tasks/rest/Ultravisor-Task-GetText.cjs +0 -109
- package/source/services/tasks/rest/Ultravisor-Task-GetXML.cjs +0 -112
- package/source/services/tasks/rest/Ultravisor-Task-RestRequest.cjs +0 -499
- package/source/services/tasks/rest/Ultravisor-Task-SendJSON.cjs +0 -150
- package/source/services/tasks/stagingfiles/Ultravisor-Task-CopyFile.cjs +0 -110
- package/source/services/tasks/stagingfiles/Ultravisor-Task-ListFiles.cjs +0 -89
- package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadBinary.cjs +0 -87
- package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadJSON.cjs +0 -67
- package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadText.cjs +0 -66
- package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadXML.cjs +0 -69
- package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteBinary.cjs +0 -95
- package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteJSON.cjs +0 -96
- package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteText.cjs +0 -99
- package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteXML.cjs +0 -102
- package/webinterface/source/views/PictView-Ultravisor-TaskEdit.js +0 -220
- 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
|
+
);
|