t39-handover-walkthrough.mjs 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. /**
  2. * 票 39 真机走查脚本(puppeteer):粗→精影像交接。
  3. *
  4. * **本仓现状**:本地 puppeteer 不可用(`@puppeteer/browsers` 目录为空,`ERR_MODULE_NOT_FOUND`),
  5. * 故本票的真机证据由维护者授权的 DSH 浏览器逐项采集(读数见票面「证据」一节)。
  6. * 本脚本按既有走查写法保留,供修复 puppeteer 后一键复跑;逻辑与当时的手工采集**逐条对应**。
  7. *
  8. * **不做的事(诚实边界)**:
  9. * · 「全程无肉眼可辨的方形跳变」是**维护者的目视判据**,本脚本只提供可观测读数与截图,不代判;
  10. * · 矩形**内部**随地形 LOD 的逐级换挡**不由整层 alpha 修复**(决策 08 明确)——
  11. * 本脚本只如实记录它是否可辨,修复评估归票 40。
  12. *
  13. * 运行(需先起 `yarn dev`,并把浏览器标签页切到前台 —— 隐藏标签页 rAF 冻结,强度推不动):
  14. * node .scratch/cockpit-shell-panel-layout/verify/t39-handover-walkthrough.mjs
  15. * 按 AGENTS.md:本脚本不在改代码后自动运行,由维护者明确要求时才跑。
  16. */
  17. import { existsSync } from 'node:fs'
  18. import { resolve } from 'node:path'
  19. const BASE = process.env.COCKPIT_URL || 'http://127.0.0.1:8000/#/cockpit'
  20. const results = []
  21. const check = (id, what, ok, detail) => {
  22. results.push({ id, what, ok })
  23. console.log(`${ok ? 'PASS' : 'FAIL'} ${id} ${what}${ok ? '' : `\n ${detail}`}`)
  24. }
  25. const puppeteerPath = resolve('node_modules/puppeteer')
  26. if (!existsSync(puppeteerPath)) {
  27. console.error('未找到本地 puppeteer(本仓已知问题:@puppeteer/browsers 为空目录)。')
  28. console.error('本票真机证据改由 DSH 浏览器采集,逐条记录见票面「证据」一节。')
  29. process.exit(2)
  30. }
  31. const puppeteer = (await import('puppeteer')).default
  32. const browser = await puppeteer.launch({ headless: 'new', args: ['--window-size=1733,1000'] })
  33. const page = await browser.newPage()
  34. await page.setViewport({ width: 1733, height: 1000 })
  35. const errors = []
  36. page.on('pageerror', (error) => errors.push(String(error && error.message)))
  37. page.on('console', (message) => {
  38. if (message.type() === 'error') errors.push(message.text())
  39. })
  40. const sleep = (ms) => new Promise((done) => setTimeout(done, ms))
  41. const readHandover = () => page.evaluate(() => {
  42. const api = window.__cockpitMap
  43. const host = document.querySelector('.cockpit-shell__map .map-canvas')
  44. return {
  45. handover: api ? api.getHandoverState() : null,
  46. fog: api ? api.getFogState() : null,
  47. areaState: host ? host.dataset.mapAreaState : null,
  48. hostAlpha: host ? host.dataset.mapHandoverAlpha : null,
  49. hostPhase: host ? host.dataset.mapHandoverPhase || host.dataset.mapHandover : null,
  50. hostBlock: host ? host.dataset.mapHandoverBlock : null,
  51. }
  52. })
  53. try {
  54. await page.goto(BASE, { waitUntil: 'domcontentloaded' })
  55. await page.waitForSelector('.cockpit-shell__map .map-canvas[data-map-engine="ready"]', { timeout: 90000 })
  56. // ---------- A1 全球/引导态:精细层必须是隐藏的 ----------
  57. {
  58. const initial = await readHandover()
  59. check(
  60. 'A1-global-hidden',
  61. '全球/引导态:精细层 alpha=0(保留粗底图),相位 global',
  62. initial.handover && initial.handover.phase === 'global' && initial.handover.alpha === 0,
  63. JSON.stringify(initial.handover),
  64. )
  65. check(
  66. 'A1b-hidden-still-prefetches',
  67. '隐藏期间仍可继续预取瓦片(**不是** show=false)—— 看不见≠停止加载',
  68. initial.handover && initial.handover.layers > 0,
  69. `layers=${initial.handover && initial.handover.layers}`,
  70. )
  71. }
  72. // ---------- A2 进入:雾先形成保护,才允许揭示 ----------
  73. {
  74. await page.evaluate(() => {
  75. const intro = document.querySelector('[data-cockpit-intro]')
  76. if (intro) intro.click()
  77. window.__cockpitMap.goHome({ immediate: false })
  78. })
  79. // 采样整段进入过程:记录 (fogStrength, alpha) 的先后关系。
  80. const samples = []
  81. for (let i = 0; i < 40; i++) {
  82. samples.push(await readHandover())
  83. await sleep(150)
  84. }
  85. const revealedEarly = samples.some((sample) => sample.handover && sample.handover.alpha > 0.05
  86. && sample.fog && sample.fog.strength < 0.99)
  87. check(
  88. 'A2-fog-before-imagery',
  89. '进入途中:**不得**在雾强度未到位(<0.99)时就把精细层推到可见(>0.05)',
  90. !revealedEarly,
  91. '采样中发现"影像先亮、雾后到"的帧 —— 正是决策 08 记的"矩形边缘先暴露"',
  92. )
  93. const settled = await readHandover()
  94. check(
  95. 'A2b-revealed-after-fog',
  96. '雾到位且瓦片就绪后,精细层应揭示(alpha→1)',
  97. settled.handover && (settled.handover.phase === 'revealed' || settled.handover.phase === 'entering'),
  98. JSON.stringify(settled.handover),
  99. )
  100. check(
  101. 'A2c-tiles-signal-is-proxy',
  102. '瓦片读数**如实标注**是代理信号(不宣称目标视野逐像素就绪)',
  103. settled.handover && settled.handover.tilesReadyIsProxy === true,
  104. JSON.stringify(settled.handover && settled.handover.tilesReadyIsProxy),
  105. )
  106. }
  107. // ---------- A3 返回:影像收起后**才**撤雾(反向交接) ----------
  108. {
  109. const order = []
  110. await page.evaluate(() => window.__cockpitMap.returnGlobal())
  111. for (let i = 0; i < 40; i++) {
  112. const sample = await readHandover()
  113. order.push({ alpha: sample.handover && sample.handover.alpha, fog: sample.fog && sample.fog.strength })
  114. await sleep(120)
  115. }
  116. // 在 alpha 仍 > 0.05 的任何一帧,雾都不该已经被撤到 0 —— 那就是"影像还在,保护没了"。
  117. const droppedWhileVisible = order.some((frame) => frame.alpha > 0.05 && frame.fog <= 0.01)
  118. check(
  119. 'A3-fog-held-until-hidden',
  120. '返回途中:精细层仍可见(alpha>0.05)时**不得**已把雾撤净',
  121. !droppedWhileVisible,
  122. '采样中发现"影像还在、雾已撤"的帧 —— 会重现矩形边缘',
  123. )
  124. const last = order[order.length - 1]
  125. check(
  126. 'A3b-both-settled',
  127. '返回结束:精细层归 0 且雾归 0',
  128. last && last.alpha <= 0.001 && last.fog <= 0.001,
  129. JSON.stringify(last),
  130. )
  131. // 同步性:影像与雾的推进**重叠**(不是"影像收完→停顿→雾才动")。
  132. const alphaMoves = order.filter((frame) => frame.alpha > 0.001 && frame.alpha < 0.999).length
  133. const fogMoves = order.filter((frame) => frame.fog > 0.001 && frame.fog < 0.999).length
  134. check(
  135. 'A3c-overlap-not-serial',
  136. '影像与雾的收束**有重叠**(不是串行等待),符合"高度与雾同步"',
  137. alphaMoves > 0 && fogMoves > 0,
  138. `alpha 过渡帧=${alphaMoves},雾过渡帧=${fogMoves}`,
  139. )
  140. }
  141. // ---------- A4 取消 / 快速反向:只认最新目标 ----------
  142. {
  143. await page.evaluate(() => {
  144. window.__cockpitMap.goHome({ immediate: true })
  145. window.__cockpitMap.enterArea()
  146. })
  147. await sleep(300)
  148. await page.evaluate(() => window.__cockpitMap.returnGlobal())
  149. await sleep(120)
  150. await page.evaluate(() => window.__cockpitMap.enterArea())
  151. await sleep(2500)
  152. const after = await readHandover()
  153. check(
  154. 'A4-latest-target-wins',
  155. '进→返回→再进:最终必须落在"已进区"的目标上,旧的反向不得生效',
  156. after.areaState === 'locked' && after.handover && after.handover.alpha > 0.5,
  157. JSON.stringify({ area: after.areaState, alpha: after.handover && after.handover.alpha }),
  158. )
  159. check(
  160. 'A4b-no-half-transparent-stuck',
  161. '快速反向不得留下半透明卡死态(落定值必须是 0 或 1)',
  162. after.handover && (after.handover.alpha === 0 || after.handover.alpha === 1),
  163. `alpha=${after.handover && after.handover.alpha}`,
  164. )
  165. }
  166. // ---------- A5 无"已遮边"承诺 ----------
  167. {
  168. const state = await readHandover()
  169. check(
  170. 'A5-no-false-promise',
  171. '读数**不得**提供"边缘已遮住"的承诺(就绪≠已遮住)',
  172. state.handover && !('protected' in state.handover) && !('coversEdges' in state.handover),
  173. JSON.stringify(state.handover),
  174. )
  175. }
  176. check('A6-no-page-errors', '全程无页面级报错', errors.length === 0, JSON.stringify(errors.slice(0, 3)))
  177. } finally {
  178. await browser.close()
  179. }
  180. const failed = results.filter((item) => !item.ok)
  181. console.log(`\nT39 走查:${results.length - failed.length}/${results.length} 通过`)
  182. if (failed.length) process.exit(1)