@pascal-app/editor 1.0.4 → 1.0.6

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 (309) hide show
  1. package/README.md +106 -0
  2. package/package.json +40 -38
  3. package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
  4. package/src/components/editor/bake-thumbnail.tsx +6 -15
  5. package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
  6. package/src/components/editor/construction-reveal-audio.tsx +40 -0
  7. package/src/components/editor/custom-camera-controls.tsx +81 -8
  8. package/src/components/editor/editor-layout-mobile.tsx +1 -1
  9. package/src/components/editor/editor-layout-v2.tsx +1 -1
  10. package/src/components/editor/export-manager.tsx +14 -19
  11. package/src/components/editor/first-person/build-collider-world.test.ts +269 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +22 -5
  13. package/src/components/editor/first-person/ground-holes.test.ts +128 -0
  14. package/src/components/editor/first-person/ground-holes.ts +130 -0
  15. package/src/components/editor/first-person/mechanism-targets.test.ts +89 -2
  16. package/src/components/editor/first-person/mechanism-targets.ts +52 -0
  17. package/src/components/editor/first-person-controls.tsx +19 -11
  18. package/src/components/editor/floor-aware-camera-controls.test.ts +24 -0
  19. package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
  20. package/src/components/editor/floorplan-panel.tsx +72 -23
  21. package/src/components/editor/follow-pascal.test.tsx +912 -0
  22. package/src/components/editor/follow-pascal.tsx +354 -0
  23. package/src/components/editor/group-actions.ts +2 -0
  24. package/src/components/editor/group-move-3d.ts +2 -0
  25. package/src/components/editor/group-rotate-handle.tsx +2 -0
  26. package/src/components/editor/group-transform-shared.test.ts +460 -0
  27. package/src/components/editor/index.tsx +166 -79
  28. package/src/components/editor/riser-diagram-panel.tsx +14 -7
  29. package/src/components/editor/room-highlight.tsx +13 -5
  30. package/src/components/editor/room-labels.test.ts +621 -128
  31. package/src/components/editor/room-labels.tsx +1090 -190
  32. package/src/components/editor/selection-manager.tsx +38 -117
  33. package/src/components/editor/slab-hole-highlights.tsx +2 -2
  34. package/src/components/editor/snapshot-capture-overlay.tsx +3 -2
  35. package/src/components/editor/snapshot-capture.test.ts +69 -58
  36. package/src/components/editor/snapshot-capture.ts +7 -1
  37. package/src/components/editor/thumbnail-generator.test.ts +350 -4
  38. package/src/components/editor/thumbnail-generator.tsx +128 -54
  39. package/src/components/editor/use-floorplan-background-placement.ts +3 -1
  40. package/src/components/editor/views/plugins-view.tsx +1 -8
  41. package/src/components/editor/views/view-bar.test.tsx +54 -0
  42. package/src/components/editor/views/view-bar.tsx +140 -17
  43. package/src/components/editor/wall-snap-beacon-layer.tsx +5 -4
  44. package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
  45. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
  46. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +3 -4
  47. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +1 -1
  48. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +29 -7
  49. package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
  50. package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
  51. package/src/components/point-ask/bubble.tsx +538 -0
  52. package/src/components/point-ask/hover-chip.tsx +91 -0
  53. package/src/components/point-ask/mode-pill.tsx +92 -0
  54. package/src/components/point-ask/optional-motion.ts +22 -0
  55. package/src/components/point-ask/outline-corners.tsx +93 -0
  56. package/src/components/point-ask/pins.tsx +711 -0
  57. package/src/components/point-ask/point-ask-layer.tsx +83 -0
  58. package/src/components/point-ask/point-ask-styles.ts +105 -0
  59. package/src/components/point-ask/region-marquee-styles.ts +15 -0
  60. package/src/components/point-ask/region-marquee.tsx +64 -0
  61. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
  62. package/src/components/systems/roof/roof-edit-system.tsx +1 -0
  63. package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
  64. package/src/components/tools/distribution/connection-compatibility.ts +43 -0
  65. package/src/components/tools/distribution/distribution-public.ts +9 -0
  66. package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
  67. package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
  68. package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
  69. package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
  70. package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
  71. package/src/components/tools/distribution/draw-alignment.ts +35 -0
  72. package/src/components/tools/distribution/ports.test.ts +56 -0
  73. package/src/components/tools/distribution/ports.ts +378 -0
  74. package/src/components/tools/distribution/run-cursor.test.ts +135 -0
  75. package/src/components/tools/distribution/run-cursor.ts +36 -0
  76. package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
  77. package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
  78. package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
  79. package/src/components/tools/distribution/run-port-snap.ts +25 -0
  80. package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
  81. package/src/components/tools/distribution/run-screen-direction.ts +30 -0
  82. package/src/components/tools/elevator/elevator-tool.tsx +3 -6
  83. package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
  84. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
  85. package/src/components/tools/item/placement-history.test.tsx +38 -0
  86. package/src/components/tools/item/placement-strategies.test.ts +46 -1
  87. package/src/components/tools/item/placement-strategies.ts +3 -6
  88. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
  89. package/src/components/tools/item/use-draft-node.test.tsx +94 -94
  90. package/src/components/tools/item/use-placement-coordinator.tsx +55 -49
  91. package/src/components/tools/registry/item-surface-move.test.ts +62 -63
  92. package/src/components/tools/registry/item-surface-move.ts +2 -0
  93. package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
  94. package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
  95. package/src/components/tools/registry/move-registry-node-tool.tsx +63 -29
  96. package/src/components/tools/tool-manager.tsx +14 -5
  97. package/src/components/tools/wall/wall-drafting.test.ts +170 -57
  98. package/src/components/ui/action-menu/action-button.tsx +6 -1
  99. package/src/components/ui/action-menu/camera-actions.tsx +7 -3
  100. package/src/components/ui/action-menu/control-modes.tsx +13 -7
  101. package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
  102. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
  103. package/src/components/ui/action-menu/index.tsx +56 -33
  104. package/src/components/ui/action-menu/measurement-control.tsx +11 -9
  105. package/src/components/ui/action-menu/placement.ts +20 -0
  106. package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
  107. package/src/components/ui/action-menu/view-toggles.tsx +47 -42
  108. package/src/components/ui/build-panel/build-panel.tsx +10 -3
  109. package/src/components/ui/command-palette/editor-commands.tsx +3 -12
  110. package/src/components/ui/command-palette/index.tsx +45 -15
  111. package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
  112. package/src/components/ui/controls/action-button.tsx +5 -2
  113. package/src/components/ui/controls/metric-control.tsx +1 -1
  114. package/src/components/ui/controls/segmented-control.tsx +21 -8
  115. package/src/components/ui/controls/select-control.tsx +1 -1
  116. package/src/components/ui/controls/slider-control.tsx +1 -1
  117. package/src/components/ui/controls/toggle-control.tsx +3 -3
  118. package/src/components/ui/controls/tool-options-panel.tsx +1 -1
  119. package/src/components/ui/floating-level-selector.tsx +150 -45
  120. package/src/components/ui/helpers/contextual-helper-panel.tsx +32 -7
  121. package/src/components/ui/helpers/helper-manager.test.tsx +54 -2
  122. package/src/components/ui/helpers/helper-manager.tsx +7 -1
  123. package/src/components/ui/helpers/item-helper.tsx +9 -1
  124. package/src/components/ui/panels/collections/collections-popover.tsx +8 -8
  125. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  126. package/src/components/ui/panels/mobile-selection-bar.tsx +6 -3
  127. package/src/components/ui/panels/multi-selection-panel.tsx +120 -3
  128. package/src/components/ui/panels/panel-wrapper.tsx +53 -27
  129. package/src/components/ui/panels/parametric-field-control.tsx +2 -2
  130. package/src/components/ui/panels/parametric-inspector.tsx +3 -3
  131. package/src/components/ui/panels/room-panel.tsx +65 -0
  132. package/src/components/ui/primitives/badge.tsx +8 -0
  133. package/src/components/ui/primitives/sidebar.tsx +1 -1
  134. package/src/components/ui/right-stack.tsx +21 -2
  135. package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
  136. package/src/components/ui/sidebar/icon-rail.tsx +3 -3
  137. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  138. package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
  139. package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
  140. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
  141. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
  142. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
  143. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +107 -23
  144. package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
  145. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
  146. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
  147. package/src/components/ui/sidebar/panels/site-panel/index.tsx +5 -12
  148. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -10
  149. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -2
  150. package/src/components/ui/sidebar/tab-bar.tsx +4 -4
  151. package/src/components/viewer/viewer-controls-bar.tsx +18 -6
  152. package/src/components/viewer/viewer-rooms.tsx +16 -0
  153. package/src/hooks/use-auto-save.integration.test.tsx +96 -38
  154. package/src/hooks/use-auto-save.ts +26 -1
  155. package/src/hooks/use-ceiling-events.test.ts +123 -118
  156. package/src/hooks/use-grid-events.test.ts +105 -105
  157. package/src/hooks/use-keyboard.test.ts +65 -16
  158. package/src/hooks/use-keyboard.ts +29 -12
  159. package/src/hooks/use-placement-notice.test.ts +24 -1
  160. package/src/hooks/use-placement-notice.ts +37 -6
  161. package/src/index.tsx +71 -24
  162. package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
  163. package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
  164. package/src/lib/add-storey.test.ts +177 -0
  165. package/src/lib/add-storey.ts +59 -0
  166. package/src/lib/below-grade.test.tsx +171 -0
  167. package/src/lib/below-grade.ts +133 -0
  168. package/src/lib/camera-facing.test.ts +24 -0
  169. package/src/lib/camera-facing.ts +24 -0
  170. package/src/lib/direct-manipulation.ts +2 -0
  171. package/src/lib/download-project-file.ts +17 -0
  172. package/src/lib/editor-level-display.ts +9 -2
  173. package/src/lib/editor-views.ts +35 -1
  174. package/src/lib/export-texture-utils.ts +2 -4
  175. package/src/lib/floor-foundation-support.test.ts +1 -0
  176. package/src/lib/floorplan/floorplan-pdfkit-document.ts +31 -16
  177. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +39 -0
  178. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +4 -0
  179. package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
  180. package/src/lib/floorplan/sheet-export.test.ts +24 -0
  181. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +34 -45
  182. package/src/lib/floorplan/site-plan/coverage.ts +2 -4
  183. package/src/lib/floorplan/site-plan/front-edge.ts +2 -1
  184. package/src/lib/floorplan/site-plan/geometry.test.ts +9 -15
  185. package/src/lib/floorplan/site-plan/geometry.ts +1 -416
  186. package/src/lib/floorplan/site-plan/index.ts +0 -16
  187. package/src/lib/floorplan/site-plan/site-annotations.test.ts +117 -0
  188. package/src/lib/floorplan/site-plan/site-annotations.ts +3 -2
  189. package/src/lib/floorplan/site-plan/site-parts.ts +14 -106
  190. package/src/lib/floorplan/site-plan/yards.test.ts +2 -1
  191. package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
  192. package/src/lib/follow-pascal-exterior.test.ts +171 -0
  193. package/src/lib/follow-pascal-exterior.ts +39 -0
  194. package/src/lib/follow-pascal-presentation.test.ts +203 -0
  195. package/src/lib/follow-pascal-presentation.ts +129 -0
  196. package/src/lib/follow-pascal-sight.ts +84 -0
  197. package/src/lib/follow-pascal-targets.test.ts +406 -0
  198. package/src/lib/follow-pascal-targets.ts +198 -0
  199. package/src/lib/follow-pascal.test.ts +350 -0
  200. package/src/lib/follow-pascal.ts +284 -0
  201. package/src/lib/fresh-planar-placement.test.ts +112 -0
  202. package/src/lib/fresh-planar-placement.ts +57 -3
  203. package/src/lib/geometry-script/author.ts +6 -1
  204. package/src/lib/geometry-script/client.ts +8 -1
  205. package/src/lib/geometry-script/finish-scripts.ts +127 -0
  206. package/src/lib/geometry-script/protocol.ts +1 -0
  207. package/src/lib/glb-export.test.ts +187 -162
  208. package/src/lib/glb-export.ts +4 -1
  209. package/src/lib/history.test.ts +12 -7
  210. package/src/lib/interaction/scope.ts +4 -0
  211. package/src/lib/level-selection.test.ts +57 -0
  212. package/src/lib/level-selection.ts +16 -1
  213. package/src/lib/lot/clean-ring.test.ts +2 -1
  214. package/src/lib/lot/drop-in.ts +9 -24
  215. package/src/lib/lot/lot-patch.test.ts +1 -1
  216. package/src/lib/lot/lot-patch.ts +2 -2
  217. package/src/lib/measurement-kind.test.ts +6 -8
  218. package/src/lib/measurements.test.ts +23 -0
  219. package/src/lib/orbit-floor-clearance.test.ts +36 -0
  220. package/src/lib/orbit-floor-clearance.ts +48 -0
  221. package/src/lib/paint-pass-through.test.ts +11 -0
  222. package/src/lib/paint-pass-through.ts +4 -2
  223. package/src/lib/paint-scope.ts +1 -0
  224. package/src/lib/parametric-node-update.ts +16 -7
  225. package/src/lib/plugin-chat.ts +15 -0
  226. package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
  227. package/src/lib/point-ask/__fixtures__/house.ts +81 -0
  228. package/src/lib/point-ask/ask-created.test.ts +195 -0
  229. package/src/lib/point-ask/ask-flow.test.ts +380 -0
  230. package/src/lib/point-ask/build-context.test.ts +173 -0
  231. package/src/lib/point-ask/build-context.ts +94 -0
  232. package/src/lib/point-ask/capture-crop.ts +129 -0
  233. package/src/lib/point-ask/choreography.ts +69 -0
  234. package/src/lib/point-ask/compare.test.ts +145 -0
  235. package/src/lib/point-ask/compare.ts +73 -0
  236. package/src/lib/point-ask/describe-target.test.ts +165 -0
  237. package/src/lib/point-ask/describe-target.ts +234 -0
  238. package/src/lib/point-ask/dictation.ts +79 -0
  239. package/src/lib/point-ask/edge-marker.test.ts +61 -0
  240. package/src/lib/point-ask/edge-marker.ts +27 -0
  241. package/src/lib/point-ask/frame-loop.ts +25 -0
  242. package/src/lib/point-ask/history-marks.test.ts +94 -0
  243. package/src/lib/point-ask/history-marks.ts +39 -0
  244. package/src/lib/point-ask/host-api.ts +32 -0
  245. package/src/lib/point-ask/outline-box.test.ts +30 -0
  246. package/src/lib/point-ask/outline-box.ts +11 -0
  247. package/src/lib/point-ask/overlay-metrics.ts +6 -0
  248. package/src/lib/point-ask/pick.test.ts +220 -0
  249. package/src/lib/point-ask/pick.ts +121 -0
  250. package/src/lib/point-ask/pin-machine.test.ts +494 -0
  251. package/src/lib/point-ask/pin-machine.ts +217 -0
  252. package/src/lib/point-ask/placement.test.ts +203 -0
  253. package/src/lib/point-ask/placement.ts +141 -0
  254. package/src/lib/point-ask/pointer-tracker.ts +34 -0
  255. package/src/lib/point-ask/projector.ts +83 -0
  256. package/src/lib/point-ask/region.test.ts +157 -0
  257. package/src/lib/point-ask/region.ts +136 -0
  258. package/src/lib/point-ask/retry.test.ts +217 -0
  259. package/src/lib/point-ask/retry.ts +37 -0
  260. package/src/lib/point-ask/scene-commit.test.ts +312 -0
  261. package/src/lib/point-ask/scene-commit.ts +32 -0
  262. package/src/lib/point-ask/session.ts +1391 -0
  263. package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
  264. package/src/lib/point-ask/spring-keyframes.ts +38 -0
  265. package/src/lib/point-ask/spring.test.ts +63 -0
  266. package/src/lib/point-ask/spring.ts +107 -0
  267. package/src/lib/point-ask/suggestions.test.ts +80 -0
  268. package/src/lib/point-ask/suggestions.ts +125 -0
  269. package/src/lib/point-ask/target-scope.test.ts +622 -0
  270. package/src/lib/point-ask/target-scope.ts +338 -0
  271. package/src/lib/point-ask/types.ts +48 -0
  272. package/src/lib/portable-export.test.ts +34 -1
  273. package/src/lib/portable-export.ts +119 -53
  274. package/src/lib/print-feature-thickness.ts +10 -1
  275. package/src/lib/print-roof-solids.test.ts +39 -1
  276. package/src/lib/print-roof-solids.ts +43 -10
  277. package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
  278. package/src/lib/room-assembly-overlay.ts +2 -3
  279. package/src/lib/room-selection.test.ts +45 -1
  280. package/src/lib/scene-clipboard.test.ts +246 -1
  281. package/src/lib/scene-clipboard.ts +44 -14
  282. package/src/lib/selection-routing.ts +1 -1
  283. package/src/lib/sfx-bus.ts +13 -0
  284. package/src/lib/sfx-player.test.ts +70 -9
  285. package/src/lib/sfx-player.ts +6 -0
  286. package/src/lib/stair-levels.test.ts +25 -0
  287. package/src/lib/stair-levels.ts +22 -3
  288. package/src/lib/terrace-draft.ts +2 -1
  289. package/src/lib/viewer-selection.ts +22 -11
  290. package/src/lib/wall-placement-pointer.test.ts +42 -0
  291. package/src/lib/wall-placement-pointer.ts +57 -0
  292. package/src/lib/window-changes.ts +35 -0
  293. package/src/store/use-audio.test.ts +146 -0
  294. package/src/store/use-audio.tsx +127 -3
  295. package/src/store/use-construction-reveal.test.ts +173 -0
  296. package/src/store/use-construction-reveal.ts +145 -0
  297. package/src/store/use-corner-snap-hint.test.ts +24 -0
  298. package/src/store/use-corner-snap-hint.ts +47 -0
  299. package/src/store/use-follow-pascal.test.ts +62 -0
  300. package/src/store/use-follow-pascal.ts +53 -0
  301. package/src/store/use-point-ask.ts +247 -0
  302. package/src/store/use-stair-build-preview.ts +25 -5
  303. package/src/store/view-layout.test.ts +27 -1
  304. package/src/store/view-layout.ts +4 -1
  305. package/src/test-utils/render-current-markup.tsx +16 -0
  306. package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
  307. package/src/components/tools/stair/stair-click-guard.ts +0 -72
  308. package/src/components/tools/stair/stair-defaults.ts +0 -15
  309. package/src/components/tools/stair/stair-tool.tsx +0 -682
@@ -0,0 +1,195 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
2
+ import {
3
+ BuildingNode,
4
+ clearSceneHistory,
5
+ emitter,
6
+ endScenePreview,
7
+ ItemNode,
8
+ isScenePreviewing,
9
+ LevelNode,
10
+ registerPointAskHandler,
11
+ useScene,
12
+ ZoneNode,
13
+ } from '@pascal-app/core'
14
+ import { type BubbleTarget, usePointAsk } from '../../store/use-point-ask'
15
+ import { endCompare, sendBubble, startCompare, startPointAskSession } from './session'
16
+
17
+ // Hold to see before, for a turn that BUILDS: "Furnish this room" on an empty room creates nodes
18
+ // the "before" has never heard of. The ask-flow suite holds turns that only change a wall; the
19
+ // owner held an ask that furnished an empty room (13 October) and the furniture stayed. Real
20
+ // stores; only the chat's words (statuses) and the DOM are stood in for.
21
+
22
+ const building = BuildingNode.parse({ id: 'building_created' })
23
+ const level = LevelNode.parse({ id: 'level_created', parentId: building.id })
24
+ const room = ZoneNode.parse({
25
+ id: 'zone_created',
26
+ parentId: level.id,
27
+ name: 'Formal Dining Room',
28
+ polygon: [
29
+ [0, 0],
30
+ [5, 0],
31
+ [5, 4],
32
+ [0, 4],
33
+ ],
34
+ })
35
+ building.children = [level.id]
36
+ level.children = [room.id]
37
+
38
+ const original = useScene.getState()
39
+ const originalWindow = globalThis.window
40
+ const originalDocument = globalThis.document
41
+ const originalRaf = globalThis.requestAnimationFrame
42
+ const originalCancelRaf = globalThis.cancelAnimationFrame
43
+ let stopSession = () => {}
44
+ let unregisterHandler = () => {}
45
+
46
+ const target = {
47
+ id: room.id,
48
+ type: 'zone',
49
+ name: 'Formal Dining Room',
50
+ levelId: level.id,
51
+ size: { length: 5, height: 0, thickness: 4 },
52
+ box: { min: [0, 0, 0], max: [5, 2.6, 4] },
53
+ } as unknown as BubbleTarget
54
+
55
+ const furniture = (id: string, position: [number, number, number]) =>
56
+ ItemNode.parse({
57
+ id,
58
+ parentId: level.id,
59
+ position,
60
+ asset: {
61
+ id: `asset_${id}`,
62
+ name: 'Chair',
63
+ category: 'chair',
64
+ src: '/fixture-chair.glb',
65
+ thumbnail: '',
66
+ dimensions: [0.5, 0.8, 0.5],
67
+ },
68
+ })
69
+
70
+ function resetScene() {
71
+ useScene.setState({
72
+ nodes: { [building.id]: building, [level.id]: level, [room.id]: room },
73
+ rootNodeIds: [building.id],
74
+ dirtyNodes: new Set(),
75
+ readOnly: false,
76
+ } as never)
77
+ clearSceneHistory()
78
+ }
79
+
80
+ beforeEach(() => {
81
+ const stub = { addEventListener() {}, removeEventListener() {} }
82
+ globalThis.window = { ...stub, innerWidth: 1280, innerHeight: 800 } as never
83
+ globalThis.document = { ...stub, hidden: false } as never
84
+ globalThis.requestAnimationFrame = (callback) => {
85
+ callback(0)
86
+ return 0
87
+ }
88
+ globalThis.cancelAnimationFrame = () => {}
89
+ resetScene()
90
+ usePointAsk.setState({ pins: {}, pinOrder: [], bubble: null, comparing: null, busy: false })
91
+ unregisterHandler = registerPointAskHandler(async () => ({ ok: true }))
92
+ stopSession = startPointAskSession()
93
+ })
94
+
95
+ afterEach(() => {
96
+ endCompare()
97
+ endScenePreview()
98
+ stopSession()
99
+ unregisterHandler()
100
+ useScene.setState(original, true)
101
+ clearSceneHistory()
102
+ globalThis.window = originalWindow
103
+ globalThis.document = originalDocument
104
+ if (originalRaf) globalThis.requestAnimationFrame = originalRaf
105
+ else Reflect.deleteProperty(globalThis, 'requestAnimationFrame')
106
+ if (originalCancelRaf) globalThis.cancelAnimationFrame = originalCancelRaf
107
+ else Reflect.deleteProperty(globalThis, 'cancelAnimationFrame')
108
+ })
109
+
110
+ async function sendAsk(text = 'furnish this room') {
111
+ usePointAsk.getState().openBubble({
112
+ targets: [target],
113
+ anchor: [2.5, 0, 2],
114
+ hit: { point: [2.5, 0, 2], normal: [0, 1, 0] },
115
+ gesture: 'click',
116
+ capturedAt: new Date().toISOString(),
117
+ camera: null,
118
+ crop: { state: 'failed' },
119
+ draft: '',
120
+ error: null,
121
+ })
122
+ await sendBubble(text, { x: 0, y: 0, width: 10, height: 10 })
123
+ const askId = usePointAsk.getState().pinOrder.at(-1)
124
+ if (!askId) throw new Error('the ask did not plant a pin')
125
+ return askId
126
+ }
127
+
128
+ const status = (askId: string, rest: Record<string, unknown>) =>
129
+ emitter.emit('point-ask:status', { askId, ...rest } as never)
130
+
131
+ /** A scene action of the agent: history tracks it, and is left as it was found. */
132
+ function agentWrite(write: () => void) {
133
+ const wasTracking = useScene.temporal.getState().isTracking
134
+ useScene.temporal.getState().resume()
135
+ try {
136
+ write()
137
+ } finally {
138
+ if (!wasTracking) useScene.temporal.getState().pause()
139
+ }
140
+ }
141
+
142
+ const has = (id: string) => Boolean(useScene.getState().nodes[id as never])
143
+
144
+ describe('hold to see before, for a turn that builds', () => {
145
+ test('the furniture the turn placed is not in the before', async () => {
146
+ const askId = await sendAsk()
147
+ status(askId, { status: 'working', sentence: 'Furnishing the room' })
148
+ agentWrite(() =>
149
+ useScene.getState().createNodes([
150
+ { node: furniture('item_table', [2.5, 0, 2]), parentId: level.id },
151
+ { node: furniture('item_chair_a', [1.5, 0, 2]), parentId: level.id },
152
+ { node: furniture('item_chair_b', [3.5, 0, 2]), parentId: level.id },
153
+ ] as never),
154
+ )
155
+ status(askId, { status: 'ended', outcome: 'completed', asked: false, wroteScene: true })
156
+ expect(usePointAsk.getState().pins[askId]?.model.state).toBe('done')
157
+ expect(has('item_table')).toBe(true)
158
+
159
+ expect(startCompare(askId)).toBe(true)
160
+ expect(isScenePreviewing()).toBe(true)
161
+ expect(has('item_table')).toBe(false)
162
+ expect(has('item_chair_a')).toBe(false)
163
+ expect(has('item_chair_b')).toBe(false)
164
+ expect(
165
+ (useScene.getState().nodes[level.id] as { children: string[] }).children.includes(
166
+ 'item_table',
167
+ ),
168
+ ).toBe(false)
169
+
170
+ endCompare()
171
+ expect(has('item_table')).toBe(true)
172
+ expect(has('item_chair_b')).toBe(true)
173
+ })
174
+
175
+ test('furniture placed in several steps, one after another, is all out of the before', async () => {
176
+ const askId = await sendAsk()
177
+ status(askId, { status: 'working', sentence: 'Furnishing the room' })
178
+ agentWrite(() =>
179
+ useScene
180
+ .getState()
181
+ .createNodes([{ node: furniture('item_table', [2.5, 0, 2]), parentId: level.id }] as never),
182
+ )
183
+ agentWrite(() =>
184
+ useScene
185
+ .getState()
186
+ .createNodes([
187
+ { node: furniture('item_chair_a', [1.5, 0, 2]), parentId: level.id },
188
+ ] as never),
189
+ )
190
+ status(askId, { status: 'ended', outcome: 'completed', asked: false, wroteScene: true })
191
+ expect(startCompare(askId)).toBe(true)
192
+ expect(has('item_table')).toBe(false)
193
+ expect(has('item_chair_a')).toBe(false)
194
+ })
195
+ })
@@ -0,0 +1,380 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
2
+ import {
3
+ BuildingNode,
4
+ clearSceneHistory,
5
+ emitter,
6
+ endScenePreview,
7
+ isScenePreviewing,
8
+ LevelNode,
9
+ registerPointAskHandler,
10
+ useScene,
11
+ WallNode,
12
+ } from '@pascal-app/core'
13
+ import { asksWait, type BubbleTarget, usePointAsk } from '../../store/use-point-ask'
14
+ import { setPointAskBehindQueue, setPointAskBusy } from './host-api'
15
+ import { endCompare, sendBubble, startCompare, startPointAskSession } from './session'
16
+
17
+ // The whole ask path with the real stores (hold to see before; an ask while Pascal is working): the session listening to the chat's
18
+ // statuses, a turn writing the scene through the store's own actions (each a history step), the pin
19
+ // reaching "done", and the hold. Only the chat's words (statuses) and the DOM are stood in for.
20
+
21
+ const building = BuildingNode.parse({ id: 'building_flow' })
22
+ const level = LevelNode.parse({ id: 'level_flow', parentId: building.id })
23
+ const wall = WallNode.parse({
24
+ id: 'wall_flow',
25
+ parentId: level.id,
26
+ start: [0, 0],
27
+ end: [4, 0],
28
+ height: 2.6,
29
+ })
30
+ building.children = [level.id]
31
+ level.children = [wall.id]
32
+
33
+ const STOCK_HEIGHT = 2.6
34
+ const original = useScene.getState()
35
+ const originalWindow = globalThis.window
36
+ const originalDocument = globalThis.document
37
+ const originalRaf = globalThis.requestAnimationFrame
38
+ const originalCancelRaf = globalThis.cancelAnimationFrame
39
+ let stopSession = () => {}
40
+ let unregisterHandler = () => {}
41
+
42
+ const target = {
43
+ id: wall.id,
44
+ type: 'wall',
45
+ name: 'Wall',
46
+ levelId: level.id,
47
+ size: { length: 4, height: STOCK_HEIGHT, thickness: 0.2 },
48
+ box: { min: [0, 0, -0.1], max: [4, STOCK_HEIGHT, 0.1] },
49
+ } as unknown as BubbleTarget
50
+
51
+ function resetScene() {
52
+ useScene.setState({
53
+ nodes: { [building.id]: building, [level.id]: level, [wall.id]: wall },
54
+ rootNodeIds: [building.id],
55
+ dirtyNodes: new Set(),
56
+ readOnly: false,
57
+ } as never)
58
+ clearSceneHistory()
59
+ }
60
+
61
+ beforeEach(() => {
62
+ const stub = { addEventListener() {}, removeEventListener() {} }
63
+ globalThis.window = { ...stub, innerWidth: 1280, innerHeight: 800 } as never
64
+ globalThis.document = { ...stub, hidden: false } as never
65
+ globalThis.requestAnimationFrame = (callback) => {
66
+ callback(0)
67
+ return 0
68
+ }
69
+ globalThis.cancelAnimationFrame = () => {}
70
+ resetScene()
71
+ usePointAsk.setState({ pins: {}, pinOrder: [], bubble: null, comparing: null, busy: false })
72
+ unregisterHandler = registerPointAskHandler(async () => ({ ok: true }))
73
+ stopSession = startPointAskSession()
74
+ })
75
+
76
+ afterEach(() => {
77
+ endCompare()
78
+ endScenePreview()
79
+ stopSession()
80
+ unregisterHandler()
81
+ useScene.setState(original, true)
82
+ clearSceneHistory()
83
+ globalThis.window = originalWindow
84
+ globalThis.document = originalDocument
85
+ if (originalRaf) globalThis.requestAnimationFrame = originalRaf
86
+ else Reflect.deleteProperty(globalThis, 'requestAnimationFrame')
87
+ if (originalCancelRaf) globalThis.cancelAnimationFrame = originalCancelRaf
88
+ else Reflect.deleteProperty(globalThis, 'cancelAnimationFrame')
89
+ })
90
+
91
+ /** The person points at the wall and sends; returns the ask's id. */
92
+ async function sendAsk() {
93
+ usePointAsk.getState().openBubble({
94
+ targets: [target],
95
+ anchor: [2, 1, 0],
96
+ hit: { point: [2, 1, 0], normal: [0, 0, 1] },
97
+ gesture: 'click',
98
+ capturedAt: new Date().toISOString(),
99
+ camera: null,
100
+ crop: { state: 'failed' },
101
+ draft: '',
102
+ error: null,
103
+ })
104
+ await sendBubble('make this wall taller', { x: 0, y: 0, width: 10, height: 10 })
105
+ const askId = usePointAsk.getState().pinOrder.at(-1)
106
+ if (!askId) throw new Error('the ask did not plant a pin')
107
+ return askId
108
+ }
109
+
110
+ const status = (askId: string, rest: Record<string, unknown>) =>
111
+ emitter.emit('point-ask:status', { askId, ...rest } as never)
112
+
113
+ /** A scene action of the agent: history tracks it, and is left as it was found (the hosted executor's contract). */
114
+ function agentWrite(write: () => void) {
115
+ const wasTracking = useScene.temporal.getState().isTracking
116
+ useScene.temporal.getState().resume()
117
+ try {
118
+ write()
119
+ } finally {
120
+ if (!wasTracking) useScene.temporal.getState().pause()
121
+ }
122
+ }
123
+
124
+ /** What the hosted executor did before it was fixed: always paused after, whatever it found. */
125
+ function leftoverPause(write: () => void) {
126
+ useScene.temporal.getState().resume()
127
+ try {
128
+ write()
129
+ } finally {
130
+ useScene.temporal.getState().pause()
131
+ }
132
+ }
133
+
134
+ const wallHeight = () => (useScene.getState().nodes[wall.id] as { height: number }).height
135
+
136
+ describe('a history left untracked after the turn', () => {
137
+ test('refuses the hold, and the card is told why (it was silent: the owner, 12 October)', async () => {
138
+ const askId = await sendAsk()
139
+ status(askId, { status: 'working', sentence: 'Raising the wall' })
140
+ leftoverPause(() => useScene.getState().updateNode(wall.id, { height: 3.4 }))
141
+ status(askId, { status: 'ended', outcome: 'completed', asked: false, wroteScene: true })
142
+ expect(startCompare(askId)).toBe(false)
143
+ expect(isScenePreviewing()).toBe(false)
144
+ expect(usePointAsk.getState().compareRefusal).toEqual({ askId, reason: 'history-held' })
145
+ })
146
+ })
147
+
148
+ describe('hold to see before, end to end', () => {
149
+ test('a turn that wrote the scene can be held to show the scene as it was', async () => {
150
+ const askId = await sendAsk()
151
+ status(askId, { status: 'working', sentence: 'Raising the wall' })
152
+ agentWrite(() => useScene.getState().updateNode(wall.id, { height: 3.4 }))
153
+ status(askId, {
154
+ status: 'ended',
155
+ outcome: 'completed',
156
+ asked: false,
157
+ wroteScene: true,
158
+ })
159
+ expect(usePointAsk.getState().pins[askId]?.model.state).toBe('done')
160
+ expect(wallHeight()).toBe(3.4)
161
+
162
+ expect(startCompare(askId)).toBe(true)
163
+ expect(isScenePreviewing()).toBe(true)
164
+ expect(wallHeight()).toBe(STOCK_HEIGHT)
165
+ expect(usePointAsk.getState().comparing).toBe(askId)
166
+
167
+ endCompare()
168
+ expect(isScenePreviewing()).toBe(false)
169
+ expect(wallHeight()).toBe(3.4)
170
+ expect(usePointAsk.getState().comparing).toBeNull()
171
+ })
172
+
173
+ test('a turn of several writes shows the state from before the first of them', async () => {
174
+ const askId = await sendAsk()
175
+ status(askId, { status: 'working', sentence: 'Building' })
176
+ agentWrite(() => useScene.getState().updateNode(wall.id, { height: 3 }))
177
+ agentWrite(() => useScene.getState().updateNode(wall.id, { height: 3.2 }))
178
+ agentWrite(() => useScene.getState().updateNode(wall.id, { height: 3.4 }))
179
+ status(askId, { status: 'ended', outcome: 'completed', asked: false, wroteScene: true })
180
+ expect(startCompare(askId)).toBe(true)
181
+ expect(wallHeight()).toBe(STOCK_HEIGHT)
182
+ })
183
+
184
+ test('a write that lands before the chat reports "working" is not part of the before', async () => {
185
+ const askId = await sendAsk()
186
+ // The chat accepted the ask; its first write comes before its first status.
187
+ agentWrite(() => useScene.getState().updateNode(wall.id, { height: 3.4 }))
188
+ status(askId, { status: 'working', sentence: 'Reading your request' })
189
+ agentWrite(() => useScene.getState().updateNode(wall.id, { height: 3.8 }))
190
+ status(askId, { status: 'ended', outcome: 'completed', asked: false, wroteScene: true })
191
+ expect(startCompare(askId)).toBe(true)
192
+ expect(wallHeight()).toBe(STOCK_HEIGHT)
193
+ })
194
+
195
+ test('a turn that is not writing does not stop the hold, even when a pin never heard its end', async () => {
196
+ const first = await sendAsk()
197
+ status(first, { status: 'working', sentence: 'Raising the wall' })
198
+ agentWrite(() => useScene.getState().updateNode(wall.id, { height: 3.4 }))
199
+ status(first, { status: 'ended', outcome: 'completed', asked: false, wroteScene: true })
200
+ // Another ask whose end the pin never heard: it says "working", but the chat is idle.
201
+ const stuck = usePointAsk.getState().pins[first]
202
+ if (!stuck) throw new Error('the ask has no pin')
203
+ usePointAsk.getState().addPin({
204
+ ...stuck,
205
+ model: { ...stuck.model, askId: 'ask_stuck', state: 'working' },
206
+ })
207
+ usePointAsk.setState({ busy: false })
208
+ expect(startCompare(first)).toBe(true)
209
+ endCompare()
210
+ usePointAsk.setState({ busy: true })
211
+ expect(startCompare(first)).toBe(false)
212
+ expect(usePointAsk.getState().compareRefusal).toEqual({
213
+ askId: first,
214
+ reason: 'turn-working',
215
+ })
216
+ })
217
+ })
218
+
219
+ function openBubbleOnWall(draft = '') {
220
+ usePointAsk.getState().openBubble({
221
+ targets: [target],
222
+ anchor: [2, 1, 0],
223
+ hit: { point: [2, 1, 0], normal: [0, 0, 1] },
224
+ gesture: 'click',
225
+ capturedAt: new Date().toISOString(),
226
+ camera: null,
227
+ crop: { state: 'failed' },
228
+ draft,
229
+ error: null,
230
+ })
231
+ }
232
+
233
+ describe('an ask while Pascal is working', () => {
234
+ test('is handed to the chat, which queues it as its own turn: the pin says queued', async () => {
235
+ const handed: string[] = []
236
+ unregisterHandler()
237
+ unregisterHandler = registerPointAskHandler(async (submit) => {
238
+ handed.push(submit.askId)
239
+ emitter.emit('point-ask:status', { askId: submit.askId, status: 'queued' } as never)
240
+ return { ok: true }
241
+ })
242
+ usePointAsk.setState({ busy: true })
243
+ openBubbleOnWall()
244
+ await sendBubble('make this wall taller', { x: 0, y: 0, width: 10, height: 10 })
245
+ const state = usePointAsk.getState()
246
+ expect(handed).toHaveLength(1)
247
+ expect(state.bubble).toBeNull()
248
+ expect(state.pinOrder).toHaveLength(1)
249
+ expect(state.pins[handed[0]!]?.model.state).toBe('queued')
250
+ })
251
+
252
+ test("takes none of the running turn's changes, and its before is the scene when its own turn starts", async () => {
253
+ unregisterHandler()
254
+ unregisterHandler = registerPointAskHandler(async (submit) => {
255
+ emitter.emit('point-ask:status', { askId: submit.askId, status: 'queued' } as never)
256
+ return { ok: true }
257
+ })
258
+ // An earlier ask is running and writes.
259
+ const running = await sendAsk()
260
+ status(running, { status: 'working', sentence: 'Raising the wall' })
261
+ usePointAsk.setState({ busy: true })
262
+ openBubbleOnWall()
263
+ await sendBubble('paint it', { x: 0, y: 0, width: 10, height: 10 })
264
+ const queued = usePointAsk.getState().pinOrder.at(-1)!
265
+ agentWrite(() => useScene.getState().updateNode(wall.id, { height: 3 }))
266
+ status(running, { status: 'ended', outcome: 'completed', asked: false, wroteScene: true })
267
+ expect(usePointAsk.getState().pins[queued]?.model.committed).toBe(false)
268
+ // Its own turn starts and writes.
269
+ status(queued, { status: 'working', sentence: 'Painting' })
270
+ agentWrite(() => useScene.getState().updateNode(wall.id, { height: 3.4 }))
271
+ status(queued, { status: 'ended', outcome: 'completed', asked: false, wroteScene: true })
272
+ const pin = usePointAsk.getState().pins[queued]?.model
273
+ expect(pin?.state).toBe('done')
274
+ expect(pin?.changedTargetIds).toEqual([wall.id])
275
+ expect(startCompare(queued)).toBe(true)
276
+ // The state it started from: the running turn's change is in it.
277
+ expect(wallHeight()).toBe(3)
278
+ })
279
+
280
+ test('a host that does not queue answers busy, and the bubble comes back with its reason', async () => {
281
+ unregisterHandler()
282
+ unregisterHandler = registerPointAskHandler(async () => ({
283
+ ok: false,
284
+ code: 'busy',
285
+ message: 'Pascal is working. Ask again when it is done.',
286
+ }))
287
+ openBubbleOnWall()
288
+ await sendBubble('make this wall taller', { x: 0, y: 0, width: 10, height: 10 })
289
+ const state = usePointAsk.getState()
290
+ expect(state.pinOrder).toHaveLength(0)
291
+ expect(state.bubble?.error).toBe('Pascal is working. Ask again when it is done.')
292
+ })
293
+ })
294
+
295
+ describe("which pin takes the scene's changes", () => {
296
+ test("a stale pin never absorbs the running ask's changes: they go to its pin alone", async () => {
297
+ // Two earlier asks said "working" and the pin never heard their end (a dropped turn, a reload of
298
+ // the chat), and they point at the same wall.
299
+ const staleWorking = await sendAsk()
300
+ status(staleWorking, { status: 'working', sentence: 'Raising the wall' })
301
+ const staleAsking = await sendAsk()
302
+ status(staleAsking, { status: 'working', sentence: 'Painting' })
303
+ status(staleAsking, { status: 'asking', question: 'Which colour?' })
304
+ // The ask that runs now.
305
+ const running = await sendAsk()
306
+ status(running, { status: 'working', sentence: 'Reading your request' })
307
+ agentWrite(() => useScene.getState().updateNode(wall.id, { height: 3.4 }))
308
+ status(running, { status: 'ended', outcome: 'completed', asked: false, wroteScene: true })
309
+
310
+ const { pins } = usePointAsk.getState()
311
+ expect(pins[running]?.model.state).toBe('done')
312
+ expect(pins[running]?.model.changedTargetIds).toEqual([wall.id])
313
+ for (const stale of [staleWorking, staleAsking]) {
314
+ expect(pins[stale]?.model.committed).toBe(false)
315
+ expect(pins[stale]?.model.changedTargetIds).toEqual([])
316
+ expect(pins[stale]?.model.alsoChangedIds).toEqual([])
317
+ expect(pins[stale]?.model.state).not.toBe('done')
318
+ }
319
+ })
320
+
321
+ test('changes made before any ask is working go to no pin', async () => {
322
+ const waiting = await sendAsk()
323
+ agentWrite(() => useScene.getState().updateNode(wall.id, { height: 3.4 }))
324
+ status(waiting, { status: 'ended', outcome: 'completed', asked: false, wroteScene: false })
325
+ expect(usePointAsk.getState().pins[waiting]?.model.committed).toBe(false)
326
+ })
327
+
328
+ test("a stale pin is settled when another ask starts working: it is not left 'working'", async () => {
329
+ const stale = await sendAsk()
330
+ status(stale, { status: 'working', sentence: 'Raising the wall' })
331
+ const running = await sendAsk()
332
+ status(running, { status: 'working', sentence: 'Reading your request' })
333
+ expect(usePointAsk.getState().pins[stale]?.model.state).not.toBe('working')
334
+ expect(usePointAsk.getState().pins[running]?.model.state).toBe('working')
335
+ })
336
+
337
+ test("a plain message's changes after an ask whose end was never heard are no ask's", async () => {
338
+ const stale = await sendAsk()
339
+ setPointAskBusy(true)
340
+ status(stale, { status: 'working', sentence: 'Raising the wall' })
341
+ // The chat goes idle without the pin having heard the end, then a plain message runs.
342
+ setPointAskBusy(false)
343
+ setPointAskBusy(true)
344
+ agentWrite(() => useScene.getState().updateNode(wall.id, { height: 3.4 }))
345
+ setPointAskBusy(false)
346
+ const pin = usePointAsk.getState().pins[stale]?.model
347
+ expect(pin?.committed).toBe(false)
348
+ expect(pin?.state).not.toBe('working')
349
+ })
350
+ })
351
+
352
+ describe('messages already queued or held', () => {
353
+ // The chat queues an ask behind messages that wait (a queue held after an error needs Resume),
354
+ // with no turn running: the bubble must say Queue, but nothing is working.
355
+ test('a new ask waits behind them: the bubble says Queue, and no turn is said to run', () => {
356
+ expect(asksWait(usePointAsk.getState())).toBe(false)
357
+ setPointAskBehindQueue(true)
358
+ const state = usePointAsk.getState()
359
+ expect(asksWait(state)).toBe(true)
360
+ expect(state.busy).toBe(false)
361
+ setPointAskBehindQueue(false)
362
+ expect(asksWait(usePointAsk.getState())).toBe(false)
363
+ })
364
+
365
+ test('a running turn makes asks wait too, whether or not anything is queued', () => {
366
+ setPointAskBusy(true)
367
+ expect(asksWait(usePointAsk.getState())).toBe(true)
368
+ setPointAskBusy(false)
369
+ expect(asksWait(usePointAsk.getState())).toBe(false)
370
+ })
371
+
372
+ test('they do not refuse a hold, nor settle a pin the way an idle chat does', async () => {
373
+ const askId = await sendAsk()
374
+ status(askId, { status: 'working', sentence: 'Raising the wall' })
375
+ agentWrite(() => useScene.getState().updateNode(wall.id, { height: 3.4 }))
376
+ status(askId, { status: 'ended', outcome: 'completed', asked: false, wroteScene: true })
377
+ setPointAskBehindQueue(true)
378
+ expect(startCompare(askId)).toBe(true)
379
+ })
380
+ })