@oneuptime/common 12.0.7 → 12.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 (186) hide show
  1. package/Models/DatabaseModels/TelemetryEntity.ts +45 -0
  2. package/Models/DatabaseModels/TelemetryEntityRelationship.ts +26 -0
  3. package/Models/DatabaseModels/WorkflowLog.ts +38 -0
  4. package/Server/API/UserAPI.ts +16 -1
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.ts +41 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.ts +17 -0
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  8. package/Server/Middleware/MasterAdminAuthorization.ts +11 -6
  9. package/Server/Services/TelemetryEntityRelationshipService.ts +3 -0
  10. package/Server/Services/TelemetryEntityService.ts +114 -1
  11. package/Server/Types/Workflow/Components/API/Utils.ts +44 -1
  12. package/Server/Types/Workflow/TriggerCode.ts +12 -0
  13. package/Server/Types/Workflow/Workflow.ts +5 -0
  14. package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +689 -0
  15. package/Server/Utils/VM/VMAPI.ts +56 -8
  16. package/Tests/App/Dashboard/MonitorCriteriaAttributeFilter.test.tsx +574 -0
  17. package/Tests/Server/API/UserProjectsAPI.test.ts +478 -4
  18. package/Tests/Server/Services/TelemetryEntityManualCreate.test.ts +248 -0
  19. package/Tests/Server/Types/Workflow/Components/ApiComponentHeaders.test.ts +192 -0
  20. package/Tests/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.test.ts +152 -0
  21. package/Tests/Server/Utils/Telemetry/InventoryEntityRegistry.test.ts +322 -0
  22. package/Tests/Server/Utils/VM/VMAPISubstitution.test.ts +243 -0
  23. package/Tests/Types/SerializableObjectDictionaryImportCycle.test.ts +197 -0
  24. package/Tests/Types/Telemetry/EntityTypeGroups.test.ts +140 -0
  25. package/Tests/Types/Workflow/StepTrace.test.ts +235 -0
  26. package/Tests/Types/Workflow/TemplateSyntax.test.ts +379 -0
  27. package/Tests/Types/Workflow/Templates.test.ts +403 -0
  28. package/Tests/UI/Components/CustomTimeRangeModal.test.tsx +459 -0
  29. package/Tests/UI/Components/DictionaryAttributeFilterRow.test.tsx +477 -0
  30. package/Tests/UI/Components/Forms/AnchoredFieldPopupKeyboard.test.tsx +382 -0
  31. package/Tests/UI/Components/Forms/ColorPickerPicking.test.tsx +569 -0
  32. package/Tests/UI/Components/Forms/ValidationJSON.test.ts +241 -0
  33. package/Tests/UI/Components/LogTimeRangePicker.test.tsx +42 -0
  34. package/Tests/UI/Components/ModalPortalDismissal.test.tsx +90 -0
  35. package/Tests/UI/Components/TelemetryTimeRangePicker.test.tsx +49 -7
  36. package/Tests/UI/Components/TimeRangePickerDropdown.test.tsx +325 -0
  37. package/Tests/UI/Components/Workflow/GraphLint.test.ts +821 -0
  38. package/Tests/UI/Components/Workflow/ModelColumnEditor.test.ts +287 -0
  39. package/Tests/UI/Components/Workflow/ModelColumnEditorServerContract.test.ts +185 -0
  40. package/Tests/UI/Components/Workflow/ModelSchema.test.ts +174 -0
  41. package/Tests/UI/Components/Workflow/RunStatusWatcher.test.ts +152 -0
  42. package/Tests/UI/Components/Workflow/Utils.test.ts +192 -0
  43. package/Tests/UI/Monitor/MonitorStepCriteriaView.test.tsx +432 -0
  44. package/Tests/Utils/Monitor/NetworkDeviceRoleUtil.test.ts +514 -0
  45. package/Tests/Utils/Monitor/NetworkTopologyUtil.test.ts +240 -0
  46. package/Tests/Utils/Schema/AnalyticsModelSchema.test.ts +469 -0
  47. package/Tests/Utils/Schema/ModelSchema.test.ts +268 -0
  48. package/Tests/Utils/Telemetry/EntityKeyNonTelemetry.test.ts +193 -0
  49. package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +31 -0
  50. package/Types/SerializableObjectDictionary.ts +133 -39
  51. package/Types/Telemetry/EntitySource.ts +40 -0
  52. package/Types/Telemetry/EntityType.ts +27 -0
  53. package/Types/Telemetry/EntityTypeGroups.ts +65 -0
  54. package/Types/Workflow/Component.ts +29 -0
  55. package/Types/Workflow/Components/API.ts +35 -0
  56. package/Types/Workflow/Components/BaseModel.ts +7 -3
  57. package/Types/Workflow/StepTrace.ts +181 -0
  58. package/Types/Workflow/TemplateSyntax.ts +499 -0
  59. package/Types/Workflow/Templates.ts +316 -0
  60. package/UI/Components/Date/CustomTimeRangeModal.tsx +278 -0
  61. package/UI/Components/Date/TimeRangePickerDropdown.tsx +250 -0
  62. package/UI/Components/Dictionary/Dictionary.tsx +23 -6
  63. package/UI/Components/Forms/Fields/ColorPicker.tsx +66 -10
  64. package/UI/Components/Forms/Fields/IconPicker.tsx +48 -10
  65. package/UI/Components/Forms/Types/Field.ts +7 -0
  66. package/UI/Components/Forms/Validation.ts +63 -1
  67. package/UI/Components/Input/Input.tsx +31 -3
  68. package/UI/Components/LogsViewer/components/LogTimeRangePicker.tsx +11 -216
  69. package/UI/Components/Modal/Modal.tsx +37 -4
  70. package/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.tsx +11 -210
  71. package/UI/Components/Workflow/ArgumentsForm.tsx +194 -7
  72. package/UI/Components/Workflow/Component.tsx +28 -26
  73. package/UI/Components/Workflow/ComponentSettingsModal.tsx +69 -8
  74. package/UI/Components/Workflow/GraphLint.ts +591 -0
  75. package/UI/Components/Workflow/ModelColumnEditor.tsx +396 -0
  76. package/UI/Components/Workflow/ModelSchema.ts +196 -0
  77. package/UI/Components/Workflow/RunForm.tsx +41 -7
  78. package/UI/Components/Workflow/RunStatusWatcher.ts +122 -0
  79. package/UI/Components/Workflow/StepTraceViewer.tsx +186 -0
  80. package/UI/Components/Workflow/Utils.ts +93 -1
  81. package/UI/Components/Workflow/Workflow.tsx +77 -4
  82. package/UI/Types/LayeredDismissal.ts +31 -0
  83. package/UI/Types/UseAnchoredFieldPopup.ts +99 -0
  84. package/Utils/Monitor/NetworkDeviceRoleUtil.ts +533 -0
  85. package/Utils/Monitor/NetworkTopologyUtil.ts +45 -7
  86. package/Utils/Telemetry/EntityKey.ts +66 -0
  87. package/build/dist/Models/DatabaseModels/TelemetryEntity.js +47 -0
  88. package/build/dist/Models/DatabaseModels/TelemetryEntity.js.map +1 -1
  89. package/build/dist/Models/DatabaseModels/TelemetryEntityRelationship.js +27 -0
  90. package/build/dist/Models/DatabaseModels/TelemetryEntityRelationship.js.map +1 -1
  91. package/build/dist/Models/DatabaseModels/WorkflowLog.js +39 -0
  92. package/build/dist/Models/DatabaseModels/WorkflowLog.js.map +1 -1
  93. package/build/dist/Server/API/UserAPI.js +16 -1
  94. package/build/dist/Server/API/UserAPI.js.map +1 -1
  95. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.js +20 -0
  96. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.js.map +1 -0
  97. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.js +12 -0
  98. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.js.map +1 -0
  99. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  100. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  101. package/build/dist/Server/Middleware/MasterAdminAuthorization.js +11 -6
  102. package/build/dist/Server/Middleware/MasterAdminAuthorization.js.map +1 -1
  103. package/build/dist/Server/Services/TelemetryEntityRelationshipService.js +3 -0
  104. package/build/dist/Server/Services/TelemetryEntityRelationshipService.js.map +1 -1
  105. package/build/dist/Server/Services/TelemetryEntityService.js +91 -1
  106. package/build/dist/Server/Services/TelemetryEntityService.js.map +1 -1
  107. package/build/dist/Server/Types/Workflow/Components/API/Utils.js +28 -0
  108. package/build/dist/Server/Types/Workflow/Components/API/Utils.js.map +1 -1
  109. package/build/dist/Server/Types/Workflow/TriggerCode.js.map +1 -1
  110. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +507 -0
  111. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -0
  112. package/build/dist/Server/Utils/VM/VMAPI.js +51 -4
  113. package/build/dist/Server/Utils/VM/VMAPI.js.map +1 -1
  114. package/build/dist/Types/SerializableObjectDictionary.js +133 -39
  115. package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
  116. package/build/dist/Types/Telemetry/EntitySource.js +39 -0
  117. package/build/dist/Types/Telemetry/EntitySource.js.map +1 -0
  118. package/build/dist/Types/Telemetry/EntityType.js +27 -0
  119. package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
  120. package/build/dist/Types/Telemetry/EntityTypeGroups.js +57 -0
  121. package/build/dist/Types/Telemetry/EntityTypeGroups.js.map +1 -0
  122. package/build/dist/Types/Workflow/Component.js +21 -0
  123. package/build/dist/Types/Workflow/Component.js.map +1 -1
  124. package/build/dist/Types/Workflow/Components/API.js +35 -0
  125. package/build/dist/Types/Workflow/Components/API.js.map +1 -1
  126. package/build/dist/Types/Workflow/Components/BaseModel.js +7 -3
  127. package/build/dist/Types/Workflow/Components/BaseModel.js.map +1 -1
  128. package/build/dist/Types/Workflow/StepTrace.js +104 -0
  129. package/build/dist/Types/Workflow/StepTrace.js.map +1 -0
  130. package/build/dist/Types/Workflow/TemplateSyntax.js +326 -0
  131. package/build/dist/Types/Workflow/TemplateSyntax.js.map +1 -0
  132. package/build/dist/Types/Workflow/Templates.js +218 -0
  133. package/build/dist/Types/Workflow/Templates.js.map +1 -0
  134. package/build/dist/UI/Components/Date/CustomTimeRangeModal.js +126 -0
  135. package/build/dist/UI/Components/Date/CustomTimeRangeModal.js.map +1 -0
  136. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js +123 -0
  137. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js.map +1 -0
  138. package/build/dist/UI/Components/Dictionary/Dictionary.js +22 -6
  139. package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
  140. package/build/dist/UI/Components/Forms/Fields/ColorPicker.js +52 -13
  141. package/build/dist/UI/Components/Forms/Fields/ColorPicker.js.map +1 -1
  142. package/build/dist/UI/Components/Forms/Fields/IconPicker.js +28 -12
  143. package/build/dist/UI/Components/Forms/Fields/IconPicker.js.map +1 -1
  144. package/build/dist/UI/Components/Forms/Validation.js +44 -0
  145. package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
  146. package/build/dist/UI/Components/Input/Input.js +12 -4
  147. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  148. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js +5 -108
  149. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js.map +1 -1
  150. package/build/dist/UI/Components/Modal/Modal.js +31 -5
  151. package/build/dist/UI/Components/Modal/Modal.js.map +1 -1
  152. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js +5 -105
  153. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js.map +1 -1
  154. package/build/dist/UI/Components/Workflow/ArgumentsForm.js +140 -11
  155. package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
  156. package/build/dist/UI/Components/Workflow/Component.js +20 -19
  157. package/build/dist/UI/Components/Workflow/Component.js.map +1 -1
  158. package/build/dist/UI/Components/Workflow/ComponentSettingsModal.js +31 -3
  159. package/build/dist/UI/Components/Workflow/ComponentSettingsModal.js.map +1 -1
  160. package/build/dist/UI/Components/Workflow/GraphLint.js +396 -0
  161. package/build/dist/UI/Components/Workflow/GraphLint.js.map +1 -0
  162. package/build/dist/UI/Components/Workflow/ModelColumnEditor.js +225 -0
  163. package/build/dist/UI/Components/Workflow/ModelColumnEditor.js.map +1 -0
  164. package/build/dist/UI/Components/Workflow/ModelSchema.js +118 -0
  165. package/build/dist/UI/Components/Workflow/ModelSchema.js.map +1 -0
  166. package/build/dist/UI/Components/Workflow/RunForm.js +22 -6
  167. package/build/dist/UI/Components/Workflow/RunForm.js.map +1 -1
  168. package/build/dist/UI/Components/Workflow/RunStatusWatcher.js +76 -0
  169. package/build/dist/UI/Components/Workflow/RunStatusWatcher.js.map +1 -0
  170. package/build/dist/UI/Components/Workflow/StepTraceViewer.js +76 -0
  171. package/build/dist/UI/Components/Workflow/StepTraceViewer.js.map +1 -0
  172. package/build/dist/UI/Components/Workflow/Utils.js +73 -1
  173. package/build/dist/UI/Components/Workflow/Utils.js.map +1 -1
  174. package/build/dist/UI/Components/Workflow/Workflow.js +51 -6
  175. package/build/dist/UI/Components/Workflow/Workflow.js.map +1 -1
  176. package/build/dist/UI/Types/LayeredDismissal.js +21 -0
  177. package/build/dist/UI/Types/LayeredDismissal.js.map +1 -0
  178. package/build/dist/UI/Types/UseAnchoredFieldPopup.js +74 -1
  179. package/build/dist/UI/Types/UseAnchoredFieldPopup.js.map +1 -1
  180. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +386 -0
  181. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -0
  182. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +35 -6
  183. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  184. package/build/dist/Utils/Telemetry/EntityKey.js +52 -0
  185. package/build/dist/Utils/Telemetry/EntityKey.js.map +1 -1
  186. package/package.json +1 -1
@@ -0,0 +1,821 @@
1
+ import {
2
+ LintGraphEdge,
3
+ LintGraphNode,
4
+ WorkflowLintIssue,
5
+ WorkflowLintResult,
6
+ WorkflowLintRule,
7
+ WorkflowLintSeverity,
8
+ lintWorkflowGraph,
9
+ } from "../../../../UI/Components/Workflow/GraphLint";
10
+ import IconProp from "../../../../Types/Icon/IconProp";
11
+ import { JSONObject } from "../../../../Types/JSON";
12
+ import ComponentMetadata, {
13
+ Argument,
14
+ ComponentInputType,
15
+ ComponentType,
16
+ NodeType,
17
+ ReturnValue,
18
+ } from "../../../../Types/Workflow/Component";
19
+ import { describe, expect, test } from "@jest/globals";
20
+
21
+ type MakeMetadataFunction = (params: {
22
+ id: string;
23
+ componentType?: ComponentType | undefined;
24
+ args?: Array<Argument> | undefined;
25
+ returnValues?: Array<ReturnValue> | undefined;
26
+ }) => ComponentMetadata;
27
+
28
+ const makeMetadata: MakeMetadataFunction = (params: {
29
+ id: string;
30
+ componentType?: ComponentType | undefined;
31
+ args?: Array<Argument> | undefined;
32
+ returnValues?: Array<ReturnValue> | undefined;
33
+ }): ComponentMetadata => {
34
+ return {
35
+ id: params.id,
36
+ title: params.id,
37
+ category: "Test",
38
+ description: "A test component",
39
+ iconProp: IconProp.Bolt,
40
+ componentType: params.componentType || ComponentType.Component,
41
+ arguments: params.args || [],
42
+ returnValues: params.returnValues || [],
43
+ inPorts: [],
44
+ outPorts: [],
45
+ };
46
+ };
47
+
48
+ type MakeNodeFunction = (params: {
49
+ nodeId: string;
50
+ componentId: string;
51
+ componentType?: ComponentType | undefined;
52
+ args?: Array<Argument> | undefined;
53
+ returnValues?: Array<ReturnValue> | undefined;
54
+ values?: JSONObject | undefined;
55
+ nodeType?: NodeType | undefined;
56
+ }) => LintGraphNode;
57
+
58
+ const makeNode: MakeNodeFunction = (params: {
59
+ nodeId: string;
60
+ componentId: string;
61
+ componentType?: ComponentType | undefined;
62
+ args?: Array<Argument> | undefined;
63
+ returnValues?: Array<ReturnValue> | undefined;
64
+ values?: JSONObject | undefined;
65
+ nodeType?: NodeType | undefined;
66
+ }): LintGraphNode => {
67
+ const componentType: ComponentType =
68
+ params.componentType || ComponentType.Component;
69
+
70
+ return {
71
+ id: params.nodeId,
72
+ data: {
73
+ error: "",
74
+ id: params.componentId,
75
+ nodeType: params.nodeType || NodeType.Node,
76
+ metadata: makeMetadata({
77
+ id: `${params.componentId}-metadata`,
78
+ componentType: componentType,
79
+ args: params.args,
80
+ returnValues: params.returnValues,
81
+ }),
82
+ metadataId: `${params.componentId}-metadata`,
83
+ internalId: `${params.nodeId}-internal`,
84
+ arguments: params.values || {},
85
+ returnValues: {},
86
+ componentType: componentType,
87
+ },
88
+ };
89
+ };
90
+
91
+ const textArgument: Argument = {
92
+ id: "message",
93
+ name: "Message",
94
+ description: "Text to send",
95
+ type: ComponentInputType.Text,
96
+ required: false,
97
+ };
98
+
99
+ const requiredUrlArgument: Argument = {
100
+ id: "url",
101
+ name: "URL",
102
+ description: "Where to send it",
103
+ type: ComponentInputType.URL,
104
+ required: true,
105
+ };
106
+
107
+ const jsonArgument: Argument = {
108
+ id: "request-body",
109
+ name: "Request Body",
110
+ description: "Body",
111
+ type: ComponentInputType.JSON,
112
+ required: false,
113
+ };
114
+
115
+ const bodyReturnValue: ReturnValue = {
116
+ id: "response-body",
117
+ name: "Response Body",
118
+ description: "What came back",
119
+ type: ComponentInputType.JSON,
120
+ required: false,
121
+ };
122
+
123
+ type MakeTriggerFunction = (
124
+ nodeId: string,
125
+ componentId: string,
126
+ ) => LintGraphNode;
127
+
128
+ const makeTrigger: MakeTriggerFunction = (
129
+ nodeId: string,
130
+ componentId: string,
131
+ ): LintGraphNode => {
132
+ return makeNode({
133
+ nodeId: nodeId,
134
+ componentId: componentId,
135
+ componentType: ComponentType.Trigger,
136
+ });
137
+ };
138
+
139
+ type RulesOfFunction = (result: WorkflowLintResult) => Array<WorkflowLintRule>;
140
+
141
+ const rulesOf: RulesOfFunction = (
142
+ result: WorkflowLintResult,
143
+ ): Array<WorkflowLintRule> => {
144
+ return result.issues.map((issue: WorkflowLintIssue) => {
145
+ return issue.rule;
146
+ });
147
+ };
148
+
149
+ describe("lintWorkflowGraph — nothing to report", () => {
150
+ test("an empty graph is silent", () => {
151
+ const result: WorkflowLintResult = lintWorkflowGraph({
152
+ nodes: [],
153
+ edges: [],
154
+ });
155
+
156
+ expect(result.issues).toEqual([]);
157
+ expect(result.errorCount).toBe(0);
158
+ expect(result.warningCount).toBe(0);
159
+ });
160
+
161
+ test("a well-formed graph is silent", () => {
162
+ const trigger: LintGraphNode = makeTrigger("n1", "manual-1");
163
+ const step: LintGraphNode = makeNode({
164
+ nodeId: "n2",
165
+ componentId: "api-post-1",
166
+ args: [requiredUrlArgument, jsonArgument],
167
+ values: {
168
+ url: "https://example.com",
169
+ "request-body": '{"a": 1}',
170
+ },
171
+ });
172
+
173
+ const result: WorkflowLintResult = lintWorkflowGraph({
174
+ nodes: [trigger, step],
175
+ edges: [{ source: "n1", target: "n2" }],
176
+ });
177
+
178
+ expect(result.issues).toEqual([]);
179
+ });
180
+
181
+ test("placeholder nodes are ignored entirely", () => {
182
+ const placeholder: LintGraphNode = makeNode({
183
+ nodeId: "n1",
184
+ componentId: "",
185
+ componentType: ComponentType.Trigger,
186
+ nodeType: NodeType.PlaceholderNode,
187
+ });
188
+
189
+ const result: WorkflowLintResult = lintWorkflowGraph({
190
+ nodes: [placeholder],
191
+ edges: [],
192
+ });
193
+
194
+ expect(result.issues).toEqual([]);
195
+ });
196
+ });
197
+
198
+ describe("lintWorkflowGraph — graph shape", () => {
199
+ test("reports a workflow with no trigger", () => {
200
+ const step: LintGraphNode = makeNode({
201
+ nodeId: "n1",
202
+ componentId: "log-1",
203
+ });
204
+
205
+ const result: WorkflowLintResult = lintWorkflowGraph({
206
+ nodes: [step],
207
+ edges: [],
208
+ });
209
+
210
+ expect(rulesOf(result)).toContain(WorkflowLintRule.NoTrigger);
211
+ expect(
212
+ result.issues.find((issue: WorkflowLintIssue) => {
213
+ return issue.rule === WorkflowLintRule.NoTrigger;
214
+ })?.nodeId,
215
+ ).toBeNull();
216
+ });
217
+
218
+ test("reports a step the trigger cannot reach, as a warning", () => {
219
+ const trigger: LintGraphNode = makeTrigger("n1", "manual-1");
220
+ const connected: LintGraphNode = makeNode({
221
+ nodeId: "n2",
222
+ componentId: "log-1",
223
+ });
224
+ const orphan: LintGraphNode = makeNode({
225
+ nodeId: "n3",
226
+ componentId: "log-2",
227
+ });
228
+
229
+ const result: WorkflowLintResult = lintWorkflowGraph({
230
+ nodes: [trigger, connected, orphan],
231
+ edges: [{ source: "n1", target: "n2" }],
232
+ });
233
+
234
+ const issue: WorkflowLintIssue | undefined = result.issues.find(
235
+ (candidate: WorkflowLintIssue) => {
236
+ return candidate.rule === WorkflowLintRule.UnreachableComponent;
237
+ },
238
+ );
239
+
240
+ expect(issue?.componentId).toBe("log-2");
241
+ expect(issue?.severity).toBe(WorkflowLintSeverity.Warning);
242
+ expect(result.warningCount).toBe(1);
243
+ });
244
+
245
+ test("follows a chain of edges when deciding reachability", () => {
246
+ const trigger: LintGraphNode = makeTrigger("n1", "manual-1");
247
+ const a: LintGraphNode = makeNode({ nodeId: "n2", componentId: "a" });
248
+ const b: LintGraphNode = makeNode({ nodeId: "n3", componentId: "b" });
249
+
250
+ const result: WorkflowLintResult = lintWorkflowGraph({
251
+ nodes: [trigger, a, b],
252
+ edges: [
253
+ { source: "n1", target: "n2" },
254
+ { source: "n2", target: "n3" },
255
+ ],
256
+ });
257
+
258
+ expect(rulesOf(result)).not.toContain(
259
+ WorkflowLintRule.UnreachableComponent,
260
+ );
261
+ });
262
+
263
+ test("reports two steps sharing an id", () => {
264
+ const trigger: LintGraphNode = makeTrigger("n1", "manual-1");
265
+ const first: LintGraphNode = makeNode({
266
+ nodeId: "n2",
267
+ componentId: "log-1",
268
+ });
269
+ const second: LintGraphNode = makeNode({
270
+ nodeId: "n3",
271
+ componentId: "log-1",
272
+ });
273
+
274
+ const result: WorkflowLintResult = lintWorkflowGraph({
275
+ nodes: [trigger, first, second],
276
+ edges: [
277
+ { source: "n1", target: "n2" },
278
+ { source: "n2", target: "n3" },
279
+ ],
280
+ });
281
+
282
+ const duplicates: Array<WorkflowLintIssue> = result.issues.filter(
283
+ (issue: WorkflowLintIssue) => {
284
+ return issue.rule === WorkflowLintRule.DuplicateComponentId;
285
+ },
286
+ );
287
+
288
+ // Both nodes are flagged, so either one shows the badge.
289
+ expect(duplicates).toHaveLength(2);
290
+ expect(duplicates[0]?.severity).toBe(WorkflowLintSeverity.Error);
291
+ });
292
+
293
+ test("reports an id containing a dot, which no reference can address", () => {
294
+ const trigger: LintGraphNode = makeTrigger("n1", "manual-1");
295
+ const dotted: LintGraphNode = makeNode({
296
+ nodeId: "n2",
297
+ componentId: "api.get.1",
298
+ });
299
+
300
+ const result: WorkflowLintResult = lintWorkflowGraph({
301
+ nodes: [trigger, dotted],
302
+ edges: [{ source: "n1", target: "n2" }],
303
+ });
304
+
305
+ expect(rulesOf(result)).toContain(WorkflowLintRule.ComponentIdContainsDot);
306
+ });
307
+ });
308
+
309
+ describe("lintWorkflowGraph — arguments", () => {
310
+ test("reports a required argument left empty", () => {
311
+ const trigger: LintGraphNode = makeTrigger("n1", "manual-1");
312
+ const step: LintGraphNode = makeNode({
313
+ nodeId: "n2",
314
+ componentId: "api-get-1",
315
+ args: [requiredUrlArgument],
316
+ values: {},
317
+ });
318
+
319
+ const result: WorkflowLintResult = lintWorkflowGraph({
320
+ nodes: [trigger, step],
321
+ edges: [{ source: "n1", target: "n2" }],
322
+ });
323
+
324
+ const issue: WorkflowLintIssue | undefined = result.issues.find(
325
+ (candidate: WorkflowLintIssue) => {
326
+ return candidate.rule === WorkflowLintRule.MissingRequiredArgument;
327
+ },
328
+ );
329
+
330
+ expect(issue?.argumentId).toBe("url");
331
+ expect(issue?.message).toMatch(/"URL" is required/);
332
+ });
333
+
334
+ test("treats whitespace and an empty object as empty", () => {
335
+ const trigger: LintGraphNode = makeTrigger("n1", "manual-1");
336
+ const blank: LintGraphNode = makeNode({
337
+ nodeId: "n2",
338
+ componentId: "a",
339
+ args: [requiredUrlArgument],
340
+ values: { url: " " },
341
+ });
342
+ const emptyObject: LintGraphNode = makeNode({
343
+ nodeId: "n3",
344
+ componentId: "b",
345
+ args: [{ ...requiredUrlArgument, type: ComponentInputType.JSON }],
346
+ values: { url: {} },
347
+ });
348
+
349
+ const result: WorkflowLintResult = lintWorkflowGraph({
350
+ nodes: [trigger, blank, emptyObject],
351
+ edges: [
352
+ { source: "n1", target: "n2" },
353
+ { source: "n2", target: "n3" },
354
+ ],
355
+ });
356
+
357
+ expect(
358
+ result.issues.filter((issue: WorkflowLintIssue) => {
359
+ return issue.rule === WorkflowLintRule.MissingRequiredArgument;
360
+ }),
361
+ ).toHaveLength(2);
362
+ });
363
+
364
+ test("does not treat false or zero as missing", () => {
365
+ const trigger: LintGraphNode = makeTrigger("n1", "manual-1");
366
+ const step: LintGraphNode = makeNode({
367
+ nodeId: "n2",
368
+ componentId: "a",
369
+ args: [
370
+ {
371
+ id: "enabled",
372
+ name: "Enabled",
373
+ description: "",
374
+ type: ComponentInputType.Boolean,
375
+ required: true,
376
+ },
377
+ {
378
+ id: "count",
379
+ name: "Count",
380
+ description: "",
381
+ type: ComponentInputType.Number,
382
+ required: true,
383
+ },
384
+ ],
385
+ values: { enabled: false, count: 0 },
386
+ });
387
+
388
+ const result: WorkflowLintResult = lintWorkflowGraph({
389
+ nodes: [trigger, step],
390
+ edges: [{ source: "n1", target: "n2" }],
391
+ });
392
+
393
+ expect(rulesOf(result)).not.toContain(
394
+ WorkflowLintRule.MissingRequiredArgument,
395
+ );
396
+ });
397
+
398
+ test("reports malformed JSON in a JSON argument", () => {
399
+ const trigger: LintGraphNode = makeTrigger("n1", "manual-1");
400
+ const step: LintGraphNode = makeNode({
401
+ nodeId: "n2",
402
+ componentId: "api-post-1",
403
+ args: [jsonArgument],
404
+ values: { "request-body": '{"a": 1,}' },
405
+ });
406
+
407
+ const result: WorkflowLintResult = lintWorkflowGraph({
408
+ nodes: [trigger, step],
409
+ edges: [{ source: "n1", target: "n2" }],
410
+ });
411
+
412
+ const issue: WorkflowLintIssue | undefined = result.issues.find(
413
+ (candidate: WorkflowLintIssue) => {
414
+ return candidate.rule === WorkflowLintRule.InvalidJSON;
415
+ },
416
+ );
417
+
418
+ expect(issue?.message).toMatch(/"Request Body" is not valid JSON/);
419
+ });
420
+
421
+ test("does not report JSON5 in an argument that is read with JSON5", () => {
422
+ const trigger: LintGraphNode = makeTrigger("n1", "manual-1");
423
+ const step: LintGraphNode = makeNode({
424
+ nodeId: "n2",
425
+ componentId: "create-one-1",
426
+ args: [
427
+ {
428
+ id: "json",
429
+ name: "Data",
430
+ description: "",
431
+ type: ComponentInputType.BaseModel,
432
+ required: false,
433
+ },
434
+ ],
435
+ values: { json: "{name: 'x',}" },
436
+ });
437
+
438
+ const result: WorkflowLintResult = lintWorkflowGraph({
439
+ nodes: [trigger, step],
440
+ edges: [{ source: "n1", target: "n2" }],
441
+ });
442
+
443
+ expect(rulesOf(result)).not.toContain(WorkflowLintRule.InvalidJSON);
444
+ });
445
+
446
+ test("still reports JSON5 in an argument the runner parses strictly", () => {
447
+ const trigger: LintGraphNode = makeTrigger("n1", "manual-1");
448
+ const step: LintGraphNode = makeNode({
449
+ nodeId: "n2",
450
+ componentId: "api-post-1",
451
+ args: [jsonArgument],
452
+ values: { "request-body": "{name: 'x',}" },
453
+ });
454
+
455
+ const result: WorkflowLintResult = lintWorkflowGraph({
456
+ nodes: [trigger, step],
457
+ edges: [{ source: "n1", target: "n2" }],
458
+ });
459
+
460
+ expect(rulesOf(result)).toContain(WorkflowLintRule.InvalidJSON);
461
+ });
462
+
463
+ test("does not report JSON that only looks malformed because of templates", () => {
464
+ const trigger: LintGraphNode = makeTrigger("n1", "manual-1");
465
+ const step: LintGraphNode = makeNode({
466
+ nodeId: "n2",
467
+ componentId: "api-post-1",
468
+ args: [jsonArgument],
469
+ values: {
470
+ "request-body": '{"retries": {{local.variables.count}}}',
471
+ },
472
+ });
473
+
474
+ const result: WorkflowLintResult = lintWorkflowGraph({
475
+ nodes: [trigger, step],
476
+ edges: [{ source: "n1", target: "n2" }],
477
+ });
478
+
479
+ expect(rulesOf(result)).not.toContain(WorkflowLintRule.InvalidJSON);
480
+ });
481
+ });
482
+
483
+ describe("lintWorkflowGraph — references", () => {
484
+ type BuildPairFunction = (referenceText: string) => {
485
+ nodes: Array<LintGraphNode>;
486
+ edges: Array<LintGraphEdge>;
487
+ };
488
+
489
+ /*
490
+ * trigger -> api-get-1 -> log-1, where log-1's Message carries the reference
491
+ * under test. api-get-1 returns "response-body".
492
+ */
493
+ const buildPair: BuildPairFunction = (
494
+ referenceText: string,
495
+ ): { nodes: Array<LintGraphNode>; edges: Array<LintGraphEdge> } => {
496
+ return {
497
+ nodes: [
498
+ makeTrigger("n1", "manual-1"),
499
+ makeNode({
500
+ nodeId: "n2",
501
+ componentId: "api-get-1",
502
+ returnValues: [bodyReturnValue],
503
+ }),
504
+ makeNode({
505
+ nodeId: "n3",
506
+ componentId: "log-1",
507
+ args: [textArgument],
508
+ values: { message: referenceText },
509
+ }),
510
+ ],
511
+ edges: [
512
+ { source: "n1", target: "n2" },
513
+ { source: "n2", target: "n3" },
514
+ ],
515
+ };
516
+ };
517
+
518
+ test("accepts a reference the picker would have written", () => {
519
+ const result: WorkflowLintResult = lintWorkflowGraph(
520
+ buildPair("{{local.components.api-get-1.returnValues.response-body}}"),
521
+ );
522
+
523
+ expect(result.issues).toEqual([]);
524
+ });
525
+
526
+ test("accepts a drill-in below the return value", () => {
527
+ const result: WorkflowLintResult = lintWorkflowGraph(
528
+ buildPair(
529
+ "{{local.components.api-get-1.returnValues.response-body.data.items[0].id}}",
530
+ ),
531
+ );
532
+
533
+ expect(result.issues).toEqual([]);
534
+ });
535
+
536
+ test("accepts variable references without checking that they exist", () => {
537
+ const result: WorkflowLintResult = lintWorkflowGraph(
538
+ buildPair("{{local.variables.token}} {{global.variables.region}}"),
539
+ );
540
+
541
+ expect(result.issues).toEqual([]);
542
+ });
543
+
544
+ test("reports a misspelled root", () => {
545
+ const result: WorkflowLintResult = lintWorkflowGraph(
546
+ buildPair("{{local.componets.api-get-1.returnValues.response-body}}"),
547
+ );
548
+
549
+ expect(rulesOf(result)).toContain(WorkflowLintRule.UnknownReferenceRoot);
550
+ });
551
+
552
+ test("reports a step id that is not in the graph", () => {
553
+ const result: WorkflowLintResult = lintWorkflowGraph(
554
+ buildPair("{{local.components.api-get-2.returnValues.response-body}}"),
555
+ );
556
+
557
+ const issue: WorkflowLintIssue | undefined = result.issues.find(
558
+ (candidate: WorkflowLintIssue) => {
559
+ return candidate.rule === WorkflowLintRule.UnknownReferencedComponent;
560
+ },
561
+ );
562
+
563
+ expect(issue?.message).toMatch(/api-get-2/);
564
+ });
565
+
566
+ test("reports a return value the referenced step does not have", () => {
567
+ const result: WorkflowLintResult = lintWorkflowGraph(
568
+ buildPair("{{local.components.api-get-1.returnValues.respones-body}}"),
569
+ );
570
+
571
+ const issue: WorkflowLintIssue | undefined = result.issues.find(
572
+ (candidate: WorkflowLintIssue) => {
573
+ return candidate.rule === WorkflowLintRule.UnknownReferencedReturnValue;
574
+ },
575
+ );
576
+
577
+ expect(issue?.message).toMatch(/respones-body/);
578
+ });
579
+
580
+ test("reports a space inside the braces, which silently never resolves", () => {
581
+ const result: WorkflowLintResult = lintWorkflowGraph(
582
+ buildPair("{{ local.components.api-get-1.returnValues.response-body }}"),
583
+ );
584
+
585
+ const issue: WorkflowLintIssue | undefined = result.issues.find(
586
+ (candidate: WorkflowLintIssue) => {
587
+ return candidate.rule === WorkflowLintRule.ReferenceHasWhitespace;
588
+ },
589
+ );
590
+
591
+ expect(issue?.severity).toBe(WorkflowLintSeverity.Error);
592
+ expect(issue?.message).toMatch(/space/);
593
+ });
594
+
595
+ test("reports a step referring to its own results", () => {
596
+ const result: WorkflowLintResult = lintWorkflowGraph({
597
+ nodes: [
598
+ makeTrigger("n1", "manual-1"),
599
+ makeNode({
600
+ nodeId: "n2",
601
+ componentId: "api-get-1",
602
+ args: [textArgument],
603
+ returnValues: [bodyReturnValue],
604
+ values: {
605
+ message:
606
+ "{{local.components.api-get-1.returnValues.response-body}}",
607
+ },
608
+ }),
609
+ ],
610
+ edges: [{ source: "n1", target: "n2" }],
611
+ });
612
+
613
+ expect(rulesOf(result)).toContain(WorkflowLintRule.SelfReference);
614
+ });
615
+
616
+ test("reports a reference to a step that runs later", () => {
617
+ const result: WorkflowLintResult = lintWorkflowGraph({
618
+ nodes: [
619
+ makeTrigger("n1", "manual-1"),
620
+ makeNode({
621
+ nodeId: "n2",
622
+ componentId: "first",
623
+ args: [textArgument],
624
+ values: {
625
+ message: "{{local.components.second.returnValues.response-body}}",
626
+ },
627
+ }),
628
+ makeNode({
629
+ nodeId: "n3",
630
+ componentId: "second",
631
+ returnValues: [bodyReturnValue],
632
+ }),
633
+ ],
634
+ edges: [
635
+ { source: "n1", target: "n2" },
636
+ { source: "n2", target: "n3" },
637
+ ],
638
+ });
639
+
640
+ const issue: WorkflowLintIssue | undefined = result.issues.find(
641
+ (candidate: WorkflowLintIssue) => {
642
+ return candidate.rule === WorkflowLintRule.ForwardReference;
643
+ },
644
+ );
645
+
646
+ expect(issue?.severity).toBe(WorkflowLintSeverity.Error);
647
+ expect(issue?.message).toMatch(/runs after this one/);
648
+ });
649
+
650
+ test("stays quiet about a step on a parallel branch, whose order is not decidable", () => {
651
+ /*
652
+ * trigger fans out to "sibling" and to "reader". The runner's queue is
653
+ * FIFO across branches, so sibling may well have run by the time reader
654
+ * does. Reporting it would be a guess.
655
+ */
656
+ const result: WorkflowLintResult = lintWorkflowGraph({
657
+ nodes: [
658
+ makeTrigger("n1", "manual-1"),
659
+ makeNode({
660
+ nodeId: "n2",
661
+ componentId: "sibling",
662
+ returnValues: [bodyReturnValue],
663
+ }),
664
+ makeNode({
665
+ nodeId: "n3",
666
+ componentId: "reader",
667
+ args: [textArgument],
668
+ values: {
669
+ message: "{{local.components.sibling.returnValues.response-body}}",
670
+ },
671
+ }),
672
+ ],
673
+ edges: [
674
+ { source: "n1", target: "n2" },
675
+ { source: "n1", target: "n3" },
676
+ ],
677
+ });
678
+
679
+ expect(result.issues).toEqual([]);
680
+ });
681
+
682
+ test("warns when the referenced step is never reached", () => {
683
+ const result: WorkflowLintResult = lintWorkflowGraph({
684
+ nodes: [
685
+ makeTrigger("n1", "manual-1"),
686
+ makeNode({
687
+ nodeId: "n2",
688
+ componentId: "reader",
689
+ args: [textArgument],
690
+ values: {
691
+ message: "{{local.components.island.returnValues.response-body}}",
692
+ },
693
+ }),
694
+ makeNode({
695
+ nodeId: "n3",
696
+ componentId: "island",
697
+ returnValues: [bodyReturnValue],
698
+ }),
699
+ ],
700
+ edges: [{ source: "n1", target: "n2" }],
701
+ });
702
+
703
+ expect(rulesOf(result)).toContain(
704
+ WorkflowLintRule.ReferenceToUnreachableComponent,
705
+ );
706
+ });
707
+
708
+ test("never reports references inside a loop body", () => {
709
+ const result: WorkflowLintResult = lintWorkflowGraph(
710
+ buildPair(
711
+ "{{#each local.components.api-get-1.returnValues.response-body}}{{status}} {{labels.name}} {{@index}} {{this}}{{/each}}",
712
+ ),
713
+ );
714
+
715
+ expect(result.issues).toEqual([]);
716
+ });
717
+
718
+ test("still checks the loop's own array path", () => {
719
+ const result: WorkflowLintResult = lintWorkflowGraph(
720
+ buildPair("{{#each local.componets.api-get-1.returnValues.x}}a{{/each}}"),
721
+ );
722
+
723
+ /*
724
+ * The {{#each}} tag is not a Reference, so it is not path-checked — this
725
+ * asserts the linter stays silent rather than inventing a rule it cannot
726
+ * apply soundly.
727
+ */
728
+ expect(result.issues).toEqual([]);
729
+ });
730
+
731
+ test("does not mistake JavaScript for a broken reference", () => {
732
+ const result: WorkflowLintResult = lintWorkflowGraph(
733
+ buildPair("if (x) {{ return 1; }}"),
734
+ );
735
+
736
+ expect(result.issues).toEqual([]);
737
+ });
738
+
739
+ test("finds references nested inside an object-shaped argument", () => {
740
+ const result: WorkflowLintResult = lintWorkflowGraph({
741
+ nodes: [
742
+ makeTrigger("n1", "manual-1"),
743
+ makeNode({
744
+ nodeId: "n2",
745
+ componentId: "api-post-1",
746
+ args: [
747
+ {
748
+ id: "request-headers",
749
+ name: "Request Headers",
750
+ description: "",
751
+ type: ComponentInputType.StringDictionary,
752
+ required: false,
753
+ },
754
+ ],
755
+ values: {
756
+ "request-headers": {
757
+ Authorization: "Bearer {{local.componets.x.returnValues.y}}",
758
+ },
759
+ },
760
+ }),
761
+ ],
762
+ edges: [{ source: "n1", target: "n2" }],
763
+ });
764
+
765
+ expect(rulesOf(result)).toContain(WorkflowLintRule.UnknownReferenceRoot);
766
+ });
767
+ });
768
+
769
+ describe("lintWorkflowGraph — roll-up", () => {
770
+ test("combines several messages for one node onto one line each", () => {
771
+ const trigger: LintGraphNode = makeTrigger("n1", "manual-1");
772
+ const step: LintGraphNode = makeNode({
773
+ nodeId: "n2",
774
+ componentId: "api.get.1",
775
+ args: [requiredUrlArgument, jsonArgument],
776
+ values: { "request-body": "{oops" },
777
+ });
778
+
779
+ const result: WorkflowLintResult = lintWorkflowGraph({
780
+ nodes: [trigger, step],
781
+ edges: [{ source: "n1", target: "n2" }],
782
+ });
783
+
784
+ const combined: string = result.errorsByNodeId["n2"] as string;
785
+
786
+ expect(combined.split("\n").length).toBeGreaterThan(1);
787
+ expect(combined).toMatch(/dot/);
788
+ expect(combined).toMatch(/required/);
789
+ });
790
+
791
+ test("counts errors and warnings separately", () => {
792
+ const trigger: LintGraphNode = makeTrigger("n1", "manual-1");
793
+ const missing: LintGraphNode = makeNode({
794
+ nodeId: "n2",
795
+ componentId: "a",
796
+ args: [requiredUrlArgument],
797
+ values: {},
798
+ });
799
+ const orphan: LintGraphNode = makeNode({ nodeId: "n3", componentId: "b" });
800
+
801
+ const result: WorkflowLintResult = lintWorkflowGraph({
802
+ nodes: [trigger, missing, orphan],
803
+ edges: [{ source: "n1", target: "n2" }],
804
+ });
805
+
806
+ expect(result.errorCount).toBe(1);
807
+ expect(result.warningCount).toBe(1);
808
+ });
809
+
810
+ test("keeps graph-wide issues out of the per-node map", () => {
811
+ const step: LintGraphNode = makeNode({ nodeId: "n1", componentId: "a" });
812
+
813
+ const result: WorkflowLintResult = lintWorkflowGraph({
814
+ nodes: [step],
815
+ edges: [],
816
+ });
817
+
818
+ expect(result.errorsByNodeId["n1"]).toBeUndefined();
819
+ expect(result.errorCount).toBe(1);
820
+ });
821
+ });