用户令「迭代完成前必须全量跑一遍所有测试」。起因是 iter27 台账只记 probe --click 未记 --full,t17_defer_probe 红了整个迭代无人发现——根因是闸清单散落 CLAUDE.md / process-conventions.md / test-conventions.md 三处、彼此不一致、且无任何机器校验。 - tools/run_all_gates.sh(新):全部验证闸的唯一入口与唯一清单,串跑冒烟/单元/探针 --click/--full/渲染/doc 三闸/静态扫描/熵增闸共十道,写回执 game/tests/.gates/receipt.json。 与 baseline.json 比对判定:基线红且红项清单未变=存量红零新增(不判失败), 基线绿转红或红项清单变化=新增红(判红)。python 解释器自动解析带 PIL 的解释器 并前置 PATH(渲染闸三个脚本依赖 PIL,用错解释器会假红)。 - .zcode/hooks/worktree-guard.sh:Bash 分支加合回闸——merge/rebase/cherry-pick 进 master 前按目标分支反查 worktree 回执,校验 commit 匹配/非 quick/无新增红三态, 任一不满足 exit 2 拦截(九态实测:匹配放行、commit 错/quick/含红/无回执/rebase 全拦)。 - game/smoke/t17_defer_probe.gd:隔离面从 main.save_manager = iso_c 改 ProbeUiUtil.isolate_save(iter27 已确立口径)。原写法对 getter-only 属性赋值无效 ——组合根 save_manager/meta 是 autoload 读视图,隔离必须落在单例上。修复后 --full 转绿。 - doc_link 三处死链归零:数据配置.md 已退役探针指路、元进度.md/天赋树.md 已改名文档链接。 - CLAUDE.md / process-conventions.md §3第10条 / test-conventions.md §4 三处 「交付前跑什么」统一为一句话=跑 run_all_gates.sh,删散落清单。 闸况:冒烟 0 / 单元 0 / --click 0 / --full 0 / 渲染 0 / doc_link 0 / doc_term 0 / 熵增 0;gdd_lint 与 static_scan 为存量红,已落 baseline.json 待判读清偿。
122 lines
5.6 KiB
GDScript
122 lines
5.6 KiB
GDScript
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||
# 演播后排窗定点探针(T17 用户令「伤害过程演完才切战斗胜利结算窗」——对应本改动的唯一关联测试):
|
||
# 真实信号路径驱动一场战斗到击杀胜利(结算行在排播队列),断言两条:
|
||
# ①胜利同帧:终局面未开 + 挂起态在案 + 队列非空(旧行为=同帧 flush+开窗,本断言即红——判别性);
|
||
# ②排播放完:终局面开启(演播完边沿派发——_on_pump_ended)。
|
||
# 存档/设置/元进度三面隔离(isolate_save 正主,meta 经 autoload 链同源),零真实档触碰。headless 可跑(不抓帧)。
|
||
extends Node
|
||
|
||
const ProbeUiUtil := preload("res://smoke/probe_ui_util.gd") # 隔离样板单点
|
||
|
||
var main
|
||
var _problems: Array = []
|
||
|
||
|
||
func _ready() -> void:
|
||
await get_tree().process_frame
|
||
main = load("res://presentation/main.tscn").instantiate()
|
||
add_child(main)
|
||
await get_tree().process_frame
|
||
# 存档/设置两面隔离(ProbeUiUtil.isolate_save 正主,先删后建防陈旧档泄入)——
|
||
# 隔离面走 attach_path 原地重置 autoload 单例:组合根 save_manager/meta 是getter-only
|
||
# 读视图,直接赋值无效,隔离必须落在单例上。
|
||
ProbeUiUtil.isolate_save(main, "t17_defer_probe", false, true, false)
|
||
# 本探针验证完整动画档的演播后排窗——强制 motion_scale=1.0(隔离档替换在 boot 后,
|
||
# 减动效已随真档 settings.cfg 应用于引擎态;减动效档=consume_now 快路、开窗即时,另属语义)
|
||
UiKit.motion_scale = 1.0
|
||
for i in 8:
|
||
await get_tree().process_frame
|
||
var lvbtn: Button = _find_button_by_sub(main._start_overlay, "开始冒险")
|
||
if lvbtn == null:
|
||
_fail("开始冒险按钮缺失")
|
||
return
|
||
lvbtn.pressed.emit()
|
||
for i in 60:
|
||
await get_tree().process_frame
|
||
if main.level_screen != null and main.level_screen.visible:
|
||
break
|
||
if main.level_screen == null or not main.level_screen.visible:
|
||
_fail("关卡画面未出现")
|
||
return
|
||
for i in 5:
|
||
await get_tree().process_frame
|
||
main.level_screen.press_candidate(0)
|
||
for i in 20:
|
||
await get_tree().process_frame
|
||
if main.run_scope.settlement == null:
|
||
_fail("战斗未启动")
|
||
return
|
||
# 直挖至击杀胜利(settle_batch 真实管线——step_row 信号→排播队列,与点击路径同链)
|
||
var ms = main.run_scope.settlement.minesweeper
|
||
var guard := 200
|
||
var t_ap_out := -1 # AP 用完/停手帧
|
||
var ap_clock := "" # 墙上时钟时间戳
|
||
var settle_clock := ""
|
||
while guard > 0 and String(main.run_scope.settlement.battle_result) == "":
|
||
guard -= 1
|
||
var plan = load("res://smoke/auto_player.gd").choose_dig(ms)
|
||
if plan.is_empty():
|
||
main.run_scope.settlement.settle_batch([{"action": "stop", "cell": Vector2i(-1, -1)}])
|
||
await get_tree().process_frame
|
||
continue
|
||
if plan.hero_cell != ms.hero_pos():
|
||
ms.settle_batch([{"action": "move", "cell": plan.hero_cell}])
|
||
ms.settle_batch([{"action": "dig", "cell": plan.dig_cell}])
|
||
if t_ap_out < 0 and main.run_scope.settlement.action_economy.phase != "Active":
|
||
t_ap_out = Time.get_ticks_msec()
|
||
ap_clock = _clock_ms()
|
||
settle_clock = ap_clock # 触发结算与 AP 用完同帧(核心同步)——同一墙上时刻
|
||
await get_tree().process_frame
|
||
if String(main.run_scope.settlement.battle_result) != "victory":
|
||
_fail("战斗未胜利(result='%s')" % str(main.run_scope.settlement.battle_result))
|
||
return
|
||
var t_settle := Time.get_ticks_msec()
|
||
var _rows_at_victory: int = main._row_pacer.pending_rows().size() # 时间戳①:结算入队完成帧(AP 耗尽/停手→五行段同步跑完→行全在队)
|
||
# 断言①胜利同帧:终局面未开 + 挂起在案 + 队列非空(判别性——旧行为同帧开窗即红)
|
||
if main._end_overlay.is_open():
|
||
_problems.append("胜利同帧终局面已开(演播后排窗失效——同帧 flush 旧路径回潮)")
|
||
if main._deferred_modal == null:
|
||
_problems.append("胜利同帧挂起态缺失(_deferred_modal == null)")
|
||
if main._row_pacer.pending_rows().is_empty():
|
||
_problems.append("胜利同帧排播队列已空(结算行未走排播链)")
|
||
var _played_rows: int = _rows_at_victory
|
||
# 断言②排播放完:终局面开启(20s 守卫——0.22s×行数 ×10 余量)
|
||
var waited := 0.0
|
||
while not main._end_overlay.is_open() and waited < 20.0:
|
||
await get_tree().process_frame
|
||
waited += get_process_delta_time()
|
||
var t_popup := Time.get_ticks_msec()
|
||
var popup_clock := _clock_ms() # 弹窗出现:墙上时钟时间戳
|
||
print("[t17_defer] AP点用完: %s" % ap_clock)
|
||
print("[t17_defer] 触发结算: %s(与 AP 用完同帧——核心同步,相差 0ms)" % settle_clock)
|
||
print("[t17_defer] 弹窗出现: %s(距触发结算 %d ms = 逐行演播时长,行数 %d)" % [popup_clock, t_popup - t_ap_out, _played_rows])
|
||
if not main._end_overlay.is_open():
|
||
_problems.append("排播放完后终局面未开(%.1fs——演播完边沿派发失守)" % waited)
|
||
if _problems.is_empty():
|
||
print("[t17_defer] PASS — 胜利同帧挂起+排播完开窗 两断言全过(waited=%.1fs)" % waited)
|
||
get_tree().quit(0)
|
||
else:
|
||
for p in _problems:
|
||
print("[t17_defer] FAIL — " + p)
|
||
get_tree().quit(2)
|
||
|
||
|
||
func _clock_ms() -> String:
|
||
var unix := Time.get_unix_time_from_system()
|
||
return "%s.%03d" % [Time.get_datetime_string_from_system(), int(fmod(unix, 1.0) * 1000.0) % 1000]
|
||
|
||
|
||
func _fail(why: String) -> void:
|
||
print("[t17_defer] FAIL — " + why)
|
||
get_tree().quit(2)
|
||
|
||
|
||
func _find_button_by_sub(root: Node, sub: String) -> Button:
|
||
if root is Button and String(root.text).contains(sub):
|
||
return root
|
||
for c in root.get_children():
|
||
var r := _find_button_by_sub(c, sub)
|
||
if r != null:
|
||
return r
|
||
return null
|