verify-region-moving-admission.mjs 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. import assert from 'node:assert/strict'
  2. import { createRegionLoadingController } from '../../src/modules/cockpit/services/regionLoading.js'
  3. // Replay the load boundary seen in the low-spec exports: coarse is ready,
  4. // close candidates remain visible, but the camera never stops moving.
  5. let clock = 0, pose = 'initial', visible = false
  6. const started = [], pending = new Map()
  7. const controller = createRegionLoadingController({
  8. regions: [1, 2].map(id => ({ id, contentCount: 1, decodedGeometryBytes: 1024 })),
  9. load: region => { started.push(region.id); return new Promise(resolve => pending.set(region.id, resolve)) },
  10. add() {}, remove() {}, requestRender() {},
  11. distance: region => visible ? region.id * 10 : Infinity,
  12. canStart: () => true, readCameraKey: () => pose, now: () => clock,
  13. setTimer: () => 1, clearTimer() {},
  14. })
  15. try {
  16. controller.refresh()
  17. visible = true
  18. for (let i = 1; i <= 8; i++) {
  19. clock = i * 180
  20. pose = 'moving-' + i
  21. controller.onRender()
  22. }
  23. assert.equal(controller.frontierSnapshot().moving, true)
  24. const whileMoving = started.slice(), movingCount = controller.snapshot().length
  25. clock += 1000
  26. controller.refresh()
  27. console.log(JSON.stringify({ whileMoving, afterSettling: started }))
  28. assert.deepEqual(started, [1, 2], 'Same candidates start under the unchanged stationary concurrency limit')
  29. assert.deepEqual(whileMoving, [1], 'Continuous movement must admit one nearest region, not starve all fine loading')
  30. assert.equal(movingCount, 1, 'Movement keeps at most one initial load in flight')
  31. console.log('PASS moving admission: visible near candidate progresses through one bounded load slot')
  32. } finally {
  33. controller.detach()
  34. for (const resolve of pending.values()) resolve({ show: false, tilesLoaded: false })
  35. await Promise.resolve()
  36. }
  37. // Boundary cases use the same public controller and explicit asynchronous loads.
  38. const flush = async () => { await Promise.resolve(); await Promise.resolve() }
  39. async function check(options, scenario) {
  40. const { count = 3, ...controllerOptions } = options
  41. let clock = 0, pose = 0
  42. const state = { visible: false, coarseReady: true }
  43. const started = [], added = [], removed = [], models = new Map(), waits = new Map()
  44. const controller = createRegionLoadingController({
  45. regions: Array.from({ length: count }, (_, i) => ({ id: i + 1, contentCount: 1, decodedGeometryBytes: 1024 })),
  46. distance: r => state.visible ? r.id * 10 : Infinity,
  47. readCameraKey: () => String(pose), now: () => clock,
  48. canStart: () => state.coarseReady,
  49. coverageReady: e => e.instance.covered === true,
  50. load: r => { started.push(r.id); return new Promise((resolve, reject) => waits.set(r.id, { resolve, reject })) },
  51. add: m => added.push(m), remove: m => removed.push(m), requestRender() {},
  52. setTimer: () => 1, clearTimer() {},
  53. ...controllerOptions,
  54. })
  55. controller.refresh()
  56. const h = {
  57. controller, state, started, added, removed,
  58. async step(ms = 180, moving = true) { clock += ms; if (moving) pose++; controller.onRender(); await flush() },
  59. async resolveLoad(id, ready = false) {
  60. const wait = waits.get(id)
  61. assert.ok(wait, 'Only pending loads can resolve')
  62. const m = { show: false, tilesLoaded: ready, covered: ready }
  63. models.set(id, m); wait.resolve(m); waits.delete(id); await flush(); return m
  64. },
  65. markReady(id) { const m = models.get(id); m.tilesLoaded = m.covered = true },
  66. async fail(id) { waits.get(id).reject(new Error('fixture load failed')); waits.delete(id); await flush() },
  67. }
  68. try { await scenario(h) } finally {
  69. controller.detach()
  70. for (const wait of waits.values()) wait.resolve({ show: false, tilesLoaded: false, covered: false })
  71. await flush()
  72. }
  73. }
  74. await check({}, async h => {
  75. h.state.visible = true
  76. await h.step()
  77. assert.deepEqual(h.started, [1], 'Nearest candidate starts first during movement')
  78. await h.resolveLoad(1) // fromUrl resolved, but content is not ready yet.
  79. await h.step()
  80. assert.deepEqual(h.started, [1], 'Streaming keeps the single moving slot occupied')
  81. assert.equal(h.controller.detailReady(1), false, 'Incomplete coverage keeps coarse fallback')
  82. h.markReady(1)
  83. await h.step()
  84. assert.deepEqual(h.started, [1, 2], 'Arrival releases the slot without waiting for camera settle')
  85. })
  86. await check({}, async h => {
  87. h.state.visible = true
  88. await h.step(300, false)
  89. assert.deepEqual(h.started, [1, 2], 'Stationary phase retains configured parallelism')
  90. await h.step()
  91. assert.equal(h.removed.length, 0, 'Moving does not cancel the two existing initial loads')
  92. await h.resolveLoad(1, true); await h.step()
  93. assert.deepEqual(h.started, [1, 2], 'Do not add a new load while one existing slot remains')
  94. await h.resolveLoad(2, true); await h.step()
  95. assert.deepEqual(h.started, [1, 2, 3], 'Resume bounded moving admission after existing work completes')
  96. })
  97. for (const budget of [{ maxContents: 1 }, { maxDecodedBytes: 1024 }]) {
  98. await check({ budget }, async h => {
  99. h.state.visible = true
  100. await h.step(); await h.resolveLoad(1, true); await h.step()
  101. await h.step(1000, false)
  102. assert.deepEqual(h.started, [1], 'Neither movement nor settling bypasses the selection budget')
  103. })
  104. }
  105. await check({ maxConcurrentLoads: 1 }, async h => {
  106. h.state.visible = true
  107. await h.step(300, false)
  108. assert.deepEqual(h.started, [1], 'Explicit single-slot configuration still applies when stationary')
  109. await h.resolveLoad(1, true); await h.step(300, false)
  110. assert.deepEqual(h.started, [1, 2])
  111. })
  112. await check({}, async h => {
  113. h.state.visible = true; h.state.coarseReady = false
  114. await h.step(); await h.step()
  115. assert.deepEqual(h.started, [], 'Coarse readiness gate remains mandatory')
  116. h.state.coarseReady = true
  117. await h.step(); assert.deepEqual(h.started, [1])
  118. h.controller.setPipeline(true)
  119. const m = await h.resolveLoad(1, true)
  120. await h.step()
  121. assert.equal(h.added.length, 0, 'Late completion cannot attach in pipeline mode')
  122. assert.deepEqual(h.removed, [m], 'Rejected late model is released exactly once')
  123. assert.deepEqual(h.started, [1], 'Pipeline mode does not admit new ground regions')
  124. })
  125. await check({ count: 1 }, async h => {
  126. h.state.visible = true; await h.step(); await h.fail(1)
  127. await h.step(); assert.deepEqual(h.started, [1], 'Failure cooldown prevents repeated starts')
  128. await h.step(15001)
  129. assert.deepEqual(h.started, [1, 1], 'Retry can progress during movement after cooldown')
  130. })
  131. await check({ count: 1 }, async h => {
  132. h.state.visible = true; await h.step()
  133. h.state.visible = false; await h.step()
  134. const m = await h.resolveLoad(1, true)
  135. assert.equal(h.added.length, 0, 'Withdrawn candidate cannot attach after async completion')
  136. assert.deepEqual(h.removed, [m])
  137. h.state.visible = true; await h.step()
  138. assert.deepEqual(h.started, [1, 1], 'Rejected completion releases its slot for a new candidate')
  139. })
  140. console.log('PASS moving admission boundaries: progress, streaming cap, static transition, budgets, coarse gate, cancellation, retry')