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
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env node
/* Copy stdin to stdout line by line, replacing the test credentials before any
* byte reaches disk. Secrets come only from MT_ACCOUNT / MT_PASSWORD in the
* environment and are never printed, logged or passed as arguments.
*
* node script/redact_stream.mjs <fifo >client.log
*
* Uses blocking reads on fd 0: async stdin on a macOS FIFO does not reliably
* report EOF, which would leave the runner waiting for the redactor. */
import fs from 'node:fs';
import { fileURLToPath } from 'node:url';
const MIN_LENGTH = 2;
export function secretLiterals(env = process.env) {
return ['MT_PASSWORD', 'MT_ACCOUNT']
.map((name) => env[name] || '')
.filter((value) => value.length >= MIN_LENGTH)
// Longest first so an account contained in a password is not half-replaced.
.sort((a, b) => b.length - a.length);
}
export function redact(line, literals = secretLiterals()) {
let out = line;
for (const literal of literals) out = out.split(literal).join('[redacted]');
return out;
}
function pump() {
const literals = secretLiterals();
const buffer = Buffer.alloc(64 * 1024);
let pending = '';
const write = (text) => {
const bytes = Buffer.from(text);
let offset = 0;
while (offset < bytes.length) offset += fs.writeSync(1, bytes, offset);
};
for (;;) {
let count;
try {
count = fs.readSync(0, buffer, 0, buffer.length, null);
} catch (error) {
if (error.code === 'EAGAIN') continue;
if (error.code === 'EOF') break;
throw error;
}
if (count === 0) break;
pending += buffer.toString('utf8', 0, count);
const lines = pending.split('\n');
pending = lines.pop();
if (lines.length > 0) write(lines.map((line) => `${redact(line, literals)}\n`).join(''));
}
// A trailing partial line is still redacted as a whole.
if (pending !== '') write(`${redact(pending, literals)}\n`);
}
if (process.argv[1] && fileURLToPath(import.meta.url) === fs.realpathSync(process.argv[1])) {
if (process.argv.includes('--help')) {
console.log('usage: node script/redact_stream.mjs < INPUT > OUTPUT (reads MT_ACCOUNT/MT_PASSWORD from env)');
} else {
pump();
}
}