- 新增 script/playable_soak.sh:先做 soak 配置/资源校验,无 --allow-gameplay 只校验不启动客户端;N(>=10) 次独立正常退出运行 + 墙钟 soak,失败即停止后续 运行并记 BLOCKED;写本批次 release-manifest.json 后聚合 - run_client_gate.sh:确认的故障代理对所有 suite 启动(共享场景的退出运行也经代理); soak 超时下限只约束 soak 客户端(validate_soak 增加 soak_client 参数) - 新增本地 127.0.0.1 故障代理、RSS 采样/内存判定、窗口/指标/流程模块及其测试 - forest_mob_render_test 输出 PASS/FAIL 标记,供 rendering_batch_test.sh 识别 - 新增 docs/FIRST-MAC-PLAYABLE-STATUS.md:如实记录 PASS/BLOCKED、已知问题与环境需求 - .gitignore 排除本地场景配置、凭据文件与运行输出 离线回归:rendering_batch_test.sh failures=0,playable_gate_test.sh PASS, node 夹具测试 PASS。未联网运行,未重建候选包。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ft3kXpNdLbKEfg5uDLfqVF
99 lines
3.6 KiB
GDScript
99 lines
3.6 KiB
GDScript
# app_flow_lifecycle_test —— §9.3 生命周期单一属主回归。
|
||
# AppFlow 跨登录 / 选人 / 游戏持有唯一 AppLifecycle;GameScene 不重复创建。
|
||
extends SceneTree
|
||
|
||
const AppFlow = preload("res://app_flow.gd")
|
||
|
||
class FakeClient extends Node:
|
||
signal phase_changed(phase: String)
|
||
signal disconnected(reason: String)
|
||
signal char_list(characters: Array)
|
||
signal char_name_changed(pid: int, name: String)
|
||
signal entered_game()
|
||
signal login_failed(reason: String)
|
||
signal char_created(slot: int)
|
||
signal char_create_failed(reason: int)
|
||
signal char_deleted(slot: int)
|
||
signal char_delete_failed()
|
||
var selects := []
|
||
var reconnects := 0
|
||
func select_character(index: int) -> bool:
|
||
selects.append(index)
|
||
return true
|
||
func reconnect() -> bool:
|
||
reconnects += 1
|
||
return true
|
||
|
||
var _fail := 0
|
||
|
||
func _ck(ok: bool, message: String) -> void:
|
||
if not ok:
|
||
_fail += 1
|
||
printerr("FAIL: " + message)
|
||
|
||
func _init() -> void:
|
||
var client := FakeClient.new()
|
||
var flow := AppFlow.new()
|
||
flow.build_game_scene = false
|
||
get_root().add_child(flow)
|
||
flow.start("", client)
|
||
await process_frame
|
||
|
||
var lifecycle_nodes: Array[Node] = []
|
||
for child in flow.get_children():
|
||
if child.name == "AppLifecycle":
|
||
lifecycle_nodes.append(child)
|
||
_ck(lifecycle_nodes.size() == 1, "AppFlow owns exactly one AppLifecycle")
|
||
_ck(flow.get_node_or_null("AppLifecycle") != null,
|
||
"AppLifecycle survives login screen construction")
|
||
_ck(client.get_parent() == flow,
|
||
"AppFlow keeps the same client beside lifecycle coordinator")
|
||
var login_snapshot: Dictionary = flow.get_playable_snapshot()
|
||
_ck(login_snapshot.get("stage", "") == "LOGIN" and not login_snapshot.get("scene_ready", true),
|
||
"playable snapshot is a read-only LOGIN summary")
|
||
|
||
# A live game keeps its scene during a transient disconnect. ReconnectUI
|
||
# owns the retry while the next character list silently re-enters the last
|
||
# selected slot instead of flashing the character-select page.
|
||
flow._game = Node.new()
|
||
flow.add_child(flow._game)
|
||
flow._state = AppFlow.GAME
|
||
flow._selected_char_slot = 2
|
||
client.disconnected.emit("wifi lost")
|
||
_ck(flow.state() == AppFlow.GAME and flow._reconnecting_game,
|
||
"game disconnect enters reconnecting state without leaving GAME")
|
||
_ck(flow._reconnect != null and flow._reconnect.is_showing(),
|
||
"game disconnect shows reconnect overlay")
|
||
client.char_list.emit([{"index": 2, "name": "Hero"}])
|
||
_ck(client.selects == [2], "reconnect re-enters remembered character slot")
|
||
client.entered_game.emit()
|
||
_ck(flow.state() == AppFlow.GAME and not flow._reconnecting_game
|
||
and not flow._reconnect.is_showing(),
|
||
"successful reconnect restores GAME and hides overlay")
|
||
var game_snapshot: Dictionary = flow.get_playable_snapshot()
|
||
_ck(game_snapshot.get("stage", "") == "GAME" and not game_snapshot.get("reconnecting", true),
|
||
"playable snapshot reports GAME after reconnect")
|
||
|
||
# Auto-login with an explicit MT_CHAR_SLOT must never fall back to another
|
||
# character, and the chosen slot must be remembered for a later reconnect.
|
||
flow._state = AppFlow.LOGIN
|
||
flow.auto_login = true
|
||
flow._auto_char_slot = 5
|
||
flow._selected_char_slot = -1
|
||
client.selects.clear()
|
||
client.char_list.emit([{"index": 0, "name": "Other"}])
|
||
_ck(client.selects.is_empty(), "absent explicit slot does not enter chars[0]")
|
||
flow._state = AppFlow.LOGIN
|
||
client.char_list.emit([{"index": 0, "name": "Other"}, {"index": 5, "name": "Fixture"}])
|
||
_ck(client.selects == [5] and flow._selected_char_slot == 5,
|
||
"explicit slot is entered and remembered for reconnect")
|
||
|
||
flow.queue_free()
|
||
await process_frame
|
||
if _fail == 0:
|
||
print("PASS: app_flow_lifecycle_test (single lifecycle owner)")
|
||
quit(0)
|
||
else:
|
||
printerr("%d check(s) failed" % _fail)
|
||
quit(1)
|