@qontinui/shared-types 0.2.4 → 0.3.0

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