test(aigc): SaaFullGraphE2eTest — SAA 真全图 e2e 首证门控测试(U7)
U7 集成顶石的门控集成测试(-Dsaa.e2e=1 + NEWAPI_KEY)。构造真 5 参 SaaGraphDispatcher(stub callback 捕获 reqVO+latch,DataSource=null 内存 checkpoint,零 DB),顺序跑 N 条一句话 create job,真 LLM(new-api 五角色)+ 真九门 harness(node/esbuild/Chrome,4320/9222 串行)+真 16 节点全图 (render→classify→design→generate→validate→scaffold→asset→build→play→player→nreview→emit/giveup)+真出口回调,测成功率。 首证(2026-06-18,mini-desktop):SAA 真全图 e2e 机制完整跑通、零 prod bug; 成功率 3/5=60%(贪吃蛇/接金币/躲障碍 succeeded,engineBundle≈211KB 含 __GameBundle; 打砖块败 I_control、点击消除败 F_wiring+H_progress,救场阶梯耗尽 giveup 真落盘)。 单款墙钟 3.5-16min(九门真玩循环+最多8救场轮固有,同 Python W-G1)。 → 成功率 60% < 80% 转正门:cutover 未切(现 worker 主链保持默认,零回归)。 gap=难门生成质量(prompt 接地/gatespec/救场调优),W-G1 生成域调优单元。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
679afa1c65
commit
bf99662c3f
@ -0,0 +1,322 @@
|
|||||||
|
package com.wanxiang.huijing.game.module.aigc.saa;
|
||||||
|
|
||||||
|
import com.wanxiang.huijing.game.module.aigc.controller.admin.task.vo.DifyCallbackReqVO;
|
||||||
|
import com.wanxiang.huijing.game.module.aigc.service.callback.DifyCallbackService;
|
||||||
|
import com.wanxiang.huijing.game.module.aigc.service.executor.AigcExecutorProperties;
|
||||||
|
import io.micrometer.observation.ObservationRegistry;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SaaFullGraphE2eTest —— U7 集成顶石:<b>首次证明 SAA 真·全图生成 e2e 跑通并测成功率</b>。
|
||||||
|
*
|
||||||
|
* <p><b>验什么(与既有 SAA 测试的区别)</b>:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link SaaNewApiGenerateSmokeTest} 只验「SAA→new-api→出工厂源码」一条窄链(无 build/play harness);</li>
|
||||||
|
* <li>{@link SaaHarnessDriveTest} 只验「金样 factory 驱动 build+九门 harness」(不接 LLM、不接全图、不接 job 契约);</li>
|
||||||
|
* <li><b>本测</b>=经 {@link SaaGraphDispatcher#dispatch} 真入口跑 {@link SaaStudioGraph} <b>16 节点全图</b>
|
||||||
|
* (render→classify→design→generate→validate→scaffold→asset→build→play→player→nreview→…→emit/giveup)
|
||||||
|
* + <b>真 LLM</b>(五角色经 new-api 网关)+ <b>真九门 harness</b>(build/play 子进程,串行真玩门 4320/9222)
|
||||||
|
* + <b>真出口</b>({@link DifyCallbackService#handleCallback} 据图终态组 reqVO)。<b>这一整条 e2e 从未跑过,本测首证。</b></li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p><b>关键:dispatch 是「异步 + 单线程执行器 + Semaphore(1) 真玩门」=多 job 严格串行后台执行</b>。
|
||||||
|
* 故本测<b>先一次性投递全部 N 条</b>(每次 dispatch 提交后台即返回 true),<b>再用一把共享 {@link CountDownLatch}(N)
|
||||||
|
* 等全部回调</b>;stub 回调按 {@code reqVO.getTraceId()} 把每条结果<b>精确归位</b>到并发 map(不会跨条错配,
|
||||||
|
* 这是修掉「每条 arm 新 latch + 240s 超时」旧设计致命缺陷的关键——旧设计下后台串行跑、回调命中的是当时
|
||||||
|
* 恰好 arm 的那条 latch,归因全错)。落库由 stub 整体旁路(不触三表写链)。
|
||||||
|
*
|
||||||
|
* <p><b>纪律(纯测试·零 prod 改动)</b>:
|
||||||
|
* <ol>
|
||||||
|
* <li>门控 {@code -Dsaa.e2e=1}(无机/无 key 时不跑,避免误判 fail);</li>
|
||||||
|
* <li>{@code saaCheckpointEnabled=false} + DataSource 传 null → <b>零 DB 依赖</b>;</li>
|
||||||
|
* <li>N 默认 5,可 {@code -Dsaa.e2e.n=K} 覆盖(串行单条含修复轮约 5~15 分钟,N=5 需约 30~75 分钟,按 mvn timeout 量裁);</li>
|
||||||
|
* <li>等待<b>总</b>超时 = {@code perBriefBudget(默认 900s) × N},可 {@code -Dsaa.e2e.perBriefSec} 调;到点仍未集齐则
|
||||||
|
* best-effort 收尾(已回调的照常计,未回调的计「(未回调)」),<b>不一条卡死全挂</b>;</li>
|
||||||
|
* <li>成功率作证据打印,<b>不硬断 80%</b>——存在性断言放宽到「至少 1 条真全图 e2e 跑通」(首证存在性,成功率留人判)。</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* <p><b>运行(mini-desktop,game-runtime 有 node+esbuild+Chrome、端口 4320/9222 空)</b>:
|
||||||
|
* <pre>
|
||||||
|
* NEWAPI_KEY=sk-... timeout 6000 mvn -pl ...aigc-server test \
|
||||||
|
* -Dtest=SaaFullGraphE2eTest -Dsaa.e2e=1 \
|
||||||
|
* -DGAME_RUNTIME_DIR=/root/game-staging/repo/game-runtime \
|
||||||
|
* -Dsurefire.failIfNoSpecifiedTests=false
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @author 造梦AI(U7 集成顶石)
|
||||||
|
*/
|
||||||
|
class SaaFullGraphE2eTest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* game-runtime 根(harness 运行时所在:node_modules/esbuild + games/_wg1-gen/_shared/serve-and-play.sh + play.cdp.cjs)。
|
||||||
|
* 优先取系统属性 GAME_RUNTIME_DIR(与 {@link SaaHarnessDriveTest} 同口径),缺则取 staging 默认。
|
||||||
|
*/
|
||||||
|
private static final String GAME_RUNTIME_DIR = System.getProperty("GAME_RUNTIME_DIR",
|
||||||
|
System.getenv().getOrDefault("GAME_RUNTIME_DIR", "/root/game-staging/repo/game-runtime"));
|
||||||
|
|
||||||
|
/** new-api 网关 host 根(SAA 内部 stripV1Suffix 自剥 /v1;内网地址按项目规则可入码)。 */
|
||||||
|
private static final String LLM_BASE = "http://100.64.0.8:3000";
|
||||||
|
|
||||||
|
/** 单条 brief 的墙钟预算秒数(用于推总等待超时;含 LLM 多步 + 修复轮 + 真玩九门,实测单条约 5~15 分钟)。 */
|
||||||
|
private static final long PER_BRIEF_BUDGET_SECONDS =
|
||||||
|
Long.getLong("saa.e2e.perBriefSec", 900L);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* N=5 条多样一句话 brief(覆盖不同玩法品类,避免单品类偶然过/不过)。可 {@code -Dsaa.e2e.n=K} 取前 K 条。
|
||||||
|
* 全部一次性投递,后台真玩门 Semaphore(1) 串行逐条跑。
|
||||||
|
*/
|
||||||
|
private static final List<String> ALL_BRIEFS = List.of(
|
||||||
|
"打砖块:挡板接球把上方砖块全部消除即胜,掉球则负。",
|
||||||
|
"贪吃蛇:操控小蛇吃食物变长,撞到自己身体或墙壁则游戏结束。",
|
||||||
|
"接金币:左右移动篮子接住从天上掉落的金币,漏接太多就失败。",
|
||||||
|
"躲障碍:角色不断前进,点击跳跃躲开迎面而来的障碍物,撞到就结束。",
|
||||||
|
"点击消除:点击三个或以上相连的同色方块把它们消掉,消满目标分数过关。"
|
||||||
|
);
|
||||||
|
|
||||||
|
/** 单条 brief 的 e2e 结果证据(回调按 traceId 精确归位)。 */
|
||||||
|
private static final class BriefResult {
|
||||||
|
final int idx;
|
||||||
|
final String brief;
|
||||||
|
final String traceId;
|
||||||
|
volatile String status = "(未回调)"; // succeeded / failed / (未回调=总超时内未集齐)
|
||||||
|
volatile int engineBundleLen = 0; // 成功路 bundle.iife.js 长度(含 __GameBundle)
|
||||||
|
volatile String failureReason = ""; // 失败路归桶(llm_error 等)
|
||||||
|
volatile boolean dispatched = false; // dispatch 投递握手是否 true
|
||||||
|
volatile long callbackAtMs = 0; // 回调命中时刻(相对 run 起点;推单条墙钟用)
|
||||||
|
volatile String failDetail = ""; // 失败时从 verdict.json/giveup-dump.json 摘的真因
|
||||||
|
|
||||||
|
BriefResult(int idx, String brief, String traceId) {
|
||||||
|
this.idx = idx;
|
||||||
|
this.brief = brief;
|
||||||
|
this.traceId = traceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean succeeded() {
|
||||||
|
return "succeeded".equals(status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* stub DifyCallbackService:按 {@code reqVO.getTraceId()} 把结果<b>精确归位</b>到 byTrace map + 倒数共享 latch,
|
||||||
|
* <b>不写 DB</b>(替掉真回调的三表写链)。后台串行回调,无并发竞争;用 ConcurrentHashMap 保险。
|
||||||
|
*/
|
||||||
|
private static final class CapturingStubCallback implements DifyCallbackService {
|
||||||
|
final CountDownLatch latch;
|
||||||
|
final Map<String, BriefResult> byTrace;
|
||||||
|
final long runStartMs;
|
||||||
|
|
||||||
|
CapturingStubCallback(int n, Map<String, BriefResult> byTrace, long runStartMs) {
|
||||||
|
this.latch = new CountDownLatch(n);
|
||||||
|
this.byTrace = byTrace;
|
||||||
|
this.runStartMs = runStartMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean handleCallback(DifyCallbackReqVO reqVO) {
|
||||||
|
// 只捕获 + 归位 + 倒数:不触任何 DB/事务(与真 DifyCallbackServiceImpl 的三表写链解耦)。
|
||||||
|
String traceId = reqVO.getTraceId();
|
||||||
|
BriefResult r = (traceId == null) ? null : byTrace.get(traceId);
|
||||||
|
if (r != null) {
|
||||||
|
r.status = reqVO.getStatus();
|
||||||
|
r.engineBundleLen = reqVO.getEngineBundle() == null ? 0 : reqVO.getEngineBundle().length();
|
||||||
|
r.failureReason = reqVO.getFailureReason() == null ? "" : reqVO.getFailureReason();
|
||||||
|
r.callbackAtMs = System.currentTimeMillis() - runStartMs;
|
||||||
|
System.out.println("[callback] #" + r.idx + " traceId=" + traceId
|
||||||
|
+ " status=" + r.status + " engineBundleLen=" + r.engineBundleLen
|
||||||
|
+ " reason=" + r.failureReason + " @+" + (r.callbackAtMs / 1000) + "s");
|
||||||
|
} else {
|
||||||
|
// 不应发生(traceId 必在 map 内):留痕,仍倒数避免 latch 永挂。
|
||||||
|
System.out.println("[callback] 未知 traceId=" + traceId + "(不在 map 内,忽略归位仍倒数)");
|
||||||
|
}
|
||||||
|
latch.countDown();
|
||||||
|
return Boolean.TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@EnabledIfSystemProperty(named = "saa.e2e", matches = "1",
|
||||||
|
disabledReason = "需 mini-desktop(node+esbuild+Chrome+端口 4320/9222 空)+ NEWAPI_KEY;无机/无 key 时跳过避免误判 fail;开关 -Dsaa.e2e=1")
|
||||||
|
void saa_full_graph_e2e_success_rate() throws Exception {
|
||||||
|
// ── 0) 前置就位:NEWAPI_KEY(密钥经环境变量,绝不入库)+ game-runtime harness 依赖 ──
|
||||||
|
String apiKey = System.getenv("NEWAPI_KEY");
|
||||||
|
assertNotNull(apiKey, "NEWAPI_KEY 未设(密钥须经环境变量注入,绝不入库)");
|
||||||
|
assertTrue(!apiKey.isBlank(), "NEWAPI_KEY 为空");
|
||||||
|
Path runtimeRoot = Paths.get(GAME_RUNTIME_DIR);
|
||||||
|
assertTrue(Files.isDirectory(runtimeRoot), "GAME_RUNTIME_DIR 不存在:" + runtimeRoot);
|
||||||
|
assertTrue(Files.exists(runtimeRoot.resolve("games/_wg1-gen/_shared/serve-and-play.sh")),
|
||||||
|
"缺 serve-and-play.sh(九门 harness 子进程地板):" + runtimeRoot);
|
||||||
|
assertTrue(Files.exists(runtimeRoot.resolve("node_modules/esbuild")),
|
||||||
|
"缺 node_modules/esbuild(build 节点依赖),请在 game-runtime 跑 npm i -D esbuild");
|
||||||
|
|
||||||
|
// N:默认 5,可 -Dsaa.e2e.n=K 取前 K 条(串行耗时大,调小便于在短 mvn timeout 内跑通若干条)。
|
||||||
|
int n = Integer.getInteger("saa.e2e.n", ALL_BRIEFS.size());
|
||||||
|
n = Math.max(1, Math.min(n, ALL_BRIEFS.size()));
|
||||||
|
List<String> briefs = ALL_BRIEFS.subList(0, n);
|
||||||
|
|
||||||
|
// ── 1) 构造真 AigcExecutorProperties(@Data 即 setter 全有)──
|
||||||
|
// checkpoint 关 + DataSource 传 null → 零 DB 依赖;dispatcher=saa 走进程内全图派发。
|
||||||
|
AigcExecutorProperties props = new AigcExecutorProperties();
|
||||||
|
props.setLlmBase(LLM_BASE); // SAA 内部 stripV1Suffix 自剥 /v1
|
||||||
|
props.setApiKey(apiKey); // 经环境变量注入
|
||||||
|
props.setDispatcher("saa"); // 走进程内 SAA 全图派发器(非 http worker)
|
||||||
|
props.setSaaGameRuntimeDir(GAME_RUNTIME_DIR);
|
||||||
|
props.setSaaMaxRepairs(5); // stage1 修复轮上限(对齐 Python studio.py 默认 5)
|
||||||
|
props.setSaaMaxPlayerRounds(1); // player 顾问轮上限
|
||||||
|
props.setSaaStage2ExtraRepairs(3); // 救场阶梯 stage2 额外修复轮(5+3 阶梯)
|
||||||
|
props.setSaaCheckpointEnabled(false); // 关 checkpoint → 不依赖 DataSource,零 DB
|
||||||
|
|
||||||
|
// ── 2) 结果容器 + stub 回调(共享 latch(N) + byTrace 精确归位)+ 派发器(5 参,sourceProjectApi=null=create 路)──
|
||||||
|
long runStart = System.currentTimeMillis();
|
||||||
|
long runStamp = runStart;
|
||||||
|
List<BriefResult> results = new ArrayList<>();
|
||||||
|
Map<String, BriefResult> byTrace = new ConcurrentHashMap<>();
|
||||||
|
for (int i = 0; i < briefs.size(); i++) {
|
||||||
|
// traceId 唯一串:图内据此派生落盘目录名 saa-<sanitize(traceId)>,回调据此精确归位。
|
||||||
|
String traceId = "saa-e2e-" + runStamp + "-" + (i + 1);
|
||||||
|
BriefResult r = new BriefResult(i + 1, briefs.get(i), traceId);
|
||||||
|
results.add(r);
|
||||||
|
byTrace.put(traceId, r);
|
||||||
|
}
|
||||||
|
CapturingStubCallback stub = new CapturingStubCallback(briefs.size(), byTrace, runStart);
|
||||||
|
// 直接 new(非 Spring 装配),@PostConstruct warmUpGraphEagerly 不会被调用——dispatch 内部 ensureGraph
|
||||||
|
// 懒构造图,首条 dispatch 即触发构造,行为等价(仅少了启动期预热)。
|
||||||
|
SaaGraphDispatcher dispatcher = new SaaGraphDispatcher(
|
||||||
|
props, stub, /*dataSource*/ null, ObservationRegistry.NOOP, /*sourceProjectApi*/ null);
|
||||||
|
|
||||||
|
// ── 3) 一次性投递全部 N 条(每次 dispatch 提交后台单线程即返回;后台串行逐条跑全图)──
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("======== SAA 真·全图 e2e 投递 " + briefs.size() + " 条(后台串行跑)========");
|
||||||
|
for (BriefResult r : results) {
|
||||||
|
Map<String, Object> job = new HashMap<>();
|
||||||
|
job.put("brief", r.brief);
|
||||||
|
job.put("traceId", r.traceId);
|
||||||
|
job.put("templateId", "generic"); // create job 契约:仅 brief/traceId/templateId(modify 字段全缺=create)
|
||||||
|
try {
|
||||||
|
r.dispatched = dispatcher.dispatch(job);
|
||||||
|
} catch (Exception e) {
|
||||||
|
r.dispatched = false;
|
||||||
|
r.status = "(dispatch异常:" + e.getClass().getSimpleName() + ")";
|
||||||
|
}
|
||||||
|
System.out.println("[dispatch] #" + r.idx + " traceId=" + r.traceId
|
||||||
|
+ " -> " + (r.dispatched ? "投递成功" : "投递失败") + " brief=" + r.brief);
|
||||||
|
if (!r.dispatched && "(未回调)".equals(r.status)) {
|
||||||
|
// 投递前置失败(game-runtime 缺/图构造失败/入参非法):本条不会有回调,预先倒数补位避免 latch 永挂。
|
||||||
|
r.status = "(投递失败)";
|
||||||
|
stub.latch.countDown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 4) 等全部回调(共享 latch(N));总超时 = perBriefBudget × N(串行执行)。到点 best-effort 收尾 ──
|
||||||
|
long totalTimeoutSec = PER_BRIEF_BUDGET_SECONDS * briefs.size();
|
||||||
|
System.out.println("======== 等待全部回调(总超时 " + totalTimeoutSec + "s = "
|
||||||
|
+ PER_BRIEF_BUDGET_SECONDS + "s/条 × " + briefs.size() + " 条,后台串行)========");
|
||||||
|
boolean allDone = stub.latch.await(totalTimeoutSec, TimeUnit.SECONDS);
|
||||||
|
long totalWallMs = System.currentTimeMillis() - runStart;
|
||||||
|
if (!allDone) {
|
||||||
|
System.out.println("[warn] 总超时 " + totalTimeoutSec + "s 到点仍有条目未回调(best-effort 收尾,未回调条计「(未回调)」)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 5) 失败/未回调路补读盘诊断(verdict.json/giveup-dump.json)──
|
||||||
|
for (BriefResult r : results) {
|
||||||
|
if (!r.succeeded() && r.dispatched) {
|
||||||
|
r.failDetail = readFailDetail(runtimeRoot, r.traceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 6) 汇总:成功率 X/N + 逐条证据 ──
|
||||||
|
long succeeded = results.stream().filter(BriefResult::succeeded).count();
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("################ SAA 真·全图 e2e 汇总 ################");
|
||||||
|
System.out.println("game-runtime = " + GAME_RUNTIME_DIR);
|
||||||
|
System.out.println("new-api base = " + LLM_BASE + "(SAA 内部剥 /v1)");
|
||||||
|
System.out.println("总墙钟 = " + (totalWallMs / 1000) + "s(后台串行跑 " + briefs.size() + " 条)");
|
||||||
|
System.out.println("成功率 = " + succeeded + "/" + briefs.size()
|
||||||
|
+ " (" + String.format("%.1f", 100.0 * succeeded / briefs.size()) + "%)");
|
||||||
|
System.out.println("------------------------------------------------------");
|
||||||
|
for (BriefResult r : results) {
|
||||||
|
System.out.println(String.format(
|
||||||
|
"#%d status=%-26s bundleLen=%-7d callback@+%-5s reason=%s",
|
||||||
|
r.idx, r.status, r.engineBundleLen,
|
||||||
|
(r.callbackAtMs > 0 ? (r.callbackAtMs / 1000) + "s" : "-"),
|
||||||
|
r.succeeded() ? "-" : (r.failureReason.isEmpty() ? "(无)" : r.failureReason)));
|
||||||
|
System.out.println(" brief: " + r.brief);
|
||||||
|
if (!r.succeeded() && !r.failDetail.isBlank()) {
|
||||||
|
System.out.println(" fail : " + r.failDetail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.out.println("######################################################");
|
||||||
|
|
||||||
|
// ── 7) 存在性断言(首证):至少 1 条真全图 e2e 跑通;成功率作证据,不硬断 80%(留人判)──
|
||||||
|
assertTrue(succeeded >= 1,
|
||||||
|
"至少 1 条 SAA 真全图 e2e 应跑通(实得 " + succeeded + "/" + briefs.size()
|
||||||
|
+ ");若全 0:检查 new-api 可达/九门 harness/端口 4320-9222 占用/图终态——见上方逐条 failDetail");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 失败诊断:从图落盘目录读 verdict.json(哪门未过)/ giveup-dump.json(救场耗尽真因)摘要。
|
||||||
|
* 落盘目录名 = {@code saa-<sanitize(traceId)>}(与 {@link SaaGraphDispatcher} buildInputs 一致,sanitize 把非 [A-Za-z0-9_-] 转 -)。
|
||||||
|
* best-effort:读不到/解析失败返空串(不抛、不连坐主流程)。
|
||||||
|
*
|
||||||
|
* @param runtimeRoot game-runtime 根
|
||||||
|
* @param traceId 本条 brief 的 traceId
|
||||||
|
* @return 失败真因摘要(verdict 未过门概览 + giveup 末次失败),无则空串
|
||||||
|
*/
|
||||||
|
private static String readFailDetail(Path runtimeRoot, String traceId) {
|
||||||
|
try {
|
||||||
|
// dispatcher 落盘目录名 = "saa-" + sanitize(traceId);sanitize 把非法字符转 -(traceId 已合法,恒等)。
|
||||||
|
String gameDir = "saa-" + traceId.replaceAll("[^A-Za-z0-9_-]", "-");
|
||||||
|
Path evidenceDir = runtimeRoot.resolve("games/_wg1-gen").resolve(gameDir).resolve("evidence");
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
var mapper = new com.fasterxml.jackson.databind.ObjectMapper();
|
||||||
|
|
||||||
|
Path verdict = evidenceDir.resolve("verdict.json");
|
||||||
|
if (Files.exists(verdict)) {
|
||||||
|
var v = mapper.readTree(verdict.toFile());
|
||||||
|
sb.append("verdict.pass=").append(v.path("pass").asBoolean(false));
|
||||||
|
var guards = v.path("guards");
|
||||||
|
if (guards.isObject()) {
|
||||||
|
sb.append(" 未过门:[");
|
||||||
|
var it = guards.fieldNames();
|
||||||
|
boolean first = true;
|
||||||
|
while (it.hasNext()) {
|
||||||
|
String g = it.next();
|
||||||
|
if (!guards.path(g).path("pass").asBoolean(false)) { // 只列未过的门(聚焦真因)
|
||||||
|
if (!first) sb.append(",");
|
||||||
|
sb.append(g);
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.append("]");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sb.append("(无 verdict.json,可能 build 前即失败或图未到 play)");
|
||||||
|
}
|
||||||
|
|
||||||
|
Path giveup = evidenceDir.resolve("giveup-dump.json");
|
||||||
|
if (Files.exists(giveup)) {
|
||||||
|
var g = mapper.readTree(giveup.toFile());
|
||||||
|
sb.append(" giveup{modelTier=").append(g.path("modelTier").asText("?"))
|
||||||
|
.append(", failCount=").append(g.path("failCount").asText("?"))
|
||||||
|
.append(", lastStageFail=").append(g.path("lastStageFail").asText("?")).append("}");
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return "(读 failDetail 异常:" + e.getMessage() + ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user