feat(迭代29 T4): L3 屏壳切换探针 screen_flow_probe,入轻闸默认段
新建 game/smoke/screen_flow_probe.gd/.tscn:走 菜单→营地→菜单→冒险→(关卡屏⇄战斗屏)→放弃→终局→菜单 六步主流程与两条回退路径,每步断言屏级互斥 + 流程状态位。与 ui_click_probe 分工:那件走「点某个按钮 有没有反应」的行为走查(含战斗推进与结算数值面),本件只走屏壳与流程状态,不推进战斗不碰结算数值 ——全库 change_scene 零命中、屏切换全靠 visible 互斥,这正是最容易被装配顺序改动悄悄破坏的面。 判据两处订正(首跑实测抓出,都写进注释防复发): ① 屏标记不能裸看 visible——战斗容器 battle_root 在局外也 visible,会把 boot 态误判成「菜单与冒险 同时在场」;冒险屏改取流程状态位 run_scope.is_adventure。 ② 互斥不能用「可见屏计数 ≤1」——终局覆盖层合法叠在冒险内容之上(终局态 is_adventure 尚未收束, 是设计如此);改为冲突对判定,合法叠加仅「终局+冒险」这一对。 变异验证:boot 后强置 camp_overlay.visible=true → 精确命中「菜单与营地同时在场」两条断言见红。 run_all.gd:负面套件(negative=true)全文输出折叠、只回显结论行——它输出里的 Parse Error 是预期产物, 回显出去会让编排层的日志扫描把集成/负面用例的正常输出误判成真实脚本错误(首跑即撞)。 入轻闸默认段(ui_contrast 之后);轻闸 --quick 墙钟 363s → 405s(新探针约 40s),轻闸上限内。
This commit is contained in:
1 parent
c4fa65ef44
commit
1458d94b0e
4 files changed
+206
-2
No files matched your search
@@ -0,0 +1,186 @@
|
||||
# screen_flow_probe.gd — L3 屏壳切换探针(迭代 29 T4)
|
||||
# 判的是「跑起来之后,屏与场景的切换对不对」:菜单 ⇄ 营地 ⇄ 冒险 ⇄ 终局 的主流程与回退路径,
|
||||
# 每一步断言 **屏级互斥**(同一时刻最多一个屏可见)+ 流程状态位落到正确值。
|
||||
# 与 ui_click_probe 的分工:那件走的是「点某个按钮有没有反应」的行为走查(含战斗推进与结算数值面);
|
||||
# 本件只走屏壳与流程状态,不推进战斗、不碰结算数值——屏级互斥是最容易被装配顺序改动悄悄破坏
|
||||
# 又最难被单测覆盖的面(全库 change_scene 零命中,屏切换全靠 visible 互斥)。
|
||||
# 运行:Godot_console.exe --headless --path . res://smoke/screen_flow_probe.tscn
|
||||
extends Node
|
||||
|
||||
const ProbeUiUtil := preload("res://smoke/probe_ui_util.gd")
|
||||
|
||||
var main: Node
|
||||
var _problems: Array[String] = []
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
await get_tree().process_frame
|
||||
main = load("res://presentation/main.tscn").instantiate()
|
||||
add_child(main)
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
|
||||
# 隔离存档(全 worktree 共享同一份 user://save.json——探针必须换注,禁碰真实档)
|
||||
ProbeUiUtil.isolate_save(main, "smoke_screen_flow", false, false, false, false)
|
||||
await get_tree().process_frame
|
||||
|
||||
await _step_boot_menu()
|
||||
await _step_menu_to_camp()
|
||||
await _step_camp_back_to_menu()
|
||||
await _step_menu_to_adventure()
|
||||
await _step_level_to_battle_and_back()
|
||||
await _step_abandon_to_end()
|
||||
await _step_end_back_to_menu()
|
||||
|
||||
if _problems.is_empty():
|
||||
print("[screen_flow_probe] PASS — 六步主流程+回退路径全绿(屏级互斥:任意时刻 ≤1 屏可见)")
|
||||
get_tree().quit(0)
|
||||
return
|
||||
print("[screen_flow_probe] FAIL — %d 处问题:" % _problems.size())
|
||||
for p in _problems:
|
||||
print("[screen_flow_probe] - " + p)
|
||||
get_tree().quit(1)
|
||||
|
||||
|
||||
# ---- 屏级互斥判据 ----
|
||||
# 屏标记取法:菜单/营地/终局看各自主内容层的 visible;冒险取流程状态位 run_scope.is_adventure
|
||||
#(战斗容器 battle_root 在局外也 visible——裸看 visible 会把 boot 态误判成「菜单与冒险同时在场」)。
|
||||
# **合法叠加只有一对**:终局覆盖层盖在冒险内容之上(终局态 is_adventure 尚未收束,是设计如此)。
|
||||
# 其余任意两屏同时为真=互斥失守。
|
||||
func _screen_flags() -> Dictionary:
|
||||
return {
|
||||
"菜单": main._start_overlay != null and main._start_overlay.visible,
|
||||
"营地": main.camp_overlay != null and main.camp_overlay.visible,
|
||||
"冒险": main.run_scope != null and main.run_scope.is_adventure,
|
||||
"终局": main._end_overlay != null and main._end_overlay.visible,
|
||||
}
|
||||
|
||||
|
||||
func _flags_text(f: Dictionary) -> String:
|
||||
var on: Array[String] = []
|
||||
for k in ["菜单", "营地", "冒险", "终局"]:
|
||||
if bool(f[k]):
|
||||
on.append(k)
|
||||
return "/".join(on) if not on.is_empty() else "无"
|
||||
|
||||
|
||||
# ---- 流程状态位(与可见性是两回事,逐步单独判)----
|
||||
func _expect_adventure_flag(step: String, expect: bool) -> void:
|
||||
var got: bool = main.run_scope != null and main.run_scope.is_adventure
|
||||
if got != expect:
|
||||
_problems.append("%s:流程状态位 is_adventure 应为 %s,实测 %s" % [step, str(expect), str(got)])
|
||||
|
||||
|
||||
func _expect_exclusive(step: String, expect: String) -> void:
|
||||
var f: Dictionary = _screen_flags()
|
||||
var pairs := [["菜单", "营地"], ["菜单", "冒险"], ["营地", "冒险"], ["终局", "营地"], ["终局", "菜单"]]
|
||||
for pair in pairs:
|
||||
var a: String = pair[0]
|
||||
var b: String = pair[1]
|
||||
if bool(f[a]) and bool(f[b]):
|
||||
_problems.append("%s:屏级互斥失守(%s 与 %s 同时在场)" % [step, a, b])
|
||||
if not bool(f[expect]):
|
||||
_problems.append("%s:期望屏「%s」不在场(实测 %s)" % [step, expect, _flags_text(f)])
|
||||
return
|
||||
for k in ["菜单", "营地", "冒险", "终局"]:
|
||||
if k == expect:
|
||||
continue
|
||||
if k == "冒险" and expect == "终局":
|
||||
continue # 合法叠加:终局覆盖在冒险内容之上
|
||||
if bool(f[k]):
|
||||
_problems.append("%s:屏「%s」不该在场(实测 %s)" % [step, k, _flags_text(f)])
|
||||
|
||||
|
||||
func _frames(n: int = 2) -> void:
|
||||
for _i in n:
|
||||
await get_tree().process_frame
|
||||
|
||||
|
||||
# ---- ① boot:菜单独占 ----
|
||||
func _step_boot_menu() -> void:
|
||||
_expect_exclusive("①boot", "菜单")
|
||||
_expect_adventure_flag("①boot", false)
|
||||
if main.debug_overlay != null and main.debug_overlay.visible:
|
||||
_problems.append("①boot:调试浮窗 boot 自动开(菜单优先失守)")
|
||||
|
||||
|
||||
# ---- ② 菜单 → 营地(真实按钮信号路径)----
|
||||
func _step_menu_to_camp() -> void:
|
||||
var campbtn: Button = ProbeUiUtil.find_button(main._start_overlay, "营地", "prefix")
|
||||
if campbtn == null:
|
||||
_problems.append("②菜单→营地:营地按钮缺失")
|
||||
return
|
||||
campbtn.pressed.emit()
|
||||
await _frames()
|
||||
_expect_exclusive("②菜单→营地", "营地")
|
||||
|
||||
|
||||
# ---- ③ 营地 → 菜单(回退路径:营地关闭发自覆盖层自身信号)----
|
||||
func _step_camp_back_to_menu() -> void:
|
||||
if main.camp_overlay == null:
|
||||
_problems.append("③营地→菜单:营地覆盖层句柄缺失")
|
||||
return
|
||||
main.camp_overlay.closed.emit()
|
||||
await _frames()
|
||||
_expect_exclusive("③营地→菜单(回退)", "菜单")
|
||||
|
||||
|
||||
# ---- ④ 菜单 → 冒险(开新冒险 → 入口停点落关卡地图屏)----
|
||||
func _step_menu_to_adventure() -> void:
|
||||
var lvbtn: Button = ProbeUiUtil.find_button(main._start_overlay, "开始冒险", "contains")
|
||||
if lvbtn == null:
|
||||
lvbtn = ProbeUiUtil.find_button(main._start_overlay, "冒险", "contains")
|
||||
if lvbtn == null:
|
||||
_problems.append("④菜单→冒险:开始冒险按钮缺失")
|
||||
return
|
||||
lvbtn.pressed.emit()
|
||||
await _frames(3)
|
||||
_expect_exclusive("④菜单→冒险", "冒险")
|
||||
_expect_adventure_flag("④菜单→冒险", true)
|
||||
if main.run_scope != null and main.run_scope.adventure == null:
|
||||
_problems.append("④菜单→冒险:冒险实例未建立(入口停点未达)")
|
||||
if main.level_screen != null and not main.level_screen.visible:
|
||||
_problems.append("④菜单→冒险:入口停点未落在关卡地图屏(level_screen 不可见)")
|
||||
|
||||
|
||||
# ---- ⑤ 冒险内屏切换:关卡地图屏 ⇄ 战斗屏(屏内两态,屏级仍独占)----
|
||||
func _step_level_to_battle_and_back() -> void:
|
||||
if not main.level_screen.visible:
|
||||
_problems.append("⑤冒险内切换:前置不在关卡地图屏")
|
||||
return
|
||||
main._exit_level_screen()
|
||||
await _frames()
|
||||
if main.level_screen.visible:
|
||||
_problems.append("⑤冒险内切换:退出关卡屏后 level_screen 仍可见")
|
||||
if main._battle_root != null and not main._battle_root.visible:
|
||||
_problems.append("⑤冒险内切换:战斗屏未接手(battle_root 不可见)")
|
||||
_expect_exclusive("⑤冒险内切换(关卡→战斗)", "冒险")
|
||||
main._enter_level_screen()
|
||||
await _frames()
|
||||
if not main.level_screen.visible:
|
||||
_problems.append("⑤冒险内切换:回到关卡地图屏失败")
|
||||
_expect_exclusive("⑤冒险内切换(战斗→关卡)", "冒险")
|
||||
|
||||
|
||||
# ---- ⑥ 放弃 → 终局(放弃路径=回退路径的非正常出口)----
|
||||
func _step_abandon_to_end() -> void:
|
||||
if main.run_scope == null or main.run_scope.adventure == null:
|
||||
_problems.append("⑥放弃→终局:前置无冒险实例")
|
||||
return
|
||||
main._on_abandon_requested()
|
||||
await _frames(4)
|
||||
_expect_exclusive("⑥放弃→终局", "终局")
|
||||
if main._end_overlay != null and not main._end_overlay.visible:
|
||||
_problems.append("⑥放弃→终局:终局覆盖层未开")
|
||||
|
||||
|
||||
# ---- ⑦ 终局 → 菜单(终局出口返回开始界面)----
|
||||
func _step_end_back_to_menu() -> void:
|
||||
if main._end_overlay == null or not main._end_overlay.visible:
|
||||
_problems.append("⑦终局→菜单:前置不在终局")
|
||||
return
|
||||
main._end_overlay.continue_pressed.emit()
|
||||
await _frames(3)
|
||||
_expect_exclusive("⑦终局→菜单", "菜单")
|
||||
if main.run_scope != null and main.run_scope.is_adventure:
|
||||
_problems.append("⑦终局→菜单:冒险态未收束(run_scope 仍标 is_adventure)")
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://smoke/screen_flow_probe.gd" id="1"]
|
||||
|
||||
[node name="ScreenFlowProbe" type="Node"]
|
||||
script = ExtResource("1")
|
||||
@@ -93,8 +93,14 @@ func _run_suite(suite: Dictionary, idx: int) -> int:
|
||||
var out := PackedStringArray()
|
||||
var code: int = OS.execute("cmd.exe", PackedStringArray(["/c", cmd]), out, true, false)
|
||||
var text := FileAccess.get_file_as_string(log) if FileAccess.file_exists(log) else ""
|
||||
print(text)
|
||||
if not bool(suite.get("negative", false)) and _has_script_error(text):
|
||||
var negative := bool(suite.get("negative", false))
|
||||
# 负面套件(坏表/违规输入是它的测试目标)全文折叠,只回显结论行——
|
||||
# 它输出里的 Parse Error 是预期产物,回显出去会让编排层的日志扫描误判成真实脚本错误。
|
||||
if negative:
|
||||
print("[run_all] %s:负面套件——全文输出已折叠(输出中的错误为其测试目标,非缺陷)" % path)
|
||||
else:
|
||||
print(text)
|
||||
if not negative and _has_script_error(text):
|
||||
push_error("[run_all] FAIL %s — 输出含脚本错误标记:%s" % [path, _script_error_line(text)])
|
||||
return 1
|
||||
if code != expect:
|
||||
|
||||
@@ -110,6 +110,12 @@ st=$?; kill_orphans
|
||||
grep -E "PASS|FAIL" "$TMP/ctp.log" | head -12
|
||||
verdict "ui_contrast_probe" "$TMP/ctp.log" "$st" || _rc=1
|
||||
|
||||
echo "======== screen_flow_probe(L3 屏壳切换·迭代 29 T4) ========"
|
||||
timeout 120 "$G" --headless --path "$GAME_WIN" res://smoke/screen_flow_probe.tscn >"$TMP/sfp.log" 2>&1
|
||||
st=$?; kill_orphans
|
||||
grep -E "screen_flow_probe" "$TMP/sfp.log" | tail -3
|
||||
verdict "screen_flow_probe" "$TMP/sfp.log" "$st" "screen_flow_probe" || _rc=1
|
||||
|
||||
if [ "$WITH_CLICK" -eq 1 ]; then
|
||||
echo "======== ui_click_probe 行为走查 ========"
|
||||
timeout 240 "$G" --headless --path "$GAME_WIN" res://smoke/ui_click_probe.tscn >"$TMP/clk.log" 2>&1
|
||||
|
||||
Reference in new issue
Block a user