feat(playable): 首个 Mac 联网内测版 STB-01 soak 工具与发布状态文档

- 新增 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
This commit is contained in:
shenlei
2026-09-11 22:06:48 +09:00
co-authored by Claude Opus 5
parent 374d4165d8
commit f39a55fdd5
66 changed files with 9189 additions and 951 deletions
+41
View File
@@ -1,6 +1,7 @@
extends SceneTree
const EffectPlayer = preload("res://fx/effect_player.gd")
const EffectRegistry = preload("res://fx/effect_registry.gd")
var failures := 0
func check(ok: bool, label: String) -> void:
@@ -84,5 +85,45 @@ func run() -> void:
check(is_equal_approx(live._particle_states[0].clock, stopped_clock),
"SceneTree leaves stopped clock unchanged")
live.free()
await registry_lifecycle()
print("effect_playback_test: failures=%d" % failures)
quit(1 if failures else 0)
# CBT-01 evidence: EffectRegistry reports the real spawn and tree-exit boundaries.
func registry_lifecycle() -> void:
var registry := EffectRegistry.new()
var spec := {"lights": [{"duration": 0.8, "diffuse": [1.0, 0.5, 0.2, 1.0]}]}
registry._path_cache["synthetic"] = "synthetic.mse"
registry._spec_cache["synthetic.mse"] = spec
var spawned: Array = []
var finished: Array = []
registry.fx_spawned.connect(func(effect: String, id: int, lifetime: int) -> void: spawned.append([effect, id, lifetime]))
registry.fx_finished.connect(func(effect: String, id: int, lifetime: int, elapsed: int, reason: String) -> void:
finished.append([effect, id, lifetime, elapsed, reason]))
var parent := Node3D.new()
root.add_child(parent)
var once := registry.spawn("synthetic", parent, true)
check(spawned.size() == 1 and spawned[0][0] == "synthetic" and spawned[0][2] == 1000,
"one-shot spawn reports its MSE-defined lifetime: %s" % [spawned])
check(finished.is_empty(), "spawn alone is not a finish")
await create_timer(1.8).timeout
check(not is_instance_valid(once), "one-shot effect left the tree on its own clock")
check(finished.size() == 1 and finished[0][1] == spawned[0][1] and finished[0][4] == "cleanup",
"tree exit reports the same id with reason cleanup: %s" % [finished])
if finished.size() == 1:
check(int(finished[0][3]) >= 1000 and int(finished[0][3]) <= 1000 + 1000,
"cleanup happened inside lifetime + 1s (elapsed %dms)" % int(finished[0][3]))
var looping := registry.spawn("synthetic", parent, false)
check(spawned.size() == 2 and spawned[1][2] == -1 and spawned[1][1] != spawned[0][1],
"looping spawn has a fresh id and no defined lifetime")
var early := registry.spawn("synthetic", parent, true)
parent.remove_child(early)
early.free()
check(finished.size() == 2 and finished[1][4] == "removed", "owner removal before cleanup reports reason removed")
parent.free()
check(finished.size() == 3 and finished[2][1] == spawned[1][1], "scene teardown closes the looping effect")
var orphan := Node3D.new()
check(registry.spawn("missing", orphan, true) == null and spawned.size() == 3,
"unresolvable effect emits no spawn event")
orphan.free()
await process_frame