@qontinui/shared-types 0.2.4 → 0.2.5

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 (539) hide show
  1. package/README.md +128 -128
  2. package/dist/{Region.d-BbpFAWF4.d.cts → Region.d-DtT3UphX.d.cts} +81 -81
  3. package/dist/{Region.d-BbpFAWF4.d.ts → Region.d-DtT3UphX.d.ts} +81 -81
  4. package/dist/{TaskRunFindingActionType.d-B4VmmbB6.d.cts → TaskRunFindingActionType.d-CNIWogcU.d.cts} +53 -53
  5. package/dist/{TaskRunFindingActionType.d-B4VmmbB6.d.ts → TaskRunFindingActionType.d-CNIWogcU.d.ts} +53 -53
  6. package/dist/accessibility.cjs.map +1 -1
  7. package/dist/accessibility.d.cts +379 -379
  8. package/dist/accessibility.d.ts +379 -379
  9. package/dist/chat.cjs.map +1 -1
  10. package/dist/{chunk-ZK5MJEGT.js → chunk-CTBDIP7K.js} +1 -1
  11. package/dist/chunk-CTBDIP7K.js.map +1 -0
  12. package/dist/{chunk-YXD4WCYQ.js → chunk-ZUZ3OLOH.js} +1 -1
  13. package/dist/chunk-ZUZ3OLOH.js.map +1 -0
  14. package/dist/config.cjs.map +1 -1
  15. package/dist/config.d.cts +105 -105
  16. package/dist/config.d.ts +105 -105
  17. package/dist/constraints.cjs.map +1 -1
  18. package/dist/constraints.d.cts +372 -372
  19. package/dist/constraints.d.ts +372 -372
  20. package/dist/execution.cjs.map +1 -1
  21. package/dist/execution.d.cts +869 -869
  22. package/dist/execution.d.ts +869 -869
  23. package/dist/execution.js +1 -1
  24. package/dist/geometry.cjs.map +1 -1
  25. package/dist/geometry.d.cts +81 -81
  26. package/dist/geometry.d.ts +81 -81
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +589 -589
  29. package/dist/index.d.ts +589 -589
  30. package/dist/index.js +2 -2
  31. package/dist/index.js.map +1 -1
  32. package/dist/library.cjs.map +1 -1
  33. package/dist/rag.cjs.map +1 -1
  34. package/dist/rag.d.cts +579 -579
  35. package/dist/rag.d.ts +579 -579
  36. package/dist/runner.cjs.map +1 -1
  37. package/dist/runner.d.cts +147 -147
  38. package/dist/runner.d.ts +147 -147
  39. package/dist/scheduler.cjs.map +1 -1
  40. package/dist/scheduler.d.cts +692 -692
  41. package/dist/scheduler.d.ts +692 -692
  42. package/dist/state-machine.cjs.map +1 -1
  43. package/dist/state-machine.d.cts +1128 -1128
  44. package/dist/state-machine.d.ts +1128 -1128
  45. package/dist/targets.cjs.map +1 -1
  46. package/dist/targets.d.cts +270 -270
  47. package/dist/targets.d.ts +270 -270
  48. package/dist/task-run.cjs.map +1 -1
  49. package/dist/task-run.d.cts +1431 -1431
  50. package/dist/task-run.d.ts +1431 -1431
  51. package/dist/tauri-events.cjs.map +1 -1
  52. package/dist/tauri-events.d.cts +310 -310
  53. package/dist/tauri-events.d.ts +310 -310
  54. package/dist/tree-events.cjs.map +1 -1
  55. package/dist/tree-events.d.cts +483 -483
  56. package/dist/tree-events.d.ts +483 -483
  57. package/dist/ui-bridge-ir.cjs.map +1 -1
  58. package/dist/ui-bridge-ir.js.map +1 -1
  59. package/dist/workflow.cjs.map +1 -1
  60. package/dist/workflow.d.cts +1634 -1634
  61. package/dist/workflow.d.ts +1634 -1634
  62. package/dist/workflow.js +1 -1
  63. package/package.json +141 -141
  64. package/src/accessibility/_api.ts +17 -17
  65. package/src/accessibility/index.ts +1 -1
  66. package/src/canvas/index.ts +319 -319
  67. package/src/chat/_api.ts +23 -23
  68. package/src/chat/index.ts +1 -1
  69. package/src/config/_api.ts +13 -13
  70. package/src/config/index.ts +1 -1
  71. package/src/constraints/_api.ts +17 -17
  72. package/src/constraints/index.ts +1 -1
  73. package/src/discovery.ts +188 -188
  74. package/src/execution/_api.ts +483 -483
  75. package/src/execution/index.ts +1 -1
  76. package/src/generated/A11yAction.d.ts +11 -11
  77. package/src/generated/AccessibilityBackend.d.ts +12 -12
  78. package/src/generated/AccessibilityBounds.d.ts +17 -17
  79. package/src/generated/AccessibilityNode.d.ts +73 -73
  80. package/src/generated/AccessibilityRole.d.ts +93 -93
  81. package/src/generated/AccessibilityRoleCriterion.d.ts +13 -13
  82. package/src/generated/AccessibilitySelector.d.ts +76 -76
  83. package/src/generated/AccessibilitySnapshot.d.ts +43 -43
  84. package/src/generated/AccessibilityState.d.ts +67 -67
  85. package/src/generated/ActionExecutionBatch.d.ts +23 -23
  86. package/src/generated/ActionExecutionBatchResponse.d.ts +24 -24
  87. package/src/generated/ActionExecutionCreate.d.ts +124 -124
  88. package/src/generated/ActionExecutionListResponse.d.ts +20 -20
  89. package/src/generated/ActionExecutionResponse.d.ts +24 -24
  90. package/src/generated/ActionReliabilityStats.d.ts +54 -54
  91. package/src/generated/ActionResponse.d.ts +45 -45
  92. package/src/generated/ActionStatus.d.ts +11 -11
  93. package/src/generated/ActionType.d.ts +41 -41
  94. package/src/generated/ActiveStatesResult.d.ts +32 -32
  95. package/src/generated/AgenticPhaseOutput.d.ts +50 -50
  96. package/src/generated/AgenticStatus.d.ts +11 -11
  97. package/src/generated/AiWorkflow.d.ts +76 -76
  98. package/src/generated/ApiAssertion.d.ts +35 -35
  99. package/src/generated/ApiAssertionOperator.d.ts +11 -11
  100. package/src/generated/ApiAssertionType.d.ts +11 -11
  101. package/src/generated/ApiContentType.d.ts +11 -11
  102. package/src/generated/ApiVariableExtraction.d.ts +24 -24
  103. package/src/generated/AvailableTransitionsResult.d.ts +34 -34
  104. package/src/generated/BaseStepFields.d.ts +72 -72
  105. package/src/generated/BatchComputeEmbeddingRequest.d.ts +27 -27
  106. package/src/generated/BatchComputeEmbeddingResponse.d.ts +23 -23
  107. package/src/generated/BatchEmbeddingResult.d.ts +23 -23
  108. package/src/generated/BetweenIterations.d.ts +35 -35
  109. package/src/generated/BoundingBox.d.ts +18 -18
  110. package/src/generated/BuildPhaseConfig.d.ts +24 -24
  111. package/src/generated/BuiltinOverrideProposal.d.ts +27 -27
  112. package/src/generated/CanonicalStep.d.ts +60 -60
  113. package/src/generated/CatchUpPolicy.d.ts +12 -12
  114. package/src/generated/Category.d.ts +24 -24
  115. package/src/generated/CheckIssueDetail.d.ts +40 -40
  116. package/src/generated/CheckType.d.ts +21 -21
  117. package/src/generated/CodeExecutionStep.d.ts +88 -88
  118. package/src/generated/CommandMode.d.ts +11 -11
  119. package/src/generated/CommandStep.d.ts +183 -183
  120. package/src/generated/CommandStepPhase.d.ts +11 -11
  121. package/src/generated/ComponentActionInfo.d.ts +24 -24
  122. package/src/generated/ComponentActionRequest.d.ts +22 -22
  123. package/src/generated/ComputeEmbeddingRequest.d.ts +24 -24
  124. package/src/generated/ComputeEmbeddingResponse.d.ts +38 -38
  125. package/src/generated/ComputeTextEmbeddingRequest.d.ts +20 -20
  126. package/src/generated/ComputeTextEmbeddingResponse.d.ts +32 -32
  127. package/src/generated/ConditionScheduleConfig.d.ts +17 -17
  128. package/src/generated/ConditionStatus.d.ts +29 -29
  129. package/src/generated/Confidence.d.ts +11 -11
  130. package/src/generated/ConfidenceLevel.d.ts +11 -11
  131. package/src/generated/Constraint.d.ts +33 -33
  132. package/src/generated/ConstraintCheck.d.ts +64 -64
  133. package/src/generated/ConstraintProposal.d.ts +37 -37
  134. package/src/generated/ConstraintResult.d.ts +32 -32
  135. package/src/generated/ConstraintSeverity.d.ts +16 -16
  136. package/src/generated/ConstraintViolation.d.ts +24 -24
  137. package/src/generated/Context.d.ts +50 -50
  138. package/src/generated/ContextAutoInclude.d.ts +34 -34
  139. package/src/generated/CoordinateSystem.d.ts +12 -12
  140. package/src/generated/Coordinates.d.ts +33 -33
  141. package/src/generated/CostTrendDataPoint.d.ts +36 -36
  142. package/src/generated/CostTrendResponse.d.ts +41 -41
  143. package/src/generated/CoverageData.d.ts +52 -52
  144. package/src/generated/CreateMcpCallInput.d.ts +59 -59
  145. package/src/generated/CreateMcpServerInput.d.ts +48 -48
  146. package/src/generated/CreateOlConfigRequest.d.ts +27 -27
  147. package/src/generated/CreateScheduledTaskRequest.d.ts +63 -63
  148. package/src/generated/CreateTaskRunRequest.d.ts +51 -51
  149. package/src/generated/CriterionOverride.d.ts +39 -39
  150. package/src/generated/CriterionType.d.ts +11 -11
  151. package/src/generated/DagApprovalStep.d.ts +80 -80
  152. package/src/generated/DagCancelStep.d.ts +76 -76
  153. package/src/generated/DagLoopStep.d.ts +80 -80
  154. package/src/generated/DecomposerConfig.d.ts +31 -31
  155. package/src/generated/DiagnosePhaseConfig.d.ts +34 -34
  156. package/src/generated/DiagnosticResult.d.ts +40 -40
  157. package/src/generated/DiscoveredElement.d.ts +48 -48
  158. package/src/generated/DiscoveredState.d.ts +49 -49
  159. package/src/generated/DiscoveredStateImage.d.ts +56 -56
  160. package/src/generated/DiscoveredTransition.d.ts +44 -44
  161. package/src/generated/DiscoveryBoundingBox.d.ts +32 -32
  162. package/src/generated/DiscoveryRequest.d.ts +40 -40
  163. package/src/generated/DiscoveryResponse.d.ts +32 -32
  164. package/src/generated/DiscoverySourceType.d.ts +14 -14
  165. package/src/generated/DiscoveryStrategy.d.ts +13 -13
  166. package/src/generated/DiscoveryTransitionTrigger.d.ts +38 -38
  167. package/src/generated/DisplayNode.d.ts +45 -45
  168. package/src/generated/DomainAssignment.d.ts +50 -50
  169. package/src/generated/DomainKnowledge.d.ts +29 -29
  170. package/src/generated/DomainVerificationResult.d.ts +36 -36
  171. package/src/generated/ElementActionRequest.d.ts +28 -28
  172. package/src/generated/ElementBbox.d.ts +36 -36
  173. package/src/generated/ElementIdentifier.d.ts +40 -40
  174. package/src/generated/ElementRect.d.ts +48 -48
  175. package/src/generated/ElementState.d.ts +47 -47
  176. package/src/generated/ElementType.d.ts +45 -45
  177. package/src/generated/EmbeddedElement.d.ts +36 -36
  178. package/src/generated/EmbeddingItem.d.ts +49 -49
  179. package/src/generated/EmbeddingListResponse.d.ts +22 -22
  180. package/src/generated/EmbeddingResultItem.d.ts +29 -29
  181. package/src/generated/EmbeddingResultsRequest.d.ts +19 -19
  182. package/src/generated/EmbeddingResultsResponse.d.ts +17 -17
  183. package/src/generated/ErrorType.d.ts +18 -18
  184. package/src/generated/ExecutePlaybookStep.d.ts +84 -84
  185. package/src/generated/ExecutionActionType.d.ts +40 -40
  186. package/src/generated/ExecutionIssueBatch.d.ts +19 -19
  187. package/src/generated/ExecutionIssueBatchResponse.d.ts +24 -24
  188. package/src/generated/ExecutionIssueCreate.d.ts +57 -57
  189. package/src/generated/ExecutionIssueDetail.d.ts +89 -89
  190. package/src/generated/ExecutionIssueListResponse.d.ts +26 -26
  191. package/src/generated/ExecutionIssueResponse.d.ts +24 -24
  192. package/src/generated/ExecutionIssueUpdate.d.ts +31 -31
  193. package/src/generated/ExecutionMatchLocation.d.ts +31 -31
  194. package/src/generated/ExecutionRunComplete.d.ts +34 -34
  195. package/src/generated/ExecutionRunCompleteResponse.d.ts +38 -38
  196. package/src/generated/ExecutionRunCreate.d.ts +40 -40
  197. package/src/generated/ExecutionRunDetail.d.ts +76 -76
  198. package/src/generated/ExecutionRunListResponse.d.ts +22 -22
  199. package/src/generated/ExecutionRunResponse.d.ts +41 -41
  200. package/src/generated/ExecutionScreenshotCreate.d.ts +59 -59
  201. package/src/generated/ExecutionScreenshotResponse.d.ts +36 -36
  202. package/src/generated/ExecutionStats.d.ts +56 -56
  203. package/src/generated/ExecutionStep.d.ts +78 -78
  204. package/src/generated/ExecutionTreeResponse.d.ts +42 -42
  205. package/src/generated/ExecutionTrendDataPoint.d.ts +36 -36
  206. package/src/generated/ExecutionTrendResponse.d.ts +46 -46
  207. package/src/generated/ExitCheckResult.d.ts +20 -20
  208. package/src/generated/ExitStrategy.d.ts +27 -27
  209. package/src/generated/ExportResult.d.ts +30 -30
  210. package/src/generated/ExtendIterationsRequest.d.ts +20 -20
  211. package/src/generated/FileChange.d.ts +20 -20
  212. package/src/generated/Finding.d.ts +40 -40
  213. package/src/generated/FindingActionType.d.ts +13 -13
  214. package/src/generated/FindingBatchCreate.d.ts +27 -27
  215. package/src/generated/FindingCategory.d.ts +25 -25
  216. package/src/generated/FindingCodeContext.d.ts +31 -31
  217. package/src/generated/FindingCreate.d.ts +52 -52
  218. package/src/generated/FindingDetail.d.ts +78 -78
  219. package/src/generated/FindingListResponse.d.ts +40 -40
  220. package/src/generated/FindingOutput.d.ts +36 -36
  221. package/src/generated/FindingSeverity.d.ts +14 -14
  222. package/src/generated/FindingStatus.d.ts +15 -15
  223. package/src/generated/FindingSummary.d.ts +53 -53
  224. package/src/generated/FindingUpdate.d.ts +33 -33
  225. package/src/generated/FindingUserInput.d.ts +27 -27
  226. package/src/generated/FindingsSummary.d.ts +47 -47
  227. package/src/generated/FullRunnerStep.d.ts +144 -144
  228. package/src/generated/GUIElementChunk.d.ts +151 -151
  229. package/src/generated/GateEvaluationResult.d.ts +36 -36
  230. package/src/generated/HealthCheckUrl.d.ts +35 -35
  231. package/src/generated/HistoricalActionQuery.d.ts +42 -42
  232. package/src/generated/HistoricalActionResult.d.ts +57 -57
  233. package/src/generated/HttpConfig.d.ts +27 -27
  234. package/src/generated/HttpMethod.d.ts +13 -13
  235. package/src/generated/IdleCondition.d.ts +17 -17
  236. package/src/generated/ImplementFixesConfig.d.ts +25 -25
  237. package/src/generated/IndividualCheckResult.d.ts +50 -50
  238. package/src/generated/InitialStateRef.d.ts +23 -23
  239. package/src/generated/InitialStatesSource.d.ts +18 -18
  240. package/src/generated/IssueSeverity.d.ts +11 -11
  241. package/src/generated/IssueSource.d.ts +11 -11
  242. package/src/generated/IssueStatus.d.ts +11 -11
  243. package/src/generated/IssueType.d.ts +21 -21
  244. package/src/generated/IterationResult.d.ts +65 -65
  245. package/src/generated/IterationVerificationResults.d.ts +53 -53
  246. package/src/generated/JobItem.d.ts +31 -31
  247. package/src/generated/JobListResponse.d.ts +20 -20
  248. package/src/generated/JobStatus.d.ts +11 -11
  249. package/src/generated/JobSummary.d.ts +30 -30
  250. package/src/generated/LLMCostSummary.d.ts +38 -38
  251. package/src/generated/LLMMetrics.d.ts +42 -42
  252. package/src/generated/LogSourceSelection.d.ts +25 -25
  253. package/src/generated/LoopInstanceStatus.d.ts +28 -28
  254. package/src/generated/LoopPhase.d.ts +26 -26
  255. package/src/generated/MatchAdjustment.d.ts +23 -23
  256. package/src/generated/MatchLocation.d.ts +22 -22
  257. package/src/generated/MatchMethod.d.ts +11 -11
  258. package/src/generated/McpCallRecord.d.ts +35 -35
  259. package/src/generated/McpCallsResult.d.ts +44 -44
  260. package/src/generated/McpConnectionRef.d.ts +25 -25
  261. package/src/generated/McpServerConfig.d.ts +77 -77
  262. package/src/generated/McpServerStatus.d.ts +42 -42
  263. package/src/generated/McpToolCallResult.d.ts +38 -38
  264. package/src/generated/McpToolInfo.d.ts +23 -23
  265. package/src/generated/McpToolInputSchema.d.ts +34 -34
  266. package/src/generated/McpTransport.d.ts +15 -15
  267. package/src/generated/ModelCostBreakdown.d.ts +36 -36
  268. package/src/generated/ModelOverrideConfig.d.ts +47 -47
  269. package/src/generated/Monitor.d.ts +48 -48
  270. package/src/generated/MonitorPosition.d.ts +13 -13
  271. package/src/generated/MouseButton.d.ts +13 -13
  272. package/src/generated/MultiLoopConfig.d.ts +32 -32
  273. package/src/generated/MultiLoopEntry.d.ts +32 -32
  274. package/src/generated/MultiLoopStatus.d.ts +37 -37
  275. package/src/generated/NativeAccessibilityStep.d.ts +121 -121
  276. package/src/generated/NavigationResult.d.ts +40 -40
  277. package/src/generated/NewConstraintProposal.d.ts +20 -20
  278. package/src/generated/NextTaskInfo.d.ts +24 -24
  279. package/src/generated/NodeMetadata.d.ts +47 -47
  280. package/src/generated/NodeStatus.d.ts +11 -11
  281. package/src/generated/NodeType.d.ts +11 -11
  282. package/src/generated/OcrEngine.d.ts +11 -11
  283. package/src/generated/OlConfig.d.ts +48 -48
  284. package/src/generated/OrchestrationLoopConfig.d.ts +87 -87
  285. package/src/generated/OrchestrationLoopStatus.d.ts +59 -59
  286. package/src/generated/Outcome.d.ts +15 -15
  287. package/src/generated/OutputLine.d.ts +23 -23
  288. package/src/generated/OutputStream.d.ts +11 -11
  289. package/src/generated/OverrideCollection.d.ts +18 -18
  290. package/src/generated/Pagination.d.ts +28 -28
  291. package/src/generated/ParserType.d.ts +17 -17
  292. package/src/generated/PathElement.d.ts +17 -17
  293. package/src/generated/PathfindingRequest.d.ts +24 -24
  294. package/src/generated/PathfindingResult.d.ts +30 -30
  295. package/src/generated/PathfindingStep.d.ts +36 -36
  296. package/src/generated/PatternOptions.d.ts +32 -32
  297. package/src/generated/PipelineConfig.d.ts +31 -31
  298. package/src/generated/PlaybackFrameRequest.d.ts +21 -21
  299. package/src/generated/PlaywrightExecutionMode.d.ts +11 -11
  300. package/src/generated/Point.d.ts +20 -20
  301. package/src/generated/PollingConfig.d.ts +14 -14
  302. package/src/generated/ProcessConfig.d.ts +96 -96
  303. package/src/generated/ProcessState.d.ts +15 -15
  304. package/src/generated/ProcessStatus.d.ts +41 -41
  305. package/src/generated/PromptStep.d.ts +92 -92
  306. package/src/generated/PromptStepPhase.d.ts +13 -13
  307. package/src/generated/RAGDashboardStats.d.ts +26 -26
  308. package/src/generated/RagCompletionEvent.d.ts +25 -25
  309. package/src/generated/RagProcessingStatus.d.ts +11 -11
  310. package/src/generated/RagProgressEvent.d.ts +27 -27
  311. package/src/generated/ReadConfigResponse.d.ts +20 -20
  312. package/src/generated/ReflectionFixOutput.d.ts +20 -20
  313. package/src/generated/Region.d.ts +39 -39
  314. package/src/generated/RepositoryInactiveCondition.d.ts +24 -24
  315. package/src/generated/RepositoryWatch.d.ts +20 -20
  316. package/src/generated/ResolvedInitialStates.d.ts +29 -29
  317. package/src/generated/ResolvedInitialStatesResult.d.ts +40 -40
  318. package/src/generated/ResourceLimits.d.ts +33 -33
  319. package/src/generated/RestartProcessStep.d.ts +88 -88
  320. package/src/generated/RetryPolicy.d.ts +24 -24
  321. package/src/generated/RetrySpec.d.ts +24 -24
  322. package/src/generated/RoleCriterion.d.ts +14 -14
  323. package/src/generated/RootCauseCategory.d.ts +18 -18
  324. package/src/generated/RoutingRule.d.ts +38 -38
  325. package/src/generated/RunPromptRequest.d.ts +56 -56
  326. package/src/generated/RunPromptResponse.d.ts +50 -50
  327. package/src/generated/RunPromptResponseData.d.ts +20 -20
  328. package/src/generated/RunStatus.d.ts +11 -11
  329. package/src/generated/RunType.d.ts +11 -11
  330. package/src/generated/Runner.d.ts +81 -81
  331. package/src/generated/RunnerCrash.d.ts +32 -32
  332. package/src/generated/RunnerFinding.d.ts +48 -48
  333. package/src/generated/RunnerFindingCodeContext.d.ts +20 -20
  334. package/src/generated/RunnerFindingUserInput.d.ts +19 -19
  335. package/src/generated/RunnerMetadata.d.ts +42 -42
  336. package/src/generated/RunnerStatus.d.ts +12 -12
  337. package/src/generated/RunnerUiError.d.ts +28 -28
  338. package/src/generated/RuntimeData.d.ts +40 -40
  339. package/src/generated/SaveWorkflowArtifactStep.d.ts +80 -80
  340. package/src/generated/ScheduleConditions.d.ts +29 -29
  341. package/src/generated/ScheduleExpression.d.ts +39 -39
  342. package/src/generated/ScheduledTask.d.ts +104 -104
  343. package/src/generated/ScheduledTaskStatus.d.ts +19 -19
  344. package/src/generated/ScheduledTaskType.d.ts +126 -126
  345. package/src/generated/SchedulerSettings.d.ts +28 -28
  346. package/src/generated/SchedulerStatus.d.ts +31 -31
  347. package/src/generated/ScreenshotAnnotation.d.ts +39 -39
  348. package/src/generated/ScreenshotAnnotationShape.d.ts +11 -11
  349. package/src/generated/ScreenshotType.d.ts +11 -11
  350. package/src/generated/ScrollDirection.d.ts +13 -13
  351. package/src/generated/SearchOptions.d.ts +33 -33
  352. package/src/generated/SearchResultItem.d.ts +19 -19
  353. package/src/generated/SearchStrategy.d.ts +14 -14
  354. package/src/generated/SemanticSearchRequest.d.ts +26 -26
  355. package/src/generated/SemanticSearchResponse.d.ts +21 -21
  356. package/src/generated/StageCondition.d.ts +37 -37
  357. package/src/generated/StageInput.d.ts +27 -27
  358. package/src/generated/StageOutput.d.ts +20 -20
  359. package/src/generated/StageTransition.d.ts +30 -30
  360. package/src/generated/StallDetectorConfig.d.ts +31 -31
  361. package/src/generated/StandardActionType.d.ts +40 -40
  362. package/src/generated/StateContext.d.ts +17 -17
  363. package/src/generated/StateDiscoveryResult.d.ts +104 -104
  364. package/src/generated/StateDiscoveryResultCreate.d.ts +65 -65
  365. package/src/generated/StateDiscoveryResultListResponse.d.ts +23 -23
  366. package/src/generated/StateDiscoveryResultSummary.d.ts +57 -57
  367. package/src/generated/StateDiscoveryResultUpdate.d.ts +47 -47
  368. package/src/generated/StateFilterItem.d.ts +18 -18
  369. package/src/generated/StateMachineConfig.d.ts +45 -45
  370. package/src/generated/StateMachineConfigCreate.d.ts +20 -20
  371. package/src/generated/StateMachineConfigFull.d.ts +67 -67
  372. package/src/generated/StateMachineConfigUpdate.d.ts +20 -20
  373. package/src/generated/StateMachineExport.d.ts +64 -64
  374. package/src/generated/StateMachineExportFormat.d.ts +39 -39
  375. package/src/generated/StateMachineImport.d.ts +25 -25
  376. package/src/generated/StateMachineState.d.ts +71 -71
  377. package/src/generated/StateMachineStateCreate.d.ts +52 -52
  378. package/src/generated/StateMachineStateUpdate.d.ts +48 -48
  379. package/src/generated/StateMachineTransition.d.ts +75 -75
  380. package/src/generated/StateMachineTransitionCreate.d.ts +53 -53
  381. package/src/generated/StateMachineTransitionUpdate.d.ts +53 -53
  382. package/src/generated/StateNodeData.d.ts +71 -71
  383. package/src/generated/StatesResponse.d.ts +19 -19
  384. package/src/generated/StdioConfig.d.ts +34 -34
  385. package/src/generated/StepExecutionConfig.d.ts +37 -37
  386. package/src/generated/StructuredFinding.d.ts +32 -32
  387. package/src/generated/StructuredOverride.d.ts +24 -24
  388. package/src/generated/StructuredSignal.d.ts +20 -20
  389. package/src/generated/SuccessCriterion.d.ts +57 -57
  390. package/src/generated/SummarizationConfig.d.ts +35 -35
  391. package/src/generated/TargetConfig.d.ts +108 -108
  392. package/src/generated/TaskCompletionResult.d.ts +90 -90
  393. package/src/generated/TaskExecutionRecord.d.ts +61 -61
  394. package/src/generated/TaskRun.d.ts +88 -88
  395. package/src/generated/TaskRunBackend.d.ts +86 -86
  396. package/src/generated/TaskRunBackendDetail.d.ts +99 -99
  397. package/src/generated/TaskRunCreate.d.ts +62 -62
  398. package/src/generated/TaskRunFilters.d.ts +38 -38
  399. package/src/generated/TaskRunFinding.d.ts +100 -100
  400. package/src/generated/TaskRunFindingActionType.d.ts +11 -11
  401. package/src/generated/TaskRunFindingCategory.d.ts +24 -24
  402. package/src/generated/TaskRunFindingCreate.d.ts +79 -79
  403. package/src/generated/TaskRunFindingFilters.d.ts +28 -28
  404. package/src/generated/TaskRunFindingSeverity.d.ts +11 -11
  405. package/src/generated/TaskRunFindingStatus.d.ts +11 -11
  406. package/src/generated/TaskRunFindingSummary.d.ts +34 -34
  407. package/src/generated/TaskRunFindingUpdate.d.ts +34 -34
  408. package/src/generated/TaskRunFindingsListResponse.d.ts +24 -24
  409. package/src/generated/TaskRunListResponse.d.ts +21 -21
  410. package/src/generated/TaskRunSession.d.ts +40 -40
  411. package/src/generated/TaskRunStatus.d.ts +11 -11
  412. package/src/generated/TaskRunUpdate.d.ts +47 -47
  413. package/src/generated/TaskType.d.ts +11 -11
  414. package/src/generated/TerminalExitEvent.d.ts +26 -26
  415. package/src/generated/TerminalInfo.d.ts +69 -69
  416. package/src/generated/TerminalOutputEvent.d.ts +29 -29
  417. package/src/generated/TestType.d.ts +11 -11
  418. package/src/generated/TextMatchType.d.ts +11 -11
  419. package/src/generated/TextSearchOptions.d.ts +30 -30
  420. package/src/generated/Ticket.d.ts +53 -53
  421. package/src/generated/TicketComment.d.ts +28 -28
  422. package/src/generated/TicketProviderConfig.d.ts +55 -55
  423. package/src/generated/TicketSource.d.ts +17 -17
  424. package/src/generated/TicketState.d.ts +15 -15
  425. package/src/generated/TimingInfo.d.ts +23 -23
  426. package/src/generated/TopMatch.d.ts +17 -17
  427. package/src/generated/TransitionAction.d.ts +93 -93
  428. package/src/generated/TransitionActionValue.d.ts +14 -14
  429. package/src/generated/TransitionEdgeData.d.ts +46 -46
  430. package/src/generated/TransitionExecutionResult.d.ts +30 -30
  431. package/src/generated/TransitionInfo.d.ts +29 -29
  432. package/src/generated/TransitionTriggerType.d.ts +13 -13
  433. package/src/generated/TreeEvent.d.ts +47 -47
  434. package/src/generated/TreeEventCreate.d.ts +30 -30
  435. package/src/generated/TreeEventListResponse.d.ts +30 -30
  436. package/src/generated/TreeEventResponse.d.ts +47 -47
  437. package/src/generated/TreeEventType.d.ts +20 -20
  438. package/src/generated/TreeNode.d.ts +54 -54
  439. package/src/generated/UIBridgeCapturedToast.d.ts +22 -22
  440. package/src/generated/UIBridgeComponent.d.ts +45 -45
  441. package/src/generated/UIBridgeElement.d.ts +62 -62
  442. package/src/generated/UIBridgeModalInfo.d.ts +40 -40
  443. package/src/generated/UIBridgeModalStack.d.ts +19 -19
  444. package/src/generated/UIBridgeSnapshot.d.ts +67 -67
  445. package/src/generated/UIBridgeToastContext.d.ts +17 -17
  446. package/src/generated/UIBridgeUndoContext.d.ts +19 -19
  447. package/src/generated/UiBridgeAction.d.ts +18 -18
  448. package/src/generated/UiBridgeAssertType.d.ts +11 -11
  449. package/src/generated/UiBridgeComparisonMode.d.ts +11 -11
  450. package/src/generated/UiBridgeDesignAuditStep.d.ts +76 -76
  451. package/src/generated/UiBridgeRequestEnvelope.d.ts +45 -45
  452. package/src/generated/UiBridgeResponseEnvelope.d.ts +66 -66
  453. package/src/generated/UiBridgeSeverity.d.ts +11 -11
  454. package/src/generated/UiBridgeStep.d.ts +127 -127
  455. package/src/generated/UiBridgeStepPhase.d.ts +14 -14
  456. package/src/generated/UiBridgeVisualAssertionStep.d.ts +97 -97
  457. package/src/generated/UnifiedStep.d.ts +57 -57
  458. package/src/generated/UnifiedWorkflow.d.ts +411 -411
  459. package/src/generated/UpdateMcpServerInput.d.ts +28 -28
  460. package/src/generated/UpdateOlConfigRequest.d.ts +32 -32
  461. package/src/generated/UpdateScheduledTaskRequest.d.ts +71 -71
  462. package/src/generated/ValidateConfigRequest.d.ts +16 -16
  463. package/src/generated/ValidateConfigResponse.d.ts +28 -28
  464. package/src/generated/VectorSearchResult.d.ts +48 -48
  465. package/src/generated/VerificationAgentContext.d.ts +25 -25
  466. package/src/generated/VerificationCategoryKind.d.ts +19 -19
  467. package/src/generated/VerificationMethod.d.ts +20 -20
  468. package/src/generated/VerificationPhaseResult.d.ts +63 -63
  469. package/src/generated/VerificationPlan.d.ts +43 -43
  470. package/src/generated/VerificationResult.d.ts +44 -44
  471. package/src/generated/VerificationResultResponse.d.ts +65 -65
  472. package/src/generated/VerificationResultsListResponse.d.ts +41 -41
  473. package/src/generated/VerificationStepDetails.d.ts +55 -55
  474. package/src/generated/VerificationStepResult.d.ts +68 -68
  475. package/src/generated/VgaAction.d.ts +61 -61
  476. package/src/generated/VgaActionKind.d.ts +14 -14
  477. package/src/generated/VgaAutomateStep.d.ts +110 -110
  478. package/src/generated/VirtualDesktop.d.ts +23 -23
  479. package/src/generated/VisualAssertionType.d.ts +11 -11
  480. package/src/generated/VisualComparisonResult.d.ts +40 -40
  481. package/src/generated/WaitOptions.d.ts +35 -35
  482. package/src/generated/WorkerCoordinationMessage.d.ts +93 -93
  483. package/src/generated/WorkerDomain.d.ts +29 -29
  484. package/src/generated/WorkerInstance.d.ts +62 -62
  485. package/src/generated/WorkerOutput.d.ts +53 -53
  486. package/src/generated/WorkerSignal.d.ts +48 -48
  487. package/src/generated/WorkerStatus.d.ts +11 -11
  488. package/src/generated/WorkflowArchitecture.d.ts +16 -16
  489. package/src/generated/WorkflowFixupMode.d.ts +11 -11
  490. package/src/generated/WorkflowFixupStep.d.ts +85 -85
  491. package/src/generated/WorkflowInfo.d.ts +28 -28
  492. package/src/generated/WorkflowMetadata.d.ts +44 -44
  493. package/src/generated/WorkflowRefStep.d.ts +94 -94
  494. package/src/generated/WorkflowStage.d.ts +126 -126
  495. package/src/generated/WorkflowStep.d.ts +80 -80
  496. package/src/generated/WorkflowStepPhase.d.ts +11 -11
  497. package/src/generated/WriteConfigRequest.d.ts +20 -20
  498. package/src/generated/WriteConfigResponse.d.ts +24 -24
  499. package/src/generated/index.ts +432 -432
  500. package/src/geometry/_api.ts +16 -16
  501. package/src/geometry/index.ts +1 -1
  502. package/src/index.ts +73 -73
  503. package/src/known-issues/index.ts +179 -179
  504. package/src/library/_api.ts +326 -326
  505. package/src/library/index.ts +1 -1
  506. package/src/rag/_api.ts +47 -47
  507. package/src/rag/index.ts +1 -1
  508. package/src/render-log.ts +371 -371
  509. package/src/runner/_api.ts +14 -14
  510. package/src/runner/index.ts +1 -1
  511. package/src/scheduler/_api.ts +21 -21
  512. package/src/scheduler/index.ts +1 -1
  513. package/src/state-machine/_api.ts +145 -145
  514. package/src/state-machine/index.ts +1 -1
  515. package/src/targets/_api.ts +47 -47
  516. package/src/targets/index.ts +1 -1
  517. package/src/task-run/_api.ts +108 -108
  518. package/src/task-run/index.ts +1 -1
  519. package/src/tauri-events/_api.ts +90 -90
  520. package/src/tauri-events/index.ts +1 -1
  521. package/src/tree-events/_api.ts +42 -42
  522. package/src/tree-events/index.ts +1 -1
  523. package/src/ui-bridge-ir/__tests__/projection.test.ts +1029 -1029
  524. package/src/ui-bridge-ir/adapter.ts +220 -220
  525. package/src/ui-bridge-ir/document.ts +114 -114
  526. package/src/ui-bridge-ir/element-criteria.ts +48 -48
  527. package/src/ui-bridge-ir/group.ts +84 -84
  528. package/src/ui-bridge-ir/index.ts +24 -24
  529. package/src/ui-bridge-ir/ir.schema.json +293 -293
  530. package/src/ui-bridge-ir/primitives.ts +102 -102
  531. package/src/ui-bridge-ir/projection.ts +921 -921
  532. package/src/ui-bridge-ir/state.ts +90 -90
  533. package/src/ui-bridge-ir/transition.ts +92 -92
  534. package/src/workflow/_api.ts +474 -474
  535. package/src/workflow/action-plan.ts +208 -208
  536. package/src/workflow/index.ts +1 -1
  537. package/src/workflow/skill.ts +152 -152
  538. package/dist/chunk-YXD4WCYQ.js.map +0 -1
  539. package/dist/chunk-ZK5MJEGT.js.map +0 -1
@@ -1,1029 +1,1029 @@
1
- /**
2
- * Tests for `projectIRToBundledPage`.
3
- *
4
- * Covers:
5
- * - Determinism (byte-identical output across two calls)
6
- * - Round-trip-against-active.spec (key-field shape match)
7
- * - Notes injection (descriptive paragraph append)
8
- * - Empty cases (zero states, zero requiredElements, zero actions)
9
- * - Criteria conversion (text -> textContent, ariaLabel -> accessibleName,
10
- * attributes -> dataAttributes)
11
- */
12
-
13
- import { describe, expect, it } from "vitest";
14
-
15
- import type { IRDocument } from "../document";
16
- import {
17
- projectIRToBundledPage,
18
- projectLegacyToIR,
19
- projectionVersion,
20
- type LegacySpec,
21
- } from "../projection";
22
-
23
- // ---------------------------------------------------------------------------
24
- // Fixtures
25
- // ---------------------------------------------------------------------------
26
-
27
- /**
28
- * Slice of `qontinui-runner/src/specs/active.spec.uibridge.json` mapped back
29
- * to IR. Three to four states, transitions between them, a few criteria
30
- * shapes (role, textContent/text, accessibleName/ariaLabel, dataAttributes).
31
- */
32
- const ACTIVE_PAGE_FIXTURE: IRDocument = {
33
- version: "1.0",
34
- id: "active",
35
- name: "Active Dashboard",
36
- description:
37
- "Active Dashboard page -- the real-time monitoring hub for running workflows.",
38
- metadata: {
39
- tags: ["active-dashboard", "monitoring", "real-time", "tier-1"],
40
- },
41
- initialState: "idle",
42
- states: [
43
- {
44
- id: "idle",
45
- name: "Idle",
46
- description: "No workflow is running.",
47
- requiredElements: [{ text: "No Active Workflow" }],
48
- isInitial: true,
49
- },
50
- {
51
- id: "running",
52
- name: "Running",
53
- description: "A workflow is actively executing.",
54
- requiredElements: [{ role: "button", text: "Stop" }],
55
- isInitial: false,
56
- },
57
- {
58
- id: "paused",
59
- name: "Paused",
60
- description: "The workflow is temporarily paused.",
61
- requiredElements: [{ role: "button", ariaLabel: "Play" }],
62
- isInitial: false,
63
- },
64
- {
65
- id: "breakpoint-paused",
66
- name: "Breakpoint Paused",
67
- description:
68
- "A workflow step with breakpoint: true has completed; execution is suspended.",
69
- requiredElements: [
70
- { text: "Breakpoint" },
71
- { attributes: { "content-label": "breakpoint step" } },
72
- ],
73
- isInitial: false,
74
- },
75
- ],
76
- transitions: [
77
- {
78
- id: "idle-to-running",
79
- name: "Start a workflow",
80
- fromStates: ["idle"],
81
- activateStates: ["running"],
82
- exitStates: ["idle"],
83
- actions: [
84
- {
85
- type: "click",
86
- target: { text: "Run Last Workflow" },
87
- waitAfter: { type: "idle", timeout: 5000 },
88
- },
89
- ],
90
- },
91
- {
92
- id: "running-to-paused",
93
- name: "Pause via play/pause button",
94
- fromStates: ["running"],
95
- activateStates: ["paused"],
96
- exitStates: ["running"],
97
- actions: [
98
- {
99
- type: "click",
100
- target: { role: "button", ariaLabel: "Pause" },
101
- waitAfter: { type: "idle", timeout: 2000 },
102
- },
103
- ],
104
- },
105
- {
106
- id: "paused-to-running",
107
- name: "Resume via play button",
108
- fromStates: ["paused"],
109
- activateStates: ["running"],
110
- exitStates: ["paused"],
111
- actions: [
112
- {
113
- type: "click",
114
- target: { role: "button", ariaLabel: "Play" },
115
- waitAfter: { type: "idle", timeout: 2000 },
116
- },
117
- ],
118
- },
119
- ],
120
- };
121
-
122
- // ---------------------------------------------------------------------------
123
- // Determinism
124
- // ---------------------------------------------------------------------------
125
-
126
- describe("projectIRToBundledPage — determinism", () => {
127
- it("produces byte-identical JSON across two calls with the same input", () => {
128
- const a = JSON.stringify(projectIRToBundledPage(ACTIVE_PAGE_FIXTURE));
129
- const b = JSON.stringify(projectIRToBundledPage(ACTIVE_PAGE_FIXTURE));
130
- expect(a).toBe(b);
131
- });
132
-
133
- it("produces byte-identical JSON regardless of insertion order of object keys in input", () => {
134
- // Two structurally identical IR docs with different in-memory key order.
135
- const reordered: IRDocument = {
136
- // Keys deliberately reordered.
137
- transitions: ACTIVE_PAGE_FIXTURE.transitions,
138
- states: ACTIVE_PAGE_FIXTURE.states,
139
- initialState: ACTIVE_PAGE_FIXTURE.initialState,
140
- metadata: ACTIVE_PAGE_FIXTURE.metadata,
141
- description: ACTIVE_PAGE_FIXTURE.description,
142
- name: ACTIVE_PAGE_FIXTURE.name,
143
- id: ACTIVE_PAGE_FIXTURE.id,
144
- version: ACTIVE_PAGE_FIXTURE.version,
145
- };
146
-
147
- const a = JSON.stringify(projectIRToBundledPage(ACTIVE_PAGE_FIXTURE));
148
- const b = JSON.stringify(projectIRToBundledPage(reordered));
149
- expect(a).toBe(b);
150
- });
151
-
152
- it("sorts keys lexicographically at the top level", () => {
153
- const out = projectIRToBundledPage(ACTIVE_PAGE_FIXTURE);
154
- const keys = Object.keys(out);
155
- expect(keys).toEqual([...keys].sort());
156
- });
157
- });
158
-
159
- // ---------------------------------------------------------------------------
160
- // Round-trip against active.spec
161
- // ---------------------------------------------------------------------------
162
-
163
- describe("projectIRToBundledPage — round-trip against active.spec", () => {
164
- const out = projectIRToBundledPage(ACTIVE_PAGE_FIXTURE);
165
-
166
- it("produces top-level shape with version, description, groups, stateMachine, metadata", () => {
167
- expect(out.version).toBe("1.0.0");
168
- expect(typeof out.description).toBe("string");
169
- expect(Array.isArray(out.groups)).toBe(true);
170
- expect(out.stateMachine).toBeDefined();
171
- expect(Array.isArray(out.stateMachine.states)).toBe(true);
172
- expect(out.metadata).toBeDefined();
173
- });
174
-
175
- it("produces one group per IR state, in input order", () => {
176
- expect(out.groups).toHaveLength(ACTIVE_PAGE_FIXTURE.states.length);
177
- expect(out.groups.map((g) => g.id)).toEqual(
178
- ACTIVE_PAGE_FIXTURE.states.map((s) => s.id),
179
- );
180
- });
181
-
182
- it("produces one assertion per requiredElement on each group", () => {
183
- const breakpointGroup = out.groups.find((g) => g.id === "breakpoint-paused");
184
- expect(breakpointGroup).toBeDefined();
185
- // breakpoint-paused has two requiredElements -> two assertions.
186
- expect(breakpointGroup!.assertions).toHaveLength(2);
187
- expect(breakpointGroup!.assertions[0]!.id).toBe("breakpoint-paused-elem-0");
188
- expect(breakpointGroup!.assertions[1]!.id).toBe("breakpoint-paused-elem-1");
189
- });
190
-
191
- it("converts text -> textContent in assertion targets", () => {
192
- const idleGroup = out.groups.find((g) => g.id === "idle")!;
193
- const assertion = idleGroup.assertions[0]!;
194
- expect(assertion.target.criteria).toEqual({ textContent: "No Active Workflow" });
195
- });
196
-
197
- it("converts ariaLabel -> accessibleName in assertion targets", () => {
198
- const pausedGroup = out.groups.find((g) => g.id === "paused")!;
199
- const assertion = pausedGroup.assertions[0]!;
200
- expect(assertion.target.criteria).toEqual({
201
- role: "button",
202
- accessibleName: "Play",
203
- });
204
- });
205
-
206
- it("converts attributes -> dataAttributes in assertion targets", () => {
207
- const bpGroup = out.groups.find((g) => g.id === "breakpoint-paused")!;
208
- // Second requiredElement uses attributes.
209
- const assertion = bpGroup.assertions[1]!;
210
- expect(assertion.target.criteria).toEqual({
211
- dataAttributes: { "content-label": "breakpoint step" },
212
- });
213
- });
214
-
215
- it("emits assertions with required legacy fields", () => {
216
- for (const group of out.groups) {
217
- for (const assertion of group.assertions) {
218
- expect(assertion.id).toBeTruthy();
219
- expect(assertion.description).toBeTruthy();
220
- expect(assertion.category).toBe("element-presence");
221
- expect(assertion.severity).toBe("critical");
222
- expect(assertion.assertionType).toBe("exists");
223
- expect(assertion.source).toBe("ai-generated");
224
- expect(assertion.reviewed).toBe(false);
225
- expect(assertion.enabled).toBe(true);
226
- expect(assertion.target.type).toBe("search");
227
- expect(assertion.target.label).toContain(
228
- // each target.label includes the state name
229
- group.name,
230
- );
231
- }
232
- }
233
- });
234
-
235
- it("places transitions under each fromState in stateMachine.states", () => {
236
- const idleSm = out.stateMachine.states.find((s) => s.id === "idle")!;
237
- expect(idleSm.transitions.map((t) => t.id)).toEqual(["idle-to-running"]);
238
-
239
- const runningSm = out.stateMachine.states.find((s) => s.id === "running")!;
240
- expect(runningSm.transitions.map((t) => t.id)).toEqual(["running-to-paused"]);
241
-
242
- const pausedSm = out.stateMachine.states.find((s) => s.id === "paused")!;
243
- expect(pausedSm.transitions.map((t) => t.id)).toEqual(["paused-to-running"]);
244
- });
245
-
246
- it("derives staysVisible from exitStates length (false when exitStates non-empty)", () => {
247
- const idleSm = out.stateMachine.states.find((s) => s.id === "idle")!;
248
- const t = idleSm.transitions[0]!;
249
- expect(t.deactivateStates).toEqual(["idle"]);
250
- expect(t.staysVisible).toBe(false);
251
- });
252
-
253
- it("renames action.type -> process[].action and converts target criteria", () => {
254
- const idleSm = out.stateMachine.states.find((s) => s.id === "idle")!;
255
- const proc = idleSm.transitions[0]!.process[0]!;
256
- expect(proc.action).toBe("click");
257
- expect(proc.target).toEqual({ textContent: "Run Last Workflow" });
258
- expect(proc.waitAfter).toEqual({ type: "idle", timeout: 5000 });
259
- });
260
-
261
- it("uses initialState fallback when state.isInitial is undefined", () => {
262
- const noExplicit: IRDocument = {
263
- version: "1.0",
264
- id: "x",
265
- name: "X",
266
- initialState: "first",
267
- states: [
268
- {
269
- id: "first",
270
- name: "First",
271
- requiredElements: [{ role: "heading" }],
272
- },
273
- {
274
- id: "second",
275
- name: "Second",
276
- requiredElements: [{ role: "button" }],
277
- },
278
- ],
279
- transitions: [],
280
- };
281
- const result = projectIRToBundledPage(noExplicit);
282
- expect(result.stateMachine.states.find((s) => s.id === "first")!.isInitial).toBe(
283
- true,
284
- );
285
- expect(
286
- result.stateMachine.states.find((s) => s.id === "second")!.isInitial,
287
- ).toBe(false);
288
- });
289
-
290
- it("propagates document tags into metadata.tags", () => {
291
- expect(out.metadata.tags).toEqual([
292
- "active-dashboard",
293
- "monitoring",
294
- "real-time",
295
- "tier-1",
296
- ]);
297
- });
298
-
299
- it("uses doc.id as metadata.component when no purpose is set", () => {
300
- expect(out.metadata.component).toBe("active");
301
- });
302
-
303
- it("uses metadata.purpose as metadata.component when present", () => {
304
- const withPurpose: IRDocument = {
305
- ...ACTIVE_PAGE_FIXTURE,
306
- metadata: { ...ACTIVE_PAGE_FIXTURE.metadata, purpose: "ActiveDashboardPage" },
307
- };
308
- const result = projectIRToBundledPage(withPurpose);
309
- expect(result.metadata.component).toBe("ActiveDashboardPage");
310
- });
311
-
312
- it("uses doc.description as the top-level description", () => {
313
- expect(out.description).toBe(ACTIVE_PAGE_FIXTURE.description);
314
- });
315
- });
316
-
317
- // ---------------------------------------------------------------------------
318
- // Notes injection
319
- // ---------------------------------------------------------------------------
320
-
321
- describe("projectIRToBundledPage — notes injection", () => {
322
- it("appends notes as a separate paragraph (two-newline separator)", () => {
323
- const out = projectIRToBundledPage(
324
- ACTIVE_PAGE_FIXTURE,
325
- "Reviewed by Joshua on 2026-04-28.",
326
- );
327
- expect(out.description).toBe(
328
- `${ACTIVE_PAGE_FIXTURE.description}\n\nReviewed by Joshua on 2026-04-28.`,
329
- );
330
- });
331
-
332
- it("treats undefined notes as no-op (no trailing newlines)", () => {
333
- const out = projectIRToBundledPage(ACTIVE_PAGE_FIXTURE);
334
- expect(out.description).toBe(ACTIVE_PAGE_FIXTURE.description);
335
- expect(out.description.endsWith("\n")).toBe(false);
336
- });
337
-
338
- it("treats empty-string notes as no-op", () => {
339
- const out = projectIRToBundledPage(ACTIVE_PAGE_FIXTURE, "");
340
- expect(out.description).toBe(ACTIVE_PAGE_FIXTURE.description);
341
- });
342
-
343
- it("falls back to doc.name when doc.description is missing", () => {
344
- const noDesc: IRDocument = {
345
- version: "1.0",
346
- id: "p",
347
- name: "Page",
348
- states: [],
349
- transitions: [],
350
- };
351
- const out = projectIRToBundledPage(noDesc, "added later");
352
- expect(out.description).toBe("Page\n\nadded later");
353
- });
354
- });
355
-
356
- // ---------------------------------------------------------------------------
357
- // Empty cases
358
- // ---------------------------------------------------------------------------
359
-
360
- describe("projectIRToBundledPage — empty cases", () => {
361
- it("handles a document with zero states", () => {
362
- const empty: IRDocument = {
363
- version: "1.0",
364
- id: "empty",
365
- name: "Empty",
366
- states: [],
367
- transitions: [],
368
- };
369
- const out = projectIRToBundledPage(empty);
370
- expect(out.groups).toEqual([]);
371
- expect(out.stateMachine.states).toEqual([]);
372
- expect(out.version).toBe("1.0.0");
373
- expect(out.metadata.component).toBe("empty");
374
- });
375
-
376
- it("projects a state with zero requiredElements to a group with empty assertions[]", () => {
377
- // Legacy specs ship architecture-only / documentation-only groups with
378
- // empty `assertions[]` (e.g. `graphql-resolver-inventory` in the runner's
379
- // `graphql-infrastructure.spec.uibridge.json`). The forward projection
380
- // must preserve that emptiness so the legacy<->IR round-trip is identity
381
- // for assertion counts; emitting a placeholder here silently inflates the
382
- // count and trips `check-spec-pairing` with an `assertionCount` mismatch.
383
- const doc: IRDocument = {
384
- version: "1.0",
385
- id: "p",
386
- name: "P",
387
- states: [
388
- {
389
- id: "no-req",
390
- name: "No Required Elements",
391
- requiredElements: [],
392
- },
393
- ],
394
- transitions: [],
395
- };
396
- const out = projectIRToBundledPage(doc);
397
- expect(out.groups).toHaveLength(1);
398
- const group = out.groups[0]!;
399
- expect(group.assertions).toEqual([]);
400
- // The state still appears in the state-machine block — emptiness is local
401
- // to `groups[].assertions[]`, not state existence.
402
- expect(out.stateMachine.states).toHaveLength(1);
403
- expect(out.stateMachine.states[0]!.id).toBe("no-req");
404
- });
405
-
406
- it("preserves empty groups across a legacy -> IR -> legacy round-trip (regression: graphql-infrastructure)", () => {
407
- // Reproduces the on-disk shape of the four runner specs that previously
408
- // produced an `assertionCount` mismatch in `check-spec-pairing`: a group
409
- // with empty `assertions[]` and a matching state-machine state with
410
- // empty `elements[]`. Round-trip identity proves the placeholder
411
- // re-emission bug stays fixed.
412
- const legacy = {
413
- version: "1.0.0",
414
- description: "Architecture-only spec with empty groups",
415
- metadata: { component: "regression" },
416
- groups: [
417
- {
418
- id: "non-empty",
419
- name: "Has assertions",
420
- description: "",
421
- category: "element-presence",
422
- assertions: [
423
- {
424
- id: "a1",
425
- description: "First assertion",
426
- category: "element-presence",
427
- severity: "critical",
428
- assertionType: "exists",
429
- target: {
430
- type: "search",
431
- criteria: { textContent: "Run" },
432
- label: "Run button",
433
- },
434
- source: "ai-generated",
435
- reviewed: false,
436
- enabled: true,
437
- },
438
- ],
439
- source: "ai-generated",
440
- },
441
- {
442
- id: "empty",
443
- name: "Empty architecture group",
444
- description: "",
445
- category: "element-presence",
446
- assertions: [],
447
- source: "ai-generated",
448
- },
449
- ],
450
- stateMachine: {
451
- states: [
452
- {
453
- id: "non-empty",
454
- name: "Has assertions",
455
- description: "",
456
- elements: [{ textContent: "Run" }],
457
- isInitial: true,
458
- transitions: [],
459
- },
460
- {
461
- id: "empty",
462
- name: "Empty architecture group",
463
- description: "",
464
- elements: [],
465
- isInitial: false,
466
- transitions: [],
467
- },
468
- ],
469
- },
470
- } as unknown as Parameters<typeof projectLegacyToIR>[0];
471
-
472
- const ir = projectLegacyToIR(legacy, { docId: "regression" });
473
- const projected = projectIRToBundledPage(ir);
474
-
475
- expect(projected.groups).toHaveLength(2);
476
- expect(projected.groups[0]!.id).toBe("non-empty");
477
- expect(projected.groups[0]!.assertions).toHaveLength(1);
478
- expect(projected.groups[1]!.id).toBe("empty");
479
- expect(projected.groups[1]!.assertions).toEqual([]);
480
-
481
- // Total assertion count is identity across the round-trip.
482
- const legacyTotal = legacy.groups.reduce(
483
- (s: number, g: { assertions: unknown[] }) => s + g.assertions.length,
484
- 0,
485
- );
486
- const projectedTotal = projected.groups.reduce(
487
- (s, g) => s + g.assertions.length,
488
- 0,
489
- );
490
- expect(projectedTotal).toBe(legacyTotal);
491
- });
492
-
493
- it("handles a state with empty-string text criteria (still emits an assertion)", () => {
494
- const doc: IRDocument = {
495
- version: "1.0",
496
- id: "p",
497
- name: "P",
498
- states: [
499
- {
500
- id: "empty-text",
501
- name: "Empty Text",
502
- requiredElements: [{ text: "" }],
503
- },
504
- ],
505
- transitions: [],
506
- };
507
- const out = projectIRToBundledPage(doc);
508
- expect(out.groups[0]!.assertions).toHaveLength(1);
509
- expect(out.groups[0]!.assertions[0]!.target.criteria).toEqual({ textContent: "" });
510
- });
511
-
512
- it("handles a transition with no actions (process: [])", () => {
513
- const doc: IRDocument = {
514
- version: "1.0",
515
- id: "p",
516
- name: "P",
517
- states: [
518
- { id: "a", name: "A", requiredElements: [{ role: "heading" }] },
519
- { id: "b", name: "B", requiredElements: [{ role: "heading" }] },
520
- ],
521
- transitions: [
522
- {
523
- id: "auto",
524
- name: "Auto transition",
525
- fromStates: ["a"],
526
- activateStates: ["b"],
527
- exitStates: ["a"],
528
- actions: [],
529
- },
530
- ],
531
- };
532
- const out = projectIRToBundledPage(doc);
533
- const aSm = out.stateMachine.states.find((s) => s.id === "a")!;
534
- expect(aSm.transitions[0]!.process).toEqual([]);
535
- expect(aSm.transitions[0]!.staysVisible).toBe(false);
536
- });
537
-
538
- it("derives staysVisible=true for a transition with no exitStates (modal-style)", () => {
539
- const doc: IRDocument = {
540
- version: "1.0",
541
- id: "p",
542
- name: "P",
543
- states: [
544
- { id: "base", name: "Base", requiredElements: [{ role: "heading" }] },
545
- { id: "modal", name: "Modal", requiredElements: [{ role: "dialog" }] },
546
- ],
547
- transitions: [
548
- {
549
- id: "open-modal",
550
- name: "Open modal",
551
- fromStates: ["base"],
552
- activateStates: ["modal"],
553
- // No exitStates -> modal-style; base stays visible underneath.
554
- actions: [{ type: "click", target: { role: "button" } }],
555
- },
556
- ],
557
- };
558
- const out = projectIRToBundledPage(doc);
559
- const baseSm = out.stateMachine.states.find((s) => s.id === "base")!;
560
- expect(baseSm.transitions[0]!.deactivateStates).toEqual([]);
561
- expect(baseSm.transitions[0]!.staysVisible).toBe(true);
562
- });
563
- });
564
-
565
- // ---------------------------------------------------------------------------
566
- // Provenance + state-level fields
567
- // ---------------------------------------------------------------------------
568
-
569
- describe("projectIRToBundledPage — provenance + state metadata", () => {
570
- it("uses state.provenance.source for group.source when present", () => {
571
- const doc: IRDocument = {
572
- version: "1.0",
573
- id: "p",
574
- name: "P",
575
- states: [
576
- {
577
- id: "s",
578
- name: "S",
579
- requiredElements: [{ role: "heading" }],
580
- provenance: { source: "hand-authored", file: "p.tsx" },
581
- },
582
- ],
583
- transitions: [],
584
- };
585
- const out = projectIRToBundledPage(doc);
586
- expect(out.groups[0]!.source).toBe("hand-authored");
587
- });
588
-
589
- it("defaults group.source to ai-generated when provenance is missing", () => {
590
- const doc: IRDocument = {
591
- version: "1.0",
592
- id: "p",
593
- name: "P",
594
- states: [
595
- {
596
- id: "s",
597
- name: "S",
598
- requiredElements: [{ role: "heading" }],
599
- },
600
- ],
601
- transitions: [],
602
- };
603
- const out = projectIRToBundledPage(doc);
604
- expect(out.groups[0]!.source).toBe("ai-generated");
605
- });
606
-
607
- it("uses state.metadata.description as group.description when state.description is missing", () => {
608
- const doc: IRDocument = {
609
- version: "1.0",
610
- id: "p",
611
- name: "P",
612
- states: [
613
- {
614
- id: "s",
615
- name: "S",
616
- requiredElements: [{ role: "heading" }],
617
- metadata: { description: "From metadata" },
618
- },
619
- ],
620
- transitions: [],
621
- };
622
- const out = projectIRToBundledPage(doc);
623
- expect(out.groups[0]!.description).toBe("From metadata");
624
- });
625
-
626
- it("falls through to empty string when neither state.description nor state.metadata.description set", () => {
627
- const doc: IRDocument = {
628
- version: "1.0",
629
- id: "p",
630
- name: "P",
631
- states: [
632
- {
633
- id: "s",
634
- name: "S",
635
- requiredElements: [{ role: "heading" }],
636
- },
637
- ],
638
- transitions: [],
639
- };
640
- const out = projectIRToBundledPage(doc);
641
- expect(out.groups[0]!.description).toBe("");
642
- });
643
- });
644
-
645
- // ---------------------------------------------------------------------------
646
- // Type / version export
647
- // ---------------------------------------------------------------------------
648
-
649
- describe("projectionVersion", () => {
650
- it('exports "1.0"', () => {
651
- expect(projectionVersion).toBe("1.0");
652
- });
653
- });
654
-
655
- // ---------------------------------------------------------------------------
656
- // Type witness — confirms LegacySpec is a structural shape with the legacy fields
657
- // ---------------------------------------------------------------------------
658
-
659
- describe("LegacySpec — structural witness", () => {
660
- it("matches the legacy shape on a minimal projection", () => {
661
- const minimal: IRDocument = {
662
- version: "1.0",
663
- id: "m",
664
- name: "M",
665
- states: [],
666
- transitions: [],
667
- };
668
- const out: LegacySpec = projectIRToBundledPage(minimal);
669
- expect(out).toMatchObject({
670
- version: "1.0.0",
671
- description: "M",
672
- groups: [],
673
- stateMachine: { states: [] },
674
- metadata: { component: "m" },
675
- });
676
- });
677
- });
678
-
679
- // ---------------------------------------------------------------------------
680
- // Inverse projection: legacy -> IR (Phase A2)
681
- // ---------------------------------------------------------------------------
682
-
683
- /**
684
- * Hand-authored minimal legacy fixture for the inverse direction. Mirrors a
685
- * trimmed `settings-general.spec.uibridge.json`-style shape with two groups,
686
- * a state-machine block, transitions with action sequences, and a
687
- * precondition on one assertion.
688
- */
689
- const LEGACY_FIXTURE: LegacySpec = {
690
- version: "1.0.0",
691
- description: "Hand-authored test fixture for inverse projection.",
692
- groups: [
693
- {
694
- id: "alpha",
695
- name: "Alpha Group",
696
- description: "First group for the round-trip test.",
697
- category: "element-presence",
698
- assertions: [
699
- {
700
- id: "alpha-elem-0",
701
- description: "Heading exists",
702
- category: "element-presence",
703
- severity: "critical",
704
- assertionType: "exists",
705
- target: {
706
- type: "search",
707
- criteria: { role: "heading", textContent: "Alpha" },
708
- label: "Alpha heading",
709
- },
710
- source: "manual",
711
- reviewed: true,
712
- enabled: true,
713
- },
714
- {
715
- id: "alpha-elem-1",
716
- description: "Subtitle exists",
717
- category: "element-presence",
718
- severity: "warning",
719
- assertionType: "exists",
720
- target: {
721
- type: "search",
722
- criteria: { textContent: "Welcome" },
723
- label: "Subtitle",
724
- },
725
- source: "manual",
726
- reviewed: true,
727
- enabled: true,
728
- precondition: "Page is loaded.",
729
- },
730
- ],
731
- source: "manual",
732
- },
733
- {
734
- id: "beta",
735
- name: "Beta Group",
736
- description: "Second group, modal-style.",
737
- category: "element-presence",
738
- assertions: [
739
- {
740
- id: "beta-elem-0",
741
- description: "Dialog exists",
742
- category: "element-presence",
743
- severity: "critical",
744
- assertionType: "exists",
745
- target: {
746
- type: "search",
747
- criteria: { role: "dialog", accessibleName: "Confirm" },
748
- label: "Confirm dialog",
749
- },
750
- source: "manual",
751
- reviewed: true,
752
- enabled: true,
753
- },
754
- ],
755
- source: "manual",
756
- },
757
- ],
758
- stateMachine: {
759
- states: [
760
- {
761
- id: "alpha",
762
- name: "Alpha Group",
763
- description: "First group for the round-trip test.",
764
- elements: [
765
- { role: "heading", textContent: "Alpha" },
766
- { textContent: "Welcome" },
767
- ],
768
- isInitial: true,
769
- transitions: [
770
- {
771
- id: "alpha-to-beta",
772
- name: "Open Beta",
773
- activateStates: ["beta"],
774
- deactivateStates: [],
775
- staysVisible: true,
776
- process: [
777
- {
778
- action: "click",
779
- target: { role: "button", textContent: "Open" },
780
- waitAfter: { type: "idle", timeout: 1000 },
781
- },
782
- ],
783
- },
784
- ],
785
- },
786
- {
787
- id: "beta",
788
- name: "Beta Group",
789
- description: "Second group, modal-style.",
790
- elements: [{ role: "dialog", accessibleName: "Confirm" }],
791
- isInitial: false,
792
- transitions: [
793
- {
794
- id: "beta-to-alpha",
795
- name: "Close Beta",
796
- activateStates: ["alpha"],
797
- deactivateStates: ["beta"],
798
- staysVisible: false,
799
- process: [
800
- {
801
- action: "click",
802
- target: { textContent: "Cancel" },
803
- },
804
- ],
805
- },
806
- ],
807
- },
808
- ],
809
- },
810
- metadata: {
811
- component: "fixture-page",
812
- tags: ["fixture", "round-trip"],
813
- },
814
- };
815
-
816
- describe("projectLegacyToIR — basic shape", () => {
817
- it("converts metadata.component to ir.id and propagates tags", () => {
818
- const ir = projectLegacyToIR(LEGACY_FIXTURE);
819
- expect(ir.id).toBe("fixture-page");
820
- expect(ir.metadata?.tags).toEqual(["fixture", "round-trip"]);
821
- });
822
-
823
- it("uses opts.docId override when supplied", () => {
824
- const ir = projectLegacyToIR(LEGACY_FIXTURE, { docId: "override" });
825
- expect(ir.id).toBe("override");
826
- });
827
-
828
- it("emits one IR state per legacy group, preserving id + name + description", () => {
829
- const ir = projectLegacyToIR(LEGACY_FIXTURE);
830
- expect(ir.states).toHaveLength(2);
831
- expect(ir.states.map((s) => s.id)).toEqual(["alpha", "beta"]);
832
- const alpha = ir.states.find((s) => s.id === "alpha")!;
833
- expect(alpha.name).toBe("Alpha Group");
834
- expect(alpha.description).toBe("First group for the round-trip test.");
835
- });
836
-
837
- it("sources requiredElements from group.assertions[].target.criteria", () => {
838
- const ir = projectLegacyToIR(LEGACY_FIXTURE);
839
- const alpha = ir.states.find((s) => s.id === "alpha")!;
840
- expect(alpha.requiredElements).toHaveLength(2);
841
- expect(alpha.requiredElements[0]).toEqual({
842
- role: "heading",
843
- text: "Alpha",
844
- });
845
- expect(alpha.requiredElements[1]).toEqual({ text: "Welcome" });
846
- });
847
-
848
- it("inverts dataAttributes -> attributes and accessibleName passthrough", () => {
849
- const ir = projectLegacyToIR(LEGACY_FIXTURE);
850
- const beta = ir.states.find((s) => s.id === "beta")!;
851
- expect(beta.requiredElements[0]).toEqual({
852
- role: "dialog",
853
- accessibleName: "Confirm",
854
- });
855
- });
856
-
857
- it("lifts the first-found assertion.precondition to state.precondition", () => {
858
- const ir = projectLegacyToIR(LEGACY_FIXTURE);
859
- const alpha = ir.states.find((s) => s.id === "alpha")!;
860
- expect(alpha.precondition).toBe("Page is loaded.");
861
- });
862
-
863
- it("derives initialState from the first SM state with isInitial:true", () => {
864
- const ir = projectLegacyToIR(LEGACY_FIXTURE);
865
- expect(ir.initialState).toBe("alpha");
866
- });
867
-
868
- it("sets state.isInitial from the SM state", () => {
869
- const ir = projectLegacyToIR(LEGACY_FIXTURE);
870
- expect(ir.states.find((s) => s.id === "alpha")!.isInitial).toBe(true);
871
- expect(ir.states.find((s) => s.id === "beta")!.isInitial).toBe(false);
872
- });
873
-
874
- it("tags every state and transition with provenance.source = 'migrated'", () => {
875
- const ir = projectLegacyToIR(LEGACY_FIXTURE);
876
- for (const s of ir.states) {
877
- expect(s.provenance?.source).toBe("migrated");
878
- }
879
- for (const t of ir.transitions) {
880
- expect(t.provenance?.source).toBe("migrated");
881
- }
882
- expect(ir.provenance?.source).toBe("migrated");
883
- });
884
-
885
- it("collapses transitions in stateMachine.states[].transitions[] into one IRTransition", () => {
886
- const ir = projectLegacyToIR(LEGACY_FIXTURE);
887
- expect(ir.transitions).toHaveLength(2);
888
- const ids = ir.transitions.map((t) => t.id).sort();
889
- expect(ids).toEqual(["alpha-to-beta", "beta-to-alpha"]);
890
- });
891
-
892
- it("inverts process[] back into transition.actions with type/target/waitAfter", () => {
893
- const ir = projectLegacyToIR(LEGACY_FIXTURE);
894
- const t = ir.transitions.find((tt) => tt.id === "alpha-to-beta")!;
895
- expect(t.actions).toHaveLength(1);
896
- expect(t.actions[0]!.type).toBe("click");
897
- expect(t.actions[0]!.target).toEqual({ role: "button", text: "Open" });
898
- expect(t.actions[0]!.waitAfter).toEqual({ type: "idle", timeout: 1000 });
899
- });
900
-
901
- it("preserves modal-style empty deactivateStates as exitStates: []", () => {
902
- const ir = projectLegacyToIR(LEGACY_FIXTURE);
903
- const t = ir.transitions.find((tt) => tt.id === "alpha-to-beta")!;
904
- expect(t.exitStates).toEqual([]);
905
- });
906
-
907
- it("handles a legacy spec with no stateMachine block (synthesizes states from groups only)", () => {
908
- const noSM = {
909
- ...LEGACY_FIXTURE,
910
- stateMachine: { states: [] },
911
- } as LegacySpec;
912
- const ir = projectLegacyToIR(noSM);
913
- expect(ir.transitions).toEqual([]);
914
- expect(ir.states).toHaveLength(2);
915
- // No isInitial set when SM block is empty.
916
- expect(ir.initialState).toBeUndefined();
917
- });
918
- });
919
-
920
- // ---------------------------------------------------------------------------
921
- // Round-trip tests
922
- // ---------------------------------------------------------------------------
923
-
924
- describe("round-trip — IR -> legacy -> IR (forward then inverse)", () => {
925
- it("preserves the active-page IR fixture's group/state/transition counts", () => {
926
- const legacy = projectIRToBundledPage(ACTIVE_PAGE_FIXTURE);
927
- const ir = projectLegacyToIR(legacy);
928
- expect(ir.states.length).toBe(ACTIVE_PAGE_FIXTURE.states.length);
929
- expect(ir.states.map((s) => s.id).sort()).toEqual(
930
- ACTIVE_PAGE_FIXTURE.states.map((s) => s.id).sort(),
931
- );
932
- expect(ir.transitions.length).toBe(ACTIVE_PAGE_FIXTURE.transitions.length);
933
- expect(ir.transitions.map((t) => t.id).sort()).toEqual(
934
- ACTIVE_PAGE_FIXTURE.transitions.map((t) => t.id).sort(),
935
- );
936
- });
937
-
938
- it("preserves the active-page IR fixture's initialState", () => {
939
- const legacy = projectIRToBundledPage(ACTIVE_PAGE_FIXTURE);
940
- const ir = projectLegacyToIR(legacy);
941
- expect(ir.initialState).toBe("idle");
942
- });
943
-
944
- it("preserves the active-page IR's tags", () => {
945
- const legacy = projectIRToBundledPage(ACTIVE_PAGE_FIXTURE);
946
- const ir = projectLegacyToIR(legacy);
947
- expect(ir.metadata?.tags).toEqual(ACTIVE_PAGE_FIXTURE.metadata?.tags);
948
- });
949
-
950
- it("preserves transition fromStates merging when the same transition is emitted under multiple SM states", () => {
951
- // Forward emits one copy of each transition under each fromState. The
952
- // active-page fixture only has one fromState per transition, but check
953
- // the dedup logic doesn't drop transitions when fromStates is single.
954
- const legacy = projectIRToBundledPage(ACTIVE_PAGE_FIXTURE);
955
- const ir = projectLegacyToIR(legacy);
956
- const orig = ACTIVE_PAGE_FIXTURE.transitions.find(
957
- (t) => t.id === "idle-to-running",
958
- )!;
959
- const round = ir.transitions.find((t) => t.id === "idle-to-running")!;
960
- expect(round.fromStates).toEqual(orig.fromStates);
961
- expect(round.activateStates).toEqual(orig.activateStates);
962
- });
963
- });
964
-
965
- describe("round-trip — legacy -> IR -> legacy (inverse then forward)", () => {
966
- it("preserves group count and group ids from a hand-authored legacy fixture", () => {
967
- const ir = projectLegacyToIR(LEGACY_FIXTURE);
968
- const back = projectIRToBundledPage(ir);
969
- expect(back.groups.length).toBe(LEGACY_FIXTURE.groups.length);
970
- expect(back.groups.map((g) => g.id)).toEqual(
971
- LEGACY_FIXTURE.groups.map((g) => g.id),
972
- );
973
- });
974
-
975
- it("preserves total assertion count", () => {
976
- const ir = projectLegacyToIR(LEGACY_FIXTURE);
977
- const back = projectIRToBundledPage(ir);
978
- const orig = LEGACY_FIXTURE.groups.reduce(
979
- (n, g) => n + g.assertions.length,
980
- 0,
981
- );
982
- const round = back.groups.reduce((n, g) => n + g.assertions.length, 0);
983
- expect(round).toBe(orig);
984
- });
985
-
986
- it("preserves transition ids in the SM block", () => {
987
- const ir = projectLegacyToIR(LEGACY_FIXTURE);
988
- const back = projectIRToBundledPage(ir);
989
- const allTids: string[] = [];
990
- for (const s of back.stateMachine.states) {
991
- for (const t of s.transitions) {
992
- if (!allTids.includes(t.id)) allTids.push(t.id);
993
- }
994
- }
995
- const origTids: string[] = [];
996
- for (const s of LEGACY_FIXTURE.stateMachine.states) {
997
- for (const t of s.transitions) {
998
- if (!origTids.includes(t.id)) origTids.push(t.id);
999
- }
1000
- }
1001
- expect(allTids.sort()).toEqual(origTids.sort());
1002
- });
1003
-
1004
- it("preserves transition action types", () => {
1005
- const ir = projectLegacyToIR(LEGACY_FIXTURE);
1006
- const back = projectIRToBundledPage(ir);
1007
- const alphaSm = back.stateMachine.states.find((s) => s.id === "alpha")!;
1008
- const t = alphaSm.transitions.find((tt) => tt.id === "alpha-to-beta")!;
1009
- expect(t.process).toHaveLength(1);
1010
- expect(t.process[0]!.action).toBe("click");
1011
- expect(t.process[0]!.target).toEqual({ role: "button", textContent: "Open" });
1012
- });
1013
-
1014
- it("preserves staysVisible on modal-style transitions (empty deactivateStates)", () => {
1015
- const ir = projectLegacyToIR(LEGACY_FIXTURE);
1016
- const back = projectIRToBundledPage(ir);
1017
- const alphaSm = back.stateMachine.states.find((s) => s.id === "alpha")!;
1018
- const t = alphaSm.transitions.find((tt) => tt.id === "alpha-to-beta")!;
1019
- expect(t.staysVisible).toBe(true);
1020
- expect(t.deactivateStates).toEqual([]);
1021
- });
1022
-
1023
- it("preserves component + tags in metadata", () => {
1024
- const ir = projectLegacyToIR(LEGACY_FIXTURE);
1025
- const back = projectIRToBundledPage(ir);
1026
- expect(back.metadata.component).toBe("fixture-page");
1027
- expect(back.metadata.tags).toEqual(["fixture", "round-trip"]);
1028
- });
1029
- });
1
+ /**
2
+ * Tests for `projectIRToBundledPage`.
3
+ *
4
+ * Covers:
5
+ * - Determinism (byte-identical output across two calls)
6
+ * - Round-trip-against-active.spec (key-field shape match)
7
+ * - Notes injection (descriptive paragraph append)
8
+ * - Empty cases (zero states, zero requiredElements, zero actions)
9
+ * - Criteria conversion (text -> textContent, ariaLabel -> accessibleName,
10
+ * attributes -> dataAttributes)
11
+ */
12
+
13
+ import { describe, expect, it } from "vitest";
14
+
15
+ import type { IRDocument } from "../document";
16
+ import {
17
+ projectIRToBundledPage,
18
+ projectLegacyToIR,
19
+ projectionVersion,
20
+ type LegacySpec,
21
+ } from "../projection";
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Fixtures
25
+ // ---------------------------------------------------------------------------
26
+
27
+ /**
28
+ * Slice of `qontinui-runner/src/specs/active.spec.uibridge.json` mapped back
29
+ * to IR. Three to four states, transitions between them, a few criteria
30
+ * shapes (role, textContent/text, accessibleName/ariaLabel, dataAttributes).
31
+ */
32
+ const ACTIVE_PAGE_FIXTURE: IRDocument = {
33
+ version: "1.0",
34
+ id: "active",
35
+ name: "Active Dashboard",
36
+ description:
37
+ "Active Dashboard page -- the real-time monitoring hub for running workflows.",
38
+ metadata: {
39
+ tags: ["active-dashboard", "monitoring", "real-time", "tier-1"],
40
+ },
41
+ initialState: "idle",
42
+ states: [
43
+ {
44
+ id: "idle",
45
+ name: "Idle",
46
+ description: "No workflow is running.",
47
+ requiredElements: [{ text: "No Active Workflow" }],
48
+ isInitial: true,
49
+ },
50
+ {
51
+ id: "running",
52
+ name: "Running",
53
+ description: "A workflow is actively executing.",
54
+ requiredElements: [{ role: "button", text: "Stop" }],
55
+ isInitial: false,
56
+ },
57
+ {
58
+ id: "paused",
59
+ name: "Paused",
60
+ description: "The workflow is temporarily paused.",
61
+ requiredElements: [{ role: "button", ariaLabel: "Play" }],
62
+ isInitial: false,
63
+ },
64
+ {
65
+ id: "breakpoint-paused",
66
+ name: "Breakpoint Paused",
67
+ description:
68
+ "A workflow step with breakpoint: true has completed; execution is suspended.",
69
+ requiredElements: [
70
+ { text: "Breakpoint" },
71
+ { attributes: { "content-label": "breakpoint step" } },
72
+ ],
73
+ isInitial: false,
74
+ },
75
+ ],
76
+ transitions: [
77
+ {
78
+ id: "idle-to-running",
79
+ name: "Start a workflow",
80
+ fromStates: ["idle"],
81
+ activateStates: ["running"],
82
+ exitStates: ["idle"],
83
+ actions: [
84
+ {
85
+ type: "click",
86
+ target: { text: "Run Last Workflow" },
87
+ waitAfter: { type: "idle", timeout: 5000 },
88
+ },
89
+ ],
90
+ },
91
+ {
92
+ id: "running-to-paused",
93
+ name: "Pause via play/pause button",
94
+ fromStates: ["running"],
95
+ activateStates: ["paused"],
96
+ exitStates: ["running"],
97
+ actions: [
98
+ {
99
+ type: "click",
100
+ target: { role: "button", ariaLabel: "Pause" },
101
+ waitAfter: { type: "idle", timeout: 2000 },
102
+ },
103
+ ],
104
+ },
105
+ {
106
+ id: "paused-to-running",
107
+ name: "Resume via play button",
108
+ fromStates: ["paused"],
109
+ activateStates: ["running"],
110
+ exitStates: ["paused"],
111
+ actions: [
112
+ {
113
+ type: "click",
114
+ target: { role: "button", ariaLabel: "Play" },
115
+ waitAfter: { type: "idle", timeout: 2000 },
116
+ },
117
+ ],
118
+ },
119
+ ],
120
+ };
121
+
122
+ // ---------------------------------------------------------------------------
123
+ // Determinism
124
+ // ---------------------------------------------------------------------------
125
+
126
+ describe("projectIRToBundledPage — determinism", () => {
127
+ it("produces byte-identical JSON across two calls with the same input", () => {
128
+ const a = JSON.stringify(projectIRToBundledPage(ACTIVE_PAGE_FIXTURE));
129
+ const b = JSON.stringify(projectIRToBundledPage(ACTIVE_PAGE_FIXTURE));
130
+ expect(a).toBe(b);
131
+ });
132
+
133
+ it("produces byte-identical JSON regardless of insertion order of object keys in input", () => {
134
+ // Two structurally identical IR docs with different in-memory key order.
135
+ const reordered: IRDocument = {
136
+ // Keys deliberately reordered.
137
+ transitions: ACTIVE_PAGE_FIXTURE.transitions,
138
+ states: ACTIVE_PAGE_FIXTURE.states,
139
+ initialState: ACTIVE_PAGE_FIXTURE.initialState,
140
+ metadata: ACTIVE_PAGE_FIXTURE.metadata,
141
+ description: ACTIVE_PAGE_FIXTURE.description,
142
+ name: ACTIVE_PAGE_FIXTURE.name,
143
+ id: ACTIVE_PAGE_FIXTURE.id,
144
+ version: ACTIVE_PAGE_FIXTURE.version,
145
+ };
146
+
147
+ const a = JSON.stringify(projectIRToBundledPage(ACTIVE_PAGE_FIXTURE));
148
+ const b = JSON.stringify(projectIRToBundledPage(reordered));
149
+ expect(a).toBe(b);
150
+ });
151
+
152
+ it("sorts keys lexicographically at the top level", () => {
153
+ const out = projectIRToBundledPage(ACTIVE_PAGE_FIXTURE);
154
+ const keys = Object.keys(out);
155
+ expect(keys).toEqual([...keys].sort());
156
+ });
157
+ });
158
+
159
+ // ---------------------------------------------------------------------------
160
+ // Round-trip against active.spec
161
+ // ---------------------------------------------------------------------------
162
+
163
+ describe("projectIRToBundledPage — round-trip against active.spec", () => {
164
+ const out = projectIRToBundledPage(ACTIVE_PAGE_FIXTURE);
165
+
166
+ it("produces top-level shape with version, description, groups, stateMachine, metadata", () => {
167
+ expect(out.version).toBe("1.0.0");
168
+ expect(typeof out.description).toBe("string");
169
+ expect(Array.isArray(out.groups)).toBe(true);
170
+ expect(out.stateMachine).toBeDefined();
171
+ expect(Array.isArray(out.stateMachine.states)).toBe(true);
172
+ expect(out.metadata).toBeDefined();
173
+ });
174
+
175
+ it("produces one group per IR state, in input order", () => {
176
+ expect(out.groups).toHaveLength(ACTIVE_PAGE_FIXTURE.states.length);
177
+ expect(out.groups.map((g) => g.id)).toEqual(
178
+ ACTIVE_PAGE_FIXTURE.states.map((s) => s.id),
179
+ );
180
+ });
181
+
182
+ it("produces one assertion per requiredElement on each group", () => {
183
+ const breakpointGroup = out.groups.find((g) => g.id === "breakpoint-paused");
184
+ expect(breakpointGroup).toBeDefined();
185
+ // breakpoint-paused has two requiredElements -> two assertions.
186
+ expect(breakpointGroup!.assertions).toHaveLength(2);
187
+ expect(breakpointGroup!.assertions[0]!.id).toBe("breakpoint-paused-elem-0");
188
+ expect(breakpointGroup!.assertions[1]!.id).toBe("breakpoint-paused-elem-1");
189
+ });
190
+
191
+ it("converts text -> textContent in assertion targets", () => {
192
+ const idleGroup = out.groups.find((g) => g.id === "idle")!;
193
+ const assertion = idleGroup.assertions[0]!;
194
+ expect(assertion.target.criteria).toEqual({ textContent: "No Active Workflow" });
195
+ });
196
+
197
+ it("converts ariaLabel -> accessibleName in assertion targets", () => {
198
+ const pausedGroup = out.groups.find((g) => g.id === "paused")!;
199
+ const assertion = pausedGroup.assertions[0]!;
200
+ expect(assertion.target.criteria).toEqual({
201
+ role: "button",
202
+ accessibleName: "Play",
203
+ });
204
+ });
205
+
206
+ it("converts attributes -> dataAttributes in assertion targets", () => {
207
+ const bpGroup = out.groups.find((g) => g.id === "breakpoint-paused")!;
208
+ // Second requiredElement uses attributes.
209
+ const assertion = bpGroup.assertions[1]!;
210
+ expect(assertion.target.criteria).toEqual({
211
+ dataAttributes: { "content-label": "breakpoint step" },
212
+ });
213
+ });
214
+
215
+ it("emits assertions with required legacy fields", () => {
216
+ for (const group of out.groups) {
217
+ for (const assertion of group.assertions) {
218
+ expect(assertion.id).toBeTruthy();
219
+ expect(assertion.description).toBeTruthy();
220
+ expect(assertion.category).toBe("element-presence");
221
+ expect(assertion.severity).toBe("critical");
222
+ expect(assertion.assertionType).toBe("exists");
223
+ expect(assertion.source).toBe("ai-generated");
224
+ expect(assertion.reviewed).toBe(false);
225
+ expect(assertion.enabled).toBe(true);
226
+ expect(assertion.target.type).toBe("search");
227
+ expect(assertion.target.label).toContain(
228
+ // each target.label includes the state name
229
+ group.name,
230
+ );
231
+ }
232
+ }
233
+ });
234
+
235
+ it("places transitions under each fromState in stateMachine.states", () => {
236
+ const idleSm = out.stateMachine.states.find((s) => s.id === "idle")!;
237
+ expect(idleSm.transitions.map((t) => t.id)).toEqual(["idle-to-running"]);
238
+
239
+ const runningSm = out.stateMachine.states.find((s) => s.id === "running")!;
240
+ expect(runningSm.transitions.map((t) => t.id)).toEqual(["running-to-paused"]);
241
+
242
+ const pausedSm = out.stateMachine.states.find((s) => s.id === "paused")!;
243
+ expect(pausedSm.transitions.map((t) => t.id)).toEqual(["paused-to-running"]);
244
+ });
245
+
246
+ it("derives staysVisible from exitStates length (false when exitStates non-empty)", () => {
247
+ const idleSm = out.stateMachine.states.find((s) => s.id === "idle")!;
248
+ const t = idleSm.transitions[0]!;
249
+ expect(t.deactivateStates).toEqual(["idle"]);
250
+ expect(t.staysVisible).toBe(false);
251
+ });
252
+
253
+ it("renames action.type -> process[].action and converts target criteria", () => {
254
+ const idleSm = out.stateMachine.states.find((s) => s.id === "idle")!;
255
+ const proc = idleSm.transitions[0]!.process[0]!;
256
+ expect(proc.action).toBe("click");
257
+ expect(proc.target).toEqual({ textContent: "Run Last Workflow" });
258
+ expect(proc.waitAfter).toEqual({ type: "idle", timeout: 5000 });
259
+ });
260
+
261
+ it("uses initialState fallback when state.isInitial is undefined", () => {
262
+ const noExplicit: IRDocument = {
263
+ version: "1.0",
264
+ id: "x",
265
+ name: "X",
266
+ initialState: "first",
267
+ states: [
268
+ {
269
+ id: "first",
270
+ name: "First",
271
+ requiredElements: [{ role: "heading" }],
272
+ },
273
+ {
274
+ id: "second",
275
+ name: "Second",
276
+ requiredElements: [{ role: "button" }],
277
+ },
278
+ ],
279
+ transitions: [],
280
+ };
281
+ const result = projectIRToBundledPage(noExplicit);
282
+ expect(result.stateMachine.states.find((s) => s.id === "first")!.isInitial).toBe(
283
+ true,
284
+ );
285
+ expect(
286
+ result.stateMachine.states.find((s) => s.id === "second")!.isInitial,
287
+ ).toBe(false);
288
+ });
289
+
290
+ it("propagates document tags into metadata.tags", () => {
291
+ expect(out.metadata.tags).toEqual([
292
+ "active-dashboard",
293
+ "monitoring",
294
+ "real-time",
295
+ "tier-1",
296
+ ]);
297
+ });
298
+
299
+ it("uses doc.id as metadata.component when no purpose is set", () => {
300
+ expect(out.metadata.component).toBe("active");
301
+ });
302
+
303
+ it("uses metadata.purpose as metadata.component when present", () => {
304
+ const withPurpose: IRDocument = {
305
+ ...ACTIVE_PAGE_FIXTURE,
306
+ metadata: { ...ACTIVE_PAGE_FIXTURE.metadata, purpose: "ActiveDashboardPage" },
307
+ };
308
+ const result = projectIRToBundledPage(withPurpose);
309
+ expect(result.metadata.component).toBe("ActiveDashboardPage");
310
+ });
311
+
312
+ it("uses doc.description as the top-level description", () => {
313
+ expect(out.description).toBe(ACTIVE_PAGE_FIXTURE.description);
314
+ });
315
+ });
316
+
317
+ // ---------------------------------------------------------------------------
318
+ // Notes injection
319
+ // ---------------------------------------------------------------------------
320
+
321
+ describe("projectIRToBundledPage — notes injection", () => {
322
+ it("appends notes as a separate paragraph (two-newline separator)", () => {
323
+ const out = projectIRToBundledPage(
324
+ ACTIVE_PAGE_FIXTURE,
325
+ "Reviewed by Joshua on 2026-04-28.",
326
+ );
327
+ expect(out.description).toBe(
328
+ `${ACTIVE_PAGE_FIXTURE.description}\n\nReviewed by Joshua on 2026-04-28.`,
329
+ );
330
+ });
331
+
332
+ it("treats undefined notes as no-op (no trailing newlines)", () => {
333
+ const out = projectIRToBundledPage(ACTIVE_PAGE_FIXTURE);
334
+ expect(out.description).toBe(ACTIVE_PAGE_FIXTURE.description);
335
+ expect(out.description.endsWith("\n")).toBe(false);
336
+ });
337
+
338
+ it("treats empty-string notes as no-op", () => {
339
+ const out = projectIRToBundledPage(ACTIVE_PAGE_FIXTURE, "");
340
+ expect(out.description).toBe(ACTIVE_PAGE_FIXTURE.description);
341
+ });
342
+
343
+ it("falls back to doc.name when doc.description is missing", () => {
344
+ const noDesc: IRDocument = {
345
+ version: "1.0",
346
+ id: "p",
347
+ name: "Page",
348
+ states: [],
349
+ transitions: [],
350
+ };
351
+ const out = projectIRToBundledPage(noDesc, "added later");
352
+ expect(out.description).toBe("Page\n\nadded later");
353
+ });
354
+ });
355
+
356
+ // ---------------------------------------------------------------------------
357
+ // Empty cases
358
+ // ---------------------------------------------------------------------------
359
+
360
+ describe("projectIRToBundledPage — empty cases", () => {
361
+ it("handles a document with zero states", () => {
362
+ const empty: IRDocument = {
363
+ version: "1.0",
364
+ id: "empty",
365
+ name: "Empty",
366
+ states: [],
367
+ transitions: [],
368
+ };
369
+ const out = projectIRToBundledPage(empty);
370
+ expect(out.groups).toEqual([]);
371
+ expect(out.stateMachine.states).toEqual([]);
372
+ expect(out.version).toBe("1.0.0");
373
+ expect(out.metadata.component).toBe("empty");
374
+ });
375
+
376
+ it("projects a state with zero requiredElements to a group with empty assertions[]", () => {
377
+ // Legacy specs ship architecture-only / documentation-only groups with
378
+ // empty `assertions[]` (e.g. `graphql-resolver-inventory` in the runner's
379
+ // `graphql-infrastructure.spec.uibridge.json`). The forward projection
380
+ // must preserve that emptiness so the legacy<->IR round-trip is identity
381
+ // for assertion counts; emitting a placeholder here silently inflates the
382
+ // count and trips `check-spec-pairing` with an `assertionCount` mismatch.
383
+ const doc: IRDocument = {
384
+ version: "1.0",
385
+ id: "p",
386
+ name: "P",
387
+ states: [
388
+ {
389
+ id: "no-req",
390
+ name: "No Required Elements",
391
+ requiredElements: [],
392
+ },
393
+ ],
394
+ transitions: [],
395
+ };
396
+ const out = projectIRToBundledPage(doc);
397
+ expect(out.groups).toHaveLength(1);
398
+ const group = out.groups[0]!;
399
+ expect(group.assertions).toEqual([]);
400
+ // The state still appears in the state-machine block — emptiness is local
401
+ // to `groups[].assertions[]`, not state existence.
402
+ expect(out.stateMachine.states).toHaveLength(1);
403
+ expect(out.stateMachine.states[0]!.id).toBe("no-req");
404
+ });
405
+
406
+ it("preserves empty groups across a legacy -> IR -> legacy round-trip (regression: graphql-infrastructure)", () => {
407
+ // Reproduces the on-disk shape of the four runner specs that previously
408
+ // produced an `assertionCount` mismatch in `check-spec-pairing`: a group
409
+ // with empty `assertions[]` and a matching state-machine state with
410
+ // empty `elements[]`. Round-trip identity proves the placeholder
411
+ // re-emission bug stays fixed.
412
+ const legacy = {
413
+ version: "1.0.0",
414
+ description: "Architecture-only spec with empty groups",
415
+ metadata: { component: "regression" },
416
+ groups: [
417
+ {
418
+ id: "non-empty",
419
+ name: "Has assertions",
420
+ description: "",
421
+ category: "element-presence",
422
+ assertions: [
423
+ {
424
+ id: "a1",
425
+ description: "First assertion",
426
+ category: "element-presence",
427
+ severity: "critical",
428
+ assertionType: "exists",
429
+ target: {
430
+ type: "search",
431
+ criteria: { textContent: "Run" },
432
+ label: "Run button",
433
+ },
434
+ source: "ai-generated",
435
+ reviewed: false,
436
+ enabled: true,
437
+ },
438
+ ],
439
+ source: "ai-generated",
440
+ },
441
+ {
442
+ id: "empty",
443
+ name: "Empty architecture group",
444
+ description: "",
445
+ category: "element-presence",
446
+ assertions: [],
447
+ source: "ai-generated",
448
+ },
449
+ ],
450
+ stateMachine: {
451
+ states: [
452
+ {
453
+ id: "non-empty",
454
+ name: "Has assertions",
455
+ description: "",
456
+ elements: [{ textContent: "Run" }],
457
+ isInitial: true,
458
+ transitions: [],
459
+ },
460
+ {
461
+ id: "empty",
462
+ name: "Empty architecture group",
463
+ description: "",
464
+ elements: [],
465
+ isInitial: false,
466
+ transitions: [],
467
+ },
468
+ ],
469
+ },
470
+ } as unknown as Parameters<typeof projectLegacyToIR>[0];
471
+
472
+ const ir = projectLegacyToIR(legacy, { docId: "regression" });
473
+ const projected = projectIRToBundledPage(ir);
474
+
475
+ expect(projected.groups).toHaveLength(2);
476
+ expect(projected.groups[0]!.id).toBe("non-empty");
477
+ expect(projected.groups[0]!.assertions).toHaveLength(1);
478
+ expect(projected.groups[1]!.id).toBe("empty");
479
+ expect(projected.groups[1]!.assertions).toEqual([]);
480
+
481
+ // Total assertion count is identity across the round-trip.
482
+ const legacyTotal = legacy.groups.reduce(
483
+ (s: number, g: { assertions: unknown[] }) => s + g.assertions.length,
484
+ 0,
485
+ );
486
+ const projectedTotal = projected.groups.reduce(
487
+ (s, g) => s + g.assertions.length,
488
+ 0,
489
+ );
490
+ expect(projectedTotal).toBe(legacyTotal);
491
+ });
492
+
493
+ it("handles a state with empty-string text criteria (still emits an assertion)", () => {
494
+ const doc: IRDocument = {
495
+ version: "1.0",
496
+ id: "p",
497
+ name: "P",
498
+ states: [
499
+ {
500
+ id: "empty-text",
501
+ name: "Empty Text",
502
+ requiredElements: [{ text: "" }],
503
+ },
504
+ ],
505
+ transitions: [],
506
+ };
507
+ const out = projectIRToBundledPage(doc);
508
+ expect(out.groups[0]!.assertions).toHaveLength(1);
509
+ expect(out.groups[0]!.assertions[0]!.target.criteria).toEqual({ textContent: "" });
510
+ });
511
+
512
+ it("handles a transition with no actions (process: [])", () => {
513
+ const doc: IRDocument = {
514
+ version: "1.0",
515
+ id: "p",
516
+ name: "P",
517
+ states: [
518
+ { id: "a", name: "A", requiredElements: [{ role: "heading" }] },
519
+ { id: "b", name: "B", requiredElements: [{ role: "heading" }] },
520
+ ],
521
+ transitions: [
522
+ {
523
+ id: "auto",
524
+ name: "Auto transition",
525
+ fromStates: ["a"],
526
+ activateStates: ["b"],
527
+ exitStates: ["a"],
528
+ actions: [],
529
+ },
530
+ ],
531
+ };
532
+ const out = projectIRToBundledPage(doc);
533
+ const aSm = out.stateMachine.states.find((s) => s.id === "a")!;
534
+ expect(aSm.transitions[0]!.process).toEqual([]);
535
+ expect(aSm.transitions[0]!.staysVisible).toBe(false);
536
+ });
537
+
538
+ it("derives staysVisible=true for a transition with no exitStates (modal-style)", () => {
539
+ const doc: IRDocument = {
540
+ version: "1.0",
541
+ id: "p",
542
+ name: "P",
543
+ states: [
544
+ { id: "base", name: "Base", requiredElements: [{ role: "heading" }] },
545
+ { id: "modal", name: "Modal", requiredElements: [{ role: "dialog" }] },
546
+ ],
547
+ transitions: [
548
+ {
549
+ id: "open-modal",
550
+ name: "Open modal",
551
+ fromStates: ["base"],
552
+ activateStates: ["modal"],
553
+ // No exitStates -> modal-style; base stays visible underneath.
554
+ actions: [{ type: "click", target: { role: "button" } }],
555
+ },
556
+ ],
557
+ };
558
+ const out = projectIRToBundledPage(doc);
559
+ const baseSm = out.stateMachine.states.find((s) => s.id === "base")!;
560
+ expect(baseSm.transitions[0]!.deactivateStates).toEqual([]);
561
+ expect(baseSm.transitions[0]!.staysVisible).toBe(true);
562
+ });
563
+ });
564
+
565
+ // ---------------------------------------------------------------------------
566
+ // Provenance + state-level fields
567
+ // ---------------------------------------------------------------------------
568
+
569
+ describe("projectIRToBundledPage — provenance + state metadata", () => {
570
+ it("uses state.provenance.source for group.source when present", () => {
571
+ const doc: IRDocument = {
572
+ version: "1.0",
573
+ id: "p",
574
+ name: "P",
575
+ states: [
576
+ {
577
+ id: "s",
578
+ name: "S",
579
+ requiredElements: [{ role: "heading" }],
580
+ provenance: { source: "hand-authored", file: "p.tsx" },
581
+ },
582
+ ],
583
+ transitions: [],
584
+ };
585
+ const out = projectIRToBundledPage(doc);
586
+ expect(out.groups[0]!.source).toBe("hand-authored");
587
+ });
588
+
589
+ it("defaults group.source to ai-generated when provenance is missing", () => {
590
+ const doc: IRDocument = {
591
+ version: "1.0",
592
+ id: "p",
593
+ name: "P",
594
+ states: [
595
+ {
596
+ id: "s",
597
+ name: "S",
598
+ requiredElements: [{ role: "heading" }],
599
+ },
600
+ ],
601
+ transitions: [],
602
+ };
603
+ const out = projectIRToBundledPage(doc);
604
+ expect(out.groups[0]!.source).toBe("ai-generated");
605
+ });
606
+
607
+ it("uses state.metadata.description as group.description when state.description is missing", () => {
608
+ const doc: IRDocument = {
609
+ version: "1.0",
610
+ id: "p",
611
+ name: "P",
612
+ states: [
613
+ {
614
+ id: "s",
615
+ name: "S",
616
+ requiredElements: [{ role: "heading" }],
617
+ metadata: { description: "From metadata" },
618
+ },
619
+ ],
620
+ transitions: [],
621
+ };
622
+ const out = projectIRToBundledPage(doc);
623
+ expect(out.groups[0]!.description).toBe("From metadata");
624
+ });
625
+
626
+ it("falls through to empty string when neither state.description nor state.metadata.description set", () => {
627
+ const doc: IRDocument = {
628
+ version: "1.0",
629
+ id: "p",
630
+ name: "P",
631
+ states: [
632
+ {
633
+ id: "s",
634
+ name: "S",
635
+ requiredElements: [{ role: "heading" }],
636
+ },
637
+ ],
638
+ transitions: [],
639
+ };
640
+ const out = projectIRToBundledPage(doc);
641
+ expect(out.groups[0]!.description).toBe("");
642
+ });
643
+ });
644
+
645
+ // ---------------------------------------------------------------------------
646
+ // Type / version export
647
+ // ---------------------------------------------------------------------------
648
+
649
+ describe("projectionVersion", () => {
650
+ it('exports "1.0"', () => {
651
+ expect(projectionVersion).toBe("1.0");
652
+ });
653
+ });
654
+
655
+ // ---------------------------------------------------------------------------
656
+ // Type witness — confirms LegacySpec is a structural shape with the legacy fields
657
+ // ---------------------------------------------------------------------------
658
+
659
+ describe("LegacySpec — structural witness", () => {
660
+ it("matches the legacy shape on a minimal projection", () => {
661
+ const minimal: IRDocument = {
662
+ version: "1.0",
663
+ id: "m",
664
+ name: "M",
665
+ states: [],
666
+ transitions: [],
667
+ };
668
+ const out: LegacySpec = projectIRToBundledPage(minimal);
669
+ expect(out).toMatchObject({
670
+ version: "1.0.0",
671
+ description: "M",
672
+ groups: [],
673
+ stateMachine: { states: [] },
674
+ metadata: { component: "m" },
675
+ });
676
+ });
677
+ });
678
+
679
+ // ---------------------------------------------------------------------------
680
+ // Inverse projection: legacy -> IR (Phase A2)
681
+ // ---------------------------------------------------------------------------
682
+
683
+ /**
684
+ * Hand-authored minimal legacy fixture for the inverse direction. Mirrors a
685
+ * trimmed `settings-general.spec.uibridge.json`-style shape with two groups,
686
+ * a state-machine block, transitions with action sequences, and a
687
+ * precondition on one assertion.
688
+ */
689
+ const LEGACY_FIXTURE: LegacySpec = {
690
+ version: "1.0.0",
691
+ description: "Hand-authored test fixture for inverse projection.",
692
+ groups: [
693
+ {
694
+ id: "alpha",
695
+ name: "Alpha Group",
696
+ description: "First group for the round-trip test.",
697
+ category: "element-presence",
698
+ assertions: [
699
+ {
700
+ id: "alpha-elem-0",
701
+ description: "Heading exists",
702
+ category: "element-presence",
703
+ severity: "critical",
704
+ assertionType: "exists",
705
+ target: {
706
+ type: "search",
707
+ criteria: { role: "heading", textContent: "Alpha" },
708
+ label: "Alpha heading",
709
+ },
710
+ source: "manual",
711
+ reviewed: true,
712
+ enabled: true,
713
+ },
714
+ {
715
+ id: "alpha-elem-1",
716
+ description: "Subtitle exists",
717
+ category: "element-presence",
718
+ severity: "warning",
719
+ assertionType: "exists",
720
+ target: {
721
+ type: "search",
722
+ criteria: { textContent: "Welcome" },
723
+ label: "Subtitle",
724
+ },
725
+ source: "manual",
726
+ reviewed: true,
727
+ enabled: true,
728
+ precondition: "Page is loaded.",
729
+ },
730
+ ],
731
+ source: "manual",
732
+ },
733
+ {
734
+ id: "beta",
735
+ name: "Beta Group",
736
+ description: "Second group, modal-style.",
737
+ category: "element-presence",
738
+ assertions: [
739
+ {
740
+ id: "beta-elem-0",
741
+ description: "Dialog exists",
742
+ category: "element-presence",
743
+ severity: "critical",
744
+ assertionType: "exists",
745
+ target: {
746
+ type: "search",
747
+ criteria: { role: "dialog", accessibleName: "Confirm" },
748
+ label: "Confirm dialog",
749
+ },
750
+ source: "manual",
751
+ reviewed: true,
752
+ enabled: true,
753
+ },
754
+ ],
755
+ source: "manual",
756
+ },
757
+ ],
758
+ stateMachine: {
759
+ states: [
760
+ {
761
+ id: "alpha",
762
+ name: "Alpha Group",
763
+ description: "First group for the round-trip test.",
764
+ elements: [
765
+ { role: "heading", textContent: "Alpha" },
766
+ { textContent: "Welcome" },
767
+ ],
768
+ isInitial: true,
769
+ transitions: [
770
+ {
771
+ id: "alpha-to-beta",
772
+ name: "Open Beta",
773
+ activateStates: ["beta"],
774
+ deactivateStates: [],
775
+ staysVisible: true,
776
+ process: [
777
+ {
778
+ action: "click",
779
+ target: { role: "button", textContent: "Open" },
780
+ waitAfter: { type: "idle", timeout: 1000 },
781
+ },
782
+ ],
783
+ },
784
+ ],
785
+ },
786
+ {
787
+ id: "beta",
788
+ name: "Beta Group",
789
+ description: "Second group, modal-style.",
790
+ elements: [{ role: "dialog", accessibleName: "Confirm" }],
791
+ isInitial: false,
792
+ transitions: [
793
+ {
794
+ id: "beta-to-alpha",
795
+ name: "Close Beta",
796
+ activateStates: ["alpha"],
797
+ deactivateStates: ["beta"],
798
+ staysVisible: false,
799
+ process: [
800
+ {
801
+ action: "click",
802
+ target: { textContent: "Cancel" },
803
+ },
804
+ ],
805
+ },
806
+ ],
807
+ },
808
+ ],
809
+ },
810
+ metadata: {
811
+ component: "fixture-page",
812
+ tags: ["fixture", "round-trip"],
813
+ },
814
+ };
815
+
816
+ describe("projectLegacyToIR — basic shape", () => {
817
+ it("converts metadata.component to ir.id and propagates tags", () => {
818
+ const ir = projectLegacyToIR(LEGACY_FIXTURE);
819
+ expect(ir.id).toBe("fixture-page");
820
+ expect(ir.metadata?.tags).toEqual(["fixture", "round-trip"]);
821
+ });
822
+
823
+ it("uses opts.docId override when supplied", () => {
824
+ const ir = projectLegacyToIR(LEGACY_FIXTURE, { docId: "override" });
825
+ expect(ir.id).toBe("override");
826
+ });
827
+
828
+ it("emits one IR state per legacy group, preserving id + name + description", () => {
829
+ const ir = projectLegacyToIR(LEGACY_FIXTURE);
830
+ expect(ir.states).toHaveLength(2);
831
+ expect(ir.states.map((s) => s.id)).toEqual(["alpha", "beta"]);
832
+ const alpha = ir.states.find((s) => s.id === "alpha")!;
833
+ expect(alpha.name).toBe("Alpha Group");
834
+ expect(alpha.description).toBe("First group for the round-trip test.");
835
+ });
836
+
837
+ it("sources requiredElements from group.assertions[].target.criteria", () => {
838
+ const ir = projectLegacyToIR(LEGACY_FIXTURE);
839
+ const alpha = ir.states.find((s) => s.id === "alpha")!;
840
+ expect(alpha.requiredElements).toHaveLength(2);
841
+ expect(alpha.requiredElements[0]).toEqual({
842
+ role: "heading",
843
+ text: "Alpha",
844
+ });
845
+ expect(alpha.requiredElements[1]).toEqual({ text: "Welcome" });
846
+ });
847
+
848
+ it("inverts dataAttributes -> attributes and accessibleName passthrough", () => {
849
+ const ir = projectLegacyToIR(LEGACY_FIXTURE);
850
+ const beta = ir.states.find((s) => s.id === "beta")!;
851
+ expect(beta.requiredElements[0]).toEqual({
852
+ role: "dialog",
853
+ accessibleName: "Confirm",
854
+ });
855
+ });
856
+
857
+ it("lifts the first-found assertion.precondition to state.precondition", () => {
858
+ const ir = projectLegacyToIR(LEGACY_FIXTURE);
859
+ const alpha = ir.states.find((s) => s.id === "alpha")!;
860
+ expect(alpha.precondition).toBe("Page is loaded.");
861
+ });
862
+
863
+ it("derives initialState from the first SM state with isInitial:true", () => {
864
+ const ir = projectLegacyToIR(LEGACY_FIXTURE);
865
+ expect(ir.initialState).toBe("alpha");
866
+ });
867
+
868
+ it("sets state.isInitial from the SM state", () => {
869
+ const ir = projectLegacyToIR(LEGACY_FIXTURE);
870
+ expect(ir.states.find((s) => s.id === "alpha")!.isInitial).toBe(true);
871
+ expect(ir.states.find((s) => s.id === "beta")!.isInitial).toBe(false);
872
+ });
873
+
874
+ it("tags every state and transition with provenance.source = 'migrated'", () => {
875
+ const ir = projectLegacyToIR(LEGACY_FIXTURE);
876
+ for (const s of ir.states) {
877
+ expect(s.provenance?.source).toBe("migrated");
878
+ }
879
+ for (const t of ir.transitions) {
880
+ expect(t.provenance?.source).toBe("migrated");
881
+ }
882
+ expect(ir.provenance?.source).toBe("migrated");
883
+ });
884
+
885
+ it("collapses transitions in stateMachine.states[].transitions[] into one IRTransition", () => {
886
+ const ir = projectLegacyToIR(LEGACY_FIXTURE);
887
+ expect(ir.transitions).toHaveLength(2);
888
+ const ids = ir.transitions.map((t) => t.id).sort();
889
+ expect(ids).toEqual(["alpha-to-beta", "beta-to-alpha"]);
890
+ });
891
+
892
+ it("inverts process[] back into transition.actions with type/target/waitAfter", () => {
893
+ const ir = projectLegacyToIR(LEGACY_FIXTURE);
894
+ const t = ir.transitions.find((tt) => tt.id === "alpha-to-beta")!;
895
+ expect(t.actions).toHaveLength(1);
896
+ expect(t.actions[0]!.type).toBe("click");
897
+ expect(t.actions[0]!.target).toEqual({ role: "button", text: "Open" });
898
+ expect(t.actions[0]!.waitAfter).toEqual({ type: "idle", timeout: 1000 });
899
+ });
900
+
901
+ it("preserves modal-style empty deactivateStates as exitStates: []", () => {
902
+ const ir = projectLegacyToIR(LEGACY_FIXTURE);
903
+ const t = ir.transitions.find((tt) => tt.id === "alpha-to-beta")!;
904
+ expect(t.exitStates).toEqual([]);
905
+ });
906
+
907
+ it("handles a legacy spec with no stateMachine block (synthesizes states from groups only)", () => {
908
+ const noSM = {
909
+ ...LEGACY_FIXTURE,
910
+ stateMachine: { states: [] },
911
+ } as LegacySpec;
912
+ const ir = projectLegacyToIR(noSM);
913
+ expect(ir.transitions).toEqual([]);
914
+ expect(ir.states).toHaveLength(2);
915
+ // No isInitial set when SM block is empty.
916
+ expect(ir.initialState).toBeUndefined();
917
+ });
918
+ });
919
+
920
+ // ---------------------------------------------------------------------------
921
+ // Round-trip tests
922
+ // ---------------------------------------------------------------------------
923
+
924
+ describe("round-trip — IR -> legacy -> IR (forward then inverse)", () => {
925
+ it("preserves the active-page IR fixture's group/state/transition counts", () => {
926
+ const legacy = projectIRToBundledPage(ACTIVE_PAGE_FIXTURE);
927
+ const ir = projectLegacyToIR(legacy);
928
+ expect(ir.states.length).toBe(ACTIVE_PAGE_FIXTURE.states.length);
929
+ expect(ir.states.map((s) => s.id).sort()).toEqual(
930
+ ACTIVE_PAGE_FIXTURE.states.map((s) => s.id).sort(),
931
+ );
932
+ expect(ir.transitions.length).toBe(ACTIVE_PAGE_FIXTURE.transitions.length);
933
+ expect(ir.transitions.map((t) => t.id).sort()).toEqual(
934
+ ACTIVE_PAGE_FIXTURE.transitions.map((t) => t.id).sort(),
935
+ );
936
+ });
937
+
938
+ it("preserves the active-page IR fixture's initialState", () => {
939
+ const legacy = projectIRToBundledPage(ACTIVE_PAGE_FIXTURE);
940
+ const ir = projectLegacyToIR(legacy);
941
+ expect(ir.initialState).toBe("idle");
942
+ });
943
+
944
+ it("preserves the active-page IR's tags", () => {
945
+ const legacy = projectIRToBundledPage(ACTIVE_PAGE_FIXTURE);
946
+ const ir = projectLegacyToIR(legacy);
947
+ expect(ir.metadata?.tags).toEqual(ACTIVE_PAGE_FIXTURE.metadata?.tags);
948
+ });
949
+
950
+ it("preserves transition fromStates merging when the same transition is emitted under multiple SM states", () => {
951
+ // Forward emits one copy of each transition under each fromState. The
952
+ // active-page fixture only has one fromState per transition, but check
953
+ // the dedup logic doesn't drop transitions when fromStates is single.
954
+ const legacy = projectIRToBundledPage(ACTIVE_PAGE_FIXTURE);
955
+ const ir = projectLegacyToIR(legacy);
956
+ const orig = ACTIVE_PAGE_FIXTURE.transitions.find(
957
+ (t) => t.id === "idle-to-running",
958
+ )!;
959
+ const round = ir.transitions.find((t) => t.id === "idle-to-running")!;
960
+ expect(round.fromStates).toEqual(orig.fromStates);
961
+ expect(round.activateStates).toEqual(orig.activateStates);
962
+ });
963
+ });
964
+
965
+ describe("round-trip — legacy -> IR -> legacy (inverse then forward)", () => {
966
+ it("preserves group count and group ids from a hand-authored legacy fixture", () => {
967
+ const ir = projectLegacyToIR(LEGACY_FIXTURE);
968
+ const back = projectIRToBundledPage(ir);
969
+ expect(back.groups.length).toBe(LEGACY_FIXTURE.groups.length);
970
+ expect(back.groups.map((g) => g.id)).toEqual(
971
+ LEGACY_FIXTURE.groups.map((g) => g.id),
972
+ );
973
+ });
974
+
975
+ it("preserves total assertion count", () => {
976
+ const ir = projectLegacyToIR(LEGACY_FIXTURE);
977
+ const back = projectIRToBundledPage(ir);
978
+ const orig = LEGACY_FIXTURE.groups.reduce(
979
+ (n, g) => n + g.assertions.length,
980
+ 0,
981
+ );
982
+ const round = back.groups.reduce((n, g) => n + g.assertions.length, 0);
983
+ expect(round).toBe(orig);
984
+ });
985
+
986
+ it("preserves transition ids in the SM block", () => {
987
+ const ir = projectLegacyToIR(LEGACY_FIXTURE);
988
+ const back = projectIRToBundledPage(ir);
989
+ const allTids: string[] = [];
990
+ for (const s of back.stateMachine.states) {
991
+ for (const t of s.transitions) {
992
+ if (!allTids.includes(t.id)) allTids.push(t.id);
993
+ }
994
+ }
995
+ const origTids: string[] = [];
996
+ for (const s of LEGACY_FIXTURE.stateMachine.states) {
997
+ for (const t of s.transitions) {
998
+ if (!origTids.includes(t.id)) origTids.push(t.id);
999
+ }
1000
+ }
1001
+ expect(allTids.sort()).toEqual(origTids.sort());
1002
+ });
1003
+
1004
+ it("preserves transition action types", () => {
1005
+ const ir = projectLegacyToIR(LEGACY_FIXTURE);
1006
+ const back = projectIRToBundledPage(ir);
1007
+ const alphaSm = back.stateMachine.states.find((s) => s.id === "alpha")!;
1008
+ const t = alphaSm.transitions.find((tt) => tt.id === "alpha-to-beta")!;
1009
+ expect(t.process).toHaveLength(1);
1010
+ expect(t.process[0]!.action).toBe("click");
1011
+ expect(t.process[0]!.target).toEqual({ role: "button", textContent: "Open" });
1012
+ });
1013
+
1014
+ it("preserves staysVisible on modal-style transitions (empty deactivateStates)", () => {
1015
+ const ir = projectLegacyToIR(LEGACY_FIXTURE);
1016
+ const back = projectIRToBundledPage(ir);
1017
+ const alphaSm = back.stateMachine.states.find((s) => s.id === "alpha")!;
1018
+ const t = alphaSm.transitions.find((tt) => tt.id === "alpha-to-beta")!;
1019
+ expect(t.staysVisible).toBe(true);
1020
+ expect(t.deactivateStates).toEqual([]);
1021
+ });
1022
+
1023
+ it("preserves component + tags in metadata", () => {
1024
+ const ir = projectLegacyToIR(LEGACY_FIXTURE);
1025
+ const back = projectIRToBundledPage(ir);
1026
+ expect(back.metadata.component).toBe("fixture-page");
1027
+ expect(back.metadata.tags).toEqual(["fixture", "round-trip"]);
1028
+ });
1029
+ });