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
+6
View File
@@ -0,0 +1,6 @@
{
"engine_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"extension_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"pck_sha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
"arch": "arm64"
}
+1
View File
@@ -0,0 +1 @@
{"monotonic_us":1000,"run_id":"fixture-run","case_id":"FIXTURE-01","connection_epoch":1,"stage":"LOGIN","kind":"phase_changed","actor_vid":0,"target_vid":0,"payload":{"source":"server"}}
+54
View File
@@ -0,0 +1,54 @@
#!/bin/sh
# Stand-in for the packaged client used by script/playable_gate_test.sh.
# The fake app's Mach-O execs this file, so it runs as the runner's own child
# PID. It never contacts a server; MT_FAKE_SCENARIO picks the behaviour.
scenario="${MT_FAKE_SCENARIO:-pass}"
write_report() {
# $1 = PASS|BLOCKED, $2 = drop-last|evidence-missing|wrong-run|""
node -e '
const fs = require("node:fs");
const path = require("node:path");
const [status, variant] = process.argv.slice(1);
const out = process.env.MT_TEST_OUTPUT;
const required = JSON.parse(fs.readFileSync(path.join(out, "required-cases.json"), "utf8")).cases;
const runId = variant === "wrong-run" ? "previous-run-0000" : process.env.MT_PLAYABLE_RUN_ID;
const ids = variant === "drop-last" ? required.slice(0, -1) : required;
const cases = ids.map((id) => ({ id, status, duration_ms: 1, reason: status === "PASS" ? "fake" : "fixture_not_ready",
evidence: variant === "evidence-missing" && id === "NET-LOGIN-01" ? ["net-login-01.png"] : [] }));
const events = ids.map((id, i) => JSON.stringify({ monotonic_us: 1000 + i, run_id: runId, case_id: id, connection_epoch: 1,
stage: "LOGIN", kind: "phase_changed", actor_vid: 0, target_vid: 0, payload: { source: "server" } }));
fs.writeFileSync(process.env.MT_TEST_EVENTS, events.join("\n") + "\n");
const report = {
schema_version: 1, run_id: runId, suite: process.env.MT_PLAYABLE_SUITE || process.env.MT_TEST_MODE, status,
build: { engine_sha256: process.env.MT_BUILD_ENGINE_SHA256, extension_sha256: process.env.MT_BUILD_EXTENSION_SHA256,
pck_sha256: process.env.MT_BUILD_PCK_SHA256, arch: process.env.MT_BUILD_ARCH },
environment: { os: "macOS", renderer: "fake", resolution: [1280, 720] },
cases, required_cases: ids, events: "events.jsonl", event_count: events.length, failures: [],
blocked: status === "BLOCKED" ? ids.map((id) => `${id}: fixture_not_ready`) : [],
coverage: { required: cases.length, passed: status === "PASS" ? cases.length : 0 },
exit_gate: { checked: false, process_code: null, errors: [] },
};
fs.writeFileSync(process.env.MT_TEST_REPORT, JSON.stringify(report, null, 2));
' "$1" "${2:-}"
}
case "$scenario" in
pass) write_report PASS; echo "PLAYABLE CLIENT: PASS" ;;
soak-pass) write_report PASS; if [ "${MT_PLAYABLE_SUITE:-}" = "soak" ]; then sleep 3; fi; echo "PLAYABLE CLIENT: PASS" ;;
exit3) write_report PASS; echo "PLAYABLE CLIENT: PASS"; exit 3 ;;
signal) write_report PASS; kill -KILL $$ ;;
hang) trap '' TERM; echo "ignoring TERM"; while :; do sleep 1; done ;;
stale) write_report PASS wrong-run ;;
no-report) echo "exited before writing a report" ;;
rid) write_report PASS; echo "WARNING: 3 shaders of type CanvasItem were never freed." ;;
leak) write_report PASS; echo "WARNING: ObjectDB instances leaked at exit (run with --verbose for details)." ;;
missing-case) write_report PASS drop-last ;;
missing-evidence) write_report PASS evidence-missing ;;
blocked) write_report BLOCKED ;;
print-secret) write_report PASS; echo "login request for $MT_ACCOUNT with $MT_PASSWORD" ;;
fifo-holder) write_report PASS; sleep 13 & exit 0 ;;
no-creds) if [ -n "${MT_ACCOUNT:-}${MT_PASSWORD:-}" ]; then echo "SCRIPT ERROR: credentials reached an offline child"; fi; write_report PASS ;;
*) echo "unknown fake scenario" >&2; exit 64 ;;
esac
exit 0
@@ -0,0 +1,18 @@
{
"schema_version": 1,
"maps": {
"<map_key from map-assets.json>": {
"races": [],
"viewpoints": [
{
"id": "flat-01",
"kind": "flat",
"status": "unconfirmed",
"server_cm": [0, 0],
"camera_yaw_deg": 45
}
]
}
},
"notes": "Copy to test/playable/forest-viewpoints.<name>.local.json (ignored). kind is flat|slope|dense|warp; server_cm is the confirmed in-game global position; status becomes confirmed only with confirmed_by set to the confirming role (not a person's name). races lists server-confirmed mob vnums for the map; empty means the 12 candidate tree monsters are placed as candidates only. Never add credentials here."
}
+34 -6
View File
@@ -1,6 +1,6 @@
{
"schema_version": 1,
"run_id": "fixture-blocked",
"run_id": "fixture-run",
"suite": "playable",
"status": "BLOCKED",
"build": {
@@ -9,11 +9,39 @@
"pck_sha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
"arch": "arm64"
},
"environment": {"os": "macOS", "renderer": "Metal", "resolution": [1280, 720]},
"cases": [{"id": "FIXTURE-01", "status": "BLOCKED", "duration_ms": 0, "reason": "fixture_not_ready", "evidence": []}],
"environment": {
"os": "macOS",
"renderer": "Metal",
"resolution": [
1280,
720
]
},
"cases": [
{
"id": "FIXTURE-01",
"status": "BLOCKED",
"duration_ms": 0,
"reason": "fixture_not_ready",
"evidence": []
}
],
"required_cases": [
"FIXTURE-01"
],
"events": "events.jsonl",
"event_count": 1,
"failures": [],
"blocked": ["fixture_not_ready"],
"coverage": {"required": 1, "passed": 0},
"exit_gate": {"checked": false, "process_code": null, "errors": []}
"blocked": [
"FIXTURE-01: fixture_not_ready"
],
"coverage": {
"required": 1,
"passed": 0
},
"exit_gate": {
"checked": false,
"process_code": null,
"errors": []
}
}
+31 -5
View File
@@ -1,6 +1,6 @@
{
"schema_version": 1,
"run_id": "fixture-valid",
"run_id": "fixture-run",
"suite": "playable",
"status": "PASS",
"build": {
@@ -9,11 +9,37 @@
"pck_sha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
"arch": "arm64"
},
"environment": {"os": "macOS", "renderer": "Metal", "resolution": [1280, 720]},
"cases": [{"id": "FIXTURE-01", "status": "PASS", "duration_ms": 1, "reason": "ok", "evidence": []}],
"environment": {
"os": "macOS",
"renderer": "Metal",
"resolution": [
1280,
720
]
},
"cases": [
{
"id": "FIXTURE-01",
"status": "PASS",
"duration_ms": 1,
"reason": "ok",
"evidence": []
}
],
"required_cases": [
"FIXTURE-01"
],
"events": "events.jsonl",
"event_count": 1,
"failures": [],
"blocked": [],
"coverage": {"required": 1, "passed": 1},
"exit_gate": {"checked": false, "process_code": null, "errors": []}
"coverage": {
"required": 1,
"passed": 1
},
"exit_gate": {
"checked": false,
"process_code": null,
"errors": []
}
}
@@ -0,0 +1,7 @@
{
"schema_version": 1,
"suite": "playable",
"cases": [
"FIXTURE-01"
]
}
+27 -3
View File
@@ -4,7 +4,11 @@
"protocol": "classic",
"server": {
"server_index": 0,
"channel_index": 0
"channel": 1,
"auth_host": "",
"auth_port": 0,
"game_host": "",
"game_port": 0
},
"character_slot": 0,
"map_key": "",
@@ -14,6 +18,26 @@
"skill_cases": [],
"resolution": [1280, 720],
"loops": 3,
"timeout_seconds": 120,
"notes": "Copy to a local ignored file and fill from the confirmed server fixture. Never add credentials here."
"timeout_seconds": 900,
"soak": {
"duration_seconds": 7200,
"rest_seconds": 30,
"warmup_rounds": 1,
"min_rounds": 10,
"reconnects": 10,
"exits": 10,
"resolutions": {
"sizes": [[1280, 720], [1440, 900], [1600, 1000]],
"switches_per_size": 10
},
"warp": {
"status": "unconfirmed",
"notes": "Set status=confirmed only after the environment owner verifies a legal portal: required (even, >=20 out-and-back legs), portal_cm, destination_map_key, return_portal_cm, cross_server."
},
"faults": {
"status": "unconfirmed",
"notes": "Set status=confirmed only with a confirmed 127.0.0.1 test proxy (mode local_proxy, per_type, unreachable_seconds 1..20, upstream {auth_host,auth_port,game_host,game_port} = the real test server). server.* and the serverlist entry must then be 127.0.0.1 proxy ports; script/run_client_gate.sh starts script/playable_fault_proxy.mjs for this client only. A server warp that hands out a direct game address bypasses the proxy. Never change global routes or firewall; active reconnect is not a substitute."
}
},
"notes": "Copy to test/playable/<name>.local.json (ignored) and fill from the confirmed server fixture: server.* must equal the serverinfo entry for server_index/channel; map_key is the asset directory holding Setting.txt; waypoints are server cm. Never add credentials here. The soak suite (STB-01) additionally needs timeout_seconds >= soak.duration_seconds + 900."
}
+68
View File
@@ -0,0 +1,68 @@
{
"schema_version": 1,
"notes": [
"每职业从测试角色真实已学技能中选择:single_target 单体、area 范围、self_buff 自身增益、flying 飞行效果。",
"status=unconfirmed:尚未由测试环境负责人确认,对应 CBT 用例为 BLOCKED,不得发布。",
"status=confirmed:填 skill_id(该职业已学、非被动)、target(enemy|self|ground)、repeats(>=10)、required_evidence(至少一个服务端证据)。",
"status=not_applicable:该职业确无此类技能,必须填 checklist_ref 指向发布清单中的确认记录。",
"脚本和测试从不补技能 ID;只改本地副本 test/playable/skill-cases.local.json 时同样适用。检查:godot --headless --path project --script playable_skill_matrix.gd -- --job WARRIOR"
],
"jobs": {
"WARRIOR": {
"single_target": {
"status": "unconfirmed"
},
"area": {
"status": "unconfirmed"
},
"self_buff": {
"status": "unconfirmed"
},
"flying": {
"status": "unconfirmed"
}
},
"ASSASSIN": {
"single_target": {
"status": "unconfirmed"
},
"area": {
"status": "unconfirmed"
},
"self_buff": {
"status": "unconfirmed"
},
"flying": {
"status": "unconfirmed"
}
},
"SURA": {
"single_target": {
"status": "unconfirmed"
},
"area": {
"status": "unconfirmed"
},
"self_buff": {
"status": "unconfirmed"
},
"flying": {
"status": "unconfirmed"
}
},
"SHAMAN": {
"single_target": {
"status": "unconfirmed"
},
"area": {
"status": "unconfirmed"
},
"self_buff": {
"status": "unconfirmed"
},
"flying": {
"status": "unconfirmed"
}
}
}
}