@alpsckr/unitycli 0.4.3 → 0.4.4

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 (420) hide show
  1. package/README.md +5 -5
  2. package/contracts/command-contract.json +109 -217
  3. package/contracts/package-files.json +374 -294
  4. package/contracts/stable-session/direct-call-semantics.json +8 -2
  5. package/contracts/tool-completion-contract.json +148 -672
  6. package/dist/bridge/admission.d.ts +17 -1
  7. package/dist/bridge/admission.js +2 -0
  8. package/dist/bridge/admission.js.map +1 -1
  9. package/dist/bridge/domains/asset.js +3 -1
  10. package/dist/bridge/domains/asset.js.map +1 -1
  11. package/dist/bridge/domains/camera.js +39 -7
  12. package/dist/bridge/domains/camera.js.map +1 -1
  13. package/dist/bridge/domains/console.js +2 -1
  14. package/dist/bridge/domains/console.js.map +1 -1
  15. package/dist/bridge/domains/doctor.d.ts +3 -0
  16. package/dist/bridge/domains/doctor.js +8 -0
  17. package/dist/bridge/domains/doctor.js.map +1 -0
  18. package/dist/bridge/domains/editor.js +0 -4
  19. package/dist/bridge/domains/editor.js.map +1 -1
  20. package/dist/bridge/domains/game-object.js +4 -12
  21. package/dist/bridge/domains/game-object.js.map +1 -1
  22. package/dist/bridge/domains/graphics.js +1 -16
  23. package/dist/bridge/domains/graphics.js.map +1 -1
  24. package/dist/bridge/domains/object.js +3 -0
  25. package/dist/bridge/domains/object.js.map +1 -1
  26. package/dist/bridge/domains/physics.js +1 -2
  27. package/dist/bridge/domains/physics.js.map +1 -1
  28. package/dist/bridge/domains/profiler.js +18 -7
  29. package/dist/bridge/domains/profiler.js.map +1 -1
  30. package/dist/bridge/domains/project-tool.js +4 -0
  31. package/dist/bridge/domains/project-tool.js.map +1 -1
  32. package/dist/bridge/domains/reflection.js +10 -2
  33. package/dist/bridge/domains/reflection.js.map +1 -1
  34. package/dist/bridge/domains/runtime-tool.js +3 -0
  35. package/dist/bridge/domains/runtime-tool.js.map +1 -1
  36. package/dist/bridge/domains/scene.js +3 -8
  37. package/dist/bridge/domains/scene.js.map +1 -1
  38. package/dist/bridge/domains/screenshot.js +30 -6
  39. package/dist/bridge/domains/screenshot.js.map +1 -1
  40. package/dist/bridge/domains/ui.js +3 -1
  41. package/dist/bridge/domains/ui.js.map +1 -1
  42. package/dist/bridge/index.d.ts +5 -5
  43. package/dist/bridge/index.js +6 -9
  44. package/dist/bridge/index.js.map +1 -1
  45. package/dist/bridge/registry.js +3 -5
  46. package/dist/bridge/registry.js.map +1 -1
  47. package/dist/bridge/session-supervisor.d.ts +6 -3
  48. package/dist/bridge/session-supervisor.js +122 -34
  49. package/dist/bridge/session-supervisor.js.map +1 -1
  50. package/dist/bridge/types.d.ts +6 -5
  51. package/dist/bridge/types.js +16 -2
  52. package/dist/bridge/types.js.map +1 -1
  53. package/dist/catalog-protocol.d.ts +64 -0
  54. package/dist/catalog-protocol.js +460 -0
  55. package/dist/catalog-protocol.js.map +1 -0
  56. package/dist/cli-commands/command.js +84 -61
  57. package/dist/cli-commands/command.js.map +1 -1
  58. package/dist/cli-commands/extensions.js +70 -50
  59. package/dist/cli-commands/extensions.js.map +1 -1
  60. package/dist/cli-commands/instances.js +1 -1
  61. package/dist/cli-commands/instances.js.map +1 -1
  62. package/dist/cli-process-owners.d.ts +6 -0
  63. package/dist/cli-process-owners.js +27 -0
  64. package/dist/cli-process-owners.js.map +1 -0
  65. package/dist/cli.d.ts +1 -1
  66. package/dist/cli.js +20 -5
  67. package/dist/cli.js.map +1 -1
  68. package/dist/contracts/command-contract.js +14 -4
  69. package/dist/contracts/command-contract.js.map +1 -1
  70. package/dist/doctor.d.ts +4 -6
  71. package/dist/doctor.js +429 -398
  72. package/dist/doctor.js.map +1 -1
  73. package/dist/editor-blocker-observer.d.ts +2 -1
  74. package/dist/editor-blocker-observer.js +40 -13
  75. package/dist/editor-blocker-observer.js.map +1 -1
  76. package/dist/extension-availability.d.ts +27 -9
  77. package/dist/extension-availability.js +96 -56
  78. package/dist/extension-availability.js.map +1 -1
  79. package/dist/extensions/descriptor-loader.d.ts +2 -2
  80. package/dist/extensions/descriptor-loader.js +3 -4
  81. package/dist/extensions/descriptor-loader.js.map +1 -1
  82. package/dist/extensions/index.d.ts +1 -1
  83. package/dist/extensions/index.js +1 -1
  84. package/dist/extensions/index.js.map +1 -1
  85. package/dist/extensions/installer.d.ts +24 -7
  86. package/dist/extensions/installer.js +142 -66
  87. package/dist/extensions/installer.js.map +1 -1
  88. package/dist/extensions/registry.js +3 -5
  89. package/dist/extensions/registry.js.map +1 -1
  90. package/dist/extensions/state-contract.d.ts +11 -0
  91. package/dist/extensions/state-contract.js +50 -0
  92. package/dist/extensions/state-contract.js.map +1 -0
  93. package/dist/instances.d.ts +13 -1
  94. package/dist/instances.js +26 -0
  95. package/dist/instances.js.map +1 -1
  96. package/dist/package-manager.d.ts +0 -24
  97. package/dist/package-manager.js +5 -58
  98. package/dist/package-manager.js.map +1 -1
  99. package/dist/project-tool-discovery.d.ts +5 -2
  100. package/dist/project-tool-discovery.js +22 -18
  101. package/dist/project-tool-discovery.js.map +1 -1
  102. package/dist/runtime-code.js +1 -1
  103. package/dist/runtime-code.js.map +1 -1
  104. package/dist/tool-name.d.ts +4 -0
  105. package/dist/tool-name.js +15 -0
  106. package/dist/tool-name.js.map +1 -0
  107. package/dist/tools/.generated-completion-registry.d.ts +2 -0
  108. package/dist/tools/.generated-completion-registry.js +763 -325
  109. package/dist/tools/.generated-completion-registry.js.map +1 -1
  110. package/dist/tools/.generated-registry.d.ts +6 -11
  111. package/dist/tools/.generated-registry.js +6 -11
  112. package/dist/tools/.generated-registry.js.map +1 -1
  113. package/dist/tools/.generated-schemas.js +11 -16
  114. package/dist/tools/.generated-schemas.js.map +1 -1
  115. package/dist/tools/asset/refresh-status.d.ts +3 -0
  116. package/dist/tools/asset/refresh-status.js +12 -0
  117. package/dist/tools/asset/refresh-status.js.map +1 -0
  118. package/dist/tools/asset/refresh.js +7 -3
  119. package/dist/tools/asset/refresh.js.map +1 -1
  120. package/dist/tools/build/player-status.d.ts +3 -0
  121. package/dist/tools/build/player-status.js +12 -0
  122. package/dist/tools/build/player-status.js.map +1 -0
  123. package/dist/tools/build/run.js +1 -1
  124. package/dist/tools/build/run.js.map +1 -1
  125. package/dist/tools/catalog-control.d.ts +22 -0
  126. package/dist/tools/catalog-control.js +335 -0
  127. package/dist/tools/catalog-control.js.map +1 -0
  128. package/dist/tools/catalog.d.ts +7 -9
  129. package/dist/tools/catalog.js +66 -45
  130. package/dist/tools/catalog.js.map +1 -1
  131. package/dist/tools/commands.d.ts +1 -1
  132. package/dist/tools/commands.js +44 -11
  133. package/dist/tools/commands.js.map +1 -1
  134. package/dist/tools/define.js +3 -2
  135. package/dist/tools/define.js.map +1 -1
  136. package/dist/tools/dispatcher.d.ts +53 -6
  137. package/dist/tools/dispatcher.js +277 -108
  138. package/dist/tools/dispatcher.js.map +1 -1
  139. package/dist/tools/editor/compile.js +1 -1
  140. package/dist/tools/editor/compile.js.map +1 -1
  141. package/dist/tools/editor/play-mode-status.d.ts +3 -0
  142. package/dist/tools/editor/play-mode-status.js +12 -0
  143. package/dist/tools/editor/play-mode-status.js.map +1 -0
  144. package/dist/tools/editor/readiness.d.ts +0 -1
  145. package/dist/tools/editor/readiness.js +0 -1
  146. package/dist/tools/editor/readiness.js.map +1 -1
  147. package/dist/tools/editor/refresh.d.ts +7 -3
  148. package/dist/tools/editor/refresh.js +20 -15
  149. package/dist/tools/editor/refresh.js.map +1 -1
  150. package/dist/tools/editor/state.d.ts +2 -2
  151. package/dist/tools/editor/state.js +13 -12
  152. package/dist/tools/editor/state.js.map +1 -1
  153. package/dist/tools/metadata/derived.js +1 -7
  154. package/dist/tools/metadata/derived.js.map +1 -1
  155. package/dist/tools/metadata/status.js +1 -12
  156. package/dist/tools/metadata/status.js.map +1 -1
  157. package/dist/tools/metadata/types.d.ts +0 -2
  158. package/dist/tools/navmesh/live.js +1 -1
  159. package/dist/tools/navmesh/live.js.map +1 -1
  160. package/dist/tools/occlusion/live.js +1 -1
  161. package/dist/tools/occlusion/live.js.map +1 -1
  162. package/dist/tools/package/deploy.js +2 -2
  163. package/dist/tools/package/deploy.js.map +1 -1
  164. package/dist/tools/package/embed.js +1 -1
  165. package/dist/tools/package/embed.js.map +1 -1
  166. package/dist/tools/package/install-status.d.ts +3 -0
  167. package/dist/tools/package/install-status.js +12 -0
  168. package/dist/tools/package/install-status.js.map +1 -0
  169. package/dist/tools/package/install.js +3 -3
  170. package/dist/tools/package/install.js.map +1 -1
  171. package/dist/tools/package/live-resolve.js +45 -12
  172. package/dist/tools/package/live-resolve.js.map +1 -1
  173. package/dist/tools/package/registry-clear.js.map +1 -1
  174. package/dist/tools/package/registry-set.js.map +1 -1
  175. package/dist/tools/package/remove-status.d.ts +3 -0
  176. package/dist/tools/package/remove-status.js +12 -0
  177. package/dist/tools/package/remove-status.js.map +1 -0
  178. package/dist/tools/package/remove.js +3 -3
  179. package/dist/tools/package/remove.js.map +1 -1
  180. package/dist/tools/package/resolve.js +3 -3
  181. package/dist/tools/package/resolve.js.map +1 -1
  182. package/dist/tools/package/restore.js +2 -2
  183. package/dist/tools/package/restore.js.map +1 -1
  184. package/dist/tools/package/scoped-registry-add.js.map +1 -1
  185. package/dist/tools/status/support.d.ts +13 -0
  186. package/dist/tools/status/support.js +28 -0
  187. package/dist/tools/status/support.js.map +1 -0
  188. package/dist/tools/test/cancel.js +2 -2
  189. package/dist/tools/test/cancel.js.map +1 -1
  190. package/dist/tools/test/run-status.d.ts +3 -0
  191. package/dist/tools/test/run-status.js +12 -0
  192. package/dist/tools/test/run-status.js.map +1 -0
  193. package/dist/tools/test/run.js +7 -4
  194. package/dist/tools/test/run.js.map +1 -1
  195. package/dist/tools/tools/group-summaries.js +10 -14
  196. package/dist/tools/tools/group-summaries.js.map +1 -1
  197. package/docs/architecture.md +4 -4
  198. package/docs/availability-differences.md +4 -4
  199. package/docs/command-execution.md +4 -3
  200. package/docs/error-codes.md +13 -5
  201. package/docs/extensions.md +1 -7
  202. package/docs/risk-and-side-effects.md +4 -3
  203. package/docs/safety-boundaries.md +3 -1
  204. package/docs/target-and-instance.md +1 -1
  205. package/docs/tool-discovery.md +17 -10
  206. package/docs/tool-execution.md +18 -7
  207. package/extensions/cinemachine/unitypkg/Editor/UnityCliExtensionAssembly.cs +3 -0
  208. package/extensions/hdrp/unitypkg/Editor/UnityCliExtensionAssembly.cs +3 -0
  209. package/extensions/probuilder/unitypkg/Editor/UnityCliExtensionAssembly.cs +3 -0
  210. package/extensions/urp/unitypkg/Editor/UnityCliExtensionAssembly.cs +3 -0
  211. package/extensions/vfx-graph/unitypkg/Editor/UnityCliExtensionAssembly.cs +3 -0
  212. package/package.json +2 -4
  213. package/skills/unitycli/SKILL.md +11 -2
  214. package/skills/unitycli/manifest.json +1 -1
  215. package/skills/unitycli/references/operation-protocol.md +3 -3
  216. package/unitypkg/Editor/AssemblyInfo.cs +5 -0
  217. package/unitypkg/Editor/Bridge/BridgeCommandRegistry.cs +49 -14
  218. package/unitypkg/Editor/Bridge/BridgeHttpServer.cs +9 -35
  219. package/unitypkg/Editor/Bridge/BridgeQueueState.cs +672 -0
  220. package/unitypkg/Editor/Bridge/BridgeWorkQueue.cs +970 -120
  221. package/unitypkg/Editor/BridgeStartupStateMachine.cs +59 -1
  222. package/unitypkg/Editor/Commands/ProjectToolCommands.cs +4 -0
  223. package/unitypkg/Editor/Commands/RuntimeToolCommands.cs +3 -0
  224. package/unitypkg/Editor/Generated/UnityCliToolCompletionRegistry.g.cs +379 -14
  225. package/unitypkg/Editor/Infrastructure/CodeExecSupport.cs +28 -13
  226. package/unitypkg/Editor/Infrastructure/RegistryOwnershipResolver.cs +132 -105
  227. package/unitypkg/Editor/Infrastructure/UnityCliActiveOperation.cs +138 -29
  228. package/unitypkg/Editor/Infrastructure/UnityCliAdmission.cs +1856 -237
  229. package/unitypkg/Editor/Infrastructure/UnityCliDoctorDiagnostics.cs +220 -0
  230. package/unitypkg/Editor/Infrastructure/UnityCliDurableStatusCoordinator.cs +118 -0
  231. package/unitypkg/Editor/Infrastructure/UnityCliEditorStateSnapshot.cs +2 -1
  232. package/unitypkg/Editor/Infrastructure/UnityCliHierarchyPager.cs +203 -0
  233. package/unitypkg/Editor/Infrastructure/UnityCliInstanceCleanup.cs +2 -1
  234. package/unitypkg/Editor/Infrastructure/UnityCliInstanceRegistry.cs +201 -63
  235. package/unitypkg/Editor/Infrastructure/UnityCliJson.cs +2 -3
  236. package/unitypkg/Editor/Infrastructure/UnityCliLiveBuildCoordinator.cs +289 -46
  237. package/unitypkg/Editor/Infrastructure/UnityCliLiveRefreshCoordinator.cs +87 -53
  238. package/unitypkg/Editor/Infrastructure/UnityCliNavMeshBakeCoordinator.cs +4 -4
  239. package/unitypkg/Editor/Infrastructure/UnityCliObjectRef.cs +128 -25
  240. package/unitypkg/Editor/Infrastructure/UnityCliObjectSerializer.cs +435 -327
  241. package/unitypkg/Editor/Infrastructure/UnityCliOcclusionBakeCoordinator.cs +4 -4
  242. package/unitypkg/Editor/Infrastructure/UnityCliOperationCoordinator.cs +351 -53
  243. package/unitypkg/Editor/Infrastructure/UnityCliPackageResolveCoordinator.cs +118 -18
  244. package/unitypkg/Editor/Infrastructure/UnityCliPackageResolveState.cs +20 -1
  245. package/unitypkg/Editor/Infrastructure/UnityCliReadGeneration.cs +97 -0
  246. package/unitypkg/Editor/Infrastructure/UnityCliReflection.cs +644 -133
  247. package/unitypkg/Editor/Infrastructure/UnityCliReloadOperation.cs +128 -41
  248. package/unitypkg/Editor/Infrastructure/UnityCliRuntimeExecutor.cs +14 -84
  249. package/unitypkg/Editor/Infrastructure/UnityCliSerializer.cs +220 -0
  250. package/unitypkg/Editor/Infrastructure/UnityCliSessionMutationLease.cs +69 -29
  251. package/unitypkg/Editor/Infrastructure/UnityCliTestRunCoordinator.cs +440 -54
  252. package/unitypkg/Editor/Infrastructure/UnityCliWaiter.cs +128 -35
  253. package/unitypkg/Editor/ProjectTools/IUnityCliTool.cs +9 -1
  254. package/unitypkg/Editor/ProjectTools/OfficialAttributeToolRegistry.cs +32 -31
  255. package/unitypkg/Editor/ProjectTools/ProjectToolRegistry.cs +607 -458
  256. package/unitypkg/Editor/ProjectTools/ProjectToolSchemaValidator.cs +35 -7
  257. package/unitypkg/Editor/ProjectTools/RegisteredTool.cs +501 -60
  258. package/unitypkg/Editor/ProjectTools/UnityCliProjectToolContext.cs +21 -0
  259. package/unitypkg/Editor/ProjectTools/UnityCliSchemaDeriver.cs +36 -7
  260. package/unitypkg/Editor/ProjectTools/UnityCliToolName.cs +30 -0
  261. package/unitypkg/Editor/Properties/AssemblyInfo.cs +3 -0
  262. package/unitypkg/Editor/Tools/asset/get-data.cs +16 -7
  263. package/unitypkg/Editor/Tools/build/BuildSceneStateTool.cs +23 -3
  264. package/unitypkg/Editor/Tools/build/BuildToolDtos.cs +2 -1
  265. package/unitypkg/Editor/Tools/build/scene/add.cs +6 -3
  266. package/unitypkg/Editor/Tools/build/scene/disable.cs +1 -1
  267. package/unitypkg/Editor/Tools/build/scene/enable.cs +1 -1
  268. package/unitypkg/Editor/Tools/build/scene/list.cs +1 -1
  269. package/unitypkg/Editor/Tools/build/scene/remove.cs +13 -3
  270. package/unitypkg/Editor/Tools/camera/CameraToolSupport.cs +38 -38
  271. package/unitypkg/Editor/Tools/camera/multiview.cs +13 -6
  272. package/unitypkg/Editor/Tools/camera/screenshot.cs +14 -7
  273. package/unitypkg/Editor/Tools/component/get.cs +16 -9
  274. package/unitypkg/Editor/Tools/component/modify.cs +6 -4
  275. package/unitypkg/Editor/Tools/console/clear.cs +7 -6
  276. package/unitypkg/Editor/Tools/console/get.cs +48 -16
  277. package/unitypkg/Editor/Tools/editor/EditorToolSupport.cs +143 -28
  278. package/unitypkg/Editor/Tools/editor/menu-items.cs +1 -1
  279. package/unitypkg/Editor/Tools/editor/play.cs +2 -11
  280. package/unitypkg/Editor/Tools/editor/selection.cs +1 -1
  281. package/unitypkg/Editor/Tools/editor/stop.cs +2 -11
  282. package/unitypkg/Editor/Tools/editor/windows.cs +1 -1
  283. package/unitypkg/Editor/Tools/go/GameObjectToolSupport.cs +54 -4
  284. package/unitypkg/Editor/Tools/go/delete.cs +1 -1
  285. package/unitypkg/Editor/Tools/go/find.cs +167 -70
  286. package/unitypkg/Editor/Tools/graphics/GraphicsToolSupport.cs +25 -0
  287. package/unitypkg/Editor/Tools/graphics/light-probe-group.cs +9 -3
  288. package/unitypkg/Editor/Tools/graphics/pipeline-info.cs +1 -1
  289. package/unitypkg/Editor/Tools/graphics/pipeline-settings.cs +1 -1
  290. package/unitypkg/Editor/Tools/graphics/quality.cs +2 -2
  291. package/unitypkg/Editor/Tools/graphics/reflection-probe.cs +3 -3
  292. package/unitypkg/Editor/Tools/graphics/stats.cs +1 -1
  293. package/unitypkg/Editor/Tools/object/ObjectToolSupport.cs +4 -4
  294. package/unitypkg/Editor/Tools/object/get-data.cs +9 -7
  295. package/unitypkg/Editor/Tools/object/modify.cs +7 -5
  296. package/unitypkg/Editor/Tools/physics/raycast.cs +24 -6
  297. package/unitypkg/Editor/Tools/prefab/stage.cs +1 -1
  298. package/unitypkg/Editor/Tools/profiler/ProfilerFrameDebuggerSupport.cs +94 -21
  299. package/unitypkg/Editor/Tools/profiler/ProfilerMemorySnapshotSupport.cs +397 -10
  300. package/unitypkg/Editor/Tools/profiler/ProfilerToolSupport.cs +18 -4
  301. package/unitypkg/Editor/Tools/profiler/frame-debugger/enable.cs +1 -1
  302. package/unitypkg/Editor/Tools/profiler/frame-debugger/events.cs +88 -87
  303. package/unitypkg/Editor/Tools/profiler/hierarchy.cs +278 -112
  304. package/unitypkg/Editor/Tools/profiler/memory-snapshot/compare.cs +9 -5
  305. package/unitypkg/Editor/Tools/profiler/memory-snapshot/list.cs +37 -35
  306. package/unitypkg/Editor/Tools/profiler/memory-snapshot/take.cs +99 -97
  307. package/unitypkg/Editor/Tools/project/info.cs +1 -1
  308. package/unitypkg/Editor/Tools/project/layers.cs +1 -1
  309. package/unitypkg/Editor/Tools/project/tags.cs +1 -1
  310. package/unitypkg/Editor/Tools/reflection/method/call.cs +9 -7
  311. package/unitypkg/Editor/Tools/reflection/method/find.cs +27 -37
  312. package/unitypkg/Editor/Tools/reflection/type/schema.cs +24 -28
  313. package/unitypkg/Editor/Tools/scene/gameobject/component.cs +8 -5
  314. package/unitypkg/Editor/Tools/scene/gameobject/components.cs +10 -7
  315. package/unitypkg/Editor/Tools/scene/gameobject-support.cs +104 -8
  316. package/unitypkg/Editor/Tools/scene/gameobject.cs +37 -16
  317. package/unitypkg/Editor/Tools/scene/hierarchy.cs +146 -142
  318. package/unitypkg/Editor/Tools/screenshot/EditorElementCaptureToolSupport.cs +77 -25
  319. package/unitypkg/Editor/Tools/screenshot/ScreenshotToolSupport.cs +315 -95
  320. package/unitypkg/Editor/Tools/screenshot/editor-element.cs +7 -5
  321. package/unitypkg/Editor/Tools/screenshot/game-view.cs +35 -21
  322. package/unitypkg/Editor/Tools/screenshot/isolated-object.cs +29 -23
  323. package/unitypkg/Editor/Tools/screenshot/orbit.cs +37 -25
  324. package/unitypkg/Editor/Tools/screenshot/scene-view.cs +25 -18
  325. package/unitypkg/Editor/Tools/screenshot/screen.cs +177 -17
  326. package/unitypkg/Editor/Tools/screenshot/surround.cs +83 -79
  327. package/unitypkg/Editor/Tools/ui/render-preview.cs +16 -11
  328. package/unitypkg/Editor/UnityCliBridge.cs +97 -34
  329. package/unitypkg/Runtime/Infrastructure/UnityCliRuntimeBridge.cs +753 -122
  330. package/unitypkg/Runtime/Properties/AssemblyInfo.cs +5 -0
  331. package/unitypkg/Runtime/UnityCli.Runtime.asmdef +1 -1
  332. package/unitypkg/Shared/AssemblyInfo.cs +4 -0
  333. package/unitypkg/Shared/UnityCliAdmissionPool.cs +265 -75
  334. package/unitypkg/Shared/UnityCliBridgeQueuePolicy.cs +410 -0
  335. package/unitypkg/Shared/UnityCliBridgeWorkItemState.cs +47 -0
  336. package/unitypkg/Shared/UnityCliExceptionProjection.cs +151 -0
  337. package/unitypkg/Shared/UnityCliQueuePublication.cs +30 -0
  338. package/unitypkg/Shared/UnityCliRequestBudget.cs +352 -0
  339. package/unitypkg/Shared/UnityCliRuntimeRequestPublication.cs +107 -0
  340. package/unitypkg/Tests/Editor/UnityCli.Editor.Tests.asmdef +2 -2
  341. package/unitypkg/Tests/Editor/UnityCliAdmissionCoordinatorIntegrationTests.cs +1329 -0
  342. package/unitypkg/Tests/Editor/UnityCliAdmissionPoolTests.cs +311 -114
  343. package/unitypkg/Tests/Editor/UnityCliAdmissionRepairStateTests.cs +94 -0
  344. package/unitypkg/Tests/Editor/UnityCliBridgeAdmissionIntegrationTests.cs +1229 -0
  345. package/unitypkg/Tests/Editor/UnityCliBridgeQueuePolicyTests.cs +412 -0
  346. package/unitypkg/Tests/Editor/UnityCliContinuationIntegrationTests.cs +292 -0
  347. package/unitypkg/Tests/Editor/UnityCliDiagnosticBoundaryTests.cs +120 -0
  348. package/unitypkg/Tests/Editor/UnityCliDurableStatusTests.cs +84 -0
  349. package/unitypkg/Tests/Editor/UnityCliExceptionProjectionTests.cs +72 -0
  350. package/unitypkg/Tests/Editor/UnityCliInstancePublicationRetryTests.cs +180 -97
  351. package/unitypkg/Tests/Editor/UnityCliMemoryReflectionIntegrationTests.cs +434 -0
  352. package/unitypkg/Tests/Editor/UnityCliRequestBudgetTests.cs +615 -0
  353. package/unitypkg/Tests/Editor/UnityCliRuntimeBridgeIntegrationTests.cs +520 -0
  354. package/unitypkg/Tests/Editor/UnityCliToolCatalogTests.cs +562 -0
  355. package/unitypkg/Tests/Runtime/UnityCli.Runtime.Tests.asmdef +23 -0
  356. package/unitypkg/Tests/Runtime/UnityCliRuntimeCatalogValidationTests.cs +29 -0
  357. package/unitypkg/package.json +1 -1
  358. package/contracts/static-capability-catalog.json +0 -48226
  359. package/dist/bridge/domains/diagnose.d.ts +0 -2
  360. package/dist/bridge/domains/diagnose.js +0 -11
  361. package/dist/bridge/domains/diagnose.js.map +0 -1
  362. package/dist/bridge/domains/extension.d.ts +0 -8
  363. package/dist/bridge/domains/extension.js +0 -20
  364. package/dist/bridge/domains/extension.js.map +0 -1
  365. package/dist/capabilities.d.ts +0 -5
  366. package/dist/capabilities.js +0 -84
  367. package/dist/capabilities.js.map +0 -1
  368. package/dist/static-capability-catalog.d.ts +0 -13
  369. package/dist/static-capability-catalog.js +0 -41
  370. package/dist/static-capability-catalog.js.map +0 -1
  371. package/dist/tools/diagnose/capabilities.d.ts +0 -13
  372. package/dist/tools/diagnose/capabilities.js +0 -34
  373. package/dist/tools/diagnose/capabilities.js.map +0 -1
  374. package/dist/tools/package/info.d.ts +0 -19
  375. package/dist/tools/package/info.js +0 -27
  376. package/dist/tools/package/info.js.map +0 -1
  377. package/dist/tools/package/list.d.ts +0 -27
  378. package/dist/tools/package/list.js +0 -21
  379. package/dist/tools/package/list.js.map +0 -1
  380. package/dist/tools/package/registry-get.d.ts +0 -10
  381. package/dist/tools/package/registry-get.js +0 -22
  382. package/dist/tools/package/registry-get.js.map +0 -1
  383. package/dist/tools/package/scoped-registry-list.d.ts +0 -14
  384. package/dist/tools/package/scoped-registry-list.js +0 -22
  385. package/dist/tools/package/scoped-registry-list.js.map +0 -1
  386. package/dist/tools/package/search.d.ts +0 -17
  387. package/dist/tools/package/search.js +0 -39
  388. package/dist/tools/package/search.js.map +0 -1
  389. package/dist/tools/package/status.d.ts +0 -21
  390. package/dist/tools/package/status.js +0 -37
  391. package/dist/tools/package/status.js.map +0 -1
  392. package/dist/tools/tools/describe.d.ts +0 -27
  393. package/dist/tools/tools/describe.js +0 -35
  394. package/dist/tools/tools/describe.js.map +0 -1
  395. package/dist/tools/tools/group-list.d.ts +0 -16
  396. package/dist/tools/tools/group-list.js +0 -38
  397. package/dist/tools/tools/group-list.js.map +0 -1
  398. package/dist/tools/tools/group.d.ts +0 -13
  399. package/dist/tools/tools/group.js +0 -34
  400. package/dist/tools/tools/group.js.map +0 -1
  401. package/dist/tools/tools/pagination.d.ts +0 -17
  402. package/dist/tools/tools/pagination.js +0 -28
  403. package/dist/tools/tools/pagination.js.map +0 -1
  404. package/dist/tools/tools/search.d.ts +0 -15
  405. package/dist/tools/tools/search.js +0 -42
  406. package/dist/tools/tools/search.js.map +0 -1
  407. package/unitypkg/Editor/Tools/diagnose/preflight.cs +0 -269
  408. package/unitypkg/Editor/Tools/editor/console.cs +0 -26
  409. package/unitypkg/Editor/Tools/extension/status.cs +0 -87
  410. package/unitypkg/Editor/Tools/go/inspect.cs +0 -49
  411. package/unitypkg/Editor/Tools/graphics/pipeline-quality.cs +0 -24
  412. package/unitypkg/Editor/Tools/graphics/probe-positions.cs +0 -58
  413. package/unitypkg/Editor/Tools/graphics/settings.cs +0 -104
  414. package/unitypkg/Editor/Tools/graphics/stats-counters.cs +0 -28
  415. package/unitypkg/Editor/Tools/graphics/stats-memory.cs +0 -21
  416. package/unitypkg/Editor/Tools/instances/list.cs +0 -71
  417. package/unitypkg/Editor/Tools/physics/raycast-all.cs +0 -67
  418. package/unitypkg/Editor/Tools/scene/build-settings.cs +0 -69
  419. package/unitypkg/Editor/Tools/scene/data.cs +0 -125
  420. package/unitypkg/Editor/Tools/tests/list.cs +0 -98
@@ -0,0 +1,1329 @@
1
+ #if UNITY_EDITOR
2
+ using System;
3
+ using System.Collections.Generic;
4
+ using System.IO;
5
+ using System.Linq;
6
+ using System.Text;
7
+ using System.Threading;
8
+ using NUnit.Framework;
9
+ using UnityEditor;
10
+ using UnityEngine;
11
+ using UnityCli.Protocol;
12
+
13
+ namespace UnityCli.Tests
14
+ {
15
+ public sealed class UnityCliAdmissionCoordinatorIntegrationTests
16
+ {
17
+ private const string InstanceId = "test-instance";
18
+ private const string Fingerprint = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
19
+ private string identityPrefix;
20
+ private BridgeQueueState queueState;
21
+ private long monotonicNowMs;
22
+
23
+ [SetUp]
24
+ public void SetUp()
25
+ {
26
+ identityPrefix = "admission-" + Guid.NewGuid().ToString("N") + "-";
27
+ monotonicNowMs = 1000L;
28
+ }
29
+
30
+ [TearDown]
31
+ public void TearDown()
32
+ {
33
+ if (queueState != null) queueState.AssertEmptyForTests();
34
+ }
35
+
36
+ [Test]
37
+ public void DirectTerminalRootClearFaultReplaysCleanupUntilAllRecoveryStateIsGone()
38
+ {
39
+ var requestId = Unique("direct-root-clear");
40
+ var digest = UnityCliRequestDigest.Compute(Encoding.UTF8.GetBytes(requestId), InstanceId);
41
+ var pool = CreatePool("direct-root-clear-session");
42
+ Consume(pool, requestId, digest, TestDeadline());
43
+ bool acquired;
44
+ Assert.That(queueState.TryAcquireLease(requestId, digest, out acquired), Is.True);
45
+ Assert.That(acquired, Is.True);
46
+ queueState.SaveOperation("direct-mutation", new Dictionary<string, object>
47
+ {
48
+ { "requestId", requestId }, { "requestDigest", digest }, { "toolName", "editor.play" },
49
+ { "editorSessionId", UnityCliBridge.EditorSessionId() }, { "phase", "effect_started" }
50
+ });
51
+ stateHookClearDirectRootOnce();
52
+
53
+ var terminal = UnityCliBridge.PublishDirectMutationTerminalForTests(queueState, requestId, digest,
54
+ UnityCliSerializer.Serialize(ProtocolEnvelopeFactory.Success(requestId, new Dictionary<string, object> { { "changed", true } })));
55
+
56
+ Assert.That(terminal, Does.Contain("\"changed\":true"));
57
+ Assert.That(queueState.OperationExists("direct-mutation"), Is.True);
58
+ Assert.That(queueState.OperationExists("direct-mutation-terminal-cleanup"), Is.True);
59
+ var directMarker = queueState.LoadOperation("direct-mutation-terminal-cleanup");
60
+ Assert.That(directMarker["requestId"], Is.EqualTo(requestId));
61
+ Assert.That(directMarker["requestDigest"], Is.EqualTo(digest));
62
+ Assert.That(directMarker["admissionRequestDigest"], Is.EqualTo(digest));
63
+ Assert.That(pool.Capture().Active, Is.EqualTo(1));
64
+ Assert.That(queueState.TryIsLeaseOwned(requestId, digest, out var owned), Is.True);
65
+ Assert.That(owned, Is.True);
66
+
67
+ var replay = UnityCliBridge.RecoverPublishedDirectMutationForTests(queueState, requestId, digest);
68
+ Assert.That(replay, Is.EqualTo(terminal));
69
+ Assert.That(queueState.OperationExists("direct-mutation"), Is.False);
70
+ Assert.That(queueState.OperationExists("direct-mutation-terminal-cleanup"), Is.False);
71
+ Assert.That(queueState.OperationExists("direct-mutation-admission-owner"), Is.False);
72
+ Assert.That(pool.Capture().Active, Is.EqualTo(0));
73
+ Assert.That(queueState.TryIsLeaseOwned(requestId, digest, out owned), Is.True);
74
+ Assert.That(owned, Is.False);
75
+ }
76
+
77
+ [Test]
78
+ public void ReloadedAcceptedDirectMutationWithoutLeaseSettlesEffectBeforeAndReplaysWithoutEffect()
79
+ {
80
+ var requestId = Unique("direct-accepted-reload");
81
+ var digest = UnityCliRequestDigest.Compute(Encoding.UTF8.GetBytes(requestId), InstanceId);
82
+ var pool = CreatePool("direct-accepted-reload-session");
83
+ Consume(pool, requestId, digest, TestDeadline());
84
+ queueState.SaveOperation("direct-mutation", new Dictionary<string, object>
85
+ {
86
+ { "requestId", requestId }, { "requestDigest", digest }, { "toolName", "editor.play" },
87
+ { "editorSessionId", UnityCliBridge.EditorSessionId() }, { "phase", "accepted_before_effect" }
88
+ });
89
+
90
+ var outcome = UnityCliBridge.UnityCliSessionMutationLease.InitializeRecoveryOrderForTests(queueState);
91
+ var replay = UnityCliBridge.RecoverPublishedDirectMutationForTests(queueState, requestId, digest);
92
+
93
+ Assert.That(outcome, Is.EqualTo("SettledEffectBefore"));
94
+ Assert.That(replay, Does.Contain("E_EXECUTION_TIMEOUT"));
95
+ Assert.That(replay, Does.Contain("no effect was executed"));
96
+ Assert.That(pool.Capture().Active, Is.EqualTo(0));
97
+ Assert.That(queueState.OperationExists("direct-mutation"), Is.False);
98
+ Assert.That(queueState.OperationExists("direct-mutation-terminal-cleanup"), Is.False);
99
+ Assert.That(queueState.OperationExists("direct-mutation-admission-owner"), Is.False);
100
+ Assert.That(queueState.PendingCompletionIntentCount, Is.EqualTo(0));
101
+ Assert.That(UnityCliBridge.ActiveRequestWorkCountForTests(queueState), Is.EqualTo(0));
102
+ Assert.That(queueState.TryIsLeaseOwned(requestId, digest, out var owned), Is.True);
103
+ Assert.That(owned, Is.False);
104
+ }
105
+
106
+ [Test]
107
+ public void CleanupPendingAcceptedDirectMutationReleasesOldLeaseThenPublishesTerminalBeforeCleanup()
108
+ {
109
+ var requestId = Unique("direct-cleanup-pending");
110
+ var digest = UnityCliRequestDigest.Compute(Encoding.UTF8.GetBytes(requestId), InstanceId);
111
+ var pool = CreatePool("direct-cleanup-pending-session");
112
+ Consume(pool, requestId, digest, TestDeadline());
113
+ queueState.SaveOperation("direct-mutation", Root(requestId, digest, "accepted_before_effect"));
114
+ Assert.That(queueState.TryAcquireLease(requestId, digest, out var acquired), Is.True);
115
+ Assert.That(acquired, Is.True);
116
+ var order = new List<string>();
117
+ queueState.TestHooks.ExpectOnce("before-terminal-store:direct-mutation", () =>
118
+ {
119
+ Assert.That(queueState.TryIsLeaseOwned(requestId, digest, out var owned), Is.True);
120
+ Assert.That(owned, Is.False);
121
+ order.Add("terminal");
122
+ });
123
+ queueState.TestHooks.ExpectOnce("before-operation-clear:direct-mutation", () =>
124
+ {
125
+ Assert.That(queueState.TryGetTerminal("direct-mutation", requestId, digest, out var terminal), Is.True);
126
+ Assert.That(UnityCliSerializer.Serialize(terminal), Does.Contain("no effect was executed"));
127
+ order.Add("root-cleanup");
128
+ });
129
+
130
+ var outcome = UnityCliBridge.UnityCliSessionMutationLease.InitializeRecoveryOrderForTests(queueState, true);
131
+
132
+ Assert.That(outcome, Is.EqualTo("SettledEffectBefore"));
133
+ Assert.That(order, Is.EqualTo(new[] { "terminal", "root-cleanup" }));
134
+ Assert.That(pool.Capture().Active, Is.EqualTo(0));
135
+ Assert.That(queueState.OperationExists("direct-mutation"), Is.False);
136
+ Assert.That(queueState.OperationExists("direct-mutation-terminal-cleanup"), Is.False);
137
+ Assert.That(queueState.TryIsLeaseOwned(requestId, digest, out var finalOwned), Is.True);
138
+ Assert.That(finalOwned, Is.False);
139
+ }
140
+
141
+ [Test]
142
+ public void ReloadedAcceptedDirectMutationWaitsForExactLeaseAndBlocksMismatch()
143
+ {
144
+ var requestId = Unique("direct-accepted-owned");
145
+ var digest = UnityCliRequestDigest.Compute(Encoding.UTF8.GetBytes(requestId), InstanceId);
146
+ var pool = CreatePool("direct-accepted-owned-session");
147
+ Consume(pool, requestId, digest, TestDeadline());
148
+ queueState.SaveOperation("direct-mutation", Root(requestId, digest, "accepted_before_effect"));
149
+ Assert.That(queueState.TryAcquireLease(requestId, digest, out var acquired), Is.True);
150
+ Assert.That(acquired, Is.True);
151
+ Assert.That(UnityCliBridge.UnityCliSessionMutationLease.InitializeRecoveryOrderForTests(queueState), Is.EqualTo("WaitingExactOwner"));
152
+ Assert.That(pool.Capture().Active, Is.EqualTo(1));
153
+ Assert.That(queueState.OperationExists("direct-mutation"), Is.True);
154
+ Assert.That(queueState.OperationExists("direct-mutation-terminal-cleanup"), Is.False);
155
+ Assert.That(queueState.OperationExists("direct-mutation-admission-owner"), Is.False);
156
+ Assert.That(queueState.TryGetTerminal("direct-mutation", requestId, digest, out var terminal), Is.False);
157
+ Assert.That(queueState.TryIsLeaseOwned(requestId, digest, out var exactOwned), Is.True);
158
+ Assert.That(exactOwned, Is.True);
159
+ queueState.ReleaseLease(requestId, digest);
160
+ var otherRequestId = Unique("direct-other-owner");
161
+ var otherDigest = UnityCliRequestDigest.Compute(Encoding.UTF8.GetBytes(otherRequestId), InstanceId);
162
+ Assert.That(queueState.TryAcquireLease(otherRequestId, otherDigest, out acquired), Is.True);
163
+ Assert.That(acquired, Is.True);
164
+ Assert.That(UnityCliBridge.UnityCliSessionMutationLease.InitializeRecoveryOrderForTests(queueState), Is.EqualTo("IdentityBlocked"));
165
+ Assert.That(pool.Capture().Active, Is.EqualTo(1));
166
+ Assert.That(queueState.OperationExists("direct-mutation"), Is.True);
167
+ queueState.ReleaseLease(otherRequestId, otherDigest);
168
+ queueState.ClearOperation("direct-mutation");
169
+ pool.Reset();
170
+ }
171
+
172
+ [TestCase("editor.play", false, true)]
173
+ [TestCase("editor.stop", true, false)]
174
+ public void ReloadedPlayModeRootFreezesTypedSuccessAfterStableTargetIsObserved(string toolName, bool before, bool targetPlaying)
175
+ {
176
+ var requestId = Unique("playmode-success");
177
+ var digest = UnityCliRequestDigest.Compute(Encoding.UTF8.GetBytes(requestId), InstanceId);
178
+ var pool = CreatePool("playmode-success-session");
179
+ Consume(pool, requestId, digest, TestDeadline());
180
+ queueState.SaveOperation("direct-mutation", PlayModeRoot(requestId, digest, toolName, before, DateTimeOffset.UtcNow.AddMinutes(1)));
181
+ Assert.That(queueState.TryAcquireLease(requestId, digest, out var acquired), Is.True);
182
+ Assert.That(acquired, Is.True);
183
+
184
+ var finalized = UnityCliBridge.TryFinalizeRecoveredPlayModeRootForTests(
185
+ queueState, requestId, digest, targetPlaying, false, false, DateTimeOffset.UtcNow);
186
+
187
+ Assert.That(finalized, Is.True);
188
+ Assert.That(queueState.TryGetTerminal("direct-mutation", requestId, digest, out var terminal), Is.True);
189
+ var serialized = UnityCliSerializer.Serialize(terminal);
190
+ Assert.That(serialized, Does.Contain("\"ok\":true"));
191
+ Assert.That(serialized, Does.Contain("\"changed\":true"));
192
+ Assert.That(serialized, Does.Contain("\"isPlaying\":" + targetPlaying.ToString().ToLowerInvariant()));
193
+ Assert.That(queueState.OperationExists("direct-mutation"), Is.False);
194
+ Assert.That(pool.Capture().Active, Is.EqualTo(0));
195
+ Assert.That(queueState.TryIsLeaseOwned(requestId, digest, out var owned), Is.True);
196
+ Assert.That(owned, Is.False);
197
+ }
198
+
199
+ [Test]
200
+ public void ReloadedPlayModeRootRetainsExactOwnerWhileTransitionIsActive()
201
+ {
202
+ var requestId = Unique("playmode-changing");
203
+ var digest = UnityCliRequestDigest.Compute(Encoding.UTF8.GetBytes(requestId), InstanceId);
204
+ var pool = CreatePool("playmode-changing-session");
205
+ Consume(pool, requestId, digest, TestDeadline());
206
+ queueState.SaveOperation("direct-mutation", PlayModeRoot(requestId, digest, "editor.play", false, DateTimeOffset.UtcNow.AddMinutes(1)));
207
+ Assert.That(queueState.TryAcquireLease(requestId, digest, out var acquired), Is.True);
208
+ Assert.That(acquired, Is.True);
209
+
210
+ var finalized = UnityCliBridge.TryFinalizeRecoveredPlayModeRootForTests(
211
+ queueState, requestId, digest, false, false, true, DateTimeOffset.UtcNow);
212
+
213
+ Assert.That(finalized, Is.False);
214
+ Assert.That(queueState.OperationExists("direct-mutation"), Is.True);
215
+ Assert.That(queueState.TryGetTerminal("direct-mutation", requestId, digest, out var terminal), Is.False);
216
+ Assert.That(pool.Capture().Active, Is.EqualTo(1));
217
+ Assert.That(queueState.TryIsLeaseOwned(requestId, digest, out var owned), Is.True);
218
+ Assert.That(owned, Is.True);
219
+ queueState.ClearOperation("direct-mutation");
220
+ queueState.ReleaseLease(requestId, digest);
221
+ pool.Reset();
222
+ }
223
+
224
+ [Test]
225
+ public void ReloadedPlayModeRootFreezesDomainErrorAtImmutableDeadline()
226
+ {
227
+ var requestId = Unique("playmode-deadline");
228
+ var digest = UnityCliRequestDigest.Compute(Encoding.UTF8.GetBytes(requestId), InstanceId);
229
+ var pool = CreatePool("playmode-deadline-session");
230
+ Consume(pool, requestId, digest, TestDeadline());
231
+ var deadline = DateTimeOffset.UtcNow.AddSeconds(-1);
232
+ queueState.SaveOperation("direct-mutation", PlayModeRoot(requestId, digest, "editor.play", false, deadline));
233
+ Assert.That(queueState.TryAcquireLease(requestId, digest, out var acquired), Is.True);
234
+ Assert.That(acquired, Is.True);
235
+
236
+ var finalized = UnityCliBridge.TryFinalizeRecoveredPlayModeRootForTests(
237
+ queueState, requestId, digest, false, false, false, deadline.AddSeconds(1));
238
+
239
+ Assert.That(finalized, Is.True);
240
+ Assert.That(queueState.TryGetTerminal("direct-mutation", requestId, digest, out var terminal), Is.True);
241
+ var serialized = UnityCliSerializer.Serialize(terminal);
242
+ Assert.That(serialized, Does.Contain("E_PLAY_MODE_TERMINAL_NOT_OBSERVED"));
243
+ Assert.That(serialized, Does.Not.Contain("outcome is unknown"));
244
+ Assert.That(queueState.OperationExists("direct-mutation"), Is.False);
245
+ Assert.That(pool.Capture().Active, Is.EqualTo(0));
246
+ }
247
+
248
+ [Test]
249
+ public void ReloadedGenericDirectMutationIsNotFinalizedFromPlayModeState()
250
+ {
251
+ var requestId = Unique("generic-effect");
252
+ var digest = UnityCliRequestDigest.Compute(Encoding.UTF8.GetBytes(requestId), InstanceId);
253
+ var pool = CreatePool("generic-effect-session");
254
+ Consume(pool, requestId, digest, TestDeadline());
255
+ queueState.SaveOperation("direct-mutation", PlayModeRoot(requestId, digest, "asset.delete", false, DateTimeOffset.UtcNow.AddMinutes(1)));
256
+ Assert.That(queueState.TryAcquireLease(requestId, digest, out var acquired), Is.True);
257
+ Assert.That(acquired, Is.True);
258
+
259
+ var finalized = UnityCliBridge.TryFinalizeRecoveredPlayModeRootForTests(
260
+ queueState, requestId, digest, true, false, false, DateTimeOffset.UtcNow);
261
+
262
+ Assert.That(finalized, Is.False);
263
+ Assert.That(queueState.OperationExists("direct-mutation"), Is.True);
264
+ Assert.That(queueState.TryGetTerminal("direct-mutation", requestId, digest, out var terminal), Is.False);
265
+ queueState.ClearOperation("direct-mutation");
266
+ queueState.ReleaseLease(requestId, digest);
267
+ pool.Reset();
268
+ }
269
+
270
+ [Test]
271
+ public void ReloadTickScheduleUsesMonotonicTimeAcrossUtcRollback()
272
+ {
273
+ var monotonic = 5000L;
274
+ var utcNow = DateTimeOffset.UtcNow;
275
+ var schedule = new UnityCliReloadTickSchedule(() => monotonic, () => utcNow);
276
+
277
+ Assert.That(schedule.TryEnter(), Is.True);
278
+ utcNow = utcNow.AddHours(-8);
279
+ monotonic += 99L;
280
+ Assert.That(schedule.TryEnter(), Is.False);
281
+ monotonic += 1L;
282
+ Assert.That(schedule.TryEnter(), Is.True);
283
+ Assert.That(schedule.UtcNow, Is.EqualTo(utcNow));
284
+ }
285
+
286
+ [Test]
287
+ public void FirstCancelTransferPublishesCompleteDispatchedMarkerAndRetryAllowsContinuationProgress()
288
+ {
289
+ var parentRequestId = Unique("first-transfer-parent");
290
+ var parentDigest = UnityCliRequestDigest.Compute(Encoding.UTF8.GetBytes(parentRequestId), InstanceId);
291
+ var cancelRequestId = Unique("first-transfer-cancel");
292
+ var cancelRequest = Request(cancelRequestId, "internal.test.run.cancel", new Dictionary<string, object>
293
+ {
294
+ { "expectedEditorSessionId", UnityCliBridge.EditorSessionId() }
295
+ });
296
+ var cancelRaw = RequestBytes(cancelRequest);
297
+ var cancelDigest = UnityCliRequestDigest.Compute(cancelRaw, InstanceId);
298
+ var runId = Guid.NewGuid().ToString();
299
+ var deadline = Deadline(cancelRequest);
300
+ var pool = CreatePool("first-cancel-transfer-session");
301
+ var grant = pool.Acquire(cancelRequestId, Fingerprint, queueState.ListenerId, queueState.ListenerGeneration);
302
+ Assert.That(grant.Outcome, Is.EqualTo(UnityCliAdmissionPool.AcquireOutcome.Granted));
303
+ queueState.SaveOperation("test-run", new Dictionary<string, object>
304
+ {
305
+ { "requestId", parentRequestId }, { "requestDigest", parentDigest },
306
+ { "editorSessionId", UnityCliBridge.EditorSessionId() },
307
+ { "absoluteDeadlineUtc", DateTimeOffset.FromUnixTimeMilliseconds(deadline).ToString("O") },
308
+ { "testOperation", new Dictionary<string, object>
309
+ {
310
+ { "phase", "running" }, { "runId", runId }, { "cancelRunId", runId },
311
+ { "cancelRequestId", cancelRequestId }, { "cancelRequestDigest", cancelDigest },
312
+ { "cancelRootRequestDigest", parentDigest }, { "cancelEffectState", "dispatched" },
313
+ { "cancelRequested", true }, { "cancelNeedsContinuation", true }
314
+ }
315
+ }
316
+ });
317
+ Assert.That(queueState.TryAcquireLease(parentRequestId, parentDigest, out var acquired), Is.True);
318
+ Assert.That(acquired, Is.True);
319
+ bool created;
320
+ var item = Enqueue(cancelRequest, cancelRaw, pool, out created);
321
+ Assert.That(created, Is.True);
322
+ var response = UnityCliSerializer.Serialize(ProtocolEnvelopeFactory.Success(cancelRequestId, new Dictionary<string, object>
323
+ {
324
+ { "terminal", false }, { "accepted", true }, { "rootRequestId", parentRequestId },
325
+ { "rootRequestDigest", parentDigest }, { "runId", runId }
326
+ }));
327
+
328
+ Assert.That(UnityCliBridge.CompleteWorkItemForTests(item, response), Is.True);
329
+
330
+ var marker = queueState.LoadOperation("test-cancel-admission-owner");
331
+ Assert.That(marker["rootRequestId"], Is.EqualTo(parentRequestId));
332
+ Assert.That(marker["rootRequestDigest"], Is.EqualTo(parentDigest));
333
+ Assert.That(marker["runId"], Is.EqualTo(runId));
334
+ Assert.That(marker["cancelRequestId"], Is.EqualTo(cancelRequestId));
335
+ Assert.That(marker["cancelRequestDigest"], Is.EqualTo(cancelDigest));
336
+ Assert.That(marker["cancelContinuationState"], Is.EqualTo("dispatched"));
337
+ Assert.That(marker["parentRepairEffectState"], Is.EqualTo("dispatched"));
338
+ Assert.That(marker["parentRepairPending"], Is.EqualTo(false));
339
+ Assert.That(marker.ContainsKey("cancelTerminalEnvelope"), Is.False);
340
+ Assert.That(pool.Capture().Records.Single().OwnerState, Is.EqualTo(UnityCliAdmissionPool.AdmissionOwnerState.Continuation));
341
+ var admissionSessionId = pool.Capture().Records.Single().SessionId;
342
+ pool.Reset();
343
+ pool = new UnityCliAdmissionPool(InstanceId, admissionSessionId, 4, 1);
344
+ queueState.AdmissionPool = pool;
345
+ Assert.That(UnityCliBridge.UnityCliTestRunCoordinator.RetryCancelAdmissionOwnerMarkerForTests(queueState), Is.False);
346
+ Assert.That(pool.Capture().Records.Single().OwnerState, Is.EqualTo(UnityCliAdmissionPool.AdmissionOwnerState.Continuation));
347
+ Assert.That(pool.Capture().RecoveryBlocked, Is.False);
348
+ Assert.That(queueState.TryGetTerminal("test-cancel", cancelRequestId, cancelDigest, out var missingTerminal), Is.False);
349
+
350
+ Assert.That(UnityCliBridge.UnityCliTestRunCoordinator.FinalizeCancelTerminalForTests(queueState,
351
+ parentRequestId, parentDigest, runId, cancelRequestId, cancelDigest,
352
+ ProtocolEnvelopeFactory.Success(cancelRequestId, new Dictionary<string, object> { { "cancelled", true } }), "cancelled"), Is.True);
353
+ Assert.That(pool.Capture().Active, Is.EqualTo(0));
354
+ Assert.That(queueState.OperationExists("test-cancel-admission-owner"), Is.False);
355
+ queueState.ClearOperation("test-run");
356
+ queueState.ReleaseLease(parentRequestId, parentDigest);
357
+ }
358
+
359
+ [Test]
360
+ public void CancelContinuationCompletesExactCancelOwnerWithoutIllegalTerminalTransfer()
361
+ {
362
+ var parentRequestId = Unique("test-parent");
363
+ var parentDigest = UnityCliRequestDigest.Compute(Encoding.UTF8.GetBytes(parentRequestId), InstanceId);
364
+ var cancelRequestId = Unique("test-cancel");
365
+ var cancelDigest = UnityCliRequestDigest.Compute(Encoding.UTF8.GetBytes(cancelRequestId), InstanceId);
366
+ var runId = Guid.NewGuid().ToString();
367
+ var deadline = TestDeadline();
368
+ var pool = CreatePool("cancel-continuation-session");
369
+ var cancelOwner = Consume(pool, cancelRequestId, cancelDigest, deadline);
370
+ queueState.SaveOperation("test-run", new Dictionary<string, object>
371
+ {
372
+ { "requestId", parentRequestId }, { "requestDigest", parentDigest },
373
+ { "editorSessionId", UnityCliBridge.EditorSessionId() },
374
+ { "absoluteDeadlineUtc", DateTimeOffset.FromUnixTimeMilliseconds(deadline).ToString("O") },
375
+ { "testOperation", new Dictionary<string, object>
376
+ {
377
+ { "phase", "running" }, { "runId", runId }, { "cancelRunId", runId },
378
+ { "cancelRequestId", cancelRequestId }, { "cancelRequestDigest", cancelDigest },
379
+ { "cancelRootRequestDigest", parentDigest }, { "cancelEffectState", "dispatched" },
380
+ { "cancelRequested", true }, { "cancelNeedsContinuation", true }
381
+ }
382
+ }
383
+ });
384
+ bool acquired;
385
+ Assert.That(queueState.TryAcquireLease(parentRequestId, parentDigest, out acquired), Is.True);
386
+ Assert.That(acquired, Is.True);
387
+ cancelOwner.TopRequestDigest = parentDigest;
388
+ UnityCliBridge.PersistAdmissionOwnerTransfer(queueState, "test-cancel-admission-owner", new Dictionary<string, object>
389
+ {
390
+ { "requestId", cancelRequestId }, { "requestDigest", parentDigest },
391
+ { "rootRequestId", parentRequestId }, { "rootRequestDigest", parentDigest },
392
+ { "runId", runId }, { "editorSessionId", UnityCliBridge.EditorSessionId() },
393
+ { "cancelRequestId", cancelRequestId }, { "cancelRequestDigest", cancelDigest },
394
+ { "cancelContinuationState", "dispatched" },
395
+ { "parentRepairEffectState", "dispatched" }, { "parentRepairPending", false }
396
+ }, cancelOwner, UnityCliAdmissionPool.AdmissionOwnerState.Continuation);
397
+
398
+ queueState.TestHooks.ExpectOnce("before-state-clear-markers", () =>
399
+ {
400
+ throw new IOException("injected cancel marker clear fault");
401
+ });
402
+ var result = UnityCliBridge.UnityCliTestRunCoordinator.FinalizeCancelTerminalForTests(queueState,
403
+ parentRequestId, parentDigest, runId, cancelRequestId, cancelDigest,
404
+ ProtocolEnvelopeFactory.Success(cancelRequestId, new Dictionary<string, object> { { "cancelled", true } }), "cancelled");
405
+
406
+ Assert.That(result, Is.False);
407
+ Assert.That(pool.Capture().Active, Is.EqualTo(0));
408
+ Assert.That(queueState.OperationExists("test-cancel-admission-owner"), Is.True);
409
+ var cancelMarker = queueState.LoadOperation("test-cancel-admission-owner");
410
+ Assert.That(cancelMarker["requestId"], Is.EqualTo(cancelRequestId));
411
+ Assert.That(cancelMarker["requestDigest"], Is.EqualTo(parentDigest));
412
+ Assert.That(cancelMarker["admissionRequestDigest"], Is.EqualTo(cancelDigest));
413
+ Assert.That(cancelMarker["rootRequestDigest"], Is.EqualTo(parentDigest));
414
+ Assert.That(cancelMarker["admissionOwnerState"], Is.EqualTo((long)UnityCliAdmissionPool.AdmissionOwnerState.WorkItem));
415
+ Assert.That(cancelMarker["admissionIntendedOwnerState"], Is.EqualTo((long)UnityCliAdmissionPool.AdmissionOwnerState.Continuation));
416
+ var completionOwner = UnityCliBridge.ReadAdmissionOwnerFields(cancelMarker, cancelRequestId);
417
+ completionOwner.TopRequestDigest = parentDigest;
418
+ Assert.That(stateHasCompletionIntent(completionOwner), Is.True);
419
+ Assert.That(UnityCliBridge.UnityCliTestRunCoordinator.RetryCancelAdmissionOwnerMarkerForTests(queueState), Is.False);
420
+ Assert.That(queueState.OperationExists("test-cancel-admission-owner"), Is.False);
421
+ Assert.That(queueState.OperationExists("test-run"), Is.True);
422
+ Assert.That(queueState.TryIsLeaseOwned(parentRequestId, parentDigest, out var parentOwned), Is.True);
423
+ Assert.That(parentOwned, Is.True);
424
+ Assert.That(queueState.TryGetTerminal("test-cancel", cancelRequestId, cancelDigest, out var exactTerminal), Is.True);
425
+ Assert.That(queueState.TryGetTerminalBySelector("test-cancel", cancelRequestId, parentDigest, out var selectedTerminal), Is.True);
426
+ Assert.That(UnityCliSerializer.Serialize(exactTerminal), Is.EqualTo(UnityCliSerializer.Serialize(selectedTerminal)));
427
+ Assert.That(pool.Capture().RecoveryBlocked, Is.False);
428
+ queueState.ClearOperation("test-run");
429
+ queueState.ReleaseLease(parentRequestId, parentDigest);
430
+ }
431
+
432
+ [Test]
433
+ public void CancelParentSaveFaultRetainsOwnerUntilRetryRepairsExactRoot()
434
+ {
435
+ var parentRequestId = Unique("save-fault-parent");
436
+ var parentDigest = UnityCliRequestDigest.Compute(Encoding.UTF8.GetBytes(parentRequestId), InstanceId);
437
+ var cancelRequestId = Unique("save-fault-cancel");
438
+ var cancelDigest = UnityCliRequestDigest.Compute(Encoding.UTF8.GetBytes(cancelRequestId), InstanceId);
439
+ var runId = Guid.NewGuid().ToString();
440
+ var deadline = TestDeadline();
441
+ var pool = CreatePool("cancel-save-fault-session");
442
+ var parentOwner = Consume(pool, parentRequestId, parentDigest, deadline);
443
+ UnityCliBridge.PersistAdmissionOwnerTransfer(queueState, "test-run-admission-owner", new Dictionary<string, object>
444
+ {
445
+ { "requestId", parentRequestId }, { "requestDigest", parentDigest },
446
+ { "editorSessionId", UnityCliBridge.EditorSessionId() }
447
+ }, parentOwner, UnityCliAdmissionPool.AdmissionOwnerState.Coordinator);
448
+ Consume(pool, cancelRequestId, cancelDigest, deadline);
449
+ queueState.SaveOperation("test-run", new Dictionary<string, object>
450
+ {
451
+ { "requestId", parentRequestId }, { "requestDigest", parentDigest },
452
+ { "editorSessionId", UnityCliBridge.EditorSessionId() },
453
+ { "absoluteDeadlineUtc", DateTimeOffset.FromUnixTimeMilliseconds(deadline).ToString("O") },
454
+ { "testOperation", new Dictionary<string, object>
455
+ {
456
+ { "phase", "running" }, { "runId", runId }, { "cancelRunId", runId },
457
+ { "cancelRequestId", cancelRequestId }, { "cancelRequestDigest", cancelDigest },
458
+ { "cancelRootRequestDigest", parentDigest }, { "cancelEffectState", "dispatching" },
459
+ { "cancelRequested", true }
460
+ }
461
+ }
462
+ });
463
+ Assert.That(queueState.TryAcquireLease(parentRequestId, parentDigest, out var acquired), Is.True);
464
+ Assert.That(acquired, Is.True);
465
+ queueState.TestHooks.ExpectOnce("before-operation-save:test-run", () => { throw new IOException("injected parent save fault"); });
466
+
467
+ var result = UnityCliBridge.UnityCliTestRunCoordinator.FinalizeCancelTerminalForTests(queueState,
468
+ parentRequestId, parentDigest, runId, cancelRequestId, cancelDigest,
469
+ ProtocolEnvelopeFactory.Success(cancelRequestId, new Dictionary<string, object> { { "cancelled", true } }), "cancelled");
470
+
471
+ Assert.That(result, Is.False);
472
+ Assert.That(pool.Capture().Active, Is.EqualTo(2));
473
+ var retained = queueState.LoadOperation("test-cancel-admission-owner");
474
+ Assert.That(retained["parentRepairEffectState"], Is.EqualTo("cancelled"));
475
+ Assert.That(retained["parentRepairPending"], Is.EqualTo(true));
476
+ var staleRoot = queueState.LoadOperation("test-run");
477
+ var staleOperation = (Dictionary<string, object>)staleRoot["testOperation"];
478
+ Assert.That(staleOperation["cancelEffectState"], Is.EqualTo("dispatching"));
479
+
480
+ Assert.That(UnityCliBridge.UnityCliTestRunCoordinator.RetryCancelAdmissionOwnerMarkerForTests(queueState), Is.False);
481
+ Assert.That(UnityCliBridge.UnityCliTestRunCoordinator.RetryCancelAdmissionOwnerMarkerForTests(queueState), Is.False);
482
+
483
+ Assert.That(pool.Capture().Active, Is.EqualTo(0));
484
+ Assert.That(queueState.OperationExists("test-cancel-admission-owner"), Is.False);
485
+ Assert.That(queueState.OperationExists("test-run-admission-owner"), Is.False);
486
+ Assert.That(queueState.OperationExists("test-run"), Is.False);
487
+ Assert.That(queueState.OperationExists("test-run-terminal-cleanup"), Is.False);
488
+ Assert.That(queueState.TryGetTerminal("test-run", parentRequestId, parentDigest, out var parentTerminal), Is.True);
489
+ Assert.That(UnityCliSerializer.Serialize(parentTerminal), Does.Contain("E_TEST_RUN_CANCELLED"));
490
+ Assert.That(queueState.TryIsLeaseOwned(parentRequestId, parentDigest, out var parentOwned), Is.True);
491
+ Assert.That(parentOwned, Is.False);
492
+ }
493
+
494
+ [Test]
495
+ public void ParentTerminalFirstWriteFailureRetriesFinishBeforeChildOwnerCleanup()
496
+ {
497
+ var parentRequestId = Unique("parent-terminal-fault");
498
+ var parentDigest = UnityCliRequestDigest.Compute(Encoding.UTF8.GetBytes(parentRequestId), InstanceId);
499
+ var cancelRequestId = Unique("parent-terminal-cancel");
500
+ var cancelDigest = UnityCliRequestDigest.Compute(Encoding.UTF8.GetBytes(cancelRequestId), InstanceId);
501
+ var runId = Guid.NewGuid().ToString();
502
+ var deadline = TestDeadline();
503
+ var pool = CreatePool("parent-terminal-fault-session");
504
+ var parentOwner = Consume(pool, parentRequestId, parentDigest, deadline);
505
+ UnityCliBridge.PersistAdmissionOwnerTransfer(queueState, "test-run-admission-owner", new Dictionary<string, object>
506
+ {
507
+ { "requestId", parentRequestId }, { "requestDigest", parentDigest },
508
+ { "editorSessionId", UnityCliBridge.EditorSessionId() }
509
+ }, parentOwner, UnityCliAdmissionPool.AdmissionOwnerState.Coordinator);
510
+ Consume(pool, cancelRequestId, cancelDigest, deadline);
511
+ queueState.SaveOperation("test-run", new Dictionary<string, object>
512
+ {
513
+ { "requestId", parentRequestId }, { "requestDigest", parentDigest },
514
+ { "editorSessionId", UnityCliBridge.EditorSessionId() },
515
+ { "absoluteDeadlineUtc", DateTimeOffset.FromUnixTimeMilliseconds(deadline).ToString("O") },
516
+ { "testOperation", new Dictionary<string, object>
517
+ {
518
+ { "phase", "running" }, { "runId", runId }, { "cancelRunId", runId },
519
+ { "cancelRequestId", cancelRequestId }, { "cancelRequestDigest", cancelDigest },
520
+ { "cancelRootRequestDigest", parentDigest }, { "cancelEffectState", "dispatching" },
521
+ { "cancelRequested", true }
522
+ }
523
+ }
524
+ });
525
+ Assert.That(queueState.TryAcquireLease(parentRequestId, parentDigest, out var acquired), Is.True);
526
+ Assert.That(acquired, Is.True);
527
+ queueState.TestHooks.ExpectOnce("before-operation-save:test-cancel-admission-owner", () =>
528
+ {
529
+ Assert.That(queueState.OperationExists("test-run"), Is.True);
530
+ Assert.That(queueState.TryGetTerminal("test-run", parentRequestId, parentDigest, out var parentBeforeMarker), Is.False);
531
+ });
532
+ queueState.TestHooks.ExpectOnce("before-terminal-store:test-run", () => { throw new IOException("injected parent terminal fault"); });
533
+
534
+ var finalized = UnityCliBridge.UnityCliTestRunCoordinator.FinalizeCancelTerminalForTests(queueState,
535
+ parentRequestId, parentDigest, runId, cancelRequestId, cancelDigest,
536
+ ProtocolEnvelopeFactory.Success(cancelRequestId, new Dictionary<string, object> { { "cancelled", true } }), "cancelled");
537
+
538
+ Assert.That(finalized, Is.False);
539
+ Assert.That(queueState.TryGetTerminal("test-cancel", cancelRequestId, cancelDigest, out var cancelTerminal), Is.True);
540
+ Assert.That(queueState.TryGetTerminal("test-run", parentRequestId, parentDigest, out var missingParentTerminal), Is.False);
541
+ var retainedRoot = queueState.LoadOperation("test-run");
542
+ var retainedOperation = (Dictionary<string, object>)retainedRoot["testOperation"];
543
+ Assert.That(retainedOperation["cancelEffectState"], Is.EqualTo("cancelled"));
544
+ Assert.That(retainedOperation.ContainsKey("cancelTerminalEnvelope"), Is.True);
545
+ Assert.That(queueState.OperationExists("test-cancel-admission-owner"), Is.True);
546
+ Assert.That(pool.Capture().Active, Is.EqualTo(2));
547
+
548
+ Assert.That(UnityCliBridge.UnityCliTestRunCoordinator.RetryCancelAdmissionOwnerMarkerForTests(queueState), Is.False);
549
+
550
+ Assert.That(queueState.TryGetTerminal("test-run", parentRequestId, parentDigest, out var parentTerminal), Is.True);
551
+ Assert.That(UnityCliSerializer.Serialize(parentTerminal), Does.Contain("E_TEST_RUN_CANCELLED"));
552
+ Assert.That(pool.Capture().Active, Is.EqualTo(0));
553
+ Assert.That(queueState.OperationExists("test-cancel-admission-owner"), Is.False);
554
+ Assert.That(queueState.OperationExists("test-run-admission-owner"), Is.False);
555
+ Assert.That(queueState.OperationExists("test-run"), Is.False);
556
+ Assert.That(queueState.TryIsLeaseOwned(parentRequestId, parentDigest, out var owned), Is.True);
557
+ Assert.That(owned, Is.False);
558
+ }
559
+
560
+ [Test]
561
+ public void CancelOutcomeUnknownDualPersistenceFaultFreezesWorkItemUntilParentFirstRepair()
562
+ {
563
+ var parentRequestId = Unique("dual-fault-parent");
564
+ var parentDigest = UnityCliRequestDigest.Compute(Encoding.UTF8.GetBytes(parentRequestId), InstanceId);
565
+ var cancelRequestId = Unique("dual-fault-cancel");
566
+ var cancelRequest = Request(cancelRequestId, "internal.test.run.cancel", new Dictionary<string, object>
567
+ {
568
+ { "expectedEditorSessionId", UnityCliBridge.EditorSessionId() }
569
+ });
570
+ var cancelRaw = RequestBytes(cancelRequest);
571
+ var cancelDigest = UnityCliRequestDigest.Compute(cancelRaw, InstanceId);
572
+ var runId = Guid.NewGuid().ToString();
573
+ var deadline = Deadline(cancelRequest);
574
+ var pool = CreatePool("cancel-dual-fault-session");
575
+ var parentOwner = Consume(pool, parentRequestId, parentDigest, deadline);
576
+ UnityCliBridge.PersistAdmissionOwnerTransfer(queueState, "test-run-admission-owner", new Dictionary<string, object>
577
+ {
578
+ { "requestId", parentRequestId }, { "requestDigest", parentDigest },
579
+ { "editorSessionId", UnityCliBridge.EditorSessionId() }
580
+ }, parentOwner, UnityCliAdmissionPool.AdmissionOwnerState.Coordinator);
581
+ Consume(pool, cancelRequestId, cancelDigest, deadline);
582
+ queueState.SaveOperation("test-run", new Dictionary<string, object>
583
+ {
584
+ { "requestId", parentRequestId }, { "requestDigest", parentDigest },
585
+ { "editorSessionId", UnityCliBridge.EditorSessionId() },
586
+ { "absoluteDeadlineUtc", DateTimeOffset.FromUnixTimeMilliseconds(deadline).ToString("O") },
587
+ { "testOperation", new Dictionary<string, object>
588
+ {
589
+ { "phase", "running" }, { "runId", runId }, { "cancelRunId", runId },
590
+ { "cancelRequestId", cancelRequestId }, { "cancelRequestDigest", cancelDigest },
591
+ { "cancelRootRequestDigest", parentDigest }, { "cancelEffectState", "dispatching" },
592
+ { "cancelRequested", true }
593
+ }
594
+ }
595
+ });
596
+ Assert.That(queueState.TryAcquireLease(parentRequestId, parentDigest, out var acquired), Is.True);
597
+ Assert.That(acquired, Is.True);
598
+ bool created;
599
+ var item = Enqueue(cancelRequest, cancelRaw, pool, out created);
600
+ Assert.That(created, Is.True);
601
+ queueState.TestHooks.ExpectOnce("before-terminal-store:test-cancel", () => { throw new IOException("injected cancel terminal fault"); });
602
+ queueState.TestHooks.ExpectOnce("before-operation-save:test-cancel-admission-owner", () => { throw new IOException("injected cancel recovery marker fault"); });
603
+
604
+ var finalized = UnityCliBridge.UnityCliTestRunCoordinator.FinalizeCancelTerminalForTests(queueState,
605
+ parentRequestId, parentDigest, runId, cancelRequestId, cancelDigest,
606
+ ProtocolEnvelopeFactory.Error(cancelRequestId, "E_EXECUTION_TIMEOUT", "Cancellation outcome is unknown."), "outcome_unknown");
607
+
608
+ Assert.That(finalized, Is.False);
609
+ Assert.That(queueState.OperationExists("test-cancel-admission-owner"), Is.False);
610
+ Assert.That(pool.Capture().Records.Single(record => record.RequestId == cancelRequestId).OwnerState,
611
+ Is.EqualTo(UnityCliAdmissionPool.AdmissionOwnerState.WorkItem));
612
+ var remainingMarkerFaults = 2;
613
+ Action markerFault = null;
614
+ markerFault = () =>
615
+ {
616
+ remainingMarkerFaults--;
617
+ if (remainingMarkerFaults > 0)
618
+ queueState.TestHooks.ExpectOnce("before-operation-save:test-cancel-admission-owner", markerFault);
619
+ throw new IOException("injected outer settlement marker fault");
620
+ };
621
+ queueState.TestHooks.ExpectOnce("before-operation-save:test-cancel-admission-owner", markerFault);
622
+ Assert.That(UnityCliBridge.CompleteWorkItemForTests(item,
623
+ UnityCliSerializer.Serialize(ProtocolEnvelopeFactory.Error(cancelRequestId, "E_EXECUTION_TIMEOUT", "Cancellation outcome is unknown."))), Is.False);
624
+ Assert.That(item.Completed.IsSet, Is.False);
625
+ Assert.That(queueState.PendingSettlementWork, Does.Contain(item));
626
+ Assert.That(pool.Capture().Records.Single(record => record.RequestId == cancelRequestId).OwnerState,
627
+ Is.EqualTo(UnityCliAdmissionPool.AdmissionOwnerState.WorkItem));
628
+
629
+ monotonicNowMs += 100L;
630
+ UnityCliBridge.RetryPendingWorkItemSettlementsForTests(queueState);
631
+ Assert.That(item.Completed.IsSet, Is.True);
632
+ Assert.That(pool.Capture().Records.Single(record => record.RequestId == cancelRequestId).OwnerState,
633
+ Is.EqualTo(UnityCliAdmissionPool.AdmissionOwnerState.Recovery));
634
+ var rebuilt = queueState.LoadOperation("test-cancel-admission-owner");
635
+ Assert.That(rebuilt["cancelContinuationState"], Is.EqualTo("continuation_pending"));
636
+ Assert.That(rebuilt["parentRepairEffectState"], Is.EqualTo("outcome_unknown"));
637
+ Assert.That(rebuilt["parentRepairPending"], Is.EqualTo(true));
638
+ Assert.That(rebuilt.ContainsKey("cancelTerminalEnvelope"), Is.True);
639
+ Assert.That(UnityCliBridge.UnityCliTestRunCoordinator.RetryCancelAdmissionOwnerMarkerForTests(queueState), Is.False);
640
+ Assert.That(queueState.TryGetTerminal("test-cancel", cancelRequestId, cancelDigest, out var rebuiltCancelTerminal), Is.True);
641
+ Assert.That(UnityCliSerializer.Serialize(rebuiltCancelTerminal), Does.Contain("Cancellation outcome is unknown"));
642
+ Assert.That(queueState.TryGetTerminal("test-run", parentRequestId, parentDigest, out var parentTerminal), Is.True);
643
+ Assert.That(UnityCliSerializer.Serialize(parentTerminal), Does.Contain("E_EXECUTION_TIMEOUT"));
644
+ Assert.That(pool.Capture().Active, Is.EqualTo(0));
645
+ Assert.That(queueState.OperationExists("test-cancel-admission-owner"), Is.False);
646
+ Assert.That(queueState.OperationExists("test-run-admission-owner"), Is.False);
647
+ Assert.That(queueState.TryIsLeaseOwned(parentRequestId, parentDigest, out var parentOwned), Is.True);
648
+ Assert.That(parentOwned, Is.False);
649
+ }
650
+
651
+ [Test]
652
+ public void DeletedParentRootFallbackUsesFrozenCandidateAndExactTerminalsAfterFirstMarkerFault()
653
+ {
654
+ var parentRequestId = Unique("deleted-parent");
655
+ var parentDigest = UnityCliRequestDigest.Compute(Encoding.UTF8.GetBytes(parentRequestId), InstanceId);
656
+ var cancelRequestId = Unique("deleted-parent-cancel");
657
+ var cancelRequest = Request(cancelRequestId, "internal.test.run.cancel", new Dictionary<string, object>
658
+ {
659
+ { "expectedEditorSessionId", UnityCliBridge.EditorSessionId() }
660
+ });
661
+ var cancelRaw = RequestBytes(cancelRequest);
662
+ var cancelDigest = UnityCliRequestDigest.Compute(cancelRaw, InstanceId);
663
+ var runId = Guid.NewGuid().ToString();
664
+ var pool = CreatePool("deleted-parent-fallback-session");
665
+ Consume(pool, cancelRequestId, cancelDigest, Deadline(cancelRequest));
666
+ bool created;
667
+ var item = Enqueue(cancelRequest, cancelRaw, pool, out created);
668
+ Assert.That(created, Is.True);
669
+ var childTerminal = ProtocolEnvelopeFactory.Success(cancelRequestId,
670
+ new Dictionary<string, object> { { "cancelled", true } });
671
+ var parentTerminal = ProtocolEnvelopeFactory.Error(parentRequestId, "E_TEST_RUN_CANCELLED",
672
+ "The exact tracked Unity test run was cancelled.");
673
+ item.FreezeCancelRecoveryCandidate(parentRequestId, parentDigest, runId, "cancelled", childTerminal);
674
+ queueState.StoreTerminalFirst("test-cancel", cancelRequestId, cancelDigest, parentDigest, childTerminal);
675
+ queueState.StoreTerminalFirst("test-run", parentRequestId, parentDigest, parentTerminal);
676
+ Assert.That(queueState.OperationExists("test-run"), Is.False);
677
+ Assert.That(queueState.TryIsLeaseOwned(parentRequestId, parentDigest, out var parentOwned), Is.True);
678
+ Assert.That(parentOwned, Is.False);
679
+ queueState.TestHooks.ExpectOnce("before-operation-save:test-cancel-admission-owner", () =>
680
+ {
681
+ throw new IOException("injected first child marker save fault after parent cleanup");
682
+ });
683
+
684
+ Assert.That(UnityCliBridge.CompleteWorkItemForTests(item,
685
+ UnityCliSerializer.Serialize(ProtocolEnvelopeFactory.Error(cancelRequestId, "E_EXECUTION_TIMEOUT",
686
+ "Cancellation completed but queue settlement is pending."))), Is.False);
687
+ Assert.That(item.Completed.IsSet, Is.False);
688
+ Assert.That(queueState.OperationExists("test-cancel-admission-owner"), Is.False);
689
+ Assert.That(pool.Capture().Records.Single().OwnerState,
690
+ Is.EqualTo(UnityCliAdmissionPool.AdmissionOwnerState.WorkItem));
691
+
692
+ monotonicNowMs += 100L;
693
+ UnityCliBridge.RetryPendingWorkItemSettlementsForTests(queueState);
694
+
695
+ Assert.That(item.Completed.IsSet, Is.True);
696
+ var rebuilt = queueState.LoadOperation("test-cancel-admission-owner");
697
+ Assert.That(rebuilt["rootRequestId"], Is.EqualTo(parentRequestId));
698
+ Assert.That(rebuilt["rootRequestDigest"], Is.EqualTo(parentDigest));
699
+ Assert.That(rebuilt["runId"], Is.EqualTo(runId));
700
+ Assert.That(rebuilt["cancelContinuationState"], Is.EqualTo("continuation_pending"));
701
+ Assert.That(rebuilt["parentRepairEffectState"], Is.EqualTo("cancelled"));
702
+ Assert.That(pool.Capture().Records.Single().OwnerState,
703
+ Is.EqualTo(UnityCliAdmissionPool.AdmissionOwnerState.Recovery));
704
+ Assert.That(UnityCliBridge.UnityCliTestRunCoordinator.RetryCancelAdmissionOwnerMarkerForTests(queueState), Is.False);
705
+ Assert.That(pool.Capture().Active, Is.EqualTo(0));
706
+ Assert.That(queueState.OperationExists("test-cancel-admission-owner"), Is.False);
707
+ Assert.That(queueState.OperationExists("test-run"), Is.False);
708
+ }
709
+
710
+ [Test]
711
+ public void CancelMarkerReloadRestoresExactPoolOwnerBeforeDedicatedCleanup()
712
+ {
713
+ var parentRequestId = Unique("reload-parent");
714
+ var parentDigest = UnityCliRequestDigest.Compute(Encoding.UTF8.GetBytes(parentRequestId), InstanceId);
715
+ var cancelRequestId = Unique("reload-cancel");
716
+ var cancelDigest = UnityCliRequestDigest.Compute(Encoding.UTF8.GetBytes(cancelRequestId), InstanceId);
717
+ var runId = Guid.NewGuid().ToString();
718
+ var deadline = TestDeadline();
719
+ var original = CreatePool("cancel-reload-session");
720
+ var owner = Consume(original, cancelRequestId, cancelDigest, deadline);
721
+ queueState.SaveOperation("test-run", new Dictionary<string, object>
722
+ {
723
+ { "requestId", parentRequestId }, { "requestDigest", parentDigest },
724
+ { "editorSessionId", UnityCliBridge.EditorSessionId() },
725
+ { "absoluteDeadlineUtc", DateTimeOffset.FromUnixTimeMilliseconds(deadline).ToString("O") },
726
+ { "testOperation", new Dictionary<string, object>
727
+ {
728
+ { "phase", "running" }, { "runId", runId }, { "cancelRunId", runId },
729
+ { "cancelRequestId", cancelRequestId }, { "cancelRequestDigest", cancelDigest },
730
+ { "cancelRootRequestDigest", parentDigest }, { "cancelEffectState", "dispatched" },
731
+ { "cancelRequested", true }, { "cancelNeedsContinuation", true }
732
+ }
733
+ }
734
+ });
735
+ Assert.That(queueState.TryAcquireLease(parentRequestId, parentDigest, out var parentAcquired), Is.True);
736
+ Assert.That(parentAcquired, Is.True);
737
+ owner.TopRequestDigest = parentDigest;
738
+ UnityCliBridge.PersistAdmissionOwnerTransfer(queueState, "test-cancel-admission-owner", new Dictionary<string, object>
739
+ {
740
+ { "requestId", cancelRequestId }, { "requestDigest", parentDigest },
741
+ { "rootRequestId", parentRequestId }, { "rootRequestDigest", parentDigest },
742
+ { "runId", runId }, { "editorSessionId", UnityCliBridge.EditorSessionId() },
743
+ { "cancelRequestId", cancelRequestId }, { "cancelRequestDigest", cancelDigest },
744
+ { "cancelContinuationState", "continuation_pending" },
745
+ { "cancelTerminalEnvelope", ProtocolEnvelopeFactory.Success(cancelRequestId, new Dictionary<string, object> { { "cancelled", true } }) },
746
+ { "parentRepairEffectState", "cancelled" }, { "parentRepairPending", true }
747
+ }, owner, UnityCliAdmissionPool.AdmissionOwnerState.Continuation);
748
+ queueState.StoreTerminalFirst("test-cancel", cancelRequestId, cancelDigest, parentDigest,
749
+ ProtocolEnvelopeFactory.Success(cancelRequestId, new Dictionary<string, object> { { "cancelled", true } }));
750
+ var validCancelMarker = queueState.LoadOperation("test-cancel-admission-owner");
751
+ var mismatchedCancelMarker = new Dictionary<string, object>(validCancelMarker)
752
+ {
753
+ ["rootRequestDigest"] = new string('b', 64)
754
+ };
755
+ queueState.SaveOperation("test-cancel-admission-owner", mismatchedCancelMarker);
756
+ var restoredSessionId = original.Capture().Records.Single().SessionId;
757
+ original.Reset();
758
+ queueState.AdmissionPool = new UnityCliAdmissionPool(InstanceId, restoredSessionId, 4, 1);
759
+ Assert.That(UnityCliBridge.UnityCliTestRunCoordinator.RetryCancelAdmissionOwnerMarkerForTests(queueState), Is.True);
760
+ Assert.That(queueState.AdmissionPool.Capture().RecoveryBlocked, Is.False);
761
+ queueState.SaveOperation("test-cancel-admission-owner", validCancelMarker);
762
+
763
+ Assert.That(UnityCliBridge.UnityCliTestRunCoordinator.RetryCancelAdmissionOwnerMarkerForTests(queueState), Is.False);
764
+ Assert.That(queueState.AdmissionPool.Capture().Active, Is.EqualTo(0));
765
+ Assert.That(queueState.AdmissionPool.Capture().RecoveryBlocked, Is.False);
766
+ Assert.That(queueState.OperationExists("test-cancel-admission-owner"), Is.False);
767
+ Assert.That(queueState.OperationExists("test-run"), Is.True);
768
+ Assert.That(queueState.TryIsLeaseOwned(parentRequestId, parentDigest, out var parentOwned), Is.True);
769
+ Assert.That(parentOwned, Is.True);
770
+ queueState.ClearOperation("test-run");
771
+ queueState.ReleaseLease(parentRequestId, parentDigest);
772
+ }
773
+
774
+ [Test]
775
+ public void DedicatedCancelMarkerRequiresCompleteCancelParentAndAdmissionIdentity()
776
+ {
777
+ var parentRequestId = Unique("identity-parent");
778
+ var parentDigest = UnityCliRequestDigest.Compute(Encoding.UTF8.GetBytes(parentRequestId), InstanceId);
779
+ var cancelRequestId = Unique("identity-cancel");
780
+ var cancelDigest = UnityCliRequestDigest.Compute(Encoding.UTF8.GetBytes(cancelRequestId), InstanceId);
781
+ var runId = Guid.NewGuid().ToString();
782
+ var deadline = TestDeadline();
783
+ var pool = CreatePool("cancel-identity-session");
784
+ var owner = Consume(pool, cancelRequestId, cancelDigest, deadline);
785
+ queueState.SaveOperation("test-run", new Dictionary<string, object>
786
+ {
787
+ { "requestId", parentRequestId }, { "requestDigest", parentDigest },
788
+ { "editorSessionId", UnityCliBridge.EditorSessionId() },
789
+ { "absoluteDeadlineUtc", DateTimeOffset.FromUnixTimeMilliseconds(deadline).ToString("O") },
790
+ { "testOperation", new Dictionary<string, object>
791
+ {
792
+ { "phase", "running" }, { "runId", runId }, { "cancelRunId", runId },
793
+ { "cancelRequestId", cancelRequestId }, { "cancelRequestDigest", cancelDigest },
794
+ { "cancelRootRequestDigest", parentDigest }, { "cancelEffectState", "dispatched" },
795
+ { "cancelRequested", true }, { "cancelNeedsContinuation", true }
796
+ }
797
+ }
798
+ });
799
+ Assert.That(queueState.TryAcquireLease(parentRequestId, parentDigest, out var acquired), Is.True);
800
+ Assert.That(acquired, Is.True);
801
+ owner.TopRequestDigest = parentDigest;
802
+ UnityCliBridge.PersistAdmissionOwnerTransfer(queueState, "test-cancel-admission-owner", new Dictionary<string, object>
803
+ {
804
+ { "requestId", cancelRequestId }, { "requestDigest", parentDigest },
805
+ { "rootRequestId", parentRequestId }, { "rootRequestDigest", parentDigest },
806
+ { "runId", runId }, { "editorSessionId", UnityCliBridge.EditorSessionId() },
807
+ { "cancelRequestId", cancelRequestId }, { "cancelRequestDigest", cancelDigest },
808
+ { "cancelContinuationState", "dispatched" },
809
+ { "parentRepairEffectState", "dispatched" }, { "parentRepairPending", false }
810
+ }, owner, UnityCliAdmissionPool.AdmissionOwnerState.Continuation);
811
+ var record = pool.Capture().Records.Single();
812
+ var valid = queueState.LoadOperation("test-cancel-admission-owner");
813
+ Assert.That(UnityCliBridge.DedicatedCancelMarkerOwnershipForTests(queueState, record), Is.EqualTo("ExactOwner"));
814
+
815
+ var wrongDigest = new Dictionary<string, object>(valid) { ["admissionRequestDigest"] = new string('b', 64) };
816
+ queueState.SaveOperation("test-cancel-admission-owner", wrongDigest);
817
+ Assert.That(UnityCliBridge.DedicatedCancelMarkerOwnershipForTests(queueState, record), Is.EqualTo("Blocked"));
818
+ var wrongToken = new Dictionary<string, object>(valid) { ["admissionToken"] = new string('c', 64) };
819
+ queueState.SaveOperation("test-cancel-admission-owner", wrongToken);
820
+ Assert.That(UnityCliBridge.DedicatedCancelMarkerOwnershipForTests(queueState, record), Is.EqualTo("Blocked"));
821
+ var wrongRoot = new Dictionary<string, object>(valid) { ["rootRequestDigest"] = new string('d', 64) };
822
+ queueState.SaveOperation("test-cancel-admission-owner", wrongRoot);
823
+ Assert.That(UnityCliBridge.DedicatedCancelMarkerOwnershipForTests(queueState, record), Is.EqualTo("Blocked"));
824
+ foreach (var mismatch in new[]
825
+ {
826
+ new KeyValuePair<string, object>("admissionSessionId", "other-session"),
827
+ new KeyValuePair<string, object>("admissionListenerId", "other-listener"),
828
+ new KeyValuePair<string, object>("admissionListenerGeneration", (long)99),
829
+ new KeyValuePair<string, object>("admissionExecutionDeadlineMs", deadline + 1L),
830
+ new KeyValuePair<string, object>("runId", Guid.NewGuid().ToString())
831
+ })
832
+ {
833
+ var changed = new Dictionary<string, object>(valid) { [mismatch.Key] = mismatch.Value };
834
+ queueState.SaveOperation("test-cancel-admission-owner", changed);
835
+ Assert.That(UnityCliBridge.DedicatedCancelMarkerOwnershipForTests(queueState, record), Is.EqualTo("Blocked"), mismatch.Key);
836
+ Assert.That(pool.Capture().Active, Is.EqualTo(1), mismatch.Key);
837
+ }
838
+
839
+ queueState.ClearOperation("test-cancel-admission-owner");
840
+ queueState.ClearOperation("test-run");
841
+ queueState.ReleaseLease(parentRequestId, parentDigest);
842
+ pool.Reset();
843
+ }
844
+
845
+ private bool stateHasCompletionIntent(UnityCliBridge.AdmissionOwnerIdentity owner)
846
+ {
847
+ return queueState != null && queueState.HasIntent(owner);
848
+ }
849
+
850
+ private void stateHookClearDirectRootOnce()
851
+ {
852
+ queueState.TestHooks.ExpectOnce("before-operation-clear:direct-mutation", () =>
853
+ {
854
+ throw new IOException("injected direct root clear fault");
855
+ });
856
+ }
857
+
858
+ [Test]
859
+ public void BuildArtifactFaultRunsRealExecuteAndExactQueueSettlement()
860
+ {
861
+ var request = BuildRequest(Unique("build-artifact-fault"));
862
+ var raw = RequestBytes(request);
863
+ var digest = UnityCliRequestDigest.Compute(raw, request.InstanceId);
864
+ var deadline = Deadline(request);
865
+ var pool = CreatePool("build-artifact-session");
866
+ Consume(pool, request.RequestId, digest, deadline);
867
+ bool created;
868
+ var item = Enqueue(request, raw, pool, out created);
869
+ Assert.That(created, Is.True);
870
+ var dependencies = BuildDependencies(prepared => new UnityCliBridge.UnityCliLiveBuildCoordinator.BuildEffectResult
871
+ {
872
+ Result = "Succeeded",
873
+ Artifact = new Dictionary<string, object>
874
+ {
875
+ { "exists", false }, { "fileCount", (long)0 }, { "size", (long)0 },
876
+ { "hash", null }, { "treeHash", null }
877
+ }
878
+ });
879
+
880
+ var response = UnityCliBridge.UnityCliLiveBuildCoordinator.Execute(queueState, request, raw, () => true, CancellationToken.None, dependencies).GetAwaiter().GetResult();
881
+
882
+ Assert.That(response, Does.Contain("E_BUILD_ARTIFACT_MISSING"));
883
+ Assert.That(response, Does.Contain("terminal"));
884
+ object terminal;
885
+ Assert.That(queueState.TryGetTerminal("build", request.RequestId, digest, out terminal), Is.True);
886
+ Assert.That(UnityCliSerializer.Serialize(terminal), Does.Contain("E_BUILD_ARTIFACT_MISSING"));
887
+ Assert.That(pool.Capture().Active, Is.EqualTo(1));
888
+ Assert.That(UnityCliBridge.CompleteWorkItemForTests(item, response), Is.True);
889
+ UnityCliBridge.UnityCliLiveBuildCoordinator.Tick(queueState);
890
+ AssertBuildSettled(pool, request.RequestId, digest);
891
+ }
892
+
893
+ [TestCase("root-read")]
894
+ [TestCase("terminal-store")]
895
+ public void BuildPostRootFaultKeepsFrozenRecoveryUntilTickConverges(string fault)
896
+ {
897
+ var request = BuildRequest(Unique("build-post-root-" + fault));
898
+ var raw = RequestBytes(request);
899
+ var digest = UnityCliRequestDigest.Compute(raw, request.InstanceId);
900
+ var pool = CreatePool("build-recovery-session");
901
+ Consume(pool, request.RequestId, digest, Deadline(request));
902
+ bool created;
903
+ var item = Enqueue(request, raw, pool, out created);
904
+ Assert.That(created, Is.True);
905
+ var dependencies = BuildDependencies(prepared => { throw new UnityCliBridge.BridgeCommandException("E_BUILD_EFFECT_TEST_FAULT", "Injected build effect fault."); });
906
+ dependencies.Boundary.ExpectOnce(fault == "root-read" ? "before-build-terminal-root-read" : "before-build-terminal-store",
907
+ () => { throw new IOException(fault == "root-read" ? "injected active root read fault" : "injected terminal store fault"); });
908
+
909
+ var response = UnityCliBridge.UnityCliLiveBuildCoordinator.Execute(queueState, request, raw, () => true, CancellationToken.None, dependencies).GetAwaiter().GetResult();
910
+
911
+ Assert.That(response, Does.Contain("E_EXECUTION_TIMEOUT"));
912
+ dependencies.Boundary.AssertDrained();
913
+ Assert.That(pool.Capture().Records.Any(record => record.RequestId == request.RequestId
914
+ && (record.OwnerState == UnityCliAdmissionPool.AdmissionOwnerState.Recovery
915
+ || record.OwnerState == UnityCliAdmissionPool.AdmissionOwnerState.TerminalCleanup)), Is.True);
916
+ Assert.That(UnityCliBridge.CompleteWorkItemForTests(item, response), Is.True);
917
+ UnityCliBridge.UnityCliLiveBuildCoordinator.Tick(queueState);
918
+
919
+ object terminal;
920
+ Assert.That(queueState.TryGetTerminal("build", request.RequestId, digest, out terminal), Is.True);
921
+ Assert.That(UnityCliSerializer.Serialize(terminal), Does.Contain("E_EXECUTION_TIMEOUT"));
922
+ AssertBuildSettled(pool, request.RequestId, digest);
923
+ }
924
+
925
+ [Test]
926
+ public void MissingBuildRootTerminalFaultRetainsRecoveryOwnerUntilRetry()
927
+ {
928
+ var requestId = Unique("missing-build-root");
929
+ var digest = UnityCliRequestDigest.Compute(Encoding.UTF8.GetBytes(requestId), InstanceId);
930
+ var pool = CreatePool("missing-build-root-session");
931
+ var owner = Consume(pool, requestId, digest, TestDeadline());
932
+ owner.TopRequestDigest = digest;
933
+ UnityCliBridge.PersistAdmissionOwnerTransfer(queueState, "build-admission-owner", new Dictionary<string, object>
934
+ {
935
+ { "requestId", requestId }, { "requestDigest", digest }, { "editorSessionId", UnityCliBridge.EditorSessionId() }
936
+ }, owner, UnityCliAdmissionPool.AdmissionOwnerState.Recovery);
937
+ queueState.TestHooks.ExpectOnce("before-terminal-store:build", () => { throw new IOException("injected build recovery terminal fault"); });
938
+
939
+ UnityCliBridge.UnityCliLiveBuildCoordinator.Tick(queueState);
940
+ Assert.That(queueState.TryGetTerminal("build", requestId, digest, out var missing), Is.False);
941
+ Assert.That(queueState.OperationExists("build-admission-owner"), Is.True);
942
+ Assert.That(pool.Capture().Active, Is.EqualTo(1));
943
+
944
+ UnityCliBridge.UnityCliLiveBuildCoordinator.Tick(queueState);
945
+ Assert.That(queueState.TryGetTerminal("build", requestId, digest, out var terminal), Is.True);
946
+ Assert.That(UnityCliSerializer.Serialize(terminal), Does.Contain("E_EXECUTION_TIMEOUT"));
947
+ Assert.That(pool.Capture().Active, Is.EqualTo(0));
948
+ Assert.That(queueState.OperationExists("build-admission-owner"), Is.False);
949
+ Assert.That(queueState.OperationExists("build-terminal-cleanup"), Is.False);
950
+ }
951
+
952
+ [Test]
953
+ public void MissingReloadRootPublishesExactTimeoutBeforeOwnerCleanup()
954
+ {
955
+ var requestId = Unique("missing-reload-root");
956
+ var digest = UnityCliRequestDigest.Compute(Encoding.UTF8.GetBytes(requestId), InstanceId);
957
+ var pool = CreatePool("missing-reload-root-session");
958
+ var owner = Consume(pool, requestId, digest, TestDeadline());
959
+ owner.TopRequestDigest = digest;
960
+ UnityCliBridge.PersistAdmissionOwnerTransfer(queueState, "refresh-admission-owner", new Dictionary<string, object>
961
+ {
962
+ { "requestId", requestId }, { "requestDigest", digest }, { "editorSessionId", UnityCliBridge.EditorSessionId() }
963
+ }, owner, UnityCliAdmissionPool.AdmissionOwnerState.Recovery);
964
+
965
+ Assert.That(UnityCliBridge.UnityCliReloadOperation.RetryAdmissionOwnerRecoveryMarkerForTests(queueState, "refresh", "Refresh"), Is.True);
966
+
967
+ Assert.That(queueState.TryGetTerminal("refresh", requestId, digest, out var terminal), Is.True);
968
+ Assert.That(UnityCliSerializer.Serialize(terminal), Does.Contain("E_EXECUTION_TIMEOUT"));
969
+ Assert.That(pool.Capture().Active, Is.EqualTo(0));
970
+ Assert.That(queueState.OperationExists("refresh-admission-owner"), Is.False);
971
+ Assert.That(queueState.OperationExists("refresh-terminal-cleanup"), Is.False);
972
+ }
973
+
974
+ [Test]
975
+ public void MissingReloadRootTerminalFaultRetainsMarkerUntilExactRetry()
976
+ {
977
+ var requestId = Unique("missing-reload-terminal-fault");
978
+ var digest = UnityCliRequestDigest.Compute(Encoding.UTF8.GetBytes(requestId), InstanceId);
979
+ var pool = CreatePool("missing-reload-terminal-fault-session");
980
+ var owner = Consume(pool, requestId, digest, TestDeadline());
981
+ owner.TopRequestDigest = digest;
982
+ UnityCliBridge.PersistAdmissionOwnerTransfer(queueState, "refresh-admission-owner", new Dictionary<string, object>
983
+ {
984
+ { "requestId", requestId }, { "requestDigest", digest }, { "editorSessionId", UnityCliBridge.EditorSessionId() }
985
+ }, owner, UnityCliAdmissionPool.AdmissionOwnerState.Recovery);
986
+ queueState.TestHooks.ExpectOnce("before-terminal-store:refresh", () => { throw new IOException("injected recovery terminal fault"); });
987
+
988
+ Assert.That(UnityCliBridge.UnityCliReloadOperation.RetryAdmissionOwnerRecoveryMarkerForTests(queueState, "refresh", "Refresh"), Is.True);
989
+ Assert.That(queueState.TryGetTerminal("refresh", requestId, digest, out var missing), Is.False);
990
+ Assert.That(queueState.OperationExists("refresh-admission-owner"), Is.True);
991
+ Assert.That(pool.Capture().Active, Is.EqualTo(1));
992
+
993
+ Assert.That(UnityCliBridge.UnityCliReloadOperation.RetryAdmissionOwnerRecoveryMarkerForTests(queueState, "refresh", "Refresh"), Is.True);
994
+ Assert.That(queueState.TryGetTerminal("refresh", requestId, digest, out var terminal), Is.True);
995
+ Assert.That(UnityCliSerializer.Serialize(terminal), Does.Contain("E_EXECUTION_TIMEOUT"));
996
+ Assert.That(pool.Capture().Active, Is.EqualTo(0));
997
+ Assert.That(queueState.OperationExists("refresh-admission-owner"), Is.False);
998
+ }
999
+
1000
+ [Test]
1001
+ public void NewPoolAndNewStoreRestoreExactCommittedTransferFromMarker()
1002
+ {
1003
+ var requestId = Unique("new-pool-new-store");
1004
+ var digest = UnityCliRequestDigest.Compute(Encoding.UTF8.GetBytes(requestId), InstanceId);
1005
+ var original = CreatePool("new-pool-new-store-session");
1006
+ var owner = Consume(original, requestId, digest, TestDeadline());
1007
+ UnityCliBridge.PersistAdmissionOwnerTransfer(queueState, "build-admission-owner", new Dictionary<string, object>
1008
+ {
1009
+ { "requestId", requestId }, { "requestDigest", digest }, { "editorSessionId", UnityCliBridge.EditorSessionId() }
1010
+ }, owner, UnityCliAdmissionPool.AdmissionOwnerState.Recovery);
1011
+ var marker = queueState.LoadOperation("build-admission-owner");
1012
+ var sessionId = original.Capture().Records.Single().SessionId;
1013
+ queueState.ClearOperation("build-admission-owner");
1014
+ original.Reset();
1015
+ var restored = new UnityCliAdmissionPool(InstanceId, sessionId, 4, 1);
1016
+ queueState = UnityCliBridge.CreateQueueStateForTests(restored, () => monotonicNowMs);
1017
+ queueState.SaveOperation("build-admission-owner", marker);
1018
+ queueState.SaveOperation("build", Root(requestId, digest, "terminal_pending"));
1019
+ Assert.That(queueState.TryAcquireLease(requestId, digest, out var acquired), Is.True);
1020
+ Assert.That(acquired, Is.True);
1021
+
1022
+ var reconciled = UnityCliBridge.ReconcileAdmissionTransferForLiveMarker(queueState, "build-admission-owner", marker, requestId, digest);
1023
+
1024
+ Assert.That(reconciled.OwnerState, Is.EqualTo(UnityCliAdmissionPool.AdmissionOwnerState.Recovery));
1025
+ Assert.That(restored.Capture().Active, Is.EqualTo(1));
1026
+ queueState.StoreTerminalFirst("build", requestId, digest,
1027
+ ProtocolEnvelopeFactory.Error(requestId, "E_EXECUTION_TIMEOUT", "New store recovery terminal."));
1028
+ UnityCliActiveOperation.BeginExactTerminalCleanup("build", requestId, digest, queueState);
1029
+ Assert.That(restored.Capture().Active, Is.EqualTo(0));
1030
+ Assert.That(queueState.OperationExists("build-admission-owner"), Is.False);
1031
+ Assert.That(queueState.OperationExists("build"), Is.False);
1032
+ Assert.That(queueState.TryIsLeaseOwned(requestId, digest, out var owned), Is.True);
1033
+ Assert.That(owned, Is.False);
1034
+ }
1035
+
1036
+ [TestCase(false, 0)]
1037
+ [TestCase(false, 1)]
1038
+ [TestCase(false, 2)]
1039
+ [TestCase(true, 1)]
1040
+ [TestCase(true, 2)]
1041
+ public void TransferMarkerAndReloadedPoolWindowsConvergeExactly(bool markerCommitted, int poolWindow)
1042
+ {
1043
+ var requestId = Unique("transfer-reload-window");
1044
+ var digest = UnityCliRequestDigest.Compute(Encoding.UTF8.GetBytes(requestId), InstanceId);
1045
+ var original = CreatePool("transfer-reload-window-session");
1046
+ var owner = Consume(original, requestId, digest, TestDeadline());
1047
+ UnityCliBridge.PersistAdmissionOwnerTransfer(queueState, "build-admission-owner", new Dictionary<string, object>
1048
+ {
1049
+ { "requestId", requestId }, { "requestDigest", digest }, { "editorSessionId", UnityCliBridge.EditorSessionId() }
1050
+ }, owner, UnityCliAdmissionPool.AdmissionOwnerState.Recovery);
1051
+ var marker = queueState.LoadOperation("build-admission-owner");
1052
+ marker["admissionTransferCommitted"] = markerCommitted;
1053
+ queueState.SaveOperation("build-admission-owner", marker);
1054
+ var source = UnityCliAdmissionPool.CreateConsumedIdentity(
1055
+ requestId,
1056
+ marker["admissionRootFingerprint"] as string,
1057
+ marker["admissionRequestDigest"] as string,
1058
+ marker["admissionToken"] as string,
1059
+ marker["admissionSessionId"] as string,
1060
+ UnityCliAdmissionPool.AdmissionOwnerState.WorkItem,
1061
+ marker["admissionListenerId"] as string,
1062
+ (long)marker["admissionListenerGeneration"],
1063
+ (long)marker["admissionExecutionDeadlineMs"]);
1064
+ var restored = new UnityCliAdmissionPool(InstanceId, source.SessionId, 4, 1);
1065
+ if (poolWindow == 0)
1066
+ Assert.That(restored.RestoreConsumed(source, UnityCliAdmissionPool.AdmissionOwnerState.WorkItem, true), Is.True);
1067
+ else if (poolWindow == 1)
1068
+ Assert.That(restored.RestoreConsumed(source, UnityCliAdmissionPool.AdmissionOwnerState.Recovery, false), Is.True);
1069
+ else
1070
+ {
1071
+ var intended = UnityCliAdmissionPool.CreateConsumedIdentity(
1072
+ source.RequestId, source.RootFingerprint, source.RequestDigest, source.Token, source.SessionId,
1073
+ UnityCliAdmissionPool.AdmissionOwnerState.Recovery, source.ListenerId, source.ListenerGeneration, source.ExecutionDeadlineMs);
1074
+ Assert.That(restored.RestoreConsumed(intended, UnityCliAdmissionPool.AdmissionOwnerState.Recovery, true), Is.True);
1075
+ }
1076
+ original.Reset();
1077
+ queueState.AdmissionPool = restored;
1078
+
1079
+ var reconciled = UnityCliBridge.ReconcileAdmissionTransferForLiveMarker(queueState, "build-admission-owner", marker, requestId, digest);
1080
+
1081
+ Assert.That(reconciled.OwnerState, Is.EqualTo(UnityCliAdmissionPool.AdmissionOwnerState.Recovery));
1082
+ var record = restored.Capture().Records.Single();
1083
+ Assert.That(record.OwnerState, Is.EqualTo(UnityCliAdmissionPool.AdmissionOwnerState.Recovery));
1084
+ Assert.That(record.IntendedOwnerState, Is.EqualTo(UnityCliAdmissionPool.AdmissionOwnerState.Recovery));
1085
+ Assert.That(record.TransferCommitted, Is.True);
1086
+ Assert.That(queueState.LoadOperation("build-admission-owner")["admissionTransferCommitted"], Is.EqualTo(true));
1087
+ queueState.StoreTerminalFirst("build", requestId, digest,
1088
+ ProtocolEnvelopeFactory.Error(requestId, "E_EXECUTION_TIMEOUT", "Transfer reload test terminal."));
1089
+ UnityCliActiveOperation.BeginExactTerminalCleanup("build", requestId, digest, queueState);
1090
+ Assert.That(restored.Capture().Active, Is.EqualTo(0));
1091
+ Assert.That(queueState.OperationExists("build-admission-owner"), Is.False);
1092
+ }
1093
+
1094
+ [TestCase(true, true)]
1095
+ [TestCase(false, true)]
1096
+ [TestCase(false, false)]
1097
+ public void CleanupMarkerReconcilesRootLeaseWindowsWithoutGlobalState(bool rootExists, bool leaseOwned)
1098
+ {
1099
+ var requestId = Unique("marker-window");
1100
+ var digest = UnityCliRequestDigest.Compute(Encoding.UTF8.GetBytes(requestId), InstanceId);
1101
+ var original = CreatePool("marker-window-session");
1102
+ var owner = Consume(original, requestId, digest, TestDeadline());
1103
+ if (leaseOwned)
1104
+ {
1105
+ bool acquired;
1106
+ Assert.That(queueState.TryAcquireLease(requestId, digest, out acquired), Is.True);
1107
+ Assert.That(acquired, Is.True);
1108
+ }
1109
+ if (rootExists) queueState.SaveOperation("build", Root(requestId, digest, "terminal_pending"));
1110
+ var marker = new Dictionary<string, object>
1111
+ {
1112
+ { "requestId", requestId }, { "requestDigest", digest },
1113
+ { "editorSessionId", UnityCliBridge.EditorSessionId() }, { "completeAdmission", true }
1114
+ };
1115
+ UnityCliBridge.PersistAdmissionOwnerTransfer(queueState, "build-terminal-cleanup", marker, owner, UnityCliAdmissionPool.AdmissionOwnerState.TerminalCleanup);
1116
+ queueState.StoreTerminalFirst("build", requestId, digest,
1117
+ ProtocolEnvelopeFactory.Error(requestId, "E_EXECUTION_TIMEOUT", "Cleanup window test terminal."));
1118
+ var sessionId = original.Capture().Records.Single().SessionId;
1119
+ original.Reset();
1120
+ queueState.AdmissionPool = new UnityCliAdmissionPool(InstanceId, sessionId, 4, 1);
1121
+
1122
+ Assert.That(UnityCliActiveOperation.RetryExactTerminalCleanup("build", queueState), Is.False);
1123
+ Assert.That(queueState.AdmissionPool.Capture().Active, Is.EqualTo(0));
1124
+ Assert.That(queueState.OperationExists("build-terminal-cleanup"), Is.False);
1125
+ Assert.That(queueState.OperationExists("build"), Is.False);
1126
+ Assert.That(queueState.TryIsLeaseOwned(requestId, digest, out var owned), Is.True);
1127
+ Assert.That(owned, Is.False);
1128
+ }
1129
+
1130
+ [Test]
1131
+ public void ReconciledMarkerRefreshesRecoveredListenerBinding()
1132
+ {
1133
+ var requestId = Unique("marker-listener-rebind");
1134
+ var digest = UnityCliRequestDigest.Compute(Encoding.UTF8.GetBytes(requestId), InstanceId);
1135
+ var deadline = TestDeadline();
1136
+ var pool = CreatePool("marker-listener-session");
1137
+ var owner = Consume(pool, requestId, digest, deadline);
1138
+ var marker = new Dictionary<string, object>
1139
+ {
1140
+ { "requestId", requestId }, { "requestDigest", digest }, { "editorSessionId", UnityCliBridge.EditorSessionId() }
1141
+ };
1142
+ UnityCliBridge.PersistAdmissionOwnerTransfer(queueState, "build-admission-owner", marker, owner, UnityCliAdmissionPool.AdmissionOwnerState.Coordinator);
1143
+ Assert.That(pool.RecoverConsumed(requestId, Fingerprint, digest, "listener-after-reload", 2, deadline), Is.True);
1144
+ var loaded = queueState.LoadOperation("build-admission-owner");
1145
+
1146
+ var reconciled = UnityCliBridge.ReconcileAdmissionTransferForLiveMarker(queueState, "build-admission-owner", loaded, requestId, digest);
1147
+
1148
+ Assert.That(reconciled.ListenerId, Is.EqualTo("listener-after-reload"));
1149
+ Assert.That(reconciled.ListenerGeneration, Is.EqualTo(2));
1150
+ var refreshed = queueState.LoadOperation("build-admission-owner");
1151
+ Assert.That(refreshed["admissionListenerId"], Is.EqualTo("listener-after-reload"));
1152
+ Assert.That(refreshed["admissionListenerGeneration"], Is.EqualTo((long)2));
1153
+ queueState.ClearOperation("build-admission-owner");
1154
+ var cleanupOwner = UnityCliBridge.CaptureAdmissionOwner(queueState, requestId, digest);
1155
+ cleanupOwner = UnityCliBridge.PersistAdmissionOwnerTransfer(queueState, "build-terminal-cleanup", new Dictionary<string, object>
1156
+ {
1157
+ { "requestId", requestId }, { "requestDigest", digest },
1158
+ { "editorSessionId", UnityCliBridge.EditorSessionId() }, { "completeAdmission", true }
1159
+ }, cleanupOwner, UnityCliAdmissionPool.AdmissionOwnerState.TerminalCleanup);
1160
+ Assert.That(UnityCliBridge.CompleteAdmissionRoot(queueState, cleanupOwner), Is.True);
1161
+ queueState.ClearOperation("build-terminal-cleanup");
1162
+ }
1163
+
1164
+ [Test]
1165
+ public void CleanupMarkerIdentityMismatchBlocksWithoutClearingMarker()
1166
+ {
1167
+ var requestId = Unique("marker-identity-mismatch");
1168
+ var digest = UnityCliRequestDigest.Compute(Encoding.UTF8.GetBytes(requestId), InstanceId);
1169
+ var deadline = TestDeadline();
1170
+ var original = CreatePool("marker-mismatch-session");
1171
+ var owner = Consume(original, requestId, digest, deadline);
1172
+ var marker = new Dictionary<string, object>
1173
+ {
1174
+ { "requestId", requestId }, { "requestDigest", digest },
1175
+ { "editorSessionId", UnityCliBridge.EditorSessionId() }, { "completeAdmission", true }
1176
+ };
1177
+ UnityCliBridge.PersistAdmissionOwnerTransfer(queueState, "build-terminal-cleanup", marker, owner, UnityCliAdmissionPool.AdmissionOwnerState.TerminalCleanup);
1178
+ var restored = new UnityCliAdmissionPool(InstanceId, original.Capture().Records.Single().SessionId, 4, 1);
1179
+ queueState.AdmissionPool = restored;
1180
+ var otherFingerprint = new string('d', 64);
1181
+ var grant = restored.Acquire(requestId, otherFingerprint, queueState.ListenerId, queueState.ListenerGeneration);
1182
+ restored.AuthorizeAndCapture(requestId, otherFingerprint, digest, grant.Token, queueState.ListenerId, queueState.ListenerGeneration, deadline);
1183
+ var loaded = queueState.LoadOperation("build-terminal-cleanup");
1184
+
1185
+ Assert.Throws<UnityCliBridge.BridgeCommandException>(() => UnityCliBridge.ReconcileAdmissionTransferForLiveMarker(queueState, "build-terminal-cleanup", loaded, requestId, digest));
1186
+ Assert.That(restored.Capture().RecoveryBlocked, Is.True);
1187
+ Assert.That(queueState.OperationExists("build-terminal-cleanup"), Is.True);
1188
+ queueState.ClearOperation("build-terminal-cleanup");
1189
+ restored.Reset();
1190
+ }
1191
+
1192
+ private UnityCliAdmissionPool CreatePool(string suffix)
1193
+ {
1194
+ var pool = new UnityCliAdmissionPool(InstanceId, Unique(suffix), 4, 1);
1195
+ queueState = UnityCliBridge.CreateQueueStateForTests(pool, () => monotonicNowMs);
1196
+ return pool;
1197
+ }
1198
+
1199
+ private UnityCliBridge.AdmissionOwnerIdentity Consume(UnityCliAdmissionPool pool, string requestId, string digest, long deadline)
1200
+ {
1201
+ var grant = pool.Acquire(requestId, Fingerprint, queueState.ListenerId, queueState.ListenerGeneration);
1202
+ Assert.That(grant.Outcome, Is.EqualTo(UnityCliAdmissionPool.AcquireOutcome.Granted));
1203
+ var result = pool.AuthorizeAndCapture(requestId, Fingerprint, digest, grant.Token, queueState.ListenerId, queueState.ListenerGeneration, deadline);
1204
+ Assert.That(result.Outcome, Is.EqualTo(UnityCliAdmissionPool.AuthorizeOutcome.Authorized));
1205
+ return UnityCliBridge.CaptureAdmissionOwner(queueState, requestId, digest);
1206
+ }
1207
+
1208
+ private UnityCliBridge.BridgeWorkItem Enqueue(BridgeRequest request, byte[] raw, UnityCliAdmissionPool pool, out bool created)
1209
+ {
1210
+ var record = pool.Capture().Records.First(value => value.RequestId == request.RequestId);
1211
+ var reservation = new UnityCliBridge.AdmissionQueueReservation
1212
+ {
1213
+ RequestId = request.RequestId,
1214
+ RequestDigest = UnityCliRequestDigest.Compute(raw, request.InstanceId),
1215
+ RootFingerprint = Fingerprint,
1216
+ Token = record.Token,
1217
+ ExecutionDeadlineMs = Deadline(request),
1218
+ HasAdmissionHeaders = true,
1219
+ IsBypass = false
1220
+ };
1221
+ int depth;
1222
+ return UnityCliBridge.EnqueuePreparedForTests(request, raw, reservation, queueState, out created, out depth);
1223
+ }
1224
+
1225
+ private BridgeRequest BuildRequest(string requestId)
1226
+ {
1227
+ return Request(requestId, "internal.build.run.start", new Dictionary<string, object>
1228
+ {
1229
+ { "expectedEditorSessionId", UnityCliBridge.EditorSessionId() },
1230
+ { "targets", new List<object>
1231
+ {
1232
+ new Dictionary<string, object>
1233
+ {
1234
+ { "target", "StandaloneWindows64" }, { "outputPath", "Builds/test-player.exe" }
1235
+ }
1236
+ }
1237
+ }
1238
+ });
1239
+ }
1240
+
1241
+ private static UnityCliBridge.UnityCliLiveBuildCoordinator.BuildExecutionDependencies BuildDependencies(Func<UnityCliBridge.UnityCliLiveBuildCoordinator.Prepared, UnityCliBridge.UnityCliLiveBuildCoordinator.BuildEffectResult> effect)
1242
+ {
1243
+ return new UnityCliBridge.UnityCliLiveBuildCoordinator.BuildExecutionDependencies(true)
1244
+ {
1245
+ PrepareTarget = (target, outputPath, options) => new UnityCliBridge.UnityCliLiveBuildCoordinator.Prepared
1246
+ {
1247
+ Target = BuildTarget.StandaloneWindows64,
1248
+ TargetName = target,
1249
+ OutputPath = Application.dataPath,
1250
+ PublicOutputPath = outputPath,
1251
+ Scenes = new string[0],
1252
+ Options = BuildOptions.None
1253
+ },
1254
+ RunEffect = effect
1255
+ };
1256
+ }
1257
+
1258
+ private void AssertBuildSettled(UnityCliAdmissionPool pool, string requestId, string digest)
1259
+ {
1260
+ Assert.That(pool.Capture().Active, Is.EqualTo(0));
1261
+ Assert.That(queueState.OperationExists("build"), Is.False);
1262
+ Assert.That(queueState.OperationExists("build-terminal-cleanup"), Is.False);
1263
+ Assert.That(queueState.OperationExists("build-admission-owner"), Is.False);
1264
+ Assert.That(queueState.TryIsLeaseOwned(requestId, digest, out var owned), Is.True);
1265
+ Assert.That(owned, Is.False);
1266
+ Assert.That(UnityCliBridge.ActiveRequestWorkCountForTests(queueState), Is.EqualTo(0));
1267
+ }
1268
+
1269
+ private static BridgeRequest Request(string requestId, string command, Dictionary<string, object> input)
1270
+ {
1271
+ return new BridgeRequest
1272
+ {
1273
+ ProtocolVersion = 2,
1274
+ InstanceId = InstanceId,
1275
+ Command = command,
1276
+ RequestId = requestId,
1277
+ Input = input,
1278
+ Execution = new BridgeExecution
1279
+ {
1280
+ AbsoluteDeadlineUtc = DateTimeOffset.UtcNow.AddMinutes(1).ToString("O"),
1281
+ CompletionContractVersion = 1,
1282
+ RequestDigestVersion = 1
1283
+ }
1284
+ };
1285
+ }
1286
+
1287
+ private static byte[] RequestBytes(BridgeRequest request)
1288
+ {
1289
+ return Encoding.UTF8.GetBytes(UnityCliSerializer.Serialize(request));
1290
+ }
1291
+
1292
+ private static long Deadline(BridgeRequest request)
1293
+ {
1294
+ return DateTimeOffset.Parse(request.Execution.AbsoluteDeadlineUtc).ToUnixTimeMilliseconds();
1295
+ }
1296
+
1297
+ private static Dictionary<string, object> Root(string requestId, string digest, string phase)
1298
+ {
1299
+ return new Dictionary<string, object>
1300
+ {
1301
+ { "requestId", requestId }, { "requestDigest", digest }, { "toolName", "editor.play" },
1302
+ { "editorSessionId", UnityCliBridge.EditorSessionId() }, { "phase", phase }
1303
+ };
1304
+ }
1305
+
1306
+ private static Dictionary<string, object> PlayModeRoot(
1307
+ string requestId, string digest, string toolName, bool before, DateTimeOffset deadline)
1308
+ {
1309
+ return new Dictionary<string, object>
1310
+ {
1311
+ { "requestId", requestId }, { "requestDigest", digest }, { "toolName", toolName },
1312
+ { "editorSessionId", UnityCliBridge.EditorSessionId() },
1313
+ { "absoluteDeadlineUtc", deadline.ToString("O") }, { "playModeWasPlaying", before },
1314
+ { "phase", "effect_started" }
1315
+ };
1316
+ }
1317
+
1318
+ private static long TestDeadline()
1319
+ {
1320
+ return DateTimeOffset.UtcNow.AddMinutes(1).ToUnixTimeMilliseconds();
1321
+ }
1322
+
1323
+ private string Unique(string suffix)
1324
+ {
1325
+ return identityPrefix + suffix;
1326
+ }
1327
+ }
1328
+ }
1329
+ #endif