@pixel-point/toolcraft 0.0.16 → 0.0.18

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 (313) hide show
  1. package/package.json +1 -1
  2. package/src/generate-performance-iteration-execution.test.mjs +2 -2
  3. package/src/generate-test-demand-only-lifecycle.mjs +14 -3
  4. package/src/generate-test-dependency-sandbox.mjs +42 -5
  5. package/src/generate-test-doc-assertions.mjs +109 -69
  6. package/src/generate-test-doc-assertions.test.mjs +65 -0
  7. package/src/generate-test-doc-policy-assertions.mjs +92 -0
  8. package/src/generate-test-file-assertions.mjs +36 -118
  9. package/src/generate-test-output-assertions.mjs +95 -0
  10. package/src/generate-test-package-assertions.mjs +6 -0
  11. package/src/generate-test-performance-boundary-assertions.mjs +45 -0
  12. package/src/generate-test-product-fixture.mjs +1 -0
  13. package/src/generate-test-product-fixture.test.mjs +18 -0
  14. package/src/generate-test-product-iteration.mjs +1 -0
  15. package/src/generate-test-product-iteration.test.mjs +18 -0
  16. package/src/generate-test-runtime-boundary-assertions.mjs +158 -0
  17. package/src/generate-test-standalone-assertions.mjs +10 -0
  18. package/src/generate-test-standalone-delivery-stages.mjs +14 -6
  19. package/src/generate.test.mjs +14 -0
  20. package/templates/runtime/contracts/component-contracts.choices.test.ts +59 -12
  21. package/templates/runtime/contracts/component-contracts.choices.ts +58 -12
  22. package/templates/runtime/contracts/component-contracts.inputs.test.ts +6 -6
  23. package/templates/runtime/contracts/component-contracts.inputs.ts +6 -6
  24. package/templates/runtime/contracts/component-contracts.media-custom.test.ts +8 -2
  25. package/templates/runtime/contracts/component-contracts.media-custom.ts +8 -2
  26. package/templates/runtime/contracts/component-contracts.runtime-acceptance.test.ts +21 -6
  27. package/templates/runtime/contracts/component-contracts.runtime-labels.test.ts +7 -1
  28. package/templates/runtime/contracts/component-contracts.runtime-panels.test.ts +4 -1
  29. package/templates/runtime/contracts/component-contracts.runtime-shell.test.ts +4 -1
  30. package/templates/runtime/contracts/component-contracts.runtime.ts +15 -10
  31. package/templates/runtime/contracts/component-contracts.test.ts +2 -0
  32. package/templates/runtime/contracts/decision-contracts.test.ts +43 -4
  33. package/templates/runtime/contracts/decision-contracts.ts +10 -10
  34. package/templates/runtime/contracts/performance-verification-policy.test.ts +59 -6
  35. package/templates/runtime/contracts/performance-verification-policy.ts +5 -2
  36. package/templates/runtime/export/artifact-download.test.ts +71 -0
  37. package/templates/runtime/export/artifact-download.ts +80 -0
  38. package/templates/runtime/export/artifact-export-request.ts +23 -0
  39. package/templates/runtime/export/artifact-export-settings.test.ts +75 -0
  40. package/templates/runtime/export/artifact-export-settings.ts +96 -0
  41. package/templates/runtime/export/artifact-frame-renderer.test.ts +107 -0
  42. package/templates/runtime/export/artifact-frame-renderer.ts +96 -0
  43. package/templates/runtime/export/artifact-frame-state.test.ts +54 -0
  44. package/templates/runtime/export/artifact-frame-state.ts +44 -0
  45. package/templates/runtime/export/artifact-scene-frame.test.ts +84 -0
  46. package/templates/runtime/export/artifact-scene-frame.ts +118 -0
  47. package/templates/runtime/export/export-background.ts +46 -0
  48. package/templates/runtime/export/export-error.ts +43 -0
  49. package/templates/runtime/export/export.test.ts +49 -226
  50. package/templates/runtime/export/image-artifact-export.test.ts +111 -0
  51. package/templates/runtime/export/image-artifact-export.ts +145 -0
  52. package/templates/runtime/export/index.ts +44 -3
  53. package/templates/runtime/export/product-export-renderer.test.ts +77 -0
  54. package/templates/runtime/export/product-export-renderer.ts +63 -0
  55. package/templates/runtime/export/video-artifact-export.test.ts +166 -0
  56. package/templates/runtime/export/video-artifact-export.ts +164 -0
  57. package/templates/runtime/export/video-encoding-backend.ts +124 -0
  58. package/templates/runtime/export/video-encoding-policy.test.ts +68 -0
  59. package/templates/runtime/export/video-encoding-policy.ts +86 -0
  60. package/templates/runtime/export/video-frame-schedule.test.ts +58 -0
  61. package/templates/runtime/export/video-frame-schedule.ts +42 -0
  62. package/templates/runtime/index.ts +3 -0
  63. package/templates/runtime/model-import/model-import-limits.ts +15 -4
  64. package/templates/runtime/react/app-shell/runtime-public-api-runtime.test.tsx +291 -0
  65. package/templates/runtime/react/app-shell/runtime-public-api-test-support.ts +21 -0
  66. package/templates/runtime/react/app-shell/runtime-public-api.test.tsx +15 -304
  67. package/templates/runtime/react/app-shell/toolcraft-app.scene-export.test.tsx +41 -27
  68. package/templates/runtime/react/app-shell/toolcraft-app.tsx +39 -11
  69. package/templates/runtime/react/app-shell/toolcraft-source-asset-context.test.tsx +16 -57
  70. package/templates/runtime/react/app-shell/toolcraft-source-asset-context.tsx +75 -434
  71. package/templates/runtime/react/app-shell/toolcraft-source-asset-owner.test.ts +194 -0
  72. package/templates/runtime/react/app-shell/toolcraft-source-asset-owner.ts +227 -0
  73. package/templates/runtime/react/app-shell/toolcraft-source-asset-provider-ownership.test.tsx +243 -0
  74. package/templates/runtime/react/app-shell/toolcraft-source-asset-test-support.ts +51 -0
  75. package/templates/runtime/react/app-shell/toolcraft-unavailable-resource-proof.test.ts +138 -0
  76. package/templates/runtime/react/app-shell/toolcraft-unavailable-resource-proof.ts +378 -0
  77. package/templates/runtime/react/canvas/canvas-shell-rendering.test.tsx +76 -0
  78. package/templates/runtime/react/canvas/canvas-shell-test-utils.tsx +13 -1
  79. package/templates/runtime/react/canvas/canvas-shell.tsx +16 -1
  80. package/templates/runtime/react/canvas/product-scene-surface.test.tsx +195 -0
  81. package/templates/runtime/react/canvas/product-scene-surface.tsx +155 -0
  82. package/templates/runtime/react/canvas/runtime-scene-export.ts +1 -1
  83. package/templates/runtime/react/controls-panel/__tests__/control-conditions.test.ts +61 -0
  84. package/templates/runtime/react/controls-panel/__tests__/controls-panel.collection-actions.test.tsx +89 -0
  85. package/templates/runtime/react/controls-panel/__tests__/controls-panel.control-visibility.test.tsx +40 -19
  86. package/templates/runtime/react/controls-panel/__tests__/controls-panel.footer-actions.test.tsx +7 -12
  87. package/templates/runtime/react/controls-panel/__tests__/controls-panel.scene-export-actions.test.tsx +18 -84
  88. package/templates/runtime/react/controls-panel/__tests__/controls-panel.source-collection.test.tsx +62 -0
  89. package/templates/runtime/react/controls-panel/__tests__/controls-panel.source-color-bank.test.tsx +67 -0
  90. package/templates/runtime/react/controls-panel/actions/controls-panel-actions.ts +43 -87
  91. package/templates/runtime/react/controls-panel/actions/export-action-runner.ts +68 -0
  92. package/templates/runtime/react/controls-panel/conditions/control-conditions.ts +32 -119
  93. package/templates/runtime/react/controls-panel/layout/controls-panel-control-group.tsx +5 -2
  94. package/templates/runtime/react/controls-panel/layout/controls-panel-layout.tsx +10 -8
  95. package/templates/runtime/react/controls-panel/layout/controls-panel-section.tsx +3 -3
  96. package/templates/runtime/react/controls-panel/renderers/controls-panel-collection-item-fields.test.tsx +63 -0
  97. package/templates/runtime/react/controls-panel/renderers/controls-panel-collection-item-fields.tsx +138 -0
  98. package/templates/runtime/react/controls-panel/renderers/controls-panel-collection-items.tsx +104 -0
  99. package/templates/runtime/react/controls-panel/renderers/controls-panel-collection-renderer.tsx +115 -236
  100. package/templates/runtime/react/controls-panel/renderers/controls-panel-file-drop-item-controls.test.tsx +95 -0
  101. package/templates/runtime/react/controls-panel/renderers/controls-panel-file-drop-item-controls.tsx +82 -0
  102. package/templates/runtime/react/controls-panel/renderers/controls-panel-media-renderer.test.tsx +73 -1
  103. package/templates/runtime/react/controls-panel/renderers/controls-panel-media-renderer.tsx +50 -1
  104. package/templates/runtime/react/controls-panel/renderers/controls-panel-renderer-registry.test.ts +1 -1
  105. package/templates/runtime/react/controls-panel/renderers/controls-panel-renderer-registry.ts +2 -1
  106. package/templates/runtime/react/controls-panel/values/controls-panel-collection-values.ts +38 -6
  107. package/templates/runtime/react/controls-panel/values/controls-panel-value-labels.ts +1 -0
  108. package/templates/runtime/react/index.ts +4 -0
  109. package/templates/runtime/react/model-rendering/model-export.ts +1 -1
  110. package/templates/runtime/react/orientation-gizmo/orientation-gizmo-selection.ts +5 -5
  111. package/templates/runtime/react/orientation-gizmo/use-toolcraft-orientation-control-selection.ts +2 -2
  112. package/templates/runtime/react/panel-host/panel-host-runtime-placement.test.tsx +30 -0
  113. package/templates/runtime/react/panel-host/panel-host.tsx +4 -0
  114. package/templates/runtime/scene/scene-bounds.test.ts +23 -0
  115. package/templates/runtime/scene/scene-bounds.ts +32 -19
  116. package/templates/runtime/schema/app-capabilities.test.ts +4 -0
  117. package/templates/runtime/schema/canvas-render-scale.ts +62 -0
  118. package/templates/runtime/schema/collection-item-controls.ts +29 -0
  119. package/templates/runtime/schema/control-applicability.test.ts +127 -0
  120. package/templates/runtime/schema/control-applicability.ts +433 -0
  121. package/templates/runtime/schema/control-schema-normalization.ts +177 -11
  122. package/templates/runtime/schema/controls-panel-actions.ts +2 -2
  123. package/templates/runtime/schema/controls-panel-section-id.test.ts +1 -1
  124. package/templates/runtime/schema/controls-panel-section-layout.ts +14 -9
  125. package/templates/runtime/schema/controls-panel-section-roundtrip.test.ts +62 -3
  126. package/templates/runtime/schema/define-toolcraft.collection-actions-item-controls.test.ts +127 -0
  127. package/templates/runtime/schema/define-toolcraft.control-sections.test.ts +10 -4
  128. package/templates/runtime/schema/define-toolcraft.file-drop-item-controls.test.ts +98 -0
  129. package/templates/runtime/schema/define-toolcraft.file-drop-normalization.test.ts +27 -0
  130. package/templates/runtime/schema/define-toolcraft.segmented-control-fit.test.ts +20 -2
  131. package/templates/runtime/schema/define-toolcraft.setup-background.test.ts +1 -1
  132. package/templates/runtime/schema/define-toolcraft.setup-canvas.test.ts +16 -3
  133. package/templates/runtime/schema/define-toolcraft.setup-render-scale-timeline.test.ts +71 -3
  134. package/templates/runtime/schema/define-toolcraft.source-collection.test.ts +79 -0
  135. package/templates/runtime/schema/define-toolcraft.ts +1 -1
  136. package/templates/runtime/schema/runtime-setup-section.ts +24 -5
  137. package/templates/runtime/schema/schema-resolvers.ts +0 -56
  138. package/templates/runtime/schema/types.ts +72 -17
  139. package/templates/runtime/state/canvas-background-state.ts +6 -0
  140. package/templates/runtime/testing/performance-render-plan-integration.test.ts +64 -0
  141. package/templates/runtime/testing/performance-renderer-pipeline-validation.ts +18 -1
  142. package/templates/starter/AGENTS.md +18 -15
  143. package/templates/starter/NOTICE.md +6 -0
  144. package/templates/starter/docs/toolcraft/README.md +2 -1
  145. package/templates/starter/docs/toolcraft/acceptance-testing.md +23 -35
  146. package/templates/starter/docs/toolcraft/agent-worklog.md +18 -3
  147. package/templates/starter/docs/toolcraft/assembly-workflow.md +13 -5
  148. package/templates/starter/docs/toolcraft/component-rules.md +8 -6
  149. package/templates/starter/docs/toolcraft/core/control-selection.md +9 -3
  150. package/templates/starter/docs/toolcraft/core/layout.md +7 -7
  151. package/templates/starter/docs/toolcraft/core/media-upload.md +4 -2
  152. package/templates/starter/docs/toolcraft/core/performance.md +14 -3
  153. package/templates/starter/docs/toolcraft/core/runtime-boundary.md +16 -3
  154. package/templates/starter/docs/toolcraft/core/setup-export.md +49 -19
  155. package/templates/starter/docs/toolcraft/core/timeline-animation.md +4 -4
  156. package/templates/starter/docs/toolcraft/custom-controls.md +6 -4
  157. package/templates/starter/docs/toolcraft/decision-contract.md +3 -1
  158. package/templates/starter/docs/toolcraft/performance.md +20 -6
  159. package/templates/starter/docs/toolcraft/renderer-technique.md +5 -1
  160. package/templates/starter/docs/toolcraft/schema-reference.md +23 -25
  161. package/templates/starter/docs/toolcraft/workflow.md +7 -2
  162. package/templates/starter/e2e/app-browser-control-applicability-requirements.spec.ts +163 -0
  163. package/templates/starter/e2e/app-browser-control-coverage.spec.ts +33 -0
  164. package/templates/starter/e2e/app-browser-infinity-canvas-evidence.spec.ts +50 -3
  165. package/templates/starter/e2e/app-browser-orientation-evidence.spec.ts +174 -21
  166. package/templates/starter/e2e/app-browser-render-scale-evidence.spec.ts +60 -6
  167. package/templates/starter/e2e/app-browser-render-scale-requirements.spec.ts +77 -1
  168. package/templates/starter/e2e/app-browser-runtime-provenance.spec.ts +8 -0
  169. package/templates/starter/e2e/app-browser-runtime-reporter.spec.ts +35 -0
  170. package/templates/starter/e2e/app-browser-runtime-requirements.spec.ts +5 -34
  171. package/templates/starter/e2e/app-browser-semantic-evidence.spec.ts +61 -16
  172. package/templates/starter/e2e/app-browser-shell.spec.ts +13 -5
  173. package/templates/starter/e2e/app-performance-path-adapters.ts +8 -1
  174. package/templates/starter/e2e/app-performance.spec.ts +16 -9
  175. package/templates/starter/e2e/{browser-conditional-output-evidence-helpers.ts → browser-background-output-evidence.ts} +0 -68
  176. package/templates/starter/e2e/browser-control-applicability-evidence.ts +114 -0
  177. package/templates/starter/e2e/browser-infinity-canvas-evidence.ts +29 -341
  178. package/templates/starter/e2e/browser-infinity-canvas-unavailable-image-evidence.ts +370 -0
  179. package/templates/starter/e2e/browser-media-export-evidence.spec.ts +77 -0
  180. package/templates/starter/e2e/browser-media-export-evidence.ts +208 -0
  181. package/templates/starter/e2e/browser-orientation-gizmo-evidence-helpers.ts +4 -0
  182. package/templates/starter/e2e/browser-orientation-gizmo-live-preconditions.ts +86 -0
  183. package/templates/starter/e2e/browser-render-scale-evidence.ts +37 -29
  184. package/templates/starter/e2e/browser-runtime-applicability-requirements.ts +84 -0
  185. package/templates/starter/e2e/browser-runtime-evidence-reporter.ts +13 -3
  186. package/templates/starter/e2e/browser-runtime-evidence-requirements.ts +72 -35
  187. package/templates/starter/e2e/browser-semantic-evidence-test-helpers.ts +25 -0
  188. package/templates/starter/e2e/browser-standard-timeline-evidence.ts +106 -0
  189. package/templates/starter/e2e/decoded-pixel-observation.spec.ts +74 -0
  190. package/templates/starter/e2e/decoded-pixel-observation.ts +227 -0
  191. package/templates/starter/e2e/export-artifact-helpers.ts +206 -25
  192. package/templates/starter/e2e/image-artifact-inspection.spec.ts +41 -0
  193. package/templates/starter/e2e/image-artifact-inspection.ts +99 -0
  194. package/templates/starter/e2e/performance-canvas-helpers.ts +22 -21
  195. package/templates/starter/e2e/performance-canvas-quality-guard.spec.ts +312 -0
  196. package/templates/starter/e2e/performance-canvas-quality-guard.ts +196 -0
  197. package/templates/starter/e2e/performance-canvas-quality-observer.ts +351 -0
  198. package/templates/starter/e2e/performance-control-layout-helpers.ts +2 -2
  199. package/templates/starter/e2e/performance-path-adapter-contract.ts +4 -4
  200. package/templates/starter/e2e/performance-path-helpers.spec.ts +203 -70
  201. package/templates/starter/e2e/performance-path-helpers.ts +210 -91
  202. package/templates/starter/e2e/performance-pipeline-evidence-continuity.spec.ts +76 -0
  203. package/templates/starter/e2e/performance-pipeline-evidence-test-fixtures.ts +11 -2
  204. package/templates/starter/e2e/performance-pipeline-invariants.ts +47 -4
  205. package/templates/starter/e2e/performance-render-scale-runner.spec.ts +140 -38
  206. package/templates/starter/e2e/video-artifact-inspection.spec.ts +64 -0
  207. package/templates/starter/e2e/video-artifact-inspection.ts +281 -0
  208. package/templates/starter/gitignore +1 -0
  209. package/templates/starter/package.json +1 -0
  210. package/templates/starter/scripts/check-toolcraft-docs.mjs +17 -0
  211. package/templates/starter/scripts/toolcraft-contract-manifest.json +1 -0
  212. package/templates/starter/scripts/toolcraft-delivery-plan-authority.test.mjs +60 -0
  213. package/templates/starter/scripts/toolcraft-delivery-plan-errors.test.mjs +7 -2
  214. package/templates/starter/scripts/toolcraft-delivery-plan.mjs +7 -3
  215. package/templates/starter/scripts/toolcraft-product-boundary-ast.mjs +95 -103
  216. package/templates/starter/scripts/toolcraft-product-boundary-module-policy.mjs +101 -0
  217. package/templates/starter/scripts/toolcraft-product-boundary.mjs +12 -0
  218. package/templates/starter/scripts/toolcraft-product-boundary.test.mjs +0 -26
  219. package/templates/starter/scripts/toolcraft-product-control-bindings.mjs +182 -0
  220. package/templates/starter/scripts/toolcraft-product-control-boundary.mjs +146 -0
  221. package/templates/starter/scripts/toolcraft-product-control-boundary.test.mjs +186 -0
  222. package/templates/starter/scripts/toolcraft-product-evidence-boundary.test.mjs +85 -20
  223. package/templates/starter/scripts/toolcraft-product-evidence-import-policy.mjs +43 -0
  224. package/templates/starter/scripts/toolcraft-product-export-boundary.mjs +157 -0
  225. package/templates/starter/scripts/toolcraft-product-export-boundary.test.mjs +196 -0
  226. package/templates/starter/scripts/toolcraft-typescript-analysis.d.mts +73 -0
  227. package/templates/starter/scripts/toolcraft-typescript-source-evidence-structure.test.mjs +20 -0
  228. package/templates/starter/scripts/toolcraft-typescript-source-evidence.mjs +1 -0
  229. package/templates/starter/scripts/toolcraft-unavailable-resource-evidence-facade-test-helpers.mjs +318 -0
  230. package/templates/starter/scripts/toolcraft-unavailable-resource-evidence-facade.test.mjs +157 -206
  231. package/templates/starter/scripts/toolcraft-verification-inventory.mjs +1 -0
  232. package/templates/starter/scripts/toolcraft-verification-receipt.test.mjs +51 -0
  233. package/templates/starter/scripts/toolcraft-vite-env-activation.test.mjs +128 -59
  234. package/templates/starter/src/app/acceptance/artifact-export-intent.ts +130 -0
  235. package/templates/starter/src/app/acceptance/conditions.ts +1 -1
  236. package/templates/starter/src/app/acceptance/control-acceptance-context.ts +2 -2
  237. package/templates/starter/src/app/acceptance/control-acceptance-coverage.ts +6 -20
  238. package/templates/starter/src/app/acceptance/control-acceptance-kind-rules.ts +24 -5
  239. package/templates/starter/src/app/acceptance/control-acceptance-policy.ts +2 -13
  240. package/templates/starter/src/app/acceptance/control-applicability-cases.test.ts +321 -0
  241. package/templates/starter/src/app/acceptance/control-applicability-cases.ts +290 -0
  242. package/templates/starter/src/app/acceptance/control-applicability.ts +332 -0
  243. package/templates/starter/src/app/acceptance/control-layout-dependency-rules.ts +20 -9
  244. package/templates/starter/src/app/acceptance/control-layout-entity-rules.ts +4 -6
  245. package/templates/starter/src/app/acceptance/control-layout-model.ts +4 -4
  246. package/templates/starter/src/app/acceptance/control-layout-section-rules.ts +3 -10
  247. package/templates/starter/src/app/acceptance/control-parts.ts +8 -0
  248. package/templates/starter/src/app/acceptance/control-section-entity-cohesion.test.ts +215 -0
  249. package/templates/starter/src/app/acceptance/control-section-entity-cohesion.ts +132 -0
  250. package/templates/starter/src/app/acceptance/control-section-inventory.test.ts +87 -0
  251. package/templates/starter/src/app/acceptance/control-section-inventory.ts +74 -44
  252. package/templates/starter/src/app/acceptance/custom-controls.ts +11 -3
  253. package/templates/starter/src/app/acceptance/export-artifact-coverage.ts +113 -0
  254. package/templates/starter/src/app/acceptance/inline-layout.ts +64 -9
  255. package/templates/starter/src/app/acceptance/media-upload.ts +5 -1
  256. package/templates/starter/src/app/acceptance/orientation-gizmo.ts +18 -11
  257. package/templates/starter/src/app/acceptance/output-background-rules.ts +2 -2
  258. package/templates/starter/src/app/acceptance/output-export-layout-rules.ts +18 -10
  259. package/templates/starter/src/app/acceptance/output-export-model.ts +54 -11
  260. package/templates/starter/src/app/acceptance/output-export.ts +35 -12
  261. package/templates/starter/src/app/acceptance/output-video-export-rules.ts +99 -0
  262. package/templates/starter/src/app/acceptance/types.ts +30 -8
  263. package/templates/starter/src/app/acceptance/validate-coverage.ts +20 -2
  264. package/templates/starter/src/app/starter-acceptance.artifact-export-intent.test.ts +239 -0
  265. package/templates/starter/src/app/starter-acceptance.background-export.test.ts +33 -0
  266. package/templates/starter/src/app/starter-acceptance.canvas-sizing.test.ts +16 -1
  267. package/templates/starter/src/app/starter-acceptance.compound-part-coverage.test.ts +65 -0
  268. package/templates/starter/src/app/starter-acceptance.contract-fixtures.test.ts +41 -0
  269. package/templates/starter/src/app/starter-acceptance.contract-fixtures.ts +112 -16
  270. package/templates/starter/src/app/starter-acceptance.control-applicability.test.ts +224 -0
  271. package/templates/starter/src/app/starter-acceptance.control-order.test.ts +58 -3
  272. package/templates/starter/src/app/starter-acceptance.control-state.test.ts +0 -111
  273. package/templates/starter/src/app/starter-acceptance.custom-control-collections.test.ts +3 -1
  274. package/templates/starter/src/app/starter-acceptance.custom-control-valid.test.ts +1 -1
  275. package/templates/starter/src/app/starter-acceptance.export-artifact.test.ts +131 -0
  276. package/templates/starter/src/app/starter-acceptance.export-test-utils.ts +22 -0
  277. package/templates/starter/src/app/{starter-acceptance.image-video-export-settings.test.ts → starter-acceptance.image-export-settings.test.ts} +37 -9
  278. package/templates/starter/src/app/starter-acceptance.interaction-ownership.test.ts +12 -0
  279. package/templates/starter/src/app/starter-acceptance.keyframes.test.ts +14 -4
  280. package/templates/starter/src/app/starter-acceptance.media-upload.fixtures.ts +11 -1
  281. package/templates/starter/src/app/starter-acceptance.media-upload.test.ts +28 -0
  282. package/templates/starter/src/app/starter-acceptance.output-export-intent.test.ts +384 -0
  283. package/templates/starter/src/app/starter-acceptance.output-export-rules.test.ts +46 -1
  284. package/templates/starter/src/app/starter-acceptance.product-output-export.test.ts +112 -105
  285. package/templates/starter/src/app/starter-acceptance.product-readiness.test.ts +12 -0
  286. package/templates/starter/src/app/starter-acceptance.section-cohesion.test.ts +1 -1
  287. package/templates/starter/src/app/starter-acceptance.section-dependencies.test.ts +35 -24
  288. package/templates/starter/src/app/starter-acceptance.section-size.test.ts +72 -0
  289. package/templates/starter/src/app/starter-acceptance.ts +13 -2
  290. package/templates/starter/src/app/starter-acceptance.video-export-settings.test.ts +336 -0
  291. package/templates/starter/src/app/starter-acceptance.view-interaction.test.ts +8 -0
  292. package/templates/starter/src/app/starter-performance-test-utils.ts +103 -0
  293. package/templates/starter/src/app/starter-performance.gates.test.ts +231 -278
  294. package/templates/starter/src/app/starter-performance.lifecycle.test.ts +261 -0
  295. package/templates/starter/src/app/test-evidence/browser-runtime-contract.ts +4 -2
  296. package/templates/starter/src/routes/index.tsx +2 -0
  297. package/templates/ui/components/composites/tabs.tsx +2 -2
  298. package/templates/ui/components/controls/collection-actions/collection-item-groups.test.tsx +38 -0
  299. package/templates/ui/components/controls/collection-actions/collection-item-groups.tsx +36 -0
  300. package/templates/ui/components/controls/collection-actions/index.ts +2 -0
  301. package/templates/ui/components/controls/file-drop/file-drop-collection-control.tsx +137 -0
  302. package/templates/ui/components/controls/file-drop/file-drop-control.test.tsx +189 -0
  303. package/templates/ui/components/controls/file-drop/file-drop-control.tsx +81 -88
  304. package/templates/ui/components/controls/file-drop/file-drop-presentation.tsx +131 -9
  305. package/templates/ui/components/controls/file-drop/file-drop-types.ts +36 -22
  306. package/templates/ui/components/controls/file-drop/index.ts +1 -0
  307. package/templates/ui/components/controls/index.ts +3 -0
  308. package/templates/ui/components/controls/range-slider/range-slider-control.tsx +1 -1
  309. package/templates/ui/components/controls/range-slider/range-slider-value.test.ts +14 -0
  310. package/templates/ui/components/controls/range-slider/range-slider-value.ts +3 -2
  311. package/templates/runtime/export/export.ts +0 -166
  312. package/templates/starter/src/app/acceptance/condition-exclusivity.test.ts +0 -60
  313. package/templates/starter/src/app/acceptance/condition-exclusivity.ts +0 -221
@@ -0,0 +1,194 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+
3
+ import { acquireToolcraftSourceAssetOwner } from "./toolcraft-source-asset-owner";
4
+ import {
5
+ createTestSourceAssetCoordinator,
6
+ createTestSourceAssetStore,
7
+ } from "./toolcraft-source-asset-test-support";
8
+
9
+ function createDeferred<T>() {
10
+ let resolve!: (value: T) => void;
11
+ const promise = new Promise<T>((resolvePromise) => {
12
+ resolve = resolvePromise;
13
+ });
14
+ return { promise, resolve };
15
+ }
16
+
17
+ afterEach(() => vi.unstubAllGlobals());
18
+
19
+ describe("Toolcraft source asset owner", () => {
20
+ it("binds the resolver without relying on the proof decorator", async () => {
21
+ type ReceiverAwareCoordinator = ReturnType<
22
+ typeof createTestSourceAssetCoordinator
23
+ > & Readonly<{ resolverBytes: Uint8Array }>;
24
+
25
+ const resolverBytes = new Uint8Array([7, 8, 9]);
26
+ const coordinator: ReceiverAwareCoordinator = {
27
+ ...createTestSourceAssetCoordinator(),
28
+ resolverBytes,
29
+ resolveResource: vi.fn(async function (
30
+ this: ReceiverAwareCoordinator,
31
+ ) {
32
+ expect(this).toBe(coordinator);
33
+ return this.resolverBytes;
34
+ }),
35
+ };
36
+ const lease = acquireToolcraftSourceAssetOwner({
37
+ createCoordinator: () => coordinator,
38
+ store: createTestSourceAssetStore(),
39
+ });
40
+ expect(lease.coordinator).toBe(coordinator);
41
+ const resolveResource = lease.coordinator.resolveResource;
42
+ if (!resolveResource) throw new Error("Expected a resource resolver.");
43
+
44
+ await expect(resolveResource(
45
+ "media:image:production-receiver",
46
+ { signal: new AbortController().signal },
47
+ )).resolves.toBe(resolverBytes);
48
+ lease.release();
49
+ });
50
+
51
+ it("attempts every cleanup and reports their failures once", async () => {
52
+ const presentationFailure = new Error("presentation cleanup failed");
53
+ const coordinatorFailure = new Error("coordinator cleanup failed");
54
+ const releaseResourceRef = vi.fn(() => {
55
+ throw presentationFailure;
56
+ });
57
+ const disposeCoordinator = vi.fn(async () => {
58
+ throw coordinatorFailure;
59
+ });
60
+ const onError = vi.fn();
61
+ const store = createTestSourceAssetStore();
62
+ const lease = acquireToolcraftSourceAssetOwner({
63
+ createCoordinator: () => createTestSourceAssetCoordinator({
64
+ dispose: disposeCoordinator,
65
+ resolveResource: vi.fn(async () => new Uint8Array([1, 2, 3])),
66
+ retainResourceRef: () => releaseResourceRef,
67
+ }),
68
+ onError,
69
+ store,
70
+ });
71
+ const resources = lease.presentationResources;
72
+ if (!resources) throw new Error("Expected presentation resources.");
73
+ await resources.retain("media:image:cleanup", "image/png");
74
+
75
+ lease.release();
76
+
77
+ await vi.waitFor(() => expect(onError).toHaveBeenCalledTimes(1));
78
+ expect(releaseResourceRef).toHaveBeenCalledTimes(1);
79
+ expect(disposeCoordinator).toHaveBeenCalledTimes(1);
80
+ const reported = onError.mock.calls[0]?.[0];
81
+ expect(reported).toBeInstanceOf(AggregateError);
82
+ expect((reported as AggregateError).errors).toEqual([
83
+ presentationFailure,
84
+ coordinatorFailure,
85
+ ]);
86
+ expect(() => acquireToolcraftSourceAssetOwner({ store })).toThrow(
87
+ "Toolcraft source asset owner failed to dispose.",
88
+ );
89
+ });
90
+
91
+ it("keeps a disposing owner registered until cleanup settles", async () => {
92
+ const disposal = createDeferred<void>();
93
+ const store = createTestSourceAssetStore();
94
+ const firstCoordinator = createTestSourceAssetCoordinator({
95
+ dispose: vi.fn(() => disposal.promise),
96
+ });
97
+ const first = acquireToolcraftSourceAssetOwner({
98
+ createCoordinator: () => firstCoordinator,
99
+ store,
100
+ });
101
+
102
+ first.release();
103
+ first.release();
104
+ await Promise.resolve();
105
+
106
+ const createReplacement = vi.fn(() => createTestSourceAssetCoordinator());
107
+ expect(() => acquireToolcraftSourceAssetOwner({
108
+ createCoordinator: createReplacement,
109
+ store,
110
+ })).toThrow("Toolcraft source asset owner is disposing.");
111
+ expect(createReplacement).not.toHaveBeenCalled();
112
+
113
+ disposal.resolve(undefined);
114
+ await vi.waitFor(() =>
115
+ expect(firstCoordinator.dispose).toHaveBeenCalledTimes(1)
116
+ );
117
+ await Promise.resolve();
118
+ const replacement = acquireToolcraftSourceAssetOwner({
119
+ createCoordinator: createReplacement,
120
+ store,
121
+ });
122
+ expect(createReplacement).toHaveBeenCalledTimes(1);
123
+ replacement.release();
124
+ });
125
+
126
+ it("reuses the active store owner regardless of later construction inputs", () => {
127
+ const store = createTestSourceAssetStore();
128
+ const firstFactory = vi.fn(() => createTestSourceAssetCoordinator());
129
+ const replacementFactory = vi.fn(() => createTestSourceAssetCoordinator());
130
+ const firstReporter = vi.fn();
131
+ const replacementReporter = vi.fn();
132
+ const first = acquireToolcraftSourceAssetOwner({
133
+ createCoordinator: firstFactory,
134
+ onError: firstReporter,
135
+ store,
136
+ });
137
+ const second = acquireToolcraftSourceAssetOwner({
138
+ createCoordinator: replacementFactory,
139
+ onError: replacementReporter,
140
+ store,
141
+ });
142
+ const failure = new Error("existing owner operation failed");
143
+
144
+ expect(second.coordinator).toBe(first.coordinator);
145
+ expect(replacementFactory).not.toHaveBeenCalled();
146
+ second.reportError(failure);
147
+ expect(firstReporter).toHaveBeenCalledWith(failure);
148
+ expect(replacementReporter).not.toHaveBeenCalled();
149
+
150
+ second.release();
151
+ first.release();
152
+ });
153
+
154
+ it("reports operational failures through the contextual error channel", () => {
155
+ const failure = new Error("model hydration failed");
156
+ const onError = vi.fn();
157
+ const lease = acquireToolcraftSourceAssetOwner({
158
+ createCoordinator: () => createTestSourceAssetCoordinator(),
159
+ onError,
160
+ store: createTestSourceAssetStore(),
161
+ });
162
+
163
+ lease.reportError(failure);
164
+
165
+ expect(onError).toHaveBeenCalledWith(failure);
166
+ lease.release();
167
+ });
168
+
169
+ it("preserves cleanup and contextual reporter failures", async () => {
170
+ const cleanupFailure = new Error("cleanup failed");
171
+ const reporterFailure = new Error("reporter failed");
172
+ const reportError = vi.fn();
173
+ vi.stubGlobal("reportError", reportError);
174
+ const lease = acquireToolcraftSourceAssetOwner({
175
+ createCoordinator: () => createTestSourceAssetCoordinator({
176
+ dispose: vi.fn(async () => Promise.reject(cleanupFailure)),
177
+ }),
178
+ onError: () => {
179
+ throw reporterFailure;
180
+ },
181
+ store: createTestSourceAssetStore(),
182
+ });
183
+
184
+ lease.release();
185
+
186
+ await vi.waitFor(() => expect(reportError).toHaveBeenCalledTimes(1));
187
+ const reported = reportError.mock.calls[0]?.[0];
188
+ expect(reported).toBeInstanceOf(AggregateError);
189
+ expect((reported as AggregateError).errors).toEqual([
190
+ cleanupFailure,
191
+ reporterFailure,
192
+ ]);
193
+ });
194
+ });
@@ -0,0 +1,227 @@
1
+ import type { ToolcraftBinaryAssetRepository } from "../../source-assets/repository/binary-asset-repository";
2
+ import { createIndexedDbToolcraftBinaryAssetRepository } from "../../source-assets/repository/indexeddb-binary-asset-repository";
3
+ import { createMemoryToolcraftBinaryAssetRepository } from "../../source-assets/repository/memory-binary-asset-repository";
4
+ import { createUnavailableToolcraftBinaryAssetRepository } from "../../source-assets/repository/unavailable-binary-asset-repository";
5
+ import {
6
+ createToolcraftSourceAssetCoordinator,
7
+ type ToolcraftSourceAssetCoordinator,
8
+ } from "../../source-assets/source-asset-coordinator";
9
+ import {
10
+ createToolcraftSourceAssetPresentationResources,
11
+ type ToolcraftSourceAssetPresentationResources,
12
+ } from "../../source-assets/source-asset-presentation-resources";
13
+ import type { ToolcraftExternalStore } from "../../state/toolcraft-external-store";
14
+
15
+ type ToolcraftSourceAssetCoordinatorFactory = (
16
+ store: ToolcraftExternalStore,
17
+ ) => ToolcraftSourceAssetCoordinator;
18
+
19
+ type ToolcraftSourceAssetCoordinatorDecorator = (
20
+ coordinator: ToolcraftSourceAssetCoordinator,
21
+ store: ToolcraftExternalStore,
22
+ ) => ToolcraftSourceAssetCoordinator;
23
+
24
+ type ToolcraftSourceAssetOwner = {
25
+ coordinator: ToolcraftSourceAssetCoordinator;
26
+ leases: number;
27
+ lifecycle: "active" | "disposing" | "failed";
28
+ lifecycleGeneration: number;
29
+ onError: (error: unknown) => void;
30
+ presentationResources: ToolcraftSourceAssetPresentationResources | null;
31
+ store: ToolcraftExternalStore;
32
+ };
33
+
34
+ type ToolcraftSourceAssetOwnerLease = Readonly<{
35
+ coordinator: ToolcraftSourceAssetCoordinator;
36
+ presentationResources: ToolcraftSourceAssetPresentationResources | null;
37
+ release: () => void;
38
+ reportError: (error: unknown) => void;
39
+ }>;
40
+
41
+ const sourceAssetOwners = new WeakMap<
42
+ ToolcraftExternalStore,
43
+ ToolcraftSourceAssetOwner
44
+ >();
45
+
46
+ function createRepository(): ToolcraftBinaryAssetRepository {
47
+ if (typeof globalThis.indexedDB === "undefined") {
48
+ return typeof navigator !== "undefined" &&
49
+ navigator.userAgent.toLowerCase().includes("jsdom")
50
+ ? createMemoryToolcraftBinaryAssetRepository()
51
+ : createUnavailableToolcraftBinaryAssetRepository();
52
+ }
53
+
54
+ try {
55
+ return createIndexedDbToolcraftBinaryAssetRepository();
56
+ } catch {
57
+ return createUnavailableToolcraftBinaryAssetRepository();
58
+ }
59
+ }
60
+
61
+ function createDefaultCoordinator(
62
+ store: ToolcraftExternalStore,
63
+ ): ToolcraftSourceAssetCoordinator {
64
+ return createToolcraftSourceAssetCoordinator({
65
+ dispatch: store.dispatch,
66
+ getState: store.getCommittedState,
67
+ repository: createRepository(),
68
+ });
69
+ }
70
+
71
+ function reportSourceAssetOwnerError(error: unknown): void {
72
+ const globalReportError = globalThis.reportError;
73
+ if (typeof globalReportError === "function") {
74
+ try {
75
+ globalReportError(error);
76
+ return;
77
+ } catch (reportingFailure) {
78
+ console.error(
79
+ "Toolcraft source asset owner error reporting failed.",
80
+ new AggregateError(
81
+ [error, reportingFailure],
82
+ "Toolcraft source asset owner operation and reporting both failed.",
83
+ ),
84
+ );
85
+ return;
86
+ }
87
+ }
88
+ console.error("Toolcraft source asset owner operation failed.", error);
89
+ }
90
+
91
+ function reportOwnerError(
92
+ owner: ToolcraftSourceAssetOwner,
93
+ error: unknown,
94
+ ): void {
95
+ try {
96
+ owner.onError(error);
97
+ } catch (reportingFailure) {
98
+ reportSourceAssetOwnerError(
99
+ new AggregateError(
100
+ [error, reportingFailure],
101
+ "Toolcraft source asset owner operation and its injected reporter both failed.",
102
+ ),
103
+ );
104
+ }
105
+ }
106
+
107
+ function createOwner(
108
+ store: ToolcraftExternalStore,
109
+ createCoordinator: ToolcraftSourceAssetCoordinatorFactory,
110
+ decorateCoordinator: ToolcraftSourceAssetCoordinatorDecorator | undefined,
111
+ onError: (error: unknown) => void,
112
+ ): ToolcraftSourceAssetOwner {
113
+ const baseCoordinator = createCoordinator(store);
114
+ const resolveResource = baseCoordinator.resolveResource;
115
+ if (resolveResource) {
116
+ baseCoordinator.resolveResource = resolveResource.bind(baseCoordinator);
117
+ }
118
+ const coordinator = decorateCoordinator
119
+ ? decorateCoordinator(baseCoordinator, store)
120
+ : baseCoordinator;
121
+ const presentationResources =
122
+ coordinator.resolveResource && coordinator.retainResourceRef
123
+ ? createToolcraftSourceAssetPresentationResources({
124
+ resolveResource: coordinator.resolveResource,
125
+ retainResourceRef: coordinator.retainResourceRef,
126
+ })
127
+ : null;
128
+ return {
129
+ coordinator,
130
+ leases: 0,
131
+ lifecycle: "active",
132
+ lifecycleGeneration: 0,
133
+ onError,
134
+ presentationResources,
135
+ store,
136
+ };
137
+ }
138
+
139
+ async function disposeOwner(owner: ToolcraftSourceAssetOwner): Promise<void> {
140
+ const failures: unknown[] = [];
141
+ try {
142
+ owner.presentationResources?.dispose();
143
+ } catch (error) {
144
+ failures.push(error);
145
+ }
146
+ try {
147
+ await owner.coordinator.dispose.call(owner.coordinator);
148
+ } catch (error) {
149
+ failures.push(error);
150
+ }
151
+
152
+ if (failures.length === 1) {
153
+ owner.lifecycle = "failed";
154
+ reportOwnerError(owner, failures[0]);
155
+ } else if (failures.length > 1) {
156
+ owner.lifecycle = "failed";
157
+ reportOwnerError(
158
+ owner,
159
+ new AggregateError(
160
+ failures,
161
+ "Toolcraft source asset owner failed to dispose all owned resources.",
162
+ ),
163
+ );
164
+ } else if (sourceAssetOwners.get(owner.store) === owner) {
165
+ sourceAssetOwners.delete(owner.store);
166
+ }
167
+ }
168
+
169
+ function scheduleOwnerDisposal(owner: ToolcraftSourceAssetOwner): void {
170
+ const lifecycleGeneration = ++owner.lifecycleGeneration;
171
+ queueMicrotask(() => {
172
+ if (
173
+ owner.lifecycle !== "active" ||
174
+ owner.leases !== 0 ||
175
+ owner.lifecycleGeneration !== lifecycleGeneration ||
176
+ sourceAssetOwners.get(owner.store) !== owner
177
+ ) {
178
+ return;
179
+ }
180
+ owner.lifecycle = "disposing";
181
+ void disposeOwner(owner);
182
+ });
183
+ }
184
+
185
+ export function acquireToolcraftSourceAssetOwner({
186
+ createCoordinator = createDefaultCoordinator,
187
+ decorateCoordinator,
188
+ onError = reportSourceAssetOwnerError,
189
+ store,
190
+ }: Readonly<{
191
+ createCoordinator?: ToolcraftSourceAssetCoordinatorFactory;
192
+ decorateCoordinator?: ToolcraftSourceAssetCoordinatorDecorator;
193
+ onError?: (error: unknown) => void;
194
+ store: ToolcraftExternalStore;
195
+ }>): ToolcraftSourceAssetOwnerLease {
196
+ let owner = sourceAssetOwners.get(store);
197
+ if (owner?.lifecycle === "disposing") {
198
+ throw new Error("Toolcraft source asset owner is disposing.");
199
+ }
200
+ if (owner?.lifecycle === "failed") {
201
+ throw new Error("Toolcraft source asset owner failed to dispose.");
202
+ }
203
+ if (!owner) {
204
+ owner = createOwner(
205
+ store,
206
+ createCoordinator,
207
+ decorateCoordinator,
208
+ onError,
209
+ );
210
+ sourceAssetOwners.set(store, owner);
211
+ }
212
+
213
+ owner.leases += 1;
214
+ owner.lifecycleGeneration += 1;
215
+ let released = false;
216
+ return Object.freeze({
217
+ coordinator: owner.coordinator,
218
+ presentationResources: owner.presentationResources,
219
+ release: () => {
220
+ if (released) return;
221
+ released = true;
222
+ owner.leases -= 1;
223
+ if (owner.leases === 0) scheduleOwnerDisposal(owner);
224
+ },
225
+ reportError: (error: unknown) => reportOwnerError(owner, error),
226
+ });
227
+ }
@@ -0,0 +1,243 @@
1
+ import * as React from "react";
2
+ import { cleanup, render, waitFor } from "@testing-library/react";
3
+ import { renderToString } from "react-dom/server";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
5
+
6
+ import type { ToolcraftSourceAssetCoordinator } from "../../source-assets/source-asset-coordinator";
7
+ import { ToolcraftSourceAssetProvider } from "./toolcraft-source-asset-context";
8
+ import {
9
+ ToolcraftSourceAssetCoordinatorProbe as CoordinatorProbe,
10
+ createTestSourceAssetCoordinator as createTestCoordinator,
11
+ createTestSourceAssetStore as createTestStore,
12
+ } from "./toolcraft-source-asset-test-support";
13
+
14
+ afterEach(cleanup);
15
+
16
+ function createDeferred<T>() {
17
+ let reject!: (reason?: unknown) => void;
18
+ const promise = new Promise<T>((_resolve, rejectPromise) => {
19
+ reject = rejectPromise;
20
+ });
21
+ return { promise, reject };
22
+ }
23
+
24
+ describe("Toolcraft source asset provider ownership", () => {
25
+ it("does not acquire or cache an owner for an uncommitted render", () => {
26
+ const store = createTestStore();
27
+ const coordinator = createTestCoordinator();
28
+ const createCoordinator = vi.fn(() => coordinator);
29
+
30
+ expect(renderToString(
31
+ <ToolcraftSourceAssetProvider
32
+ createCoordinator={createCoordinator}
33
+ store={store}
34
+ >
35
+ <span>Server probe</span>
36
+ </ToolcraftSourceAssetProvider>,
37
+ )).toBe("");
38
+ expect(createCoordinator).not.toHaveBeenCalled();
39
+
40
+ const rendered = render(
41
+ <ToolcraftSourceAssetProvider
42
+ createCoordinator={createCoordinator}
43
+ store={store}
44
+ >
45
+ <span data-testid="committed-source-asset-child" />
46
+ </ToolcraftSourceAssetProvider>,
47
+ );
48
+
49
+ expect(rendered.getByTestId("committed-source-asset-child")).toBeDefined();
50
+ expect(createCoordinator).toHaveBeenCalledTimes(1);
51
+ });
52
+
53
+ it("preserves the injected coordinator receiver through the provider", async () => {
54
+ type ReceiverAwareCoordinator = ToolcraftSourceAssetCoordinator & {
55
+ resolverBytes: Uint8Array;
56
+ };
57
+
58
+ const resolverBytes = new Uint8Array([4, 5, 6]);
59
+ const coordinator: ReceiverAwareCoordinator = {
60
+ ...createTestCoordinator(),
61
+ resolverBytes,
62
+ resolveResource: vi.fn(async function (
63
+ this: ReceiverAwareCoordinator,
64
+ ) {
65
+ expect(this).toBe(coordinator);
66
+ return this.resolverBytes;
67
+ }),
68
+ };
69
+ const observed: ToolcraftSourceAssetCoordinator[] = [];
70
+ render(
71
+ <ToolcraftSourceAssetProvider
72
+ createCoordinator={() => coordinator}
73
+ store={createTestStore()}
74
+ >
75
+ <CoordinatorProbe capture={(value) => observed.push(value)} />
76
+ </ToolcraftSourceAssetProvider>,
77
+ );
78
+ const resolveResource = observed[0]?.resolveResource;
79
+ if (!resolveResource) throw new Error("Expected a resource resolver.");
80
+
81
+ await expect(resolveResource(
82
+ "media:image:receiver-aware",
83
+ { signal: new AbortController().signal },
84
+ )).resolves.toBe(resolverBytes);
85
+ });
86
+
87
+ it("reports hydration failures through the contextual owner channel", async () => {
88
+ const failure = new Error("model hydration failed");
89
+ const onError = vi.fn();
90
+ const coordinator = createTestCoordinator({
91
+ hydrateModels: vi.fn(async () => Promise.reject(failure)),
92
+ });
93
+ const rendered = render(
94
+ <ToolcraftSourceAssetProvider
95
+ createCoordinator={() => coordinator}
96
+ onError={onError}
97
+ store={createTestStore()}
98
+ >
99
+ <span />
100
+ </ToolcraftSourceAssetProvider>,
101
+ );
102
+
103
+ await waitFor(() => expect(onError).toHaveBeenCalledWith(failure));
104
+ rendered.unmount();
105
+ expect(onError).toHaveBeenCalledTimes(1);
106
+ });
107
+
108
+ it("keeps same-store construction inputs stable across prop identity changes", async () => {
109
+ const store = createTestStore();
110
+ const firstReporter = vi.fn();
111
+ const replacementReporter = vi.fn();
112
+ const hydration = createDeferred<void>();
113
+ const hydrationFailure = new Error("original owner hydration failed");
114
+ const coordinator = createTestCoordinator({
115
+ hydrateModels: vi.fn(() => hydration.promise),
116
+ });
117
+ const observed: ToolcraftSourceAssetCoordinator[] = [];
118
+ const firstFactory = vi.fn(() => coordinator);
119
+ const replacementFactory = vi.fn(() => createTestCoordinator());
120
+ const rendered = render(
121
+ <ToolcraftSourceAssetProvider
122
+ createCoordinator={firstFactory}
123
+ onError={firstReporter}
124
+ store={store}
125
+ >
126
+ <CoordinatorProbe capture={(value) => observed.push(value)} />
127
+ <span data-testid="stable-source-asset-child" />
128
+ </ToolcraftSourceAssetProvider>,
129
+ );
130
+
131
+ rendered.rerender(
132
+ <ToolcraftSourceAssetProvider
133
+ createCoordinator={replacementFactory}
134
+ onError={replacementReporter}
135
+ store={store}
136
+ >
137
+ <CoordinatorProbe capture={(value) => observed.push(value)} />
138
+ <span data-testid="stable-source-asset-child" />
139
+ </ToolcraftSourceAssetProvider>,
140
+ );
141
+
142
+ expect(rendered.getByTestId("stable-source-asset-child")).toBeDefined();
143
+ expect(observed.length).toBeGreaterThan(0);
144
+ expect(observed.every((value) => value === coordinator)).toBe(true);
145
+ expect(firstFactory).toHaveBeenCalledTimes(1);
146
+ expect(replacementFactory).not.toHaveBeenCalled();
147
+ hydration.reject(hydrationFailure);
148
+ await waitFor(() => expect(firstReporter).toHaveBeenCalledWith(
149
+ hydrationFailure,
150
+ ));
151
+ expect(replacementReporter).not.toHaveBeenCalled();
152
+ });
153
+
154
+ it("uses current construction inputs when the store changes", async () => {
155
+ const firstStore = createTestStore();
156
+ const secondStore = createTestStore();
157
+ const firstReporter = vi.fn();
158
+ const secondReporter = vi.fn();
159
+ const secondFailure = new Error("second store hydration failed");
160
+ const firstCoordinator = createTestCoordinator();
161
+ const secondCoordinator = createTestCoordinator({
162
+ hydrateModels: vi.fn(async () => Promise.reject(secondFailure)),
163
+ });
164
+ const firstFactory = vi.fn(() => firstCoordinator);
165
+ const secondFactory = vi.fn(() => secondCoordinator);
166
+ const rendered = render(
167
+ <ToolcraftSourceAssetProvider
168
+ createCoordinator={firstFactory}
169
+ onError={firstReporter}
170
+ store={firstStore}
171
+ >
172
+ <span data-testid="switched-source-asset-child" />
173
+ </ToolcraftSourceAssetProvider>,
174
+ );
175
+
176
+ rendered.rerender(
177
+ <ToolcraftSourceAssetProvider
178
+ createCoordinator={secondFactory}
179
+ onError={secondReporter}
180
+ store={secondStore}
181
+ >
182
+ <span data-testid="switched-source-asset-child" />
183
+ </ToolcraftSourceAssetProvider>,
184
+ );
185
+
186
+ await waitFor(() => expect(secondReporter).toHaveBeenCalledWith(
187
+ secondFailure,
188
+ ));
189
+ expect(rendered.getByTestId("switched-source-asset-child")).toBeDefined();
190
+ expect(firstFactory).toHaveBeenCalledWith(firstStore);
191
+ expect(secondFactory).toHaveBeenCalledWith(secondStore);
192
+ expect(firstReporter).not.toHaveBeenCalledWith(secondFailure);
193
+ });
194
+
195
+ it("supports the deprecated error reporter alias for all owner failures", async () => {
196
+ const operationalFailure = new Error("model hydration failed through alias");
197
+ const disposalFailure = new Error("owner disposal failed through alias");
198
+ const onDisposeError = vi.fn();
199
+ const coordinator = createTestCoordinator({
200
+ dispose: vi.fn(async () => Promise.reject(disposalFailure)),
201
+ hydrateModels: vi.fn(async () => Promise.reject(operationalFailure)),
202
+ });
203
+ const rendered = render(
204
+ <ToolcraftSourceAssetProvider
205
+ createCoordinator={() => coordinator}
206
+ onDisposeError={onDisposeError}
207
+ store={createTestStore()}
208
+ >
209
+ <span />
210
+ </ToolcraftSourceAssetProvider>,
211
+ );
212
+
213
+ await waitFor(() => expect(onDisposeError).toHaveBeenCalledWith(
214
+ operationalFailure,
215
+ ));
216
+ rendered.unmount();
217
+ await waitFor(() => expect(onDisposeError).toHaveBeenCalledWith(
218
+ disposalFailure,
219
+ ));
220
+ });
221
+
222
+ it("prefers onError over the deprecated reporter alias", async () => {
223
+ const failure = new Error("primary reporter failure");
224
+ const onError = vi.fn();
225
+ const onDisposeError = vi.fn();
226
+ const rendered = render(
227
+ <ToolcraftSourceAssetProvider
228
+ createCoordinator={() => createTestCoordinator({
229
+ hydrateModels: vi.fn(async () => Promise.reject(failure)),
230
+ })}
231
+ onDisposeError={onDisposeError}
232
+ onError={onError}
233
+ store={createTestStore()}
234
+ >
235
+ <span />
236
+ </ToolcraftSourceAssetProvider>,
237
+ );
238
+
239
+ await waitFor(() => expect(onError).toHaveBeenCalledWith(failure));
240
+ expect(onDisposeError).not.toHaveBeenCalled();
241
+ rendered.unmount();
242
+ });
243
+ });