合并(QA harness 三批·测试面真输入化+覆盖总览面板+自动试玩扩展): 批1=两探针52处emit站点51处转真输入管线+1接线断言保留+probe_coverage_recorder覆盖登记件(锚链归一化键)+§2条文+滚轮press-release成对/折叠区滚入视野两实证规范; 批2=B覆盖总览面板四节(六层闸况/能力行×六层灯/控件下钻/本体对账)+义务层提取器7段103行+六源回执明细化闸壳挂点+本体枚举器865实体对账+豁免清单母版+登记件seen面; 批3=journey_player全动线机器自玩(16类屏到达/异常三类含归因通道/误报指纹跨跑静默实测生效/真输入命中30控件并入登记)。三批读数: 批1探针孤立直跑绿+全量档绿; 批2全量档绿+面板浏览器真实入口四节验过; 批3首跑机器复现在案缺陷「开局被开始层盖住」(营地交互修复批承接中)+二跑收敛PASS exit=0。方案偏差20条回写qa-harness讨论文档§10-§12; 三处环境/机制堵坑(.gd行尾LF/全局类缓存扫描/夹具清槽+排版帧)。合成并master c1acb47c(pre-merge钩子批)——本合并即该钩子第二次实弹
ci / gates (push) Successful in 7m15s
ci / gates (push) Successful in 7m15s
This commit is contained in:
commit
d3c8539062
16 files changed
+1737
-82
No files matched your search
@@ -20,6 +20,8 @@
|
||||
|
||||
凡模态覆盖层,探针必须断言:打开后自身 visible 且遮挡物已隐藏(模态被遮挡 = 功能存在但不可达)。
|
||||
|
||||
**UI 驱动断言须走真输入管线(2026-11-06 立)**:探针里「点按钮/点场景物件」类驱动断言,一律经引擎输入管线承载(合成 InputEvent 走 `push_input`,press+release 成对;合成滚轮同样必须 press+release 成对——只发 press 会把鼠标焦点钉死在按下点控件上,后续点击全部失效);信号直发(`pressed.emit()` 等)绕过命中测试,保不了「玩家点击可达」,只许用于真输入到不了的面并当场标〔接线断言〕(现役例:置灰态穿透钉处理器兜底)。折叠区控件先以真输入滚轮滚入可视区再点。每个动线断言钉「落点」判定(点了之后到哪个屏/哪层可见),防驱动中断后的空转假绿。真输入命中控件时自动登记控件覆盖(探针共用面登记件落 `.gates/` 观测产物),覆盖面生长不靠人记。
|
||||
|
||||
## 3. 缺陷修复必须沉淀回归断言
|
||||
|
||||
每个缺陷修复必须在探针或冒烟中留下能抓住该缺陷的断言(例:封印动作耗尽 AP → 必须进入第 2 轮;
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
# probe_coverage_recorder.gd — 真输入覆盖登记件(探针共用面)
|
||||
# 是什么:探针/C 驱动件把合成 InputEvent 经 Viewport.push_input 送进引擎输入管线时,登记
|
||||
# 「真输入实际命中了哪些交互控件」,探针收尾落 JSONP 式 .js 到 tests/.gates/coverage/
|
||||
# (.js 供 file:// 页面 <script src> 直载——fetch 本地 json 会被 CORS 拦)。
|
||||
# 成员面(两源):① 场景树全部 BaseButton——pressed 沿自动接线(重扫懒触发,见下);
|
||||
# ② 自绘命中类——现役接 camp_scene_view.object_pressed(信号带物件 id,key 追加 #物件id 后缀),
|
||||
# 其余自绘命中面走 note_hit() 显式口。
|
||||
# 真输入旗标:push 前置位、帧末清位——只有旗标在位期间的命中入账;信号直发(〔接线断言〕保留站)
|
||||
# 与产品码自发射永不入账。命中=控件真的收到事件:hitbox 错位/遮挡时零命中,缺口自然暴露——
|
||||
# 这是「新控件落地→覆盖登记自动出现它」的数据源。
|
||||
# key 规则(契约「场景文件路径+节点路径」的实况落地偏差,详见 qa-harness 讨论文档回写节):
|
||||
# 本仓库 UI 大头由代码构建(main_ui_build/ui_kit 不设节点名),运行时节点路径全为 @类@序
|
||||
# 临时名——契约原样落地产出空登记。故键取「锚链归一化」形态:自主根到控件只收集带稳定身份的
|
||||
# 锚(具名节点用其名、带脚本节点用脚本文件名;@ 临时名段整体不入键),控件本体记 类#钮面文本,
|
||||
# 同键控件按接线遍历序追加 #序位。键在确定构建序下可复现;UI 构建序变更会平移同键序位——
|
||||
# 表现为键漂移而非静默丢失,属测量面可接受代价。
|
||||
# 用法:探针 _ready 里 `_recorder = ProbeCoverageRecorder.new()` → `attach(main, "<探针名>")`;
|
||||
# 收尾 `dump_js()`。探针共用面不入 S 系扫描面(code-conventions §4.13 分档)。
|
||||
extends RefCounted
|
||||
|
||||
const ProbeUiUtil := preload("res://probe/probe_ui_util.gd")
|
||||
|
||||
var _main: Node
|
||||
var _probe_name: String
|
||||
var _pending := false # 真输入在途旗标
|
||||
var _camp: Control = null # 营地场景根(自绘命中单点)
|
||||
var _hits: Dictionary = {} # 稳定 key(String) → 命中次数(int)
|
||||
var _node_keys: Dictionary = {} # 接线时分配的节点键(instance_id → key)
|
||||
var _key_seen: Dictionary = {} # 基键出现次数(同键控件按遍历序追加序位)
|
||||
var _wired: Dictionary = {} # 已连 pressed 的按钮实例 id → true(防重连)
|
||||
var _skipped_temp := 0 # 临时名命中笔数(不入 key,只计数)
|
||||
|
||||
|
||||
# ---- 装载与接线 ----
|
||||
|
||||
func attach(main: Node, probe_name: String) -> void:
|
||||
_main = main
|
||||
_probe_name = probe_name
|
||||
ProbeUiUtil.coverage_hook = self # 共用件真输入口统一过旗标(push_click/push_click_at/push_motion)
|
||||
_wire_buttons()
|
||||
_wire_camp_scene()
|
||||
|
||||
|
||||
# 懒重扫:每次真输入 push 前接线一遍新入树的按钮(开层重建的钮行在下次点击前接上);
|
||||
# 已释放按钮的连接随节点释放失效,登记表按 instance_id 清扫。
|
||||
func mark_real_input() -> void:
|
||||
for id in _wired.keys():
|
||||
if instance_from_id(int(id)) == null:
|
||||
_wired.erase(id)
|
||||
_node_keys.erase(id)
|
||||
_wire_buttons()
|
||||
if not _pending: # 同帧多次 push 只挂一次帧末清位旗(重复 connect 会报引擎错)
|
||||
_pending = true
|
||||
_main.get_tree().process_frame.connect(_clear_pending, CONNECT_ONE_SHOT)
|
||||
|
||||
|
||||
func _clear_pending() -> void:
|
||||
_pending = false
|
||||
|
||||
|
||||
func _wire_buttons() -> void:
|
||||
_wire_into(_main)
|
||||
|
||||
|
||||
func _wire_into(node: Node) -> void:
|
||||
if node is BaseButton:
|
||||
var id := node.get_instance_id()
|
||||
if not _wired.has(id):
|
||||
_wired[id] = true
|
||||
_node_keys[id] = _assign_key(node)
|
||||
(node as BaseButton).pressed.connect(_on_button_pressed.bind(id))
|
||||
for child in node.get_children():
|
||||
_wire_into(child)
|
||||
|
||||
|
||||
func _wire_camp_scene() -> void:
|
||||
_camp = _find_by_script(_main, "camp_scene_view")
|
||||
if _camp != null:
|
||||
_camp.object_pressed.connect(_on_camp_object)
|
||||
|
||||
|
||||
func _find_by_script(root: Node, script_name: String) -> Control:
|
||||
var s: Script = root.get_script()
|
||||
if s != null and String(s.resource_path).contains(script_name) and root is Control:
|
||||
return root as Control
|
||||
for child in root.get_children():
|
||||
var found := _find_by_script(child, script_name)
|
||||
if found != null:
|
||||
return found
|
||||
return null
|
||||
|
||||
|
||||
# ---- 键分配(锚链归一化,接线时一次算好存表)----
|
||||
|
||||
func _assign_key(n: Node) -> String:
|
||||
var chain := _anchor_chain(n)
|
||||
var leaf := ""
|
||||
if n is BaseButton:
|
||||
leaf = "%s#%s" % [n.get_class(), String((n as BaseButton).text)]
|
||||
else:
|
||||
leaf = n.get_class()
|
||||
var base := "%s:%s/%s" % [String(_main.scene_file_path), "/".join(chain), leaf]
|
||||
# 同键控件按接线遍历序加序位(#1 省略)——重复键可分账,序位随构建序漂移属已知代价
|
||||
_key_seen[base] = int(_key_seen.get(base, 0)) + 1
|
||||
if int(_key_seen[base]) > 1:
|
||||
base += "#%d" % int(_key_seen[base])
|
||||
return base
|
||||
|
||||
|
||||
# 自主根到 n 的稳定锚链:具名节点取其名、带脚本节点取脚本文件名;@ 临时名段不入链。
|
||||
func _anchor_chain(n: Node) -> Array[String]:
|
||||
var chain: Array[String] = []
|
||||
var cur: Node = n
|
||||
while cur != null and cur != _main:
|
||||
var nm := String(cur.name)
|
||||
if not nm.begins_with("@"):
|
||||
chain.push_front(nm)
|
||||
elif cur.get_script() != null:
|
||||
var sp := String((cur.get_script() as Script).resource_path)
|
||||
chain.push_front(sp.get_file().trim_suffix(".gd"))
|
||||
cur = cur.get_parent()
|
||||
if chain.is_empty():
|
||||
chain.push_front("(root)")
|
||||
return chain
|
||||
|
||||
|
||||
# ---- 显式登记口(自绘命中类无信号可连时的单点)----
|
||||
|
||||
func note_hit(n: Node, suffix: String = "") -> void:
|
||||
if not _pending or n == null:
|
||||
return
|
||||
var id := n.get_instance_id()
|
||||
var key := String(_node_keys.get(id, ""))
|
||||
if key == "":
|
||||
key = _assign_key(n)
|
||||
_node_keys[id] = key
|
||||
_bump(key + suffix)
|
||||
|
||||
|
||||
# ---- 命中处理 ----
|
||||
|
||||
func _on_button_pressed(id: int) -> void:
|
||||
if not _pending:
|
||||
return
|
||||
var key := String(_node_keys.get(id, ""))
|
||||
if key == "":
|
||||
_skipped_temp += 1
|
||||
return
|
||||
_bump(key)
|
||||
|
||||
|
||||
func _on_camp_object(key: StringName) -> void:
|
||||
if not _pending or _camp == null:
|
||||
return
|
||||
var id := _camp.get_instance_id()
|
||||
var base := String(_node_keys.get(id, ""))
|
||||
if base == "":
|
||||
base = _assign_key(_camp)
|
||||
_node_keys[id] = base
|
||||
_bump(base + "#" + String(key))
|
||||
|
||||
|
||||
func _bump(key: String) -> void:
|
||||
_hits[key] = int(_hits.get(key, 0)) + 1
|
||||
|
||||
|
||||
# ---- 场景树全量交互面枚举(控件缺口数据源:seen−hit=「存在但零真输入覆盖」)----
|
||||
# 成员面与登记同源:场景树全部 BaseButton(main 建树常驻——未开过的屏的钮也算存在;
|
||||
# 已接线钮复用接线键防序位错位,未接线者落盘期才补键——落盘是终态动作,序位计数此后不再消费)
|
||||
# +营地物件(经 db 布局表 hotspot_key 公共数据面枚举;物件命中见 _on_camp_object 的 #物件id 后缀)。
|
||||
func enumerate_seen() -> Array[String]:
|
||||
var keys: Array[String] = []
|
||||
_seen_into(_main, keys)
|
||||
var db = _main.get("db")
|
||||
if db != null and db.get("presentation") != null:
|
||||
var layout = db.presentation.camp_scene_layout
|
||||
if layout != null:
|
||||
for spec in layout.layers:
|
||||
var hk: StringName = spec.hotspot_key
|
||||
var ck := "camp_object#" + String(hk)
|
||||
if hk != &"" and not keys.has(ck):
|
||||
keys.append(ck)
|
||||
return keys
|
||||
|
||||
|
||||
func _seen_into(node: Node, keys: Array[String]) -> void:
|
||||
if node is BaseButton:
|
||||
var id := node.get_instance_id()
|
||||
var key := String(_node_keys.get(id, ""))
|
||||
if key == "":
|
||||
key = _assign_key(node)
|
||||
_node_keys[id] = key
|
||||
if not keys.has(key):
|
||||
keys.append(key)
|
||||
for child in node.get_children():
|
||||
_seen_into(child, keys)
|
||||
|
||||
|
||||
# ---- 落盘(JSONP 式 .js——file:// 页面 <script src> 直载)----
|
||||
|
||||
func dump_js() -> void:
|
||||
var dir := DirAccess.open("res://")
|
||||
if dir == null:
|
||||
push_error("[coverage] res:// 打不开,登记未落盘")
|
||||
return
|
||||
var mk := dir.make_dir_recursive("tests/.gates/coverage")
|
||||
if mk != OK and not dir.dir_exists("tests/.gates/coverage"):
|
||||
push_error("[coverage] coverage 目录建不了(err=%d),登记未落盘" % mk)
|
||||
return
|
||||
var path := "res://tests/.gates/coverage/probe_hits_%s.js" % _probe_name
|
||||
var f := FileAccess.open(path, FileAccess.WRITE)
|
||||
if f == null:
|
||||
push_error("[coverage] 登记文件打不开:%s" % path)
|
||||
return
|
||||
var total := 0
|
||||
for k in _hits:
|
||||
total += int(_hits[k])
|
||||
var seen := enumerate_seen()
|
||||
var payload := {
|
||||
"meta": {
|
||||
"probe": _probe_name,
|
||||
"seed": OS.get_environment("PROBE_SEED"),
|
||||
"scene": String(_main.scene_file_path),
|
||||
"generated_at": Time.get_datetime_string_from_system(),
|
||||
"driver": "InputEvent via Viewport.push_input(真输入管线)",
|
||||
"key_shape": "锚链归一化(具名/脚本锚+类#文本+序位;@ 临时名不入键)",
|
||||
"hits_total": total,
|
||||
"seen_total": seen.size(),
|
||||
"skipped_temp_names": _skipped_temp,
|
||||
},
|
||||
"hits": _hits,
|
||||
"seen": seen,
|
||||
}
|
||||
var js := "/* 真输入覆盖登记——探针每次运行自动覆写,人工修订无效(豁免面走人工清单,不落本文件)。 */\n"
|
||||
js += "window.__QA_COVERAGE__ = window.__QA_COVERAGE__ || {};\n"
|
||||
js += "window.__QA_COVERAGE__.probe_real_input = window.__QA_COVERAGE__.probe_real_input || {};\n"
|
||||
js += "window.__QA_COVERAGE__.probe_real_input[%s] = %s;\n" % [
|
||||
JSON.stringify(_probe_name), JSON.stringify(payload, " ")]
|
||||
f.store_string(js)
|
||||
f.close()
|
||||
print("[coverage] 真输入命中登记已落盘 %s(控件 %d 处 / 命中 %d 次 / 临时名跳过 %d)" % [
|
||||
path, _hits.size(), total, _skipped_temp])
|
||||
@@ -0,0 +1 @@
|
||||
uid://d1uhtlw3kmhxw
|
||||
@@ -120,13 +120,20 @@ static func isolate_save(main, tag: String, mark_first_done: bool = false, with_
|
||||
|
||||
# ---- 真输入点击承载(test-conventions §8.3「真实入口」判定口径)----
|
||||
# 合成鼠标事件经 Viewport.push_input 进引擎输入管线(hitbox/命中测试参与),press+release 成对
|
||||
#(BaseButton pressed 信号走 release 沿——ui_input_probe :717-733 / screen_flow_probe :484-499 同式)。
|
||||
#(BaseButton pressed 信号走 release 沿——ui_input_probe :717-733 先例式)。
|
||||
# 信号直发(pressed.emit())只许标「接线验证」,不得充当真实入口断言的驱动。
|
||||
# coverage_hook = 真输入覆盖登记件(probe_coverage_recorder.gd)挂点:本件三个 push 口是探针
|
||||
# 真输入的单点通道,push 前通知登记件置旗标——命中记账只认旗标在位期间(信号直发不入账)。
|
||||
static var coverage_hook: Object = null
|
||||
|
||||
|
||||
static func push_click(main, ctrl: Control) -> void:
|
||||
push_click_at(main, ctrl.get_global_rect().get_center())
|
||||
|
||||
|
||||
static func push_click_at(main, pos: Vector2) -> void:
|
||||
if coverage_hook != null:
|
||||
coverage_hook.mark_real_input()
|
||||
var down := InputEventMouseButton.new()
|
||||
down.position = pos
|
||||
down.global_position = pos
|
||||
@@ -141,6 +148,71 @@ static func push_click_at(main, pos: Vector2) -> void:
|
||||
main.get_viewport().push_input(up, true)
|
||||
|
||||
|
||||
# 悬停真输入(tooltip/悬停态断言的承载——motion 进管线触发 mouse_entered/exited 沿)
|
||||
static func push_motion(main, pos: Vector2) -> void:
|
||||
if coverage_hook != null:
|
||||
coverage_hook.mark_real_input()
|
||||
var mv := InputEventMouseMotion.new()
|
||||
mv.position = pos
|
||||
mv.global_position = pos
|
||||
main.get_viewport().push_input(mv, true)
|
||||
|
||||
|
||||
# 滚轮真输入(dir=+1 向下翻 / -1 向上翻——折叠区行滚入视野的唯一真输入通道)。
|
||||
# wheel 合成必须 press+release 成对:硬件滚轮虽无 release 沿,但引擎 GUI 会把按键事件的
|
||||
# press 记为鼠标焦点且等 release 才释放——只发 press 会把焦点钉死在按下点控件上,
|
||||
# 后续左键点击全被路由给焦点控件(实证:滚轮后 fork/返回营地两钮点击全部失效)。
|
||||
static func push_wheel(main, pos: Vector2, dir: int) -> void:
|
||||
if coverage_hook != null:
|
||||
coverage_hook.mark_real_input()
|
||||
var idx := MOUSE_BUTTON_WHEEL_DOWN if dir > 0 else MOUSE_BUTTON_WHEEL_UP
|
||||
var w := InputEventMouseButton.new()
|
||||
w.position = pos
|
||||
w.global_position = pos
|
||||
w.button_index = idx
|
||||
w.pressed = true
|
||||
main.get_viewport().push_input(w, true)
|
||||
var w2 := InputEventMouseButton.new()
|
||||
w2.position = pos
|
||||
w2.global_position = pos
|
||||
w2.button_index = idx
|
||||
w2.pressed = false
|
||||
main.get_viewport().push_input(w2, true)
|
||||
|
||||
|
||||
# 窗板定位(窗壳视图子树固定链:遮罩 ColorRect → CenterContainer → 窗板 PanelContainer——
|
||||
# screen_flow_probe._window_panel 同式,探针族共用单点)
|
||||
static func window_panel(view: Control) -> PanelContainer:
|
||||
if view == null or view.get_child_count() == 0:
|
||||
return null
|
||||
var dim := view.get_child(0)
|
||||
if dim.get_child_count() == 0:
|
||||
return null
|
||||
var center := dim.get_child(0)
|
||||
if center.get_child_count() == 0 or not (center.get_child(0) is PanelContainer):
|
||||
return null
|
||||
return center.get_child(0) as PanelContainer
|
||||
|
||||
|
||||
# 折叠区行滚入窗板可视区(真输入滚轮驱动;滚动不重建行,rect 随滚更新)。
|
||||
# 判据:钮 rect 整体落在窗板内(上缘留 8px、下缘让开底栏 64px——返回营地钮带高约 56px)。
|
||||
# 保险丝 60 次滚叶防异常内容死循环;窗板缺失或钮句柄失效即返回(调用方前置断言兜底)。
|
||||
static func scroll_button_into_view(main, view: Control, btn: Control) -> void:
|
||||
var panel := window_panel(view)
|
||||
if panel == null or btn == null:
|
||||
return
|
||||
var guard := 60
|
||||
while guard > 0:
|
||||
var r := btn.get_global_rect()
|
||||
var pr := panel.get_global_rect()
|
||||
if r.position.y >= pr.position.y + 8.0 and r.end.y <= pr.end.y - 64.0:
|
||||
return
|
||||
var down := r.end.y > pr.end.y - 64.0
|
||||
push_wheel(main, pr.get_center(), 1 if down else -1)
|
||||
guard -= 1
|
||||
await main.get_tree().process_frame
|
||||
|
||||
|
||||
# 真输入拖拽沿(press→motion×steps→release 成对经 push_input 进引擎输入管线——卷轴视口批
|
||||
# R3/R5/R6 驱动沿;沿 push_click_at 同式。relative 按步显式置值——合成事件的位移量由事件
|
||||
# 自身承载,被测面按 relative 逐帧平移)
|
||||
@@ -167,7 +239,7 @@ static func push_drag_at(main, pos: Vector2, delta: Vector2, steps: int = 4) ->
|
||||
|
||||
|
||||
# 真输入滚轮沿(单档滚轮事件经 push_input——卷轴视口批 R4 驱动沿;方向映射在被测面,
|
||||
# 本工具只发原始滚轮档)
|
||||
# 本工具只发原始滚轮档)。press+release 成对(同 push_wheel 注——只发 press 钉死鼠标焦点)。
|
||||
static func push_wheel_at(main, pos: Vector2, wheel_down: bool = true) -> void:
|
||||
var ev := InputEventMouseButton.new()
|
||||
ev.position = pos
|
||||
@@ -175,6 +247,12 @@ static func push_wheel_at(main, pos: Vector2, wheel_down: bool = true) -> void:
|
||||
ev.button_index = MOUSE_BUTTON_WHEEL_DOWN if wheel_down else MOUSE_BUTTON_WHEEL_UP
|
||||
ev.pressed = true
|
||||
main.get_viewport().push_input(ev, true)
|
||||
var ev2 := InputEventMouseButton.new()
|
||||
ev2.position = pos
|
||||
ev2.global_position = pos
|
||||
ev2.button_index = MOUSE_BUTTON_WHEEL_DOWN if wheel_down else MOUSE_BUTTON_WHEEL_UP
|
||||
ev2.pressed = false
|
||||
main.get_viewport().push_input(ev2, true)
|
||||
|
||||
|
||||
# 营地经进档开层(卡② mn9 新路径——campbtn 已撤;槽模板注入隔离命名空间,防碰真实槽
|
||||
|
||||
@@ -8,9 +8,11 @@
|
||||
extends Node
|
||||
|
||||
const ProbeUiUtil := preload("res://probe/probe_ui_util.gd")
|
||||
const ProbeCoverageRecorder := preload("res://probe/probe_coverage_recorder.gd")
|
||||
|
||||
var main: Node
|
||||
var _problems: Array[String] = []
|
||||
var _recorder: RefCounted = null # 真输入覆盖登记件(probe_coverage_recorder.gd——attach 后全探针真输入自动记账)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -20,6 +22,10 @@ func _ready() -> void:
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
|
||||
# 真输入覆盖登记(命中控件自动记账,收尾落 tests/.gates/coverage/——覆盖面生长不靠人记)
|
||||
_recorder = ProbeCoverageRecorder.new()
|
||||
_recorder.attach(main, "screen_flow")
|
||||
|
||||
# 隔离存档(全 worktree 共享同一份 user://save.json——探针必须换注,禁碰真实档)
|
||||
ProbeUiUtil.isolate_save(main, "smoke_screen_flow", false, false, false)
|
||||
await get_tree().process_frame
|
||||
@@ -35,6 +41,7 @@ func _ready() -> void:
|
||||
await _step_abandon_to_end()
|
||||
await _step_end_back_to_menu()
|
||||
|
||||
_recorder.dump_js()
|
||||
if _problems.is_empty():
|
||||
print("[screen_flow_probe] PASS — 六步主流程+回退路径全绿(屏级互斥:任意时刻 ≤1 屏可见)")
|
||||
get_tree().quit(0)
|
||||
@@ -122,11 +129,11 @@ func _step_camp_back_to_menu() -> void:
|
||||
if main.ui.modals.camp.scene == null:
|
||||
_problems.append("③营地→菜单:营地场景根句柄缺失〔句柄子容器批 V1〕")
|
||||
return
|
||||
main.ui.modals.camp.scene.object_pressed.emit(&"campfire")
|
||||
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"campfire").get_center()) # 真输入(营地场景 _gui_input 命中——大门段同式)
|
||||
await _frames()
|
||||
if not main.ui.modals.menu.confirm_dialog.visible:
|
||||
_problems.append("③篝火:二次确认弹窗未现(camp.md v2.2 必须先确认)〔营地图批 V7〕")
|
||||
main.ui.modals.menu.confirm_dialog.confirmed.emit() # 二次确认离场(confirmed 先于关层——卡④ T4)
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.menu.confirm_dialog._confirm_btn) # 真点「确认」离场(confirmed 先于关层——卡④ T4)
|
||||
await _frames()
|
||||
_expect_exclusive("③营地→菜单(回退)", "菜单")
|
||||
|
||||
@@ -150,7 +157,7 @@ func _step_save_select_open_back() -> void:
|
||||
if back_btn == null:
|
||||
_problems.append("[savesel] sl10 返回钮缺失(页壳顶条)")
|
||||
else:
|
||||
back_btn.pressed.emit()
|
||||
ProbeUiUtil.push_click(main, back_btn) # 真点「返回」→ 关选档层
|
||||
await get_tree().process_frame
|
||||
if main.ui.modals.menu.save_select_overlay.visible or not main.ui.screens.start_overlay.visible:
|
||||
_problems.append("[savesel] sl10 返回后选档未隐藏或开始层未恢复(关闭链失守)")
|
||||
@@ -228,7 +235,7 @@ func _step_end_back_to_menu() -> void:
|
||||
if main.ui.modals.run.end_overlay == null or not main.ui.modals.run.end_overlay.visible:
|
||||
_problems.append("⑦终局→菜单:前置不在终局")
|
||||
return
|
||||
main.ui.modals.run.end_overlay.continue_pressed.emit()
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.run.end_overlay._btn) # 真点「继续」(_btn=continue_pressed 同源句柄)
|
||||
await _frames(3)
|
||||
_expect_exclusive("⑦终局→菜单", "菜单")
|
||||
if main.run_scope != null and main.run_scope.is_adventure:
|
||||
@@ -254,8 +261,10 @@ func _step_camp_esc_and_hud() -> void:
|
||||
await _frames()
|
||||
if main.ui.modals.camp.esc_overlay.visible:
|
||||
_problems.append("[esc] echo 事件唤起浮窗(触发沿 pressed 且非 echo 失守)〔ESC批 V1〕")
|
||||
main.ui.modals.camp.scene.object_pressed.emit(&"dummy") # 子界面开态
|
||||
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"dummy").get_center()) # 子界面开态(真输入命中)
|
||||
await _frames()
|
||||
if not main.ui.modals.camp.tree.visible:
|
||||
_problems.append("[esc] 子界面未随场景点选打开(后续关窗断言的前置落点)")
|
||||
_push_esc()
|
||||
await _frames()
|
||||
if main.ui.modals.camp.tree.visible:
|
||||
@@ -285,7 +294,7 @@ func _step_camp_esc_and_hud() -> void:
|
||||
if not main.ui.modals.camp.esc_overlay.visible or main.ui.screens.start_overlay.visible:
|
||||
_problems.append("[esc] 关设置后未回浮窗或回弹主菜单(契约 3 语境分支失守)〔ESC批 V4〕〔ESC批 B3〕")
|
||||
# 返回主菜单动线(契约 4——浮窗自收+营地隐+选档收+菜单显)
|
||||
main.ui.modals.camp.esc_overlay.return_requested.emit()
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.camp.esc_overlay._return_btn) # 真点「返回主菜单」
|
||||
await _frames()
|
||||
if main.ui.modals.camp.esc_overlay.visible or main.ui.modals.camp.scene.visible or main.ui.modals.menu.save_select_overlay.visible or not main.ui.screens.start_overlay.visible:
|
||||
_problems.append("[esc] 返回主菜单动线失守(浮窗自收/营地隐/选档收/菜单显)〔ESC批 V3〕〔ESC批 B3〕")
|
||||
@@ -323,11 +332,11 @@ func _step_camp_window_shell() -> void:
|
||||
if prev_back == null:
|
||||
_problems.append("[winshell] %s 底栏「返回营地」钮缺失〔窗口化批 V4〕" % String(opened.back()))
|
||||
else:
|
||||
prev_back.pressed.emit()
|
||||
ProbeUiUtil.push_click(main, prev_back) # 真点「返回营地」关前窗
|
||||
await _frames()
|
||||
if prev.visible or not main.ui.modals.camp.scene.visible:
|
||||
_problems.append("[winshell] %s 关闭钮按下未隐窗或场景根失守〔窗口化批 V4〕" % String(opened.back()))
|
||||
main.ui.modals.camp.scene.object_pressed.emit(k)
|
||||
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(k).get_center()) # 真输入开窗(场景 _gui_input 命中)
|
||||
await _frames()
|
||||
opened.append(k)
|
||||
var view: Control = views[k]
|
||||
@@ -362,7 +371,10 @@ func _step_camp_window_shell() -> void:
|
||||
if vis_n != 1:
|
||||
_problems.append("[winshell] %s 开态五窗 visible 计数=%d 应=1(互斥单开失守)〔窗口化批 V5〕" % [String(k), vis_n])
|
||||
# 树窗点选遮挡(V5b——树窗开态向锻炉命中区推真实鼠标点:被遮罩挡=零新窗零响应)
|
||||
main.ui.modals.camp.scene.object_pressed.emit(&"dummy")
|
||||
# 真输入化订正:前窗(图鉴)先经真实关闭钮清账再开树窗——emit 直发可穿透遮罩开窗,
|
||||
# 真输入下「无窗态场景命中」是唯一合法开窗路径(遮罩语义由本段正面验证)。
|
||||
await _close_all_windows(views)
|
||||
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"dummy").get_center())
|
||||
await _frames()
|
||||
var tree_view: Control = views[&"dummy"]
|
||||
if not tree_view.visible:
|
||||
@@ -370,8 +382,7 @@ func _step_camp_window_shell() -> void:
|
||||
else:
|
||||
var hotspot: Rect2 = main.ui.modals.camp.scene.get_hotspot_rect(&"forge")
|
||||
var c := hotspot.get_center()
|
||||
_push_click(c)
|
||||
_push_release(c)
|
||||
ProbeUiUtil.push_click_at(main, c) # 遮挡试探真点击——遮罩生效则零命中零响应
|
||||
await _frames()
|
||||
var vis_n2 := 0
|
||||
for w in five:
|
||||
@@ -383,7 +394,7 @@ func _step_camp_window_shell() -> void:
|
||||
# 随栈 close 恢复语义逐层清账,V6/V8 从干净单窗态起步)
|
||||
await _close_all_windows(views)
|
||||
# V6 ESC 先关窗(生涯窗开态 Esc→关生涯窗、浮窗不唤起)
|
||||
main.ui.modals.camp.scene.object_pressed.emit(&"notebook")
|
||||
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"notebook").get_center())
|
||||
await _frames()
|
||||
if not career.visible:
|
||||
_problems.append("[winshell] 生涯窗未开(V6 前置中断)〔窗口化批 V6〕")
|
||||
@@ -395,7 +406,7 @@ func _step_camp_window_shell() -> void:
|
||||
if main.ui.modals.camp.esc_overlay.visible:
|
||||
_problems.append("[winshell] 关窗同拍唤起浮窗(应无窗才进浮窗)〔窗口化批 V6〕")
|
||||
# V7 内容超高窗内滚动(树窗+真实投影喂加长行册)
|
||||
main.ui.modals.camp.scene.object_pressed.emit(&"dummy")
|
||||
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"dummy").get_center())
|
||||
await _frames()
|
||||
if not tree_view.visible:
|
||||
_problems.append("[winshell] 树窗未开(V7 前置中断)〔窗口化批 V7〕")
|
||||
@@ -420,7 +431,7 @@ func _step_camp_window_shell() -> void:
|
||||
# 清窗经真实钮(同 V6 前置——干净单窗态起步)
|
||||
await _close_all_windows(views)
|
||||
# V8 生涯同壳(窗板同几何+返回钮在位+内容列滚动面在)
|
||||
main.ui.modals.camp.scene.object_pressed.emit(&"notebook")
|
||||
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"notebook").get_center())
|
||||
await _frames()
|
||||
if not career.visible:
|
||||
_problems.append("[winshell] 生涯窗未开(V8 前置中断)〔窗口化批 V8〕")
|
||||
@@ -445,7 +456,7 @@ func _step_camp_window_shell() -> void:
|
||||
# 段尾收闭:生涯窗经真实「返回」钮关闭(on_camp_closed 关闭名单不含生涯窗——防残留泄段)
|
||||
var career_back: Button = ProbeUiUtil.find_button(career, "返回", "prefix")
|
||||
if career_back != null:
|
||||
career_back.pressed.emit()
|
||||
ProbeUiUtil.push_click(main, career_back) # 真点「返回」收闭生涯窗
|
||||
await _frames()
|
||||
if career.visible:
|
||||
_problems.append("[winshell] 生涯「返回」钮未隐窗〔窗口化批 V4〕")
|
||||
@@ -463,7 +474,7 @@ func _close_all_windows(views: Dictionary) -> void:
|
||||
if back == null:
|
||||
_problems.append("[winshell] %s 底栏「返回营地」钮缺失〔窗口化批 V4〕" % String(k))
|
||||
continue
|
||||
back.pressed.emit()
|
||||
ProbeUiUtil.push_click(main, back) # 真点「返回营地」逐窗清账
|
||||
await _frames()
|
||||
|
||||
|
||||
@@ -480,25 +491,7 @@ func _window_panel(view: Control) -> PanelContainer:
|
||||
return center.get_child(0) as PanelContainer
|
||||
|
||||
|
||||
# 真实鼠标合成(press+release 成对——BaseButton pressed 信号默认 release 触发,ui_input_probe :718 先例)
|
||||
func _push_click(pos: Vector2) -> void:
|
||||
var ev := InputEventMouseButton.new()
|
||||
ev.position = pos
|
||||
ev.global_position = pos
|
||||
ev.button_index = MOUSE_BUTTON_LEFT
|
||||
ev.pressed = true
|
||||
main.get_viewport().push_input(ev, true)
|
||||
|
||||
|
||||
func _push_release(pos: Vector2) -> void:
|
||||
var ev := InputEventMouseButton.new()
|
||||
ev.position = pos
|
||||
ev.global_position = pos
|
||||
ev.button_index = MOUSE_BUTTON_LEFT
|
||||
ev.pressed = false
|
||||
main.get_viewport().push_input(ev, true)
|
||||
|
||||
|
||||
# 键盘真输入合成(鼠标点击统一走 ProbeUiUtil.push_click_at——覆盖登记单点通道)
|
||||
func _push_esc(echo := false) -> void:
|
||||
var ev := InputEventKey.new()
|
||||
ev.keycode = KEY_ESCAPE
|
||||
|
||||
@@ -11,8 +11,10 @@ extends Node
|
||||
|
||||
const AutoPlayer := preload("res://sim/auto_player.gd") # O4:自动玩家策略单一实现
|
||||
const ProbeUiUtil := preload("res://probe/probe_ui_util.gd") # T6 收口:树寻址/隔离样板单点
|
||||
const ProbeCoverageRecorder := preload("res://probe/probe_coverage_recorder.gd")
|
||||
|
||||
var main: Node
|
||||
var _recorder: RefCounted = null # 真输入覆盖登记件(probe_coverage_recorder.gd——attach 后全探针真输入自动记账)
|
||||
var _last_state := {}
|
||||
var _stall_frames := 0
|
||||
var _closed_via_abandon := false # T17——预算耗尽经放弃路径收口(断点段返回断言豁免位)
|
||||
@@ -48,6 +50,9 @@ func _ready() -> void:
|
||||
main = load("res://presentation/main.tscn").instantiate()
|
||||
add_child(main)
|
||||
await get_tree().process_frame
|
||||
# 真输入覆盖登记(命中控件自动记账,收尾落 tests/.gates/coverage/——覆盖面生长不靠人记)
|
||||
_recorder = ProbeCoverageRecorder.new()
|
||||
_recorder.attach(main, "ui_click")
|
||||
var t_ui_ready := Time.get_ticks_msec() - t0
|
||||
# 调试浮窗 boot 不开(菜单优先——入口=UI 最顶层全局浮层角标,
|
||||
# 任何 UI 不遮挡;浮层角标遮挡判据见设置面段)
|
||||
@@ -67,7 +72,7 @@ func _ready() -> void:
|
||||
await get_tree().process_frame
|
||||
if not main.ui.modals.camp.scene.visible:
|
||||
_problems.append("营地场景根未打开(进档开层新路径)〔句柄子容器批 V1〕")
|
||||
main.ui.modals.camp.scene.object_pressed.emit(&"dummy") # 场景点选真实路由(木桩→天赋树子界面——卡④ T5)
|
||||
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"dummy").get_center()) # 场景点选真输入(木桩→天赋树子界面——卡④ T5;_gui_input 命中)
|
||||
await get_tree().process_frame
|
||||
if main.ui.modals.camp.tree._tree_buy_btns.is_empty():
|
||||
_problems.append("树淬炼购买钮缺失(四柜台拆解后树=独立子界面)〔句柄子容器批 V1〕")
|
||||
@@ -85,7 +90,8 @@ func _ready() -> void:
|
||||
else:
|
||||
var bal_before: int = iso_c.meta_state.keystone_balance()
|
||||
var price: int = int(main.db.crossrun.meta_tree.row_by_id("sp_hp").get("price", 0))
|
||||
buy_btn.pressed.emit() # 合成购买点击 → _on_camp_tree_buy → meta.tree_buy(写调用唯一归组合根)
|
||||
await ProbeUiUtil.scroll_button_into_view(main, main.ui.modals.camp.tree, buy_btn) # 折叠区行先滚入窗板可视区(真输入滚轮)
|
||||
ProbeUiUtil.push_click(main, buy_btn) # 真点购买 → _on_camp_tree_buy → meta.tree_buy(写调用唯一归组合根)
|
||||
await get_tree().process_frame
|
||||
if int(iso_c.meta_state.keystone_balance()) != bal_before - price:
|
||||
_problems.append("营地树购买未出账(%d→%d 应 −%d)〔句柄子容器批 V1〕〔营地图批 V4〕" % [bal_before, iso_c.meta_state.keystone_balance(), price])
|
||||
@@ -99,7 +105,8 @@ func _ready() -> void:
|
||||
if String(card.node_id) == "att_socket1":
|
||||
socket_btn = card.btn
|
||||
if socket_btn != null:
|
||||
socket_btn.pressed.emit()
|
||||
await ProbeUiUtil.scroll_button_into_view(main, main.ui.modals.camp.tree, socket_btn)
|
||||
ProbeUiUtil.push_click(main, socket_btn) # 真点解锁(点亮同调位前置)
|
||||
await get_tree().process_frame
|
||||
main.ui.modals.camp.tree.refresh_camp(main._camp_data())
|
||||
await get_tree().process_frame
|
||||
@@ -110,7 +117,8 @@ func _ready() -> void:
|
||||
if fork_btn == null:
|
||||
_problems.append("Fork 解锁钮缺失(点击路径)〔句柄子容器批 V1〕")
|
||||
else:
|
||||
fork_btn.pressed.emit()
|
||||
await ProbeUiUtil.scroll_button_into_view(main, main.ui.modals.camp.tree, fork_btn) # fork 行在折叠区(rect 越窗底栏——实证误中「返回营地」钮)
|
||||
ProbeUiUtil.push_click(main, fork_btn) # 真点解锁(展开 a/b 双态钮)
|
||||
await get_tree().process_frame
|
||||
main.ui.modals.camp.tree.refresh_camp(main._camp_data())
|
||||
await get_tree().process_frame
|
||||
@@ -121,7 +129,8 @@ func _ready() -> void:
|
||||
if side_b == null:
|
||||
_problems.append("Fork 双态切换钮缺失(解锁后应展开 a/b 钮)〔句柄子容器批 V1〕")
|
||||
else:
|
||||
side_b.pressed.emit() # 合成切态点击 → _on_camp_fork_toggle → meta.tree_toggle_fork
|
||||
await ProbeUiUtil.scroll_button_into_view(main, main.ui.modals.camp.tree, side_b)
|
||||
ProbeUiUtil.push_click(main, side_b) # 真点切态 → _on_camp_fork_toggle → meta.tree_toggle_fork
|
||||
await get_tree().process_frame
|
||||
var iso_fk = load("res://core/save_manager.gd").new("user://smoke_click_probe_camp_slot_1.json")
|
||||
iso_fk.load_from_disk()
|
||||
@@ -130,9 +139,15 @@ func _ready() -> void:
|
||||
# ---- 营地灰字取色断言(迭代36 wc4——四子界面拆分后按视图分扫;卡④ T5 迁移):
|
||||
# 图鉴锁文→codex 视图(锚随形态订正去『柜台』);秘宝行未解锁/同调形态/档位/刷新行→treasure 视图
|
||||
# _workshop_holder 子树。font_color 逐处 == STONE_DEEP;树链段已闭树界面。
|
||||
main.ui.modals.camp.tree.closed.emit() # 树界面返回(关层归 presenter 单口)
|
||||
var tree_back: Button = ProbeUiUtil.find_button(main.ui.modals.camp.tree, "返回营地")
|
||||
if tree_back == null:
|
||||
_problems.append("树界面「返回营地」钮缺失(真输入化驱动中断)〔句柄子容器批 V1〕")
|
||||
else:
|
||||
ProbeUiUtil.push_click(main, tree_back) # 真点返回(关层归 presenter 单口——closed 同源钮)
|
||||
await get_tree().process_frame
|
||||
main.ui.modals.camp.scene.object_pressed.emit(&"chest") # 宝箱→秘宝匣(灰字四源位宿主)
|
||||
if main.ui.modals.camp.tree.visible:
|
||||
_problems.append("树窗「返回营地」真点击后未隐窗(真输入化补钉——落点判定)〔句柄子容器批 V1〕")
|
||||
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"chest").get_center()) # 宝箱→秘宝匣(灰字四源位宿主)
|
||||
await get_tree().process_frame
|
||||
var ws_labels: Array = []
|
||||
_collect_labels_by_text(main.ui.modals.camp.treasure._workshop_holder, ws_labels)
|
||||
@@ -169,9 +184,13 @@ func _ready() -> void:
|
||||
_problems.append("营地灰字:刷新行未用 STONE_DEEP(wc4 :525)〔句柄子容器批 V1〕")
|
||||
if not refresh_inked:
|
||||
_problems.append("营地灰字:刷新行 label 未找到(wc4 定位锚失守)〔句柄子容器批 V1〕")
|
||||
main.ui.modals.camp.treasure.closed.emit()
|
||||
var treasure_back: Button = ProbeUiUtil.find_button(main.ui.modals.camp.treasure, "返回营地")
|
||||
if treasure_back == null:
|
||||
_problems.append("秘宝匣「返回营地」钮缺失(真输入化驱动中断)〔句柄子容器批 V1〕")
|
||||
else:
|
||||
ProbeUiUtil.push_click(main, treasure_back) # 真点返回(closed 同源钮)
|
||||
await get_tree().process_frame
|
||||
main.ui.modals.camp.scene.object_pressed.emit(&"board") # 布告板→图鉴占位(锁文宿主)
|
||||
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"board").get_center()) # 布告板→图鉴占位(锁文宿主)
|
||||
await get_tree().process_frame
|
||||
var codex_labels: Array = []
|
||||
_collect_labels_by_text(main.ui.modals.camp.codex, codex_labels)
|
||||
@@ -181,7 +200,11 @@ func _ready() -> void:
|
||||
for l in atlas_locks:
|
||||
if l.get_theme_color("font_color") != UiKit.STONE_DEEP:
|
||||
_problems.append("营地灰字:图鉴锁文未用 STONE_DEEP(wc4 :167)〔句柄子容器批 V1〕〔营地图批 V6〕")
|
||||
main.ui.modals.camp.codex.closed.emit()
|
||||
var codex_back: Button = ProbeUiUtil.find_button(main.ui.modals.camp.codex, "返回营地")
|
||||
if codex_back == null:
|
||||
_problems.append("图鉴「返回营地」钮缺失(真输入化驱动中断)〔句柄子容器批 V1〕")
|
||||
else:
|
||||
ProbeUiUtil.push_click(main, codex_back) # 真点返回(closed 同源钮)
|
||||
await get_tree().process_frame
|
||||
main._on_camp_closed() # 关层收闭(营地与选档层同收+开始层恢复——卡④ T3)
|
||||
await get_tree().process_frame
|
||||
@@ -213,7 +236,7 @@ func _ready() -> void:
|
||||
_problems.append("[savesel] 槽1 新档钮缺失(sl5/sl12 驱动中断)")
|
||||
else:
|
||||
var samples: Array = []
|
||||
new_btn.pressed.emit() # 真实按钮信号路径 → _on_save_select_new → create_slot+open_camp
|
||||
ProbeUiUtil.push_click(main, new_btn) # 真点「新档」→ _on_save_select_new → create_slot+open_camp
|
||||
for fi in 3:
|
||||
await get_tree().process_frame
|
||||
samples.append([main.ui.modals.menu.save_select_overlay.visible, main.ui.modals.camp.scene.visible, main.ui.screens.start_overlay.visible])
|
||||
@@ -239,7 +262,7 @@ func _ready() -> void:
|
||||
if load_btn == null:
|
||||
_problems.append("[savesel] 槽2 进档钮缺失(sl6 驱动中断——夹具应有档)")
|
||||
else:
|
||||
load_btn.pressed.emit()
|
||||
ProbeUiUtil.push_click(main, load_btn) # 真点「进档」→ 激活槽2 开营地
|
||||
await get_tree().process_frame
|
||||
if main.save_manager.slots.active_slot() != 2:
|
||||
_problems.append("[savesel] sl6 进档链失守(active=%d 应=2——激活该槽)" % main.save_manager.slots.active_slot())
|
||||
@@ -254,7 +277,7 @@ func _ready() -> void:
|
||||
if del_btn == null:
|
||||
_problems.append("[savesel] 槽2 删档钮缺失(sl7 驱动中断)")
|
||||
else:
|
||||
del_btn.pressed.emit()
|
||||
ProbeUiUtil.push_click(main, del_btn) # 真点「删档」一击只开窗
|
||||
await get_tree().process_frame
|
||||
if not main.ui.modals.menu.confirm_dialog.is_open():
|
||||
_problems.append("[savesel] sl7 一击未只开确认窗(直删形态)")
|
||||
@@ -265,7 +288,7 @@ func _ready() -> void:
|
||||
_problems.append("[savesel] sl7 确认窗未经模态栈开(直开旁路——wr2 单口失守)")
|
||||
elif not main.ui.flow.locks_input():
|
||||
_problems.append("[savesel] sl7 确认窗开时输入锁未生效(锁域失守——wr2)")
|
||||
main.ui.modals.menu.confirm_dialog.cancelled.emit() # 取消路径(真实信号——cancelled 钮同源)
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.menu.confirm_dialog._cancel_btn) # 真点「取消」(cancelled 钮同源)
|
||||
await get_tree().process_frame
|
||||
if main.ui.modals.menu.confirm_dialog.is_open() or not FileAccess.file_exists(csel_tpl % 2):
|
||||
_problems.append("[savesel] sl7 取消路径失守(应只关窗、文件仍在)")
|
||||
@@ -273,9 +296,9 @@ func _ready() -> void:
|
||||
_problems.append("[savesel] sl8 取消后消费方状态量未清(分派面残留)")
|
||||
if main.ui.flow.stack._occluded.has(main.ui.modals.menu.confirm_dialog):
|
||||
_problems.append("[savesel] sl7 取消后确认窗遮挡账未清(关侧未走模态栈——wr2)")
|
||||
del_btn.pressed.emit() # 再开窗走确认路径
|
||||
ProbeUiUtil.push_click(main, del_btn) # 真点再开窗走确认路径
|
||||
await get_tree().process_frame
|
||||
main.ui.modals.menu.confirm_dialog.confirmed.emit() # 确认路径(真实信号——confirmed 钮同源)
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.menu.confirm_dialog._confirm_btn) # 真点「确认」(confirmed 钮同源)
|
||||
await get_tree().process_frame
|
||||
if FileAccess.file_exists(csel_tpl % 2) or main.ui.modals.menu.confirm_dialog.is_open():
|
||||
_problems.append("[savesel] sl7 确认路径失守(应删档+关窗)")
|
||||
@@ -343,7 +366,7 @@ func _ready() -> void:
|
||||
if settings_close == null:
|
||||
_problems.append("设置面返回按钮缺失")
|
||||
else:
|
||||
settings_close.pressed.emit()
|
||||
ProbeUiUtil.push_click(main, settings_close) # 真点「返回」收设置面
|
||||
await get_tree().process_frame
|
||||
if main.ui.modals.menu.settings_overlay.visible or not main.ui.screens.start_overlay.visible:
|
||||
_problems.append("设置面关闭后未恢复开始层")
|
||||
@@ -377,7 +400,7 @@ func _ready() -> void:
|
||||
if career_close == null:
|
||||
_problems.append("生涯页返回按钮缺失")
|
||||
else:
|
||||
career_close.pressed.emit()
|
||||
ProbeUiUtil.push_click(main, career_close) # 真点「返回」收生涯页
|
||||
await get_tree().process_frame
|
||||
if main.ui.modals.menu.career_overlay.visible or not main.ui.screens.start_overlay.visible:
|
||||
_problems.append("生涯页关闭后未恢复开始层")
|
||||
@@ -463,7 +486,11 @@ func _ready() -> void:
|
||||
# 分派链序:商店层=选路页上的模态覆盖(停点呈现为
|
||||
# level_screen+shop 双 visible)——必须先于 level_screen 分派,否则下层候选点击
|
||||
# 空转烧尽预算(实证:lv2 商店停点双 visible 循环 12000 帧 done=false)。
|
||||
main.ui.modals.menu.shop_overlay.leave_pressed.emit()
|
||||
var shop_leave: Button = ProbeUiUtil.find_button(main.ui.modals.menu.shop_overlay, "离开商店")
|
||||
if shop_leave == null:
|
||||
_problems.append("商店「离开商店」钮缺失(真输入化驱动中断)")
|
||||
else:
|
||||
ProbeUiUtil.push_click(main, shop_leave) # 真点离店(leave_pressed 同源钮)放行选路
|
||||
_seals_placed.clear()
|
||||
continue
|
||||
if main.ui.modals.run.level_screen.visible:
|
||||
@@ -476,12 +503,13 @@ func _ready() -> void:
|
||||
if mpb == null or not mpb.visible or mpb.disabled:
|
||||
_problems.append("地图屏状态角标缺失/未点亮/被置灰(P1 丁)")
|
||||
else:
|
||||
mpb.pressed.emit()
|
||||
ProbeUiUtil.push_click(main, mpb) # 真点地图屏角标开状态面板
|
||||
if not main.ui.modals.run.status_panel.visible or not main.ui.modals.run.level_screen.visible:
|
||||
_problems.append("地图屏角标开状态面板失败或地图屏被误藏(P2 甲层级失守)")
|
||||
await get_tree().process_frame # 面板当帧开——容器排版帧后钮 rect 才有效(真点击依赖排版)
|
||||
var rbtn2: Button = ProbeUiUtil.find_button(main.ui.modals.run.status_panel, "继续", "prefix")
|
||||
if rbtn2 != null:
|
||||
rbtn2.pressed.emit()
|
||||
ProbeUiUtil.push_click(main, rbtn2) # 真点「继续」关面板
|
||||
if main.ui.modals.run.status_panel.visible or not main.ui.modals.run.level_screen.visible:
|
||||
_problems.append("状态面板关闭后地图屏态失守(恢复语义破)")
|
||||
# 地图屏加成帽换代断言(迭代36 卡A wa2——「特性」→「属性值提升」;帽 Label 常在树;
|
||||
@@ -519,7 +547,8 @@ func _ready() -> void:
|
||||
if next_btn == null:
|
||||
_problems.append("大奖励屏确认按钮缺失")
|
||||
else:
|
||||
next_btn.pressed.emit()
|
||||
await get_tree().process_frame # 停点层当帧开——排版帧后真点击
|
||||
ProbeUiUtil.push_click(main, next_btn) # 真点「进入下一层」推进 L2(落点判定见下)
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
if main.run_scope.runner == null or int(main.run_scope.runner.level_row.get("level_index", -1)) != 2:
|
||||
@@ -540,7 +569,7 @@ func _ready() -> void:
|
||||
if main.ui.modals.run.end_overlay.verdict_text() == "":
|
||||
_problems.append("胜利结算框判定行缺失")
|
||||
_settlement_confirms += 1
|
||||
main.ui.modals.run.end_overlay._btn.pressed.emit() # 「下一节点」真实信号路径
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.run.end_overlay._btn) # 真点「下一节点」(结算框已过帧排版)
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
if main.run_scope.settlement_stop_timer.is_armed() or not main.ui.modals.run.level_screen.visible:
|
||||
@@ -555,7 +584,7 @@ func _ready() -> void:
|
||||
# (_adventure_mode)在真终局仍 true——会把真终局当中途面点掉返回键(实证)。
|
||||
var _adv_running: bool = main.run_scope.adventure != null and String(main.run_scope.adventure.state) == "running"
|
||||
if main.run_scope.is_adventure and _adv_running:
|
||||
main.ui.modals.run.end_overlay._btn.pressed.emit()
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.run.end_overlay._btn) # 中途结算面真点「继续」推进
|
||||
continue
|
||||
if not main.ui.flow.locks_input():
|
||||
_problems.append("终局 overlay 打开时输入未锁定")
|
||||
@@ -612,7 +641,7 @@ func _ready() -> void:
|
||||
if main.ui.modals.run.end_overlay.is_open():
|
||||
# 中途结算面(state=running)=点继续推进;真终局(settling)=断点段前提满足,收口。
|
||||
if main.run_scope.is_adventure and main.run_scope.adventure != null and String(main.run_scope.adventure.state) == "running":
|
||||
main.ui.modals.run.end_overlay._btn.pressed.emit()
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.run.end_overlay._btn) # 中途结算面真点「继续」推进
|
||||
else:
|
||||
break
|
||||
elif main.ui.modals.run.curio_overlay.visible:
|
||||
@@ -626,18 +655,20 @@ func _ready() -> void:
|
||||
elif main.ui.modals.run.node_overlay.visible:
|
||||
main.ui.modals.run.node_overlay.press_continue()
|
||||
elif main.ui.modals.menu.shop_overlay.visible:
|
||||
main.ui.modals.menu.shop_overlay.leave_pressed.emit()
|
||||
var shop_leave2: Button = ProbeUiUtil.find_button(main.ui.modals.menu.shop_overlay, "离开商店")
|
||||
if shop_leave2 != null:
|
||||
ProbeUiUtil.push_click(main, shop_leave2) # 真点离店(收口段同主循环分派)
|
||||
elif main.ui.screens.inter_overlay.visible:
|
||||
var next_btn2: Button = ProbeUiUtil.find_button(main.ui.screens.inter_overlay, "进入下一层", "prefix")
|
||||
if next_btn2 != null:
|
||||
next_btn2.pressed.emit()
|
||||
ProbeUiUtil.push_click(main, next_btn2) # 真点「进入下一层」(收口段)
|
||||
elif main.ui.modals.run.level_screen.visible:
|
||||
var abandon_btn: Button = ProbeUiUtil.find_button(main.ui.modals.run.level_screen, "放弃", "prefix")
|
||||
if abandon_btn != null:
|
||||
_closed_via_abandon = true
|
||||
abandon_btn.pressed.emit()
|
||||
ProbeUiUtil.push_click(main, abandon_btn) # 真点「放弃」一击
|
||||
await get_tree().process_frame
|
||||
abandon_btn.pressed.emit() # 二击确认(3s 窗内)
|
||||
ProbeUiUtil.push_click(main, abandon_btn) # 真点二击确认(3s 窗内)
|
||||
for i in 10:
|
||||
await get_tree().process_frame
|
||||
break
|
||||
@@ -665,7 +696,7 @@ func _ready() -> void:
|
||||
if main.ui.modals.run.end_overlay._btn == null:
|
||||
_problems.append("断点段:终局面返回按钮缺失")
|
||||
else:
|
||||
main.ui.modals.run.end_overlay._btn.pressed.emit() # 真实按钮信号路径(_btn=继续/返回共用句柄)
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.run.end_overlay._btn) # 真点「返回」(_btn=继续/返回共用句柄)
|
||||
await get_tree().process_frame
|
||||
if not main.ui.screens.start_overlay.visible:
|
||||
# 豁免:放弃路径收口时终局「返回」后仍挂三选一/关卡推进残留态(停点链),
|
||||
@@ -693,7 +724,7 @@ func _ready() -> void:
|
||||
_problems.append("大门无断点:未确认即进冒险〔营地图批 V3〕")
|
||||
if main.ui.modals.menu.confirm_dialog._secondary_btn.visible:
|
||||
_problems.append("大门无断点:次钮应隐藏(两出口形态)〔营地图批 V3〕")
|
||||
main.ui.modals.menu.confirm_dialog.cancelled.emit()
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.menu.confirm_dialog._cancel_btn) # 真点「取消」(大门取消出口)
|
||||
await get_tree().process_frame
|
||||
if not main.ui.modals.camp.scene.visible or main.run_scope.is_adventure or main._camp_data()["balance"] != bal_gate:
|
||||
_problems.append("大门取消:未无变更退回营地(写口零调用/营地根 visible 失守)〔营地图批 V3〕")
|
||||
@@ -795,6 +826,7 @@ func _ready() -> void:
|
||||
exit_code = 1
|
||||
if not _problems.is_empty():
|
||||
exit_code = 1
|
||||
_recorder.dump_js() # 收尾落盘(大门段/开场行段点击在循环退出后——落盘必须在 quit 前)
|
||||
get_tree().quit(exit_code)
|
||||
|
||||
|
||||
@@ -821,7 +853,7 @@ func _run_modal_probes_once() -> void:
|
||||
# ① 图例模态(pattern 6):打开 → 遮挡物隐藏 + 输入锁定;关闭 → 恢复
|
||||
if not _tested_legend:
|
||||
_tested_legend = true
|
||||
main.ui.battle.board_zone.ap_bar._btn_legend.pressed.emit()
|
||||
ProbeUiUtil.push_click(main, main.ui.battle.board_zone.ap_bar._btn_legend) # 真点图例钮开层
|
||||
if not main.ui.modals.run.legend_overlay.is_open():
|
||||
_problems.append("图例打开失败")
|
||||
if main.ui.modals.run.end_overlay.is_open() or main.ui.screens.start_overlay.visible:
|
||||
@@ -851,7 +883,7 @@ func _run_modal_probes_once() -> void:
|
||||
_problems.append("战斗屏状态角标缺失/未点亮/被置灰(P1 丁入口失守 visible=%s disabled=%s)"
|
||||
% [str(pbtn != null and pbtn.visible), str(pbtn != null and pbtn.disabled)])
|
||||
else:
|
||||
pbtn.pressed.emit() # 真实按钮信号路径 → 组合根 _on_status_badge_pressed
|
||||
ProbeUiUtil.push_click(main, pbtn) # 真点状态角标 → 组合根 _on_status_badge_pressed
|
||||
if not main.ui.modals.run.status_panel.visible:
|
||||
_problems.append("状态面板未随角标打开(组合根接线失守)")
|
||||
if not main.ui.flow.locks_input():
|
||||
@@ -903,30 +935,32 @@ func _run_modal_probes_once() -> void:
|
||||
if resume_btn == null:
|
||||
_problems.append("状态面板关闭钮缺失(「继续」)")
|
||||
else:
|
||||
resume_btn.pressed.emit()
|
||||
await get_tree().process_frame # 面板当帧开——排版帧后真点击
|
||||
ProbeUiUtil.push_click(main, resume_btn) # 真点「继续」关面板恢复
|
||||
if main.ui.modals.run.status_panel.visible or main.ui.flow.locks_input():
|
||||
_problems.append("状态面板关闭后未恢复(visible=%s locked=%s——R3 残留)"
|
||||
% [str(main.ui.modals.run.status_panel.visible), str(main.ui.flow.locks_input())])
|
||||
# P1 守卫:图例开着 → 两处入口角标置灰;置灰态点击被处理器兜住(探针 emit 绕过 disabled)
|
||||
main.ui.battle.board_zone.ap_bar._btn_legend.pressed.emit()
|
||||
# P1 守卫:图例开着 → 两处入口角标置灰;置灰态点击被处理器兜住
|
||||
#(置灰态穿透判定保留 emit 直发——真输入到不了置灰钮,处理器级防线只能以〔接线断言〕钉住)
|
||||
ProbeUiUtil.push_click(main, main.ui.battle.board_zone.ap_bar._btn_legend) # 真点图例钮复开
|
||||
if not main.ui.battle.board_zone.status_badge.disabled:
|
||||
_problems.append("图例开着时战斗屏状态角标未置灰(P1 守卫失守)")
|
||||
main.ui.battle.board_zone.status_badge.pressed.emit()
|
||||
main.ui.battle.board_zone.status_badge.pressed.emit() # 〔接线断言〕置灰态 emit 穿透——钉处理器兜底(真输入被 disabled 拦,测不到此层)
|
||||
if main.ui.modals.run.status_panel.visible:
|
||||
_problems.append("置灰态仍可开状态面板(守卫兜底失守)")
|
||||
main.ui.modals.run.legend_overlay.close()
|
||||
# ③ tooltip(状态效果槽悬停——来源+语义+剩余):有槽可见时触发
|
||||
# ③ tooltip(状态效果槽悬停——来源+语义+剩余):有槽可见时触发(真输入 motion 进管线触发 entered/exited 沿)
|
||||
if not _tested_tooltip:
|
||||
var row = main.ui.battle.stage.hero_standee.slot_row # 状态 tooltip 载体(B 区站桩)
|
||||
for child in row.get_children():
|
||||
if child is PanelContainer and child.visible and child.has_meta("eff"):
|
||||
_tested_tooltip = true
|
||||
child.mouse_entered.emit()
|
||||
ProbeUiUtil.push_motion(main, (child as PanelContainer).get_global_rect().get_center())
|
||||
if not row._tip.visible:
|
||||
_problems.append("状态槽 tooltip 未显示")
|
||||
elif not row._tip_label.text.contains("来源") or not row._tip_label.text.contains("剩余"):
|
||||
_problems.append("状态槽 tooltip 缺来源/剩余(%s)" % row._tip_label.text.replace("\n", "|"))
|
||||
child.mouse_exited.emit()
|
||||
ProbeUiUtil.push_motion(main, Vector2(-40, -40)) # 移出一切控件(视口外沿触发 exited)
|
||||
if row._tip.visible:
|
||||
_problems.append("状态槽 tooltip 未随移出隐藏")
|
||||
break
|
||||
@@ -1027,8 +1061,8 @@ func _random_unknown_adjacent(minesweeper) -> Vector2i:
|
||||
|
||||
|
||||
func _click_stop() -> void:
|
||||
# 停手走真实按钮信号路径(AP 条 → 组合根 settle_batch——排雷玩法 D1×6① 回归载体)
|
||||
main.ui.battle.board_zone.ap_bar._btn_stop.pressed.emit()
|
||||
# 停手走真实点击路径(AP 条「停手」钮 → 组合根 settle_batch——排雷玩法 D1×6① 回归载体)
|
||||
ProbeUiUtil.push_click(main, main.ui.battle.board_zone.ap_bar._btn_stop)
|
||||
|
||||
|
||||
# ---- T1 D1:锁期/重放断言(每 run 一次,脚本化场景——演播期豁免的断言化承载) ----
|
||||
|
||||
@@ -0,0 +1,587 @@
|
||||
# journey_player.gd — 全动线机器自玩驱动(C·发现环节去人化)
|
||||
# 是什么:headless 场景驱动件——实例化 main、隔离存档,按旅程段驱动全动线(开机→设置→存档三操作
|
||||
# 新档/删档/进档→营地功能窗逐窗开合→Esc 回退→大门开冒险→关卡推进→战斗→各停点→终局收束→回菜单),
|
||||
# 每屏到达记账+段间异常上报,收尾落 JSONP 报告并入探针覆盖登记(真输入事件同源记账)。
|
||||
# 判据(契约 §5):每个屏到达过+段间零 fatal。
|
||||
# 异常上报三类(其余静默):①诊断收集器 fatal 级(settlement battle_result=fatal_error/fatal 屏)
|
||||
# ②段间超时(单段 >30s 未达预期停点;战斗段另设状态无变化挂死阈值)③屏状态与预期不符。
|
||||
# RID 泄漏等已知日志噪音静默。
|
||||
# 归因字段:每条上报机器自填——段名/期望屏/实测屏/上一动作/通道标签(真实输入-按钮/
|
||||
# 真实输入-场景热点/真实输入-键盘/方法-地图命中口/方法-棋盘口)。原归因制度残骸=本结构内字段。
|
||||
# 误报收敛:误报指纹(类+段+归因摘要)落 .gates/coverage/journey_misfire_ledger.json——同一指纹
|
||||
# 第 2 次起自动登记静默(计数照记、不再进上报清单);连 3 批零真缺陷判据收紧归用户复核轮。
|
||||
# 驱动纪律:按钮/场景物件全走真输入(push_input 管线,同探针判据——ProbeUiUtil 单点通道)。
|
||||
# 两处方法口如实标注通道:地图候选=press_candidate(自绘布局命中路径,探针族既定入口);
|
||||
# 棋盘格=click_cell(BoardView 公共点击口,ui_input_probe 先例——棋盘管线面由该探针承载)。
|
||||
# 停点分发的视图内部读数(_continue_btn/_node_pending 等)沿用探针族直读受控例外(iter53 反哺点 9 同款在册)。
|
||||
# 运行:Godot_console.exe --headless --path . res://sim/journey_player.tscn
|
||||
extends Node
|
||||
|
||||
const ProbeUiUtil := preload("res://probe/probe_ui_util.gd")
|
||||
const ProbeCoverageRecorder := preload("res://probe/probe_coverage_recorder.gd")
|
||||
const AutoPlayer := preload("res://sim/auto_player.gd")
|
||||
|
||||
const SEGMENT_TIMEOUT_MS := 30000 # 段间超时判据(契约 §5——单段 30s 未达预期停点)
|
||||
const BATTLE_STALL_FRAMES := 90 # 战斗段状态无变化挂死阈值(演播期豁免同探针口径)
|
||||
const ADVENTURE_BUDGET_FRAMES := 60000 # 冒险段帧预算(全停点循环兜底)
|
||||
const MISFIRE_PATH := "res://tests/.gates/coverage/journey_misfire_ledger.json"
|
||||
const REPORT_PATH := "res://tests/.gates/coverage/journey_report.js"
|
||||
|
||||
var main: Node
|
||||
var _recorder: RefCounted = null
|
||||
var _problems: Array = [] # 异常上报(三类;指纹二次起静默不入此列)
|
||||
var _silent_count := 0 # 误报静默笔数(回执可见)
|
||||
var _screens_seen: Dictionary = {} # 屏到达记账(判据面)
|
||||
var _screen_trace: Array = [] # 屏到达变迁轨迹(诊断面——新屏首现时点+上一动作)
|
||||
var _misfire: Dictionary = {} # 指纹 → 次数(跨跑落盘)
|
||||
var _last_state := {} # 战斗段状态哈希(挂死检测)
|
||||
var _stall := 0
|
||||
var _last_action := "(无)" # 归因初判:上一动作描述
|
||||
var _map_status_done := false # 地图屏状态角标一次性
|
||||
var _legend_done := false # 图例开合一次性(每战斗面一次)
|
||||
var _status_done := false # 战斗屏状态面板一次性
|
||||
var _refresh_done := false # 刷新确认链一次性
|
||||
|
||||
|
||||
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
|
||||
_load_misfire()
|
||||
_recorder = ProbeCoverageRecorder.new()
|
||||
_recorder.attach(main, "journey")
|
||||
ProbeUiUtil.isolate_save(main, "journey_player", false, false, true)
|
||||
main.save_manager.slots.attach_slot_template("user://journey_player_slot_%d.json")
|
||||
for i in range(1, 4): # 夹具清场——陈旧槽文件会翻有档态、新档钮缺席(open_camp_via_savesel 同款坑注)
|
||||
for suf in ["", ".tmp", ".corrupt.bak"]:
|
||||
var sp: String = main.save_manager.slots.slot_path(i) + suf
|
||||
if FileAccess.file_exists(sp):
|
||||
DirAccess.remove_absolute(sp)
|
||||
main.ui.modals.menu.debug_overlay.set_sliders(400, 200) # 高加成保 L1 可完成(探针先例——夹具注)
|
||||
main.ui.modals.menu.debug_overlay.seed_edit.text = "42"
|
||||
|
||||
await _phase_expect("开机落菜单", ["菜单"])
|
||||
await _phase_menu_settings()
|
||||
await _phase_new_slot(1)
|
||||
await _phase_camp_windows()
|
||||
await _phase_camp_esc_return()
|
||||
await _phase_new_and_delete(2)
|
||||
await _phase_load_slot(1)
|
||||
await _phase_gate_start()
|
||||
await _phase_adventure()
|
||||
await _phase_end_back_menu()
|
||||
|
||||
_recorder.dump_js()
|
||||
_dump_report()
|
||||
_save_misfire()
|
||||
var fatal := _fatal_count()
|
||||
if _problems.is_empty() and fatal == 0 and _screens_seen.has("菜单") and _screens_seen.has("终局"):
|
||||
print("[journey] PASS — 全动线走查完成(屏到达 %d 类,异常上报 0,fatal 0,误报静默 %d)" % [
|
||||
_screens_seen.size(), _silent_count])
|
||||
get_tree().quit(0)
|
||||
return
|
||||
print("[journey] FAIL — 异常 %d(静默 %d)/ fatal %d / 屏到达 %d 类" % [
|
||||
_problems.size(), _silent_count, fatal, _screens_seen.size()])
|
||||
for p in _problems:
|
||||
print("[journey] - " + JSON.stringify(p, " "))
|
||||
get_tree().quit(1)
|
||||
|
||||
|
||||
# ---- 屏状态读取与记账 ----
|
||||
|
||||
func _screen_state() -> Dictionary:
|
||||
var st := {
|
||||
"菜单": main.ui.screens.start_overlay != null and main.ui.screens.start_overlay.visible,
|
||||
"选档": main.ui.modals.menu.save_select_overlay.visible,
|
||||
"设置": main.ui.modals.menu.settings_overlay.visible,
|
||||
"生涯": main.ui.modals.menu.career_overlay.visible,
|
||||
"确认弹窗": main.ui.modals.menu.confirm_dialog.is_open(),
|
||||
"营地": main.ui.modals.camp.scene.visible,
|
||||
"锻造窗": main.ui.modals.camp.forge.visible,
|
||||
"树窗": main.ui.modals.camp.tree.visible,
|
||||
"秘宝窗": main.ui.modals.camp.treasure.visible,
|
||||
"图鉴窗": main.ui.modals.camp.codex.visible,
|
||||
"Esc浮窗": main.ui.modals.camp.esc_overlay.visible,
|
||||
"冒险": main.run_scope != null and main.run_scope.is_adventure,
|
||||
"关卡屏": main.ui.modals.run.level_screen != null and main.ui.modals.run.level_screen.visible,
|
||||
"三选一": main.ui.modals.run.curio_overlay.visible,
|
||||
"商店": main.ui.modals.menu.shop_overlay.visible,
|
||||
"大奖励": main.ui.screens.inter_overlay != null and main.ui.screens.inter_overlay.visible,
|
||||
"节点": main.ui.modals.run.node_overlay.visible,
|
||||
"终局": main.ui.modals.run.end_overlay != null and main.ui.modals.run.end_overlay.is_open(),
|
||||
}
|
||||
var in_battle: bool = bool(st["冒险"]) and not bool(st["关卡屏"]) and not bool(st["终局"]) \
|
||||
and main.run_scope != null and main.run_scope.settlement != null \
|
||||
and String(main.run_scope.settlement.battle_result) == ""
|
||||
st["战斗"] = in_battle
|
||||
return st
|
||||
|
||||
|
||||
func _record_screens() -> void:
|
||||
var st := _screen_state()
|
||||
for k in st:
|
||||
if bool(st[k]) and not _screens_seen.has(k):
|
||||
_screens_seen[k] = true
|
||||
_screen_trace.append({"t": Time.get_datetime_string_from_system(), "screen": k, "last_action": _last_action})
|
||||
_check_exclusion(st)
|
||||
|
||||
|
||||
var _excl_reported: Dictionary = {} # 屏互斥违约对(本跑一次性上报——跨跑由误报指纹静默)
|
||||
|
||||
# 屏级互斥(探针同款判据对):合法叠加仅「终局盖冒险」;违约=③屏状态与预期不符
|
||||
func _check_exclusion(st: Dictionary) -> void:
|
||||
var pairs := [["菜单", "营地"], ["菜单", "冒险"], ["营地", "冒险"], ["终局", "营地"], ["终局", "菜单"], ["菜单", "战斗"]]
|
||||
for pair in pairs:
|
||||
if bool(st.get(pair[0], false)) and bool(st.get(pair[1], false)):
|
||||
var fk: String = str(pair[0]) + "×" + str(pair[1])
|
||||
if not _excl_reported.has(fk):
|
||||
_excl_reported[fk] = true
|
||||
_report("screen_mismatch", "屏级互斥", "%s 与 %s 不应同场" % [pair[0], pair[1]])
|
||||
|
||||
|
||||
func _active_screens() -> Array:
|
||||
var on: Array = []
|
||||
var st := _screen_state()
|
||||
for k in st:
|
||||
if bool(st[k]):
|
||||
on.append(k)
|
||||
return on
|
||||
|
||||
|
||||
func _fatal_count() -> int:
|
||||
var n := 0
|
||||
if main.run_scope != null and main.run_scope.settlement != null \
|
||||
and String(main.run_scope.settlement.battle_result) == "fatal_error":
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
# ---- 异常上报(三类+归因+误报收敛) ----
|
||||
|
||||
func _report(kind: String, seg: String, expect: String) -> void:
|
||||
var finger_src := "%s|%s|%s|%s" % [kind, seg, expect, ",".join(_active_screens())]
|
||||
var finger := str(finger_src.hash())
|
||||
_misfire[finger] = int(_misfire.get(finger, 0)) + 1
|
||||
if int(_misfire[finger]) >= 2:
|
||||
_silent_count += 1 # 同一误报源第 2 次自动登记静默(契约 §5 收敛口径)
|
||||
return
|
||||
_problems.append({
|
||||
"kind": kind, "segment": seg, "expect": expect,
|
||||
"actual_screens": _active_screens(), "last_action": _last_action,
|
||||
"fatal": _fatal_count(), "at": Time.get_datetime_string_from_system(),
|
||||
})
|
||||
|
||||
|
||||
func _load_misfire() -> void:
|
||||
if FileAccess.file_exists(MISFIRE_PATH):
|
||||
var parsed: Variant = JSON.parse_string(FileAccess.get_file_as_string(MISFIRE_PATH))
|
||||
if parsed is Dictionary:
|
||||
_misfire = parsed
|
||||
|
||||
|
||||
func _save_misfire() -> void:
|
||||
var f := FileAccess.open(MISFIRE_PATH, FileAccess.WRITE)
|
||||
if f != null:
|
||||
f.store_string(JSON.stringify(_misfire, " "))
|
||||
f.close()
|
||||
|
||||
|
||||
# ---- 段间等待(超时判据承载) ----
|
||||
|
||||
func _frames(n: int = 2) -> void:
|
||||
for _i in n:
|
||||
await get_tree().process_frame
|
||||
_record_screens()
|
||||
|
||||
|
||||
# 期望屏集合在超时内逐帧到达;到达即记账返回。超时=异常②超时+③屏状态不符双记(同源指纹静默收敛)。
|
||||
func _phase_expect(seg: String, expect_keys: Array) -> void:
|
||||
_last_action = "等待段「%s」落点(期望 %s)" % [seg, ",".join(expect_keys)]
|
||||
var t0 := Time.get_ticks_msec()
|
||||
while Time.get_ticks_msec() - t0 < SEGMENT_TIMEOUT_MS:
|
||||
await get_tree().process_frame
|
||||
_record_screens()
|
||||
var st := _screen_state()
|
||||
var all_on := true
|
||||
for k in expect_keys:
|
||||
all_on = all_on and bool(st.get(k, false))
|
||||
if all_on:
|
||||
await _frames(2) # 开层排版帧——当帧可见时容器未排,紧随的控件真点击会落空(探针件同款坑)
|
||||
return
|
||||
_report("timeout", seg, "屏 %s 应在场" % ",".join(expect_keys))
|
||||
_report("screen_mismatch", seg, "屏 %s 应在场" % ",".join(expect_keys))
|
||||
_save_misfire()
|
||||
|
||||
|
||||
# ---- 旅程段 ----
|
||||
|
||||
func _phase_menu_settings() -> void:
|
||||
# 设置开合(开始层真实入口——功能界面全覆盖义务)
|
||||
var btn: Button = ProbeUiUtil.find_button(main.ui.screens.start_overlay, "设置", "prefix")
|
||||
if btn == null:
|
||||
_report("screen_mismatch", "设置入口", "开始层「设置」钮在场")
|
||||
return
|
||||
_last_action = "真点「设置」钮"
|
||||
ProbeUiUtil.push_click(main, btn)
|
||||
await _phase_expect("设置开", ["设置"])
|
||||
var back: Button = ProbeUiUtil.find_button(main.ui.modals.menu.settings_overlay, "返回", "prefix")
|
||||
if back != null:
|
||||
_last_action = "真点设置「返回」钮"
|
||||
ProbeUiUtil.push_click(main, back)
|
||||
await _phase_expect("设置收", ["菜单"])
|
||||
|
||||
|
||||
func _phase_new_slot(slot: int) -> void:
|
||||
# 开机→存档·新档(选档层真实入口:进入存档→槽位新档→落营地)
|
||||
var esb: Button = ProbeUiUtil.find_button(main.ui.screens.start_overlay, "进入存档", "prefix")
|
||||
if esb == null:
|
||||
_report("screen_mismatch", "选档入口", "开始层「进入存档」钮在场")
|
||||
return
|
||||
_last_action = "真点「进入存档」钮"
|
||||
ProbeUiUtil.push_click(main, esb)
|
||||
await _phase_expect("选档层开", ["选档"])
|
||||
var nb: Button = main.ui.modals.menu.save_select_overlay._new_btns.get(slot)
|
||||
if nb == null:
|
||||
_report("screen_mismatch", "新档槽%d" % slot, "槽%d 新档钮在场" % slot)
|
||||
return
|
||||
_last_action = "真点槽%d「新档」钮" % slot
|
||||
ProbeUiUtil.push_click(main, nb)
|
||||
await _phase_expect("新档落营地", ["营地"])
|
||||
|
||||
|
||||
func _phase_load_slot(slot: int) -> void:
|
||||
var esb: Button = ProbeUiUtil.find_button(main.ui.screens.start_overlay, "进入存档", "prefix")
|
||||
if esb == null:
|
||||
_report("screen_mismatch", "选档入口", "开始层「进入存档」钮在场")
|
||||
return
|
||||
_last_action = "真点「进入存档」钮(进档链)"
|
||||
ProbeUiUtil.push_click(main, esb)
|
||||
await _phase_expect("选档层开", ["选档"])
|
||||
var lb: Button = main.ui.modals.menu.save_select_overlay._load_btns.get(slot)
|
||||
if lb == null:
|
||||
_report("screen_mismatch", "进档槽%d" % slot, "槽%d 进档钮在场" % slot)
|
||||
return
|
||||
_last_action = "真点槽%d「进档」钮" % slot
|
||||
ProbeUiUtil.push_click(main, lb)
|
||||
await _phase_expect("进档落营地", ["营地"])
|
||||
|
||||
|
||||
func _phase_new_and_delete(slot: int) -> void:
|
||||
# 新档→篝火回菜单→选档删档(二次确认双路径真点击)——存档三操作收口
|
||||
await _phase_new_slot(slot)
|
||||
var camp: Control = main.ui.modals.camp.scene
|
||||
_last_action = "真点篝火热点(回退动线)"
|
||||
ProbeUiUtil.push_click_at(main, camp.get_hotspot_rect(&"campfire").get_center())
|
||||
await _phase_expect("篝火确认弹窗", ["确认弹窗"])
|
||||
var ok: Button = main.ui.modals.menu.confirm_dialog._confirm_btn
|
||||
if ok != null:
|
||||
_last_action = "真点篝火确认「确认」钮"
|
||||
ProbeUiUtil.push_click(main, ok)
|
||||
await _phase_expect("回菜单", ["菜单"])
|
||||
# 删档:进选档→槽位删档一击开窗→真点取消→再删→真点确认(文件三态走查归探针既有套件——C 只验到达)
|
||||
var esb: Button = ProbeUiUtil.find_button(main.ui.screens.start_overlay, "进入存档", "prefix")
|
||||
if esb != null:
|
||||
_last_action = "真点「进入存档」钮(删档链)"
|
||||
ProbeUiUtil.push_click(main, esb)
|
||||
await _phase_expect("选档层开", ["选档"])
|
||||
var db: Button = main.ui.modals.menu.save_select_overlay._delete_btns.get(slot)
|
||||
if db != null:
|
||||
_last_action = "真点槽%d「删档」钮一击" % slot
|
||||
ProbeUiUtil.push_click(main, db)
|
||||
await _phase_expect("删档确认窗", ["确认弹窗"])
|
||||
var cc: Button = main.ui.modals.menu.confirm_dialog._cancel_btn
|
||||
if cc != null:
|
||||
_last_action = "真点删档「取消」钮"
|
||||
ProbeUiUtil.push_click(main, cc)
|
||||
await _frames(2)
|
||||
_last_action = "真点槽%d「删档」钮二击" % slot
|
||||
ProbeUiUtil.push_click(main, db)
|
||||
await _phase_expect("删档确认窗", ["确认弹窗"])
|
||||
var ok2: Button = main.ui.modals.menu.confirm_dialog._confirm_btn
|
||||
if ok2 != null:
|
||||
_last_action = "真点删档「确认」钮"
|
||||
ProbeUiUtil.push_click(main, ok2)
|
||||
await _frames(3)
|
||||
var back: Button = ProbeUiUtil.find_button(main.ui.modals.menu.save_select_overlay, "返回", "prefix")
|
||||
if back != null:
|
||||
_last_action = "真点选档「返回」钮"
|
||||
ProbeUiUtil.push_click(main, back)
|
||||
await _phase_expect("选档收", ["菜单"])
|
||||
|
||||
|
||||
func _phase_camp_windows() -> void:
|
||||
# 营地功能窗逐窗真点开合(锻造/秘宝/树/图鉴/生涯——campfire/gate 归动线段)
|
||||
var views := {
|
||||
&"forge": ["锻造窗", main.ui.modals.camp.forge],
|
||||
&"chest": ["秘宝窗", main.ui.modals.camp.treasure],
|
||||
&"dummy": ["树窗", main.ui.modals.camp.tree],
|
||||
&"board": ["图鉴窗", main.ui.modals.camp.codex],
|
||||
&"notebook": ["生涯", main.ui.modals.menu.career_overlay],
|
||||
}
|
||||
for key: StringName in [&"forge", &"chest", &"dummy", &"board", &"notebook"]:
|
||||
var camp: Control = main.ui.modals.camp.scene
|
||||
_last_action = "真点营地物件热点 %s" % String(key)
|
||||
ProbeUiUtil.push_click_at(main, camp.get_hotspot_rect(key).get_center())
|
||||
await _phase_expect("%s窗开" % String(key), [views[key][0]])
|
||||
var back: Button = ProbeUiUtil.find_button(views[key][1], "返回营地")
|
||||
if back == null:
|
||||
back = ProbeUiUtil.find_button(views[key][1], "返回", "prefix")
|
||||
if back != null:
|
||||
_last_action = "真点 %s 窗返回钮" % String(key)
|
||||
ProbeUiUtil.push_click(main, back)
|
||||
await _frames(3)
|
||||
|
||||
|
||||
func _phase_camp_esc_return() -> void:
|
||||
# 营地 Esc 浮窗→「返回主菜单」真点(回退动线+浮窗功能面到达)
|
||||
_push_esc()
|
||||
await _phase_expect("Esc浮窗开", ["Esc浮窗"])
|
||||
var rb: Button = main.ui.modals.camp.esc_overlay._return_btn
|
||||
if rb != null:
|
||||
_last_action = "真点浮窗「返回主菜单」钮"
|
||||
ProbeUiUtil.push_click(main, rb)
|
||||
await _phase_expect("浮窗回菜单", ["菜单"])
|
||||
|
||||
|
||||
func _phase_gate_start() -> void:
|
||||
# 大门确认→开冒险(动线收口——确认后落点=冒险,非回菜单)
|
||||
var camp: Control = main.ui.modals.camp.scene
|
||||
_last_action = "真点大门热点"
|
||||
ProbeUiUtil.push_click_at(main, camp.get_hotspot_rect(&"gate").get_center())
|
||||
await _phase_expect("大门确认弹窗", ["确认弹窗"])
|
||||
var ok: Button = main.ui.modals.menu.confirm_dialog._confirm_btn
|
||||
if ok != null:
|
||||
_last_action = "真点大门「确认」钮"
|
||||
ProbeUiUtil.push_click(main, ok)
|
||||
await _phase_expect("冒险发起", ["冒险"])
|
||||
# 已知缺陷现场(发现环义务已尽):大门开局后开始层滞留盖屏=「开局被开始层盖住」
|
||||
# (营地交互修复批承接中、合回前 master 在案)——开始层可见直入 locks_input 谓词,战斗全程锁死。
|
||||
# C 上报后以夹具面隐藏开始层继续走查(通道=夹具修整,非真输入;缺陷本体不因此掩盖)。
|
||||
await _frames(2)
|
||||
if main.ui.screens.start_overlay != null and main.ui.screens.start_overlay.visible:
|
||||
_report("screen_mismatch", "大门开局", "开始层滞留盖屏(已知缺陷——营地交互修复批承接中)")
|
||||
_last_action = "夹具修整:隐藏滞留开始层以解锁战斗走查(缺陷已上报在案)"
|
||||
main.ui.screens.start_overlay.visible = false
|
||||
await _frames(2)
|
||||
|
||||
|
||||
func _phase_adventure() -> void:
|
||||
# 关卡推进段:入口选路→战斗→各停点分发→真终局收口(帧预算兜底)
|
||||
var budget := ADVENTURE_BUDGET_FRAMES
|
||||
while budget > 0:
|
||||
budget -= 1
|
||||
await get_tree().process_frame
|
||||
_record_screens()
|
||||
var st := _screen_state()
|
||||
if bool(st["节点"]):
|
||||
_last_action = "节点层「继续」(方法-视图口)"
|
||||
main.ui.modals.run.node_overlay.press_continue()
|
||||
continue
|
||||
if bool(st["三选一"]):
|
||||
_drive_curio()
|
||||
continue
|
||||
if bool(st["商店"]):
|
||||
var lv: Button = ProbeUiUtil.find_button(main.ui.modals.menu.shop_overlay, "离开商店")
|
||||
if lv != null:
|
||||
_last_action = "真点「离开商店」钮"
|
||||
ProbeUiUtil.push_click(main, lv)
|
||||
continue
|
||||
if bool(st["大奖励"]):
|
||||
var nb: Button = ProbeUiUtil.find_button(main.ui.screens.inter_overlay, "进入下一层", "prefix")
|
||||
if nb != null:
|
||||
_last_action = "真点大奖励「进入下一层」钮"
|
||||
ProbeUiUtil.push_click(main, nb)
|
||||
await _frames(2)
|
||||
continue
|
||||
if bool(st["关卡屏"]):
|
||||
_drive_map_once()
|
||||
continue
|
||||
if bool(st["战斗"]):
|
||||
await _drive_battle_once()
|
||||
continue
|
||||
var endo: Control = main.ui.modals.run.end_overlay
|
||||
if endo != null and endo.is_open():
|
||||
var armed: bool = main.run_scope.settlement_stop_timer.is_armed()
|
||||
var running: bool = main.run_scope.is_adventure and main.run_scope.adventure != null \
|
||||
and String(main.run_scope.adventure.state) == "running"
|
||||
if armed or running:
|
||||
_last_action = "真点结算面「继续/下一节点」钮"
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.run.end_overlay._btn)
|
||||
await _frames(2)
|
||||
continue
|
||||
break # 真终局(失败/放弃/打穿)——收口段交棒
|
||||
if budget <= 0:
|
||||
_last_action = "冒险段超时(诊断:%s)" % JSON.stringify({
|
||||
"pacer_pending": main.ui.screens.row_pacer.pending_rows().size(),
|
||||
"locked": main.ui.flow.locks_input(),
|
||||
"battle_result": String(main.run_scope.settlement.battle_result) if main.run_scope.settlement != null else "?",
|
||||
"level": int(main.run_scope.runner.level_row.get("level_index", -1)) if main.run_scope.runner != null else -1,
|
||||
"round": int(main.run_scope.settlement.round_index) if main.run_scope.settlement != null else -1,
|
||||
"ap": int(main.run_scope.settlement.action_economy.ap) if main.run_scope.settlement != null else -1,
|
||||
})
|
||||
_report("timeout", "冒险段", "帧预算耗尽未达真终局")
|
||||
_save_misfire()
|
||||
|
||||
|
||||
func _drive_map_once() -> void:
|
||||
# 地图屏:状态角标真点开合一次+候选命中(方法-地图命中口——自绘布局路径,探针族同款入口)
|
||||
if not _map_status_done and main.ui.modals.run.level_screen.status_badge != null \
|
||||
and not main.ui.modals.run.level_screen.status_badge.disabled:
|
||||
_map_status_done = true
|
||||
var badge: Button = main.ui.modals.run.level_screen.status_badge
|
||||
_last_action = "真点地图屏状态角标"
|
||||
ProbeUiUtil.push_click(main, badge)
|
||||
await _frames(3) # 面板开层排版帧——同帧点「继续」会落空、输入锁滞留全战役(实证)
|
||||
var resumed: Button = ProbeUiUtil.find_button(main.ui.modals.run.status_panel, "继续", "prefix")
|
||||
if resumed != null and main.ui.modals.run.status_panel.visible:
|
||||
ProbeUiUtil.push_click(main, resumed)
|
||||
_last_action = "地图候选命中(方法-地图命中口)"
|
||||
main.ui.modals.run.level_screen.press_candidate(0)
|
||||
|
||||
|
||||
func _drive_curio() -> void:
|
||||
# 三选一停点:待选态点首件+确认;兑付/已消费态点继续(方法-视图口——curio 停点既定驱动面)
|
||||
var co: Control = main.ui.modals.run.curio_overlay
|
||||
_last_action = "三选一停点分发(方法-视图口)"
|
||||
if co._continue_btn.visible:
|
||||
co.press_continue()
|
||||
elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._node_pending.is_empty():
|
||||
co.press_card(0)
|
||||
co.press_confirm()
|
||||
elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._pending.is_empty():
|
||||
co.press_card(0)
|
||||
co.press_confirm()
|
||||
else:
|
||||
co.press_continue()
|
||||
|
||||
|
||||
func _drive_battle_once() -> void:
|
||||
# 战斗单步:挂死检测+功能钮真点(图例/状态/刷新确认链/停手)+棋盘挖格(方法-棋盘口)
|
||||
var settlement = main.run_scope.settlement
|
||||
var diag: Dictionary = settlement.diagnostic_snapshot()
|
||||
var st := {
|
||||
"round": diag.round, "phase": diag.action_economy.phase, "ap": diag.action_economy.ap,
|
||||
"revealed": diag.minesweeper_revealed,
|
||||
}
|
||||
var pacer_idle: bool = main.ui.screens.row_pacer.pending_rows().is_empty()
|
||||
if JSON.stringify(st) == JSON.stringify(_last_state) and pacer_idle \
|
||||
and not main.ui.flow.locks_input():
|
||||
_stall += 1
|
||||
if _stall >= BATTLE_STALL_FRAMES:
|
||||
_report("timeout", "战斗段", "状态无变化达 %d 帧" % BATTLE_STALL_FRAMES)
|
||||
_stall = 0
|
||||
_save_misfire()
|
||||
_last_state = {} # 重置基线防同帧连报
|
||||
else:
|
||||
_stall = 0
|
||||
_last_state = st
|
||||
if not pacer_idle or main.ui.flow.locks_input():
|
||||
return
|
||||
var ap_bar: Control = main.ui.battle.board_zone.ap_bar
|
||||
# 功能钮真点(一次性——图例开合/状态面板开合/刷新确认链)
|
||||
if not _legend_done:
|
||||
_legend_done = true
|
||||
_last_action = "真点图例钮"
|
||||
ProbeUiUtil.push_click(main, ap_bar._btn_legend)
|
||||
await _frames(2)
|
||||
var lclose: Button = ProbeUiUtil.find_button(main.ui.modals.run.legend_overlay, "关闭")
|
||||
if lclose != null and main.ui.modals.run.legend_overlay.is_open():
|
||||
ProbeUiUtil.push_click(main, lclose)
|
||||
await _frames(2)
|
||||
return
|
||||
if not _status_done and not main.ui.battle.board_zone.status_badge.disabled:
|
||||
_status_done = true
|
||||
_last_action = "真点战斗屏状态角标"
|
||||
ProbeUiUtil.push_click(main, main.ui.battle.board_zone.status_badge)
|
||||
await _frames(3) # 面板开层排版帧(同地图屏实证坑)
|
||||
var resumed: Button = ProbeUiUtil.find_button(main.ui.modals.run.status_panel, "继续", "prefix")
|
||||
if resumed != null and main.ui.modals.run.status_panel.visible:
|
||||
ProbeUiUtil.push_click(main, resumed)
|
||||
await _frames(2)
|
||||
return
|
||||
if not _refresh_done and ap_bar.is_refresh_ready():
|
||||
_refresh_done = true
|
||||
_last_action = "真点「弃图刷新」钮+确认链"
|
||||
ProbeUiUtil.push_click(main, ap_bar._btn_refresh)
|
||||
await _frames(2)
|
||||
var cbtn: Button = ProbeUiUtil.find_button(ap_bar, "确认弃图")
|
||||
if cbtn != null:
|
||||
ProbeUiUtil.push_click(main, cbtn)
|
||||
await _frames(2)
|
||||
return
|
||||
# 棋盘动作(方法-棋盘口——BoardView 公共点击口,ui_input_probe 先例;C 的按钮面全走真输入)
|
||||
var minesweeper = settlement.minesweeper
|
||||
var plan: Dictionary = AutoPlayer.choose_dig(minesweeper)
|
||||
if plan.is_empty():
|
||||
plan = AutoPlayer.gamble_pick(minesweeper)
|
||||
if plan.is_empty():
|
||||
_last_action = "真点「停手」钮(无计划)"
|
||||
ProbeUiUtil.push_click(main, ap_bar._btn_stop)
|
||||
await _frames(2)
|
||||
return
|
||||
_last_action = "棋盘挖格 %s(方法-棋盘口)" % str(plan.dig_cell)
|
||||
if plan.hero_cell != minesweeper.hero_pos():
|
||||
main.ui.battle.board_zone.board_view.click_cell(plan.hero_cell, MOUSE_BUTTON_LEFT)
|
||||
main.ui.battle.board_zone.board_view.click_cell(plan.dig_cell, MOUSE_BUTTON_LEFT)
|
||||
await _frames(1)
|
||||
|
||||
|
||||
func _phase_end_back_menu() -> void:
|
||||
# 终局收束:真点「返回」→ 回菜单;结算框/中途面已在冒险段分发
|
||||
var t0 := Time.get_ticks_msec()
|
||||
while Time.get_ticks_msec() - t0 < SEGMENT_TIMEOUT_MS:
|
||||
await get_tree().process_frame
|
||||
_record_screens()
|
||||
var endo: Control = main.ui.modals.run.end_overlay
|
||||
if endo != null and endo.is_open() and not main.run_scope.settlement_stop_timer.is_armed():
|
||||
break
|
||||
if main.ui.modals.run.end_overlay == null or not main.ui.modals.run.end_overlay.is_open():
|
||||
_report("timeout", "终局收束", "真终局面未在超时内到达")
|
||||
_save_misfire()
|
||||
return
|
||||
var btn: Button = main.ui.modals.run.end_overlay._btn
|
||||
if btn != null:
|
||||
_last_action = "真点终局「返回」钮"
|
||||
ProbeUiUtil.push_click(main, btn)
|
||||
await _phase_expect("终局回菜单", ["菜单"])
|
||||
|
||||
|
||||
func _push_esc() -> void:
|
||||
var ev := InputEventKey.new()
|
||||
ev.keycode = KEY_ESCAPE
|
||||
ev.physical_keycode = KEY_ESCAPE
|
||||
ev.pressed = true
|
||||
main.get_viewport().push_input(ev, true) # 键盘真输入(通道=真实输入-键盘)
|
||||
|
||||
|
||||
# ---- 报告落盘(JSONP) ----
|
||||
|
||||
func _dump_report() -> void:
|
||||
var dir := DirAccess.open("res://")
|
||||
if dir != null:
|
||||
dir.make_dir_recursive("tests/.gates/coverage")
|
||||
var payload := {
|
||||
"meta": {
|
||||
"generated_at": Time.get_datetime_string_from_system(),
|
||||
"seed": OS.get_environment("PROBE_SEED"),
|
||||
"screens_seen_total": _screens_seen.size(),
|
||||
"anomalies_reported": _problems.size(),
|
||||
"anomalies_silent": _silent_count,
|
||||
"fatal_count": _fatal_count(),
|
||||
"anomaly_classes": "fatal/timeout/screen_mismatch(其余静默;误报指纹二次起静默)",
|
||||
},
|
||||
"screens_seen": _screens_seen.keys(),
|
||||
"screen_trace": _screen_trace,
|
||||
"anomalies": _problems,
|
||||
}
|
||||
var f := FileAccess.open(REPORT_PATH, FileAccess.WRITE)
|
||||
if f != null:
|
||||
var js := "/* 全动线自玩报告——journey_player.gd 随跑覆写;误报核销归用户复核轮。 */\n"
|
||||
js += "window.__QA_COVERAGE__ = window.__QA_COVERAGE__ || {};\n"
|
||||
js += "window.__QA_COVERAGE__.journey = %s;\n" % JSON.stringify(payload, " ")
|
||||
f.store_string(js)
|
||||
f.close()
|
||||
print("[journey] 报告已落盘 %s(屏到达 %d 类 / 上报 %d / 静默 %d)" % [
|
||||
REPORT_PATH, _screens_seen.size(), _problems.size(), _silent_count])
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://cjourneyplayer1"]
|
||||
|
||||
[ext_resource type="Script" path="res://sim/journey_player.gd" id="1"]
|
||||
|
||||
[node name="JourneyPlayer" type="Node"]
|
||||
script = ExtResource("1")
|
||||
@@ -1,9 +1,11 @@
|
||||
# 本目录只入库 baseline.json(存量基线,需随代码走以保证各 worktree 判定口径一致)。
|
||||
# receipt.json 是每跑批覆写的当次回执、ci-status.json/ci-history.jsonl 是已退役的 CI 回执判定与流水账(判定只在远端,本机残留)、
|
||||
# ci.lock/ci.rerun 是跑批件锁与补跑标记、*.log 是跑批当场日志——均为中间产物,不入库。
|
||||
# ci.lock/ci.rerun 是跑批件锁与补跑标记、*.log 是跑批当场日志、coverage/ 是探针真输入覆盖登记
|
||||
#(JSONP 式 .js,每次探针运行自动覆写——观测产物非事实源,不入库)——均为中间产物,不入库。
|
||||
receipt.json
|
||||
ci-status.json
|
||||
ci-history.jsonl
|
||||
ci.lock
|
||||
ci.rerun
|
||||
*.log
|
||||
coverage/
|
||||
@@ -0,0 +1,158 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
r"""coverage_enum.py — 本体枚举器(游戏内容↔测试验证对账,B 面板·义务层双源之机器源)
|
||||
|
||||
两个文本扫描器+一次比对,全程不依赖跑游戏(交互命中维度除外——由探针覆盖登记另行承载):
|
||||
|
||||
扫描器 1(游戏本体枚举)——「游戏有什么」:
|
||||
game/data/tables/*.tres 行集主键(`"xxx_id": "yyy"` 形态,逐表逐行)
|
||||
game/data/schemas/*.gd 词表常量与字段名(const 块内的引号 token+var 字段名)
|
||||
game/presentation/**/*.tscn BaseButton 族节点(scene+节点名)
|
||||
game/presentation/**/*.gd _gui_input/_input 自绘命中类(脚本名)
|
||||
game/presentation/views/*.gd 屏与模态清单(*_screen/*_overlay/*_panel 视图类)
|
||||
|
||||
扫描器 2(验证关联)——「验证了什么」:复用 tools/extract_checks.py 的断言静态提取
|
||||
(unit/**+integration/**+system_test.gd 全量),实体 id 在「断言标签+cond+源文件+函数」
|
||||
文料中出现即记一次关联。
|
||||
|
||||
比对=差集:本体存在、验证零关联=缺口清单,按类别分组、标首见批次(本工具生成时间)。
|
||||
|
||||
诚实边界(契约 §7 在案):grep 关联有假阳假阴——断言测了实体但文本没带 id=假缺口(人工核销
|
||||
兜);文本带 id 但断言弱=假覆盖(oracle 改造堵)。推荐约定「断言文本含被测实体 id」提高精度,
|
||||
不立规约。交互点(按钮/物件被点过没有)不在本工具判据内——那归探针真输入覆盖登记。
|
||||
|
||||
用法: python tools/coverage_enum.py [--out <路径>]
|
||||
产出: game/tests/.gates/coverage/ontology_gaps.js(JSONP,B 面板消费)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
GAME = ROOT / "game"
|
||||
DEFAULT_OUT = GAME / "tests" / ".gates" / "coverage" / "ontology_gaps.js"
|
||||
|
||||
TRES_ID_RE = re.compile(r'"(\w+_id)":\s*"([^"]+)"')
|
||||
SCHEMA_TOKEN_RE = re.compile(r'"([a-z][a-z0-9_]{2,40})"')
|
||||
SCHEMA_VAR_RE = re.compile(r"^@?export\s+var\s+(\w+)\s*:", re.M)
|
||||
TSCN_NODE_RE = re.compile(r'\[node name="([^"]+)"[^>]*type="(\w*Button\w*)"')
|
||||
GUI_INPUT_RE = re.compile(r"^func (_gui_input|_input)\(", re.M)
|
||||
VIEW_KIND_RE = re.compile(r"(screen|overlay|panel)$")
|
||||
|
||||
|
||||
def scan_tables() -> list[dict]:
|
||||
out: list[dict] = []
|
||||
tables = GAME / "data" / "tables"
|
||||
for f in sorted(tables.glob("*.tres")):
|
||||
text = f.read_text(encoding="utf-8", errors="replace")
|
||||
seen: set[tuple[str, str]] = set()
|
||||
for m in TRES_ID_RE.finditer(text):
|
||||
key = (m.group(1), m.group(2))
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
out.append({"category": "表行:%s" % f.stem, "id": m.group(2), "id_field": m.group(1)})
|
||||
return out
|
||||
|
||||
|
||||
def scan_schemas() -> list[dict]:
|
||||
out: list[dict] = []
|
||||
schemas = GAME / "data" / "schemas"
|
||||
for f in sorted(schemas.glob("*.gd")):
|
||||
text = f.read_text(encoding="utf-8", errors="replace")
|
||||
tokens = set(SCHEMA_TOKEN_RE.findall(text))
|
||||
fields = set(SCHEMA_VAR_RE.findall(text))
|
||||
for t in sorted(tokens):
|
||||
out.append({"category": "词表/枚值:%s" % f.stem, "id": t, "id_field": "token"})
|
||||
for fl in sorted(fields):
|
||||
out.append({"category": "schema字段:%s" % f.stem, "id": fl, "id_field": "field"})
|
||||
return out
|
||||
|
||||
|
||||
def scan_tscn_buttons() -> list[dict]:
|
||||
out: list[dict] = []
|
||||
for f in sorted(GAME.glob("presentation/**/*.tscn")):
|
||||
text = f.read_text(encoding="utf-8", errors="replace")
|
||||
for m in TSCN_NODE_RE.finditer(text):
|
||||
out.append({"category": "场景按钮", "id": "%s:%s" % (f.name, m.group(1)), "id_field": m.group(2)})
|
||||
return out
|
||||
|
||||
|
||||
def scan_gui_input_classes() -> list[dict]:
|
||||
out: list[dict] = []
|
||||
for f in sorted(GAME.glob("presentation/**/*.gd")):
|
||||
text = f.read_text(encoding="utf-8", errors="replace")
|
||||
kinds = set(GUI_INPUT_RE.findall(text))
|
||||
for k in kinds:
|
||||
out.append({"category": "自绘命中类", "id": f.stem, "id_field": k})
|
||||
break # 一类一行(一个脚本同具两面也只记一次)
|
||||
return out
|
||||
|
||||
|
||||
def scan_screens() -> list[dict]:
|
||||
out: list[dict] = []
|
||||
for f in sorted((GAME / "presentation" / "views").glob("*.gd")):
|
||||
if VIEW_KIND_RE.search(f.stem):
|
||||
out.append({"category": "屏与模态", "id": f.stem, "id_field": "view"})
|
||||
return out
|
||||
|
||||
|
||||
def assertion_corpus() -> tuple[list[dict], str]:
|
||||
sys.path.insert(0, str(ROOT / "tools"))
|
||||
import extract_checks as ec # noqa: PLC0415 —— 复用现役提取机制(契约 §7 指定扩面)
|
||||
rows, _problems = ec.extract_all(ec.default_sources())
|
||||
parts: list[str] = []
|
||||
for r in rows:
|
||||
parts.append("%s %s %s %s" % (r.get("src", ""), r.get("func", ""),
|
||||
r.get("label_literal", ""), r.get("cond", "")))
|
||||
return rows, "\n".join(parts)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||
ap = argparse.ArgumentParser(description="本体枚举器(游戏内容↔测试验证对账)")
|
||||
ap.add_argument("--out", default=str(DEFAULT_OUT))
|
||||
args = ap.parse_args()
|
||||
entities = scan_tables() + scan_schemas() + scan_tscn_buttons() + scan_gui_input_classes() + scan_screens()
|
||||
_rows, corpus = assertion_corpus()
|
||||
categories: dict[str, dict] = {}
|
||||
for ent in entities:
|
||||
cat = categories.setdefault(ent["category"], {"total": 0, "matched": 0, "gaps": []})
|
||||
cat["total"] += 1
|
||||
if ent["id"] and ent["id"] in corpus:
|
||||
cat["matched"] += 1
|
||||
else:
|
||||
cat["gaps"].append({"id": ent["id"], "id_field": ent.get("id_field", "")})
|
||||
payload = {
|
||||
"meta": {
|
||||
"generated_at": datetime.datetime.now().isoformat(timespec="seconds"),
|
||||
"entities_total": len(entities),
|
||||
"corpus_rows": len(_rows),
|
||||
"honest_boundary": "grep 关联有假阳假阴——文本没带 id 的假缺口人工核销;交互命中归探针真输入登记",
|
||||
},
|
||||
"categories": [
|
||||
{"category": k, "total": v["total"], "matched": v["matched"], "gaps": v["gaps"]}
|
||||
for k, v in sorted(categories.items())
|
||||
],
|
||||
}
|
||||
out = Path(args.out)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
js = "/* 本体对账清单——coverage_enum.py 随跑重生成,人工核销走豁免清单(coverage_exemptions.js),不落本文件。 */\n"
|
||||
js += "window.__QA_COVERAGE__ = window.__QA_COVERAGE__ || {};\n"
|
||||
js += "window.__QA_COVERAGE__.ontology = %s;\n" % json.dumps(payload, ensure_ascii=False, indent=2)
|
||||
out.write_text(js, encoding="utf-8", newline="\n")
|
||||
gap_total = sum(len(c["gaps"]) for c in payload["categories"])
|
||||
print("[ontology] 本体实体 %d / 断言文料行 %d / 缺口 %d(%d 类)→ %s" % (
|
||||
len(entities), len(_rows), gap_total, len(payload["categories"]), out))
|
||||
for c in payload["categories"]:
|
||||
print(" %s:%d 实体 / %d 缺口" % (c["category"], c["total"], len(c["gaps"])))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,11 @@
|
||||
/* 覆盖豁免清单——人工维护(S13 基线人工修订同款纪律;面板与工具只读本文件,禁代码生成)。
|
||||
* 用途:登记「确认不需要真输入覆盖/不需要断言关联」的条目,B 面板对在册条目亮豁免灯、不亮缺口。
|
||||
* 形态:abilities=能力清单行「能力」列原文(精确匹配);controls=控件登记键或其中缀(包含匹配);
|
||||
* ontology=本体实体 id(精确匹配)。每条必须带 reason——无理由的豁免条目面板标红示警。 */
|
||||
window.__QA_COVERAGE__ = window.__QA_COVERAGE__ || {};
|
||||
window.__QA_COVERAGE__.exemptions = {
|
||||
"meta": {"note": "人工维护;改后刷新面板即生效,无生成步骤"},
|
||||
"abilities": [],
|
||||
"controls": [],
|
||||
"ontology": []
|
||||
};
|
||||
@@ -0,0 +1,184 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
r"""coverage_receipts.py — 六源回执明细化(B 覆盖总览面板·执行层)
|
||||
|
||||
各闸跑批把当场日志落 `.gates/*.log`;本工具在跑批末尾被闸壳调用一次,把六层日志解析成
|
||||
「名×结果×计数」明细,按闸各写一个 JSONP .js 到 `.gates/coverage/`,供
|
||||
tools/coverage_report.html 经 <script src> 直载。
|
||||
|
||||
六层与日志源(层键=面板消费键,层名=呈现名):
|
||||
smoke 冒烟测试 smoke_boot.log 摘要「N 项检查,M 项失败」
|
||||
unit 单元与集成测试 unit.log 每套件「[run_all] PASS/FAIL res://…(exit=N)」
|
||||
system 系统测试 system_test.log 摘要「N 项检查,M 项失败」
|
||||
probe 场景探针 probe.log 每探针段「======== 名(段)========」+该段 PASS/FAIL 行
|
||||
render 渲染闸 render.log golden 清单与对比结果
|
||||
static 静态扫描 static_scan.log 每个 S 系条目「[static_scan] S# ok/FAIL(…)」
|
||||
|
||||
判据口径:
|
||||
- 明细粒度=「名×结果×计数」,不记逐条断言文本(契约 §4 成本口径);
|
||||
- 日志缺失=该层 {"result": "not_run"}——面板亮灰灯,不冒充绿;
|
||||
- 本工具失败不翻转闸判定(观测件非判定件),但必须打红字——静默丢数据等于断言蒸发。
|
||||
|
||||
用法: python tools/coverage_receipts.py [--stamp-dir <.gates 路径>]
|
||||
产出: <stamp-dir>/coverage/gate_<层键>.js × 6
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
DEFAULT_STAMP = ROOT / "game" / "tests" / ".gates"
|
||||
|
||||
SUMMARY_RE = re.compile(r"摘要[::]\s*\[?[^\]]*\]?\s*摘要[::]\s*(\d+)\s*项检查,(\d+)\s*项失败")
|
||||
SUITE_RE = re.compile(r"\[run_all\] (PASS|FAIL) (res://\S+?\.gd)(?:\s*\(exit=(-?\d+)\))?")
|
||||
PROBE_SECTION_RE = re.compile(r"^======== (\S+)(?:\s+(.+?))? ========")
|
||||
PROBE_RESULT_RE = re.compile(r"^\[[^\]]+\] (PASS|FAIL)")
|
||||
GOLDEN_RE = re.compile(r"\[render_gate\] \[golden\] (PASS|FAIL)((\d+) 屏黄金图")
|
||||
GOLDEN_CROP_RE = re.compile(r"\[render_gate\] \[golden_crop\] (\S+?) —")
|
||||
STATIC_RE = re.compile(r"^\[static_scan\] (S\d+(?:/S\d+)*)\s+(ok|FAIL)", re.M)
|
||||
STATIC_GDSTYLE_RE = re.compile(r"^\[gdstyle_gate\] (ok|FAIL)", re.M)
|
||||
|
||||
|
||||
def read(stamp: Path, name: str) -> str:
|
||||
p = stamp / name
|
||||
if not p.is_file():
|
||||
return ""
|
||||
return p.read_text(encoding="utf-8", errors="replace")
|
||||
|
||||
|
||||
def parse_summary_layer(log: str) -> dict:
|
||||
if not log:
|
||||
return {"result": "not_run"}
|
||||
m = SUMMARY_RE.search(log)
|
||||
verdict = "绿" if "[run_smoke] PASS" in log or "[run_system_test] PASS" in log else (
|
||||
"红" if ("FAIL" in log or "摘要" in log and m and int(m.group(2)) > 0) else "灰")
|
||||
if m:
|
||||
return {"result": verdict, "checks": int(m.group(1)), "fails": int(m.group(2))}
|
||||
return {"result": verdict}
|
||||
|
||||
|
||||
def parse_unit(log: str) -> dict:
|
||||
if not log:
|
||||
return {"result": "not_run"}
|
||||
items = [{"name": m.group(2), "result": m.group(1), "exit": int(m.group(3) or 0)}
|
||||
for m in SUITE_RE.finditer(log)]
|
||||
red = [i for i in items if i["result"] == "FAIL"]
|
||||
verdict = "绿" if "[run_all] all green" in log and not red else ("红" if red or "FAIL" in log else "灰")
|
||||
return {"result": verdict, "suites_total": len(items), "items": items}
|
||||
|
||||
|
||||
def parse_probe(log: str) -> dict:
|
||||
if not log:
|
||||
return {"result": "not_run"}
|
||||
items: list[dict] = []
|
||||
cur: dict | None = None
|
||||
for line in log.splitlines():
|
||||
sm = PROBE_SECTION_RE.match(line.strip())
|
||||
if sm:
|
||||
seg = (sm.group(2) or "").strip()
|
||||
cur = {"name": sm.group(1), "segment": seg, "result": "灰"}
|
||||
items.append(cur)
|
||||
continue
|
||||
rm = PROBE_RESULT_RE.match(line.strip())
|
||||
if rm and cur is not None and cur["result"] == "灰":
|
||||
cur["result"] = rm.group(1)
|
||||
judged = [i for i in items if i["result"] != "灰"]
|
||||
verdict = "红" if (any(i["result"] == "FAIL" for i in judged) or "[probe_gate] 全绿" not in log) else "绿"
|
||||
return {"result": verdict, "probes_total": len(judged), "items": items}
|
||||
|
||||
|
||||
def parse_render(log: str) -> dict:
|
||||
if not log:
|
||||
return {"result": "not_run"}
|
||||
items: list[dict] = []
|
||||
gm = GOLDEN_RE.search(log)
|
||||
if gm:
|
||||
items.append({"name": "golden 逐像素对比", "result": gm.group(1), "screens": int(gm.group(2))})
|
||||
screens: list[str] = []
|
||||
for m in GOLDEN_CROP_RE.finditer(log):
|
||||
if m.group(1) not in screens:
|
||||
screens.append(m.group(1))
|
||||
if screens:
|
||||
items.append({"name": "golden 逐屏裁剪", "result": "PASS", "screens": screens})
|
||||
verdict = "红" if "FAIL" in log or "[render_gate] 全绿" not in log else "绿"
|
||||
return {"result": verdict, "items": items}
|
||||
|
||||
|
||||
def parse_static(log: str) -> dict:
|
||||
if not log:
|
||||
return {"result": "not_run"}
|
||||
items = [{"name": m.group(1), "result": m.group(2)} for m in STATIC_RE.finditer(log)]
|
||||
gm = STATIC_GDSTYLE_RE.search(log)
|
||||
if gm:
|
||||
items.insert(0, {"name": "gdstyle", "result": gm.group(1)})
|
||||
red = [i for i in items if i["result"] != "ok"]
|
||||
verdict = "红" if red or "FAIL" in log else "绿"
|
||||
return {"result": verdict, "items_total": len(items), "items": items}
|
||||
|
||||
|
||||
LAYERS = [
|
||||
("smoke", "冒烟测试", "smoke_boot.log", parse_summary_layer),
|
||||
("unit", "单元与集成测试", "unit.log", parse_unit),
|
||||
("system", "系统测试", "system_test.log", parse_summary_layer),
|
||||
("probe", "场景探针", "probe.log", parse_probe),
|
||||
("render", "渲染闸", "render.log", parse_render),
|
||||
("static", "静态扫描", "static_scan.log", parse_static),
|
||||
]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||
ap = argparse.ArgumentParser(description="六源回执明细化(B 面板·执行层)")
|
||||
ap.add_argument("--stamp-dir", default=str(DEFAULT_STAMP))
|
||||
args = ap.parse_args()
|
||||
stamp = Path(args.stamp_dir)
|
||||
out_dir = stamp / "coverage"
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
failed = False
|
||||
wrote = 0
|
||||
for key, label, log_name, fn in LAYERS:
|
||||
try:
|
||||
detail = fn(read(stamp, log_name))
|
||||
detail["meta"] = {
|
||||
"layer": key,
|
||||
"label": label,
|
||||
"source_log": log_name,
|
||||
"generated_at": datetime.datetime.now().isoformat(timespec="seconds"),
|
||||
}
|
||||
payload = {"receipt": detail}
|
||||
js = "/* 闸回执明细化——coverage_receipts.py 随每次跑批覆写,人工修订无效。 */\n"
|
||||
js += "window.__QA_COVERAGE__ = window.__QA_COVERAGE__ || {};\n"
|
||||
js += "window.__QA_COVERAGE__.gate_receipts = window.__QA_COVERAGE__.gate_receipts || {};\n"
|
||||
js += "window.__QA_COVERAGE__.gate_receipts[%s] = %s;\n" % (
|
||||
json.dumps(key), json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
(out_dir / ("gate_%s.js" % key)).write_text(js, encoding="utf-8", newline="\n")
|
||||
wrote += 1
|
||||
print("[receipts] %s(%s)=%s" % (label, log_name, detail.get("result", "?")))
|
||||
except Exception as exc: # noqa: BLE001 —— 观测件逐层容错,红字可见不静默
|
||||
failed = True
|
||||
print("[receipts] 红:%s 层解析失败(%s):%r" % (label, log_name, exc))
|
||||
if wrote == 0:
|
||||
print("[receipts] 红:六层明细一件未落盘——禁静默")
|
||||
return 1
|
||||
# 探针登记文件清单(B 面板 file:// 无法列目录——按现存 probe_hits_*.js 落 manifest 供 <script src>)
|
||||
probe_files = sorted(p.name for p in out_dir.glob("probe_hits_*.js"))
|
||||
manifest = {
|
||||
"meta": {"generated_at": datetime.datetime.now().isoformat(timespec="seconds")},
|
||||
"probes": probe_files,
|
||||
}
|
||||
mjs = "/* 探针登记清单——coverage_receipts.py 随跑批按现存文件覆写,勿手编。 */\n"
|
||||
mjs += "window.__QA_COVERAGE__ = window.__QA_COVERAGE__ || {};\n"
|
||||
mjs += "window.__QA_COVERAGE__.probe_manifest = %s;\n" % json.dumps(manifest, ensure_ascii=False)
|
||||
(out_dir / "probe_manifest.js").write_text(mjs, encoding="utf-8", newline="\n")
|
||||
print("[receipts] 探针登记清单 %d 件 → probe_manifest.js" % len(probe_files))
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,226 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>QA 覆盖总览面板——Dungeon Sweeper</title>
|
||||
<style>
|
||||
:root { --ok:#3a7d44; --bad:#a33; --off:#9aa; --na:#d5d5d0; --ex:#b8860b; --ink:#2c2a26; --paper:#f4f1ea; }
|
||||
body { font-family: "Microsoft YaHei", sans-serif; margin: 0; background: var(--paper); color: var(--ink); }
|
||||
header { padding: 14px 20px 10px; border-bottom: 3px solid var(--ink); background: #fff; }
|
||||
h1 { font-size: 19px; margin: 0 0 4px; }
|
||||
h2 { font-size: 16px; margin: 22px 0 6px; border-left: 5px solid var(--ink); padding-left: 8px; }
|
||||
.note { font-size: 12px; color: #555; margin: 3px 0; }
|
||||
.warn { color: var(--bad); font-weight: bold; }
|
||||
main { padding: 12px 20px 40px; }
|
||||
table { border-collapse: collapse; width: 100%; background: #fff; font-size: 12.5px; }
|
||||
th, td { border: 1px solid #cfc9bd; padding: 4px 7px; text-align: left; vertical-align: top; }
|
||||
th { background: #ebe6da; }
|
||||
.lamp { display: inline-block; width: 13px; height: 13px; border-radius: 50%; margin-right: 3px; vertical-align: -2px; }
|
||||
.ok { background: var(--ok); } .bad { background: var(--bad); }
|
||||
.off { background: var(--off); } .na { background: var(--na); } .ex { background: #fff; border: 3px double var(--ex); }
|
||||
.chip { display: inline-block; padding: 1px 8px; border-radius: 9px; color: #fff; font-size: 12px; }
|
||||
details { margin: 4px 0 10px; } summary { cursor: pointer; font-size: 13px; }
|
||||
.gaplist { font-size: 12px; color: #444; line-height: 1.7; }
|
||||
.seg { margin-bottom: 18px; }
|
||||
code { background: #eceade; padding: 0 3px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>QA 覆盖总览面板 <span style="font-weight:normal;font-size:13px">(义务 × 执行合并视图——只读)</span></h1>
|
||||
<div class="note">状态灯:绿=该层最近跑批过 红=跑批挂 灰=该层无承载/未跑 双圈=豁免(<code>tools/coverage_exemptions.js</code> 人工维护)。</div>
|
||||
<div class="note" id="staleness"></div>
|
||||
<div class="note"><b>完备性边界</b>:控件级视图只对「被探针/C 打开过的屏」负责;未打开过的屏由动线断言+C 探索兜底。
|
||||
本体对账为文本 grep 关联——有假阳假阴(断言测了实体但文本没带 id=假缺口,人工核销;文本带 id 但断言弱=假覆盖),缺口须人工复核后再立事项。</div>
|
||||
<div class="note">数据源:随最近一次闸批落盘于 <code>game/tests/.gates/coverage/</code>;刷新本页即读最新。</div>
|
||||
</header>
|
||||
<main>
|
||||
<h2>一、六层闸况</h2>
|
||||
<div id="gateSummary"></div>
|
||||
|
||||
<h2>二、能力清单 × 六层状态灯(按旅程段)</h2>
|
||||
<div class="note">灯的判据:用例文件落哪层就钉哪层的回执(单元=逐套件 PASS/FAIL;探针/系统/冒烟/渲染/静态=层判据);该层无对应承载=灰。</div>
|
||||
<div id="capabilityMatrix"></div>
|
||||
|
||||
<h2>三、控件级交互覆盖下钻(探针真输入登记)</h2>
|
||||
<div class="note">交互源=探针真输入命中登记(push_input 管线命中);「存在但零命中」=控件缺口——hitbox 错位/遮挡/驱动空洞在此暴露。展示源=渲染闸 golden 逐屏登记。</div>
|
||||
<div id="controlDrill"></div>
|
||||
|
||||
<h2>四、本体对账(游戏内容 ↔ 测试验证)</h2>
|
||||
<div class="note">缺口=本体存在、断言文料零关联;若该 id 出现在探针命中登记则降级为「探针承载」。首见批次=本工具生成时间,逐批对比看新面孔。</div>
|
||||
<div id="ontologyGaps"></div>
|
||||
</main>
|
||||
<script src="./coverage_exemptions.js"></script>
|
||||
<script src="../game/tests/.gates/coverage/capability_list.js"></script>
|
||||
<script src="../game/tests/.gates/coverage/gate_smoke.js"></script>
|
||||
<script src="../game/tests/.gates/coverage/gate_unit.js"></script>
|
||||
<script src="../game/tests/.gates/coverage/gate_system.js"></script>
|
||||
<script src="../game/tests/.gates/coverage/gate_probe.js"></script>
|
||||
<script src="../game/tests/.gates/coverage/gate_render.js"></script>
|
||||
<script src="../game/tests/.gates/coverage/gate_static.js"></script>
|
||||
<script src="../game/tests/.gates/coverage/probe_manifest.js"></script>
|
||||
<script src="../game/tests/.gates/coverage/ontology_gaps.js"></script>
|
||||
<script>
|
||||
/* 探针登记文件由 manifest 同步补挂(document.write 在解析期阻塞加载,渲染段前数据已就位) */
|
||||
(function () {
|
||||
const probes = ((window.__QA_COVERAGE__ || {}).probe_manifest || {}).probes || [];
|
||||
for (const p of probes) {
|
||||
document.write('<script src="../game/tests/.gates/coverage/' + encodeURIComponent(p) + '"><\/script>');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
"use strict";
|
||||
const C = window.__QA_COVERAGE__ || {};
|
||||
function esc(s) { return String(s == null ? "" : s).replace(/[&<>"]/g, c => ({"&":"&","<":"<",">":">",'"':"""}[c])); }
|
||||
function lamp(cls, title) { return `<span class="lamp ${cls}" title="${esc(title)}"></span>`; }
|
||||
function chip(txt, cls) { return `<span class="chip" style="background:${cls}">${esc(txt)}</span>`; }
|
||||
const VCOLOR = {"绿":"var(--ok)","红":"var(--bad)","灰":"var(--off)","not_run":"var(--off)"};
|
||||
|
||||
// ── 数据新鲜度(陈旧可见——防静默旧数据) ──
|
||||
(function () {
|
||||
const stamps = [];
|
||||
const cl = (C.capability_list || {}).meta; if (cl) stamps.push(`义务层 ${cl.generated_at}`);
|
||||
const rc = C.gate_receipts || {};
|
||||
for (const k in rc) { const m = (rc[k].receipt || {}).meta; if (m) stamps.push(`${m.label} ${m.generated_at}`); }
|
||||
const on = (C.ontology || {}).meta; if (on) stamps.push(`本体 ${on.generated_at}`);
|
||||
const pm = (C.probe_manifest || {}).meta; if (pm) stamps.push(`探针登记 ${pm.generated_at}`);
|
||||
document.getElementById("staleness").innerHTML = "数据时间:" + stamps.map(esc).join(" · ");
|
||||
})();
|
||||
|
||||
// ── 一、六层闸况 ──
|
||||
(function () {
|
||||
const rc = C.gate_receipts || {};
|
||||
const ORDER = ["smoke", "unit", "system", "probe", "render", "static"];
|
||||
const LABEL = {smoke:"冒烟测试", unit:"单元与集成测试", system:"系统测试", probe:"场景探针", render:"渲染闸", static:"静态扫描"};
|
||||
let html = "<table><tr><th>层</th><th>判定</th><th>明细</th></tr>";
|
||||
for (const k of ORDER) {
|
||||
const r = (rc[k] || {}).receipt;
|
||||
if (!r) { html += `<tr><td>${LABEL[k]}</td><td>${chip("缺数据", "var(--off)")}</td><td>—</td></tr>`; continue; }
|
||||
const v = r.result === "not_run" ? "未跑" : r.result;
|
||||
const color = VCOLOR[r.result] || "var(--off)";
|
||||
let detail = `共 ${r.items ? r.items.length : 0} 项`;
|
||||
if (r.checks != null) detail = `${r.checks} 项检查 / ${r.fails} 失败`;
|
||||
if (r.suites_total != null) detail = `${r.suites_total} 套件`;
|
||||
if (r.probes_total != null) detail = `${r.probes_total} 探针`;
|
||||
html += `<tr><td>${LABEL[k]}</td><td>${chip(v, color)}</td><td>${esc(detail)} <span class="note">源=${esc(r.meta.source_log)} @ ${esc(r.meta.generated_at)}</span></td></tr>`;
|
||||
}
|
||||
html += "</table><details><summary>单元与集成逐套件明细</summary><div class='gaplist'>";
|
||||
const unit = ((rc.unit || {}).receipt || {}).items || [];
|
||||
for (const it of unit) {
|
||||
html += `${lamp(it.result === "PASS" ? "ok" : "bad", it.result)} ${esc(it.name.replace("res://tests/", ""))}${it.exit ? "(exit=" + esc(it.exit) + ")" : ""}<br>`;
|
||||
}
|
||||
html += "</div></details><details><summary>场景探针逐段明细</summary><div class='gaplist'>";
|
||||
const pr = ((rc.probe || {}).receipt || {}).items || [];
|
||||
for (const it of pr) {
|
||||
html += `${lamp(it.result === "PASS" ? "ok" : (it.result === "FAIL" ? "bad" : "off"), it.result)} ${esc(it.name)}${it.segment ? "(" + esc(it.segment) + ")" : ""}<br>`;
|
||||
}
|
||||
html += "</div></details><details><summary>静态扫描逐条明细</summary><div class='gaplist'>";
|
||||
const st = ((rc.static || {}).receipt || {}).items || [];
|
||||
for (const it of st) {
|
||||
html += `${lamp(it.result === "ok" ? "ok" : "bad", it.result)} ${esc(it.name)}<br>`;
|
||||
}
|
||||
html += "</div></details>";
|
||||
document.getElementById("gateSummary").innerHTML = html;
|
||||
})();
|
||||
|
||||
// ── 二、能力清单 × 六层灯 ──
|
||||
(function () {
|
||||
const cl = C.capability_list;
|
||||
if (!cl || !cl.segments) { document.getElementById("capabilityMatrix").innerHTML = "<p class='warn'>义务层缺数据——先跑 tools/extract_capability_list.py</p>"; return; }
|
||||
const rc = C.gate_receipts || {};
|
||||
const ex = ((C.exemptions || {}).abilities || []).map(s => String(s));
|
||||
function layerOf(caseFile) {
|
||||
if (/system_test/.test(caseFile)) return "system";
|
||||
if (/probe\//.test(caseFile) || /_probe\.gd/.test(caseFile)) return "probe";
|
||||
if (/golden|render/.test(caseFile)) return "render";
|
||||
return "unit";
|
||||
}
|
||||
function rowLight(row) {
|
||||
if (ex.includes(row.ability)) return lamp("ex", "豁免(coverage_exemptions.js 在册)");
|
||||
if (!row.cases || !row.cases.length) return lamp("off", "无代表用例指针");
|
||||
let worst = null; const seenLayers = new Set();
|
||||
for (const c of row.cases) {
|
||||
const layer = layerOf(c);
|
||||
seenLayers.add(layer);
|
||||
const r = (rc[layer] || {}).receipt;
|
||||
if (!r || r.result === "not_run") { worst = worst || "na"; continue; }
|
||||
if (r.result === "红") return lamp("bad", `${layer} 层跑批挂`);
|
||||
if (layer === "unit") {
|
||||
const items = r.items || [];
|
||||
const hit = items.filter(it => c.indexOf(it.name.replace("res://tests/", "")) >= 0 || it.name.indexOf(c.split(":")[0].replace(".gd", "")) >= 0);
|
||||
if (!hit.length) { worst = worst || "off"; continue; }
|
||||
if (hit.some(it => it.result !== "PASS")) return lamp("bad", "代表套件红:" + c);
|
||||
worst = worst || "ok";
|
||||
} else {
|
||||
worst = worst || (r.result === "绿" ? "ok" : "off");
|
||||
}
|
||||
}
|
||||
if (worst === "ok") return lamp("ok", "承载层绿");
|
||||
if (worst === "na") return lamp("na", "承载层本批未跑");
|
||||
return lamp("off", "该层无对应承载(灰)");
|
||||
}
|
||||
let html = "";
|
||||
for (const seg of cl.segments) {
|
||||
if (!seg.rows.length) continue;
|
||||
html += `<div class="seg"><h3 style="font-size:14px;margin:8px 0 4px">${esc(seg.segment)}(${seg.rows.length} 行)</h3>`;
|
||||
html += "<table><tr><th style='width:32%'>能力</th><th>系统</th><th style='width:14%'>灯</th><th style='width:26%'>代表用例</th></tr>";
|
||||
for (const row of seg.rows) {
|
||||
html += `<tr><td>${esc(row.ability)}</td><td>${esc(row.system)}</td><td>${rowLight(row)}</td><td>${row.cases.map(esc).join("<br>")}</td></tr>`;
|
||||
}
|
||||
html += "</table></div>";
|
||||
}
|
||||
document.getElementById("capabilityMatrix").innerHTML = html;
|
||||
})();
|
||||
|
||||
// ── 三、控件级下钻 ──
|
||||
(function () {
|
||||
const pm = C.probe_manifest || {probes: []};
|
||||
const ex = ((C.exemptions || {}).controls || []).map(s => String(s));
|
||||
const rd = ((C.gate_receipts || {}).render || {}).receipt || {};
|
||||
const renderScreens = [];
|
||||
for (const it of (rd.items || [])) if (it.screens instanceof Array) renderScreens.push(...it.screens);
|
||||
let html = "";
|
||||
if (renderScreens.length) {
|
||||
html += `<p class="note"><b>渲染展示登记(golden 逐屏)</b>:${renderScreens.map(esc).join("、")}</p>`;
|
||||
}
|
||||
for (const pf of pm.probes) {
|
||||
const name = pf.replace(/^probe_hits_/, "").replace(/\.js$/, "");
|
||||
// 同名重复 <script> 无法动态注入——manifest 落盘时同步内联一份数据入口由面板经 window 取
|
||||
const data = (window.__QA_COVERAGE__.probe_real_input || {})[name];
|
||||
if (!data) { html += `<p class="warn">${esc(name)}:登记文件未加载(检查 script 标签与 manifest)</p>`; continue; }
|
||||
const hits = data.hits || {};
|
||||
const seen = data.seen || [];
|
||||
const gaps = seen.filter(k => !(k in hits) && !ex.some(e => k.indexOf(e) >= 0));
|
||||
html += `<details open><summary><b>${esc(name)}</b>:命中 ${Object.keys(hits).length} 控件 / 存在 ${seen.length} 控件 / 零命中缺口 ${gaps.length}(@ ${esc(data.meta.generated_at)})</summary><div class='gaplist'>`;
|
||||
for (const g of gaps) html += `${lamp("off", "存在但零真输入命中")} ${esc(g)}<br>`;
|
||||
html += "<b>已命中:</b><br>";
|
||||
for (const k of Object.keys(hits).sort()) html += `${lamp("ok", "真输入命中 " + hits[k] + " 次")} ${esc(k)}×${hits[k]}<br>`;
|
||||
html += "</div></details>";
|
||||
}
|
||||
document.getElementById("controlDrill").innerHTML = html || "<p class='warn'>探针登记缺数据——先跑探针与 tools/coverage_receipts.py</p>";
|
||||
})();
|
||||
|
||||
// ── 四、本体对账 ──
|
||||
(function () {
|
||||
const on = C.ontology;
|
||||
if (!on || !on.categories) { document.getElementById("ontologyGaps").innerHTML = "<p class='warn'>本体数据缺——先跑 tools/coverage_enum.py</p>"; return; }
|
||||
const ex = ((C.exemptions || {}).ontology || []).map(s => String(s));
|
||||
const hitTexts = [];
|
||||
const pri = (C.probe_real_input || {});
|
||||
for (const k in pri) for (const hk of Object.keys(pri[k].hits || {})) hitTexts.push(hk);
|
||||
let html = `<p class="note">实体 ${esc(on.meta.entities_total)} · 断言文料 ${esc(on.meta.corpus_rows)} 行 · 首见批次 ${esc(on.meta.generated_at)}</p>`;
|
||||
for (const cat of on.categories) {
|
||||
const shown = cat.gaps.filter(g => !ex.includes(g.id));
|
||||
const probed = shown.filter(g => hitTexts.some(h => h.indexOf(g.id) >= 0));
|
||||
const pure = shown.filter(g => !probed.includes(g));
|
||||
html += `<details><summary><b>${esc(cat.category)}</b>:${cat.total} 实体 / 关联 ${cat.matched} / 缺口 ${shown.length}(其中探针承载 ${probed.length})</summary><div class='gaplist'>`;
|
||||
for (const g of probed) html += `${lamp("ok", "探针真输入命中承载")} ${esc(g.id)} <span class="note">(断言文料零关联——交互覆盖由探针承载)</span><br>`;
|
||||
for (const g of pure) html += `${lamp("off", "验证零关联缺口——人工复核后再立事项")} ${esc(g.id)}<br>`;
|
||||
html += "</div></details>";
|
||||
}
|
||||
document.getElementById("ontologyGaps").innerHTML = html;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
r"""extract_capability_list.py — 能力清单义务层提取器(B 覆盖总览面板·义务层)
|
||||
|
||||
把 game/tests/README.md「能力清单」的四列表(能力|系统|真实入口|代表用例)按旅程段
|
||||
解析成机器可读 JSONP .js,供 tools/coverage_report.html 经 <script src> 直载
|
||||
(file:// 下 fetch 本地 json 被 CORS 拦——JSONP 是 B 面板全数据源的统一形态)。
|
||||
|
||||
用法:
|
||||
python tools/extract_capability_list.py [--readme <路径>] [--out <路径>]
|
||||
|
||||
判据口径:
|
||||
- 只认表头恰为「能力|系统|真实入口|代表用例」的四列表(覆盖域表等五列表不取);
|
||||
- 旅程段标题形态=「**旅程段N:名称**」,段名进每行的 segment 字段;
|
||||
- 代表用例列按「;」切分成用例指针数组(`文件:函数` 形态逐字保真);
|
||||
- 本工具是义务层的静态投影,产出随 README 变更重跑即得——面板只读本产物,禁在面板里改义务。
|
||||
|
||||
产出(JSONP):
|
||||
window.__QA_COVERAGE__.capability_list = {
|
||||
"meta": {"generated_at", "source", "rows_total", "segments_total"},
|
||||
"segments": [{"segment": 名称, "rows": [{"ability", "system", "entry", "cases": []}]}],
|
||||
}
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
DEFAULT_README = ROOT / "game" / "tests" / "README.md"
|
||||
DEFAULT_OUT = ROOT / "game" / "tests" / ".gates" / "coverage" / "capability_list.js"
|
||||
|
||||
SEGMENT_RE = re.compile(r"^\*\*旅程段[一二三四五六七八九十]+[::](.+?)\*\*\s*$")
|
||||
|
||||
|
||||
|
||||
def split_row(line: str) -> list[str]:
|
||||
body = line.strip()
|
||||
if body.startswith("|"):
|
||||
body = body[1:]
|
||||
if body.endswith("|"):
|
||||
body = body[:-1]
|
||||
return [c.strip() for c in body.split("|")]
|
||||
|
||||
|
||||
def parse(readme: Path) -> dict:
|
||||
lines = readme.read_text(encoding="utf-8").splitlines()
|
||||
segments: list[dict] = []
|
||||
cur_seg: str | None = None
|
||||
cur_rows: list[dict] | None = None
|
||||
in_capability_table = False
|
||||
for line in lines:
|
||||
seg = SEGMENT_RE.match(line.strip())
|
||||
if seg:
|
||||
if cur_seg is not None:
|
||||
segments.append({"segment": cur_seg, "rows": cur_rows or []})
|
||||
cur_seg = seg.group(1).strip()
|
||||
cur_rows = []
|
||||
in_capability_table = False
|
||||
continue
|
||||
if cur_seg is None:
|
||||
continue
|
||||
if not line.strip().startswith("|"):
|
||||
in_capability_table = False
|
||||
continue
|
||||
cells = split_row(line)
|
||||
if all(not set(c) - {"-", " ", ":"} for c in cells) and cells:
|
||||
continue # 表头分隔行 |---|---|
|
||||
if (len(cells) == 4 and cells[0] == "能力" and cells[1] == "系统"
|
||||
and cells[2] == "真实入口" and cells[3].startswith("代表用例")):
|
||||
in_capability_table = True
|
||||
continue
|
||||
if in_capability_table and len(cells) == 4:
|
||||
ability, system, entry, cases = cells
|
||||
cur_rows.append({
|
||||
"ability": ability.strip("`"),
|
||||
"system": system,
|
||||
"entry": entry,
|
||||
"cases": [c.strip().strip("`") for c in cases.split(";") if c.strip()],
|
||||
})
|
||||
if cur_seg is not None:
|
||||
segments.append({"segment": cur_seg, "rows": cur_rows or []})
|
||||
rows_total = sum(len(s["rows"]) for s in segments)
|
||||
return {
|
||||
"meta": {
|
||||
"generated_at": datetime.datetime.now().isoformat(timespec="seconds"),
|
||||
"source": str(readme),
|
||||
"rows_total": rows_total,
|
||||
"segments_total": len(segments),
|
||||
},
|
||||
"segments": segments,
|
||||
}
|
||||
|
||||
|
||||
def dump_js(payload: dict, out: Path) -> None:
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
js = "/* 能力清单义务层投影——由 extract_capability_list.py 生成,随 README 变更重跑;人工修订无效(义务源=README 能力清单)。 */\n"
|
||||
js += "window.__QA_COVERAGE__ = window.__QA_COVERAGE__ || {};\n"
|
||||
js += "window.__QA_COVERAGE__.capability_list = %s;\n" % json.dumps(payload, ensure_ascii=False, indent=2)
|
||||
out.write_text(js, encoding="utf-8", newline="\n")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||
ap = argparse.ArgumentParser(description="能力清单义务层提取(B 面板·义务层)")
|
||||
ap.add_argument("--readme", default=str(DEFAULT_README))
|
||||
ap.add_argument("--out", default=str(DEFAULT_OUT))
|
||||
args = ap.parse_args()
|
||||
readme = Path(args.readme)
|
||||
if not readme.is_file():
|
||||
print("[capability] README 不存在:%s" % readme)
|
||||
return 1
|
||||
payload = parse(readme)
|
||||
dump_js(payload, Path(args.out))
|
||||
print("[capability] 义务层投影已落盘 %s(旅程段 %d / 能力行 %d)" % (
|
||||
args.out, payload["meta"]["segments_total"], payload["meta"]["rows_total"]))
|
||||
if payload["meta"]["rows_total"] == 0:
|
||||
print("[capability] 红:能力行数为 0——表头或旅程段标题形态不匹配,禁当空义务面用")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -682,5 +682,7 @@ RC=$?
|
||||
echo ""
|
||||
echo "════════════════════════════════════════════"
|
||||
printf ' 回执:%s\n 基线:%s\n commit:%s\n' "$RECEIPT" "$BASELINE" "$(git -C "$REPO" rev-parse --short HEAD 2>/dev/null || echo '?')"
|
||||
# B 覆盖总览面板·执行层六源明细化(观测件非判定件——解析失败打红字不翻闸判,coverage_receipts.py 头注)
|
||||
"$PY" "$REPO/tools/coverage_receipts.py" --stamp-dir "$STAMP_DIR" || true
|
||||
echo "════════════════════════════════════════════"
|
||||
exit $RC
|
||||
@@ -245,7 +245,7 @@
|
||||
|
||||
#### 测试面真输入化分期与存量断言逐条精锚(2026-11-05 登记,来源=批次移交)
|
||||
|
||||
**实况**:证据=test-conventions §8.3「判据出处行锚」「真实入口判定口径」两条文(本批落);链式锚机检已在闸(capability_audit 链断 0,80 套件全承接)。剩余面=①营地七物件段等 ~40 处 emit 站点仍为接线验证(大门/进档链已于本批真输入化,能力行 :158 残险在案)②存量 996 条断言的判据出处逐条精锚(链式锚已保可核,精锚提升出处粒度)。
|
||||
**实况**:证据=test-conventions §8.3「判据出处行锚」「真实入口判定口径」两条文+§2「UI 驱动断言须走真输入管线」条文(批1补,qa-harness 分支 2cbd2ca9);链式锚机检已在闸(capability_audit 链断 0,80 套件全承接)。已承接=两探针 52 处 emit 站点(51 转真输入管线+1 置灰穿透标〔接线断言〕)。剩余面=①视图层测试方法面(press_candidate/press_continue/press_card/press_confirm/press_refresh_for_probe 等探针消费口,非 emit 站点)②存量 996 条断言的判据出处逐条精锚(链式锚已保可核,精锚提升出处粒度)。
|
||||
|
||||
**改动面**:游戏代码
|
||||
|
||||
@@ -253,7 +253,7 @@
|
||||
|
||||
#### QA 覆盖总览面板与自动试玩扩展——自动发现测试验证缺漏(2026-11-05 登记,来源=会话提及)
|
||||
|
||||
**实况**:方案全文=`D:\code\qa-harness-discussion\QA-Harness讨论结果-中间报告.md`(三批定案,全部三关试点);证据=真实入口缺口在册:能力行「营地七物件命中区点选与分界面可达」残险行 game/tests/README.md:158(emit 直发标「接线验证」、真输入承载待建)+test-conventions §8.3 真实入口判定口径条款(2026-11-05 收窄)。①探针存量 emit 真输入化(~40 处站点,承接上条)②B 覆盖总览面板+本体枚举器(六源回执明细化+能力清单提取+HTML 双击即开+游戏内容↔测试验证对账)③C 自动试玩扩展(auto_player 扩全动线全功能,异常上报含机器自填归因)。裁决在案勿重提:A 执行覆盖矩阵不做、D 缺口闸不做、变异测试禁入、归因登记不立独立制度。
|
||||
**实况**:方案全文=`D:\code\qa-harness-discussion\QA-Harness讨论结果-中间报告.md`(三批定案;批1实施偏差八条回写其 §10);证据=能力行「营地七物件命中区点选与分界面可达」残险 game/tests/README.md:158+test-conventions §8.3/§2 条文。进度=①②已落地(worktree qa-harness:批1=两探针 51 处真输入化+1 接线断言保留+覆盖登记件+§2 条文;批2=B 面板四节+义务层提取器+六源回执明细化闸壳挂点+本体枚举器 865 实体对账,工具四件入 tools/,提交 bb9ce8a1)③已落地(journey_player 场景驱动件:全动线 16 类屏到达+异常三类上报+误报指纹跨跑静默收敛实测生效+真输入命中并入覆盖登记 30 控件;首跑机器复现在案缺陷「开局被开始层盖住」)。裁决在案勿重提:A 执行覆盖矩阵不做、D 缺口闸不做、变异测试禁入、归因登记不立独立制度。
|
||||
|
||||
**改动面**:游戏代码+工具代码+文档
|
||||
|
||||
|
||||
Reference in new issue
Block a user