test(saa): 003-U1 验收门工具化——SaaFullGraphE2eTest n≥10 + minSuccessRate 硬门 + token 成本会计

把 SaaFullGraphE2eTest 从「内置 5 条 / 只断≥1 跑通」改造成可作 flip 前置硬门的客观量测工具
(plan 2026-06-18-003 / 003-U1;该路代码 001-U1~U4 已在 dev/2.0.0 落地,本单元只工具化验收门):

- brief:内置扩 5→12 条多品类(偏 realtime/physics/tap-targets/clickable)+ -Dsaa.e2e.briefFile
  外部文件(行式,去空白/跳空行/跳 # 注释,解析空则回退内置);-Dsaa.e2e.n 取前 K(未设=全量)。
- 成功率门:-Dsaa.e2e.minSuccessRate 可配硬断言;默认不设=仅存在性(保 003-U1「先实测真基线再收敛」、
  不破普通构建),显式 0.8 即 flip(003-U2)前置硬门。
- 成本会计:从回调 trace best-effort 抽 prompt+completion token + 升 stage2 计数作成本证据;
  -Dsaa.e2e.maxTokensPerGame 仅软警告不硬阻断(遵创始人「质量优先、允许临时破价、记账对账」)。
  ¥折算 blocked on F3(SaaGraphDispatcher cost 整段省略,§6.3)→ 不造假¥。
- 纯逻辑抽 8 静态方法 + 新增模型无关单测 SaaFullGraphE2eGateLogicTest(覆盖 happy+边界)。

验证:mvn test-compile 全 reactor BUILD SUCCESS;SaaFullGraphE2eGateLogicTest 12/12 过;
e2e 无 -Dsaa.e2e=1 仍 Skipped(门控完好)。质量收敛到≥80% 须 mini-desktop 权威量测(blocked,非本单元)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lili 2026-06-18 10:52:32 -07:00
parent 9b280d668e
commit 730b399dd6
2 changed files with 418 additions and 15 deletions

View File

@ -0,0 +1,171 @@
package com.wanxiang.huijing.game.module.aigc.saa;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* SaaFullGraphE2eGateLogicTest 003-U1 验收门工具化的<b>纯逻辑模型无关单测</b>
*
* <p><b>为何独立成测</b>{@link SaaFullGraphE2eTest} 主体 {@code @EnabledIfSystemProperty(saa.e2e=1)} mini-desktop+模型+九门
* 普通构建跳过不可验但其新增的brief 解析 / 取前 N / 门槛解析 / 成功率门 / token·模型档抽取全是<b>纯函数</b>
* 与模型无关抽出后此测在普通 {@code mvn test} 即跑<b>无需 LLM / NEWAPI_KEY / -Dsaa.e2e</b>作工具化逻辑的客观回归门
*
* <p>覆盖happy + 边界/null/越界/非法+ 等价类对齐 ce-workTest Scenario Completeness
*
* @author 造梦AI003-U1 验收门工具化
*/
class SaaFullGraphE2eGateLogicTest {
// parseBriefLines行式解析去空白 / 跳空行 / # 注释
@Test
@DisplayName("parseBriefLines去首尾空白 + 跳空行 + 跳 # 注释 + 保留正文")
void parseBriefLines_basic() {
List<String> raw = Arrays.asList(
"# 这是注释,应跳过",
" 打砖块:挡板接球消砖。 ", // 含首尾空白 trim
"", // 空行跳过
" ", // 纯空白行跳过
"贪吃蛇:吃食物变长。",
"#另一条注释",
"接金币:接住金币。");
List<String> out = SaaFullGraphE2eTest.parseBriefLines(raw);
assertEquals(3, out.size(), "应只剩 3 条正文");
assertEquals("打砖块:挡板接球消砖。", out.get(0), "首尾空白应被 trim");
assertEquals("贪吃蛇:吃食物变长。", out.get(1));
assertEquals("接金币:接住金币。", out.get(2));
}
@Test
@DisplayName("parseBriefLinesnull 入参 → 空列表(不抛)")
void parseBriefLines_null() {
assertTrue(SaaFullGraphE2eTest.parseBriefLines(null).isEmpty());
}
@Test
@DisplayName("parseBriefLines含 null 行 + CRLF 尾 \\r 被 trim 掉")
void parseBriefLines_nullLineAndCrlf() {
List<String> raw = Arrays.asList("有效行\r", null, "另一行");
List<String> out = SaaFullGraphE2eTest.parseBriefLines(raw);
assertEquals(2, out.size());
assertEquals("有效行", out.get(0), "CRLF 的 \\r 应被 trim 掉(跨平台 brief 文件)");
}
// clampToN取前 N=全量 / clamp [1,size]
@Test
@DisplayName("clampToNn=null → 全量")
void clampToN_nullN_full() {
List<String> src = Arrays.asList("a", "b", "c");
assertEquals(3, SaaFullGraphE2eTest.clampToN(src, null).size());
}
@Test
@DisplayName("clampToNn 越上界 → 收到 sizen<1 → 收到 1n 居中 → n")
void clampToN_clamp() {
List<String> src = Arrays.asList("a", "b", "c");
assertEquals(3, SaaFullGraphE2eTest.clampToN(src, 99).size(), "n>size → size");
assertEquals(1, SaaFullGraphE2eTest.clampToN(src, 0).size(), "n<1 → 1");
assertEquals(1, SaaFullGraphE2eTest.clampToN(src, -5).size(), "负 n → 1");
assertEquals(2, SaaFullGraphE2eTest.clampToN(src, 2).size(), "居中 → n");
}
@Test
@DisplayName("clampToNnull / 空列表 → 安全返回空")
void clampToN_nullOrEmpty() {
assertTrue(SaaFullGraphE2eTest.clampToN(null, 5).isEmpty());
assertTrue(SaaFullGraphE2eTest.clampToN(new ArrayList<>(), 5).isEmpty());
}
// parseRate门槛解析null/非法0clamp [0,1]
@Test
@DisplayName("parseRatenull/空/非法 → 0.0(不设硬门,仅存在性)")
void parseRate_offCases() {
assertEquals(0.0, SaaFullGraphE2eTest.parseRate(null));
assertEquals(0.0, SaaFullGraphE2eTest.parseRate(""));
assertEquals(0.0, SaaFullGraphE2eTest.parseRate(" "));
assertEquals(0.0, SaaFullGraphE2eTest.parseRate("abc"), "非法值按不设门处理,不抛");
}
@Test
@DisplayName("parseRate合法值原样越界 clamp 到 [0,1];带空白可解析")
void parseRate_validAndClamp() {
assertEquals(0.8, SaaFullGraphE2eTest.parseRate("0.8"));
assertEquals(0.5, SaaFullGraphE2eTest.parseRate(" 0.5 "), "首尾空白应 trim");
assertEquals(1.0, SaaFullGraphE2eTest.parseRate("1.5"), "上界 clamp 到 1.0");
assertEquals(0.0, SaaFullGraphE2eTest.parseRate("-0.2"), "负值 clamp 到 0.0");
}
// passesGate成功率硬门
@Test
@DisplayName("passesGate达门含正好等于/ 不达门 / 无样本 / 门槛0")
void passesGate_cases() {
assertTrue(SaaFullGraphE2eTest.passesGate(8, 10, 0.8), "8/10=0.8 正好等于门槛 → 过(>=");
assertTrue(SaaFullGraphE2eTest.passesGate(9, 10, 0.8), "9/10 > 0.8 → 过");
assertFalse(SaaFullGraphE2eTest.passesGate(7, 10, 0.8), "7/10 < 0.8 → 不过");
assertTrue(SaaFullGraphE2eTest.passesGate(10, 10, 1.0), "满分对 100% 门 → 过");
assertFalse(SaaFullGraphE2eTest.passesGate(1, 0, 0.8), "total<=0 无样本 → 不过(防 0 除)");
assertTrue(SaaFullGraphE2eTest.passesGate(0, 10, 0.0), "门槛 0 → 恒过(存在性另管)");
}
// extractTotalTokens / extractModelTier成本会计抽取
@Test
@DisplayName("extractTotalTokenstokens.{prompt,completion} 求和Number/Long/数字串)")
void extractTotalTokens_sum() {
Map<String, Object> trace = new LinkedHashMap<>();
Map<String, Object> tokens = new LinkedHashMap<>();
tokens.put("prompt", 1200L); // dispatcher Long
tokens.put("completion", 800); // 兼容 Integer
trace.put("tokens", tokens);
assertEquals(2000L, SaaFullGraphE2eTest.extractTotalTokens(trace));
// 数字串容错
Map<String, Object> strTokens = new HashMap<>();
strTokens.put("prompt", "300");
strTokens.put("completion", "20");
Map<String, Object> trace2 = new HashMap<>();
trace2.put("tokens", strTokens);
assertEquals(320L, SaaFullGraphE2eTest.extractTotalTokens(trace2));
}
@Test
@DisplayName("extractTotalTokens缺 prompt → 仅 completiontrace=null / 无 tokens / tokens 非 Map → -1无数据")
void extractTotalTokens_missing() {
Map<String, Object> onlyCompletion = new HashMap<>();
onlyCompletion.put("completion", 50L);
Map<String, Object> trace = new HashMap<>();
trace.put("tokens", onlyCompletion);
assertEquals(50L, SaaFullGraphE2eTest.extractTotalTokens(trace), "缺 prompt → 按 0 + completion");
assertEquals(-1L, SaaFullGraphE2eTest.extractTotalTokens(null), "trace=null → -1");
assertEquals(-1L, SaaFullGraphE2eTest.extractTotalTokens(new HashMap<>()), "无 tokens 键 → -1");
Map<String, Object> badTokens = new HashMap<>();
badTokens.put("tokens", "not-a-map");
assertEquals(-1L, SaaFullGraphE2eTest.extractTotalTokens(badTokens), "tokens 非 Map → -1");
}
@Test
@DisplayName("extractModelTier有 stage2 → stage2缺 / trace=null → stage1未升档")
void extractModelTier_cases() {
Map<String, Object> stage2 = new HashMap<>();
stage2.put("modelTier", "stage2");
assertEquals("stage2", SaaFullGraphE2eTest.extractModelTier(stage2));
assertEquals("stage1", SaaFullGraphE2eTest.extractModelTier(new HashMap<>()), "缺 modelTier → stage1");
assertEquals("stage1", SaaFullGraphE2eTest.extractModelTier(null), "trace=null → stage1");
}
}

View File

@ -44,21 +44,30 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
* <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>brief 默认内置多品类10 {@code -Dsaa.e2e.briefFile=<路径>} 外部文件
* 行式去首尾空白跳空行与 {@code #} 注释行解析为空则回退内置 {@code -Dsaa.e2e.n=K} 取前 K
* 串行单条含修复轮约 5~15 分钟N=10 需约 50~150 分钟 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>
* <li><b>成功率门003-U1</b>成功率始终作证据打印 + <b>存在性下限</b>1 条跑通始终断言
* <b>flip 前置硬门</b>仅当显式 {@code -Dsaa.e2e.minSuccessRate=0.8} 设定时才硬断成功率门槛
* 默认不设=基线量测模式 003-U1先实测真基线再收敛不破普通构建</li>
* <li><b>成本会计003-U1·token 代理</b>汇总每条 prompt+completion token + stage2强档救场计数作成本证据
* {@code -Dsaa.e2e.maxTokensPerGame=N} 设上限时仅<b>软警告</b>不硬阻断遵创始人裁决质量优先允许临时破价记账对账
* <b></b>SAA 路只有 token 计量¥ follow-up F3{@code SaaGraphDispatcher} cost 整段省略§6.3 此处<b>不造假¥</b></li>
* </ol>
*
* <p><b>运行mini-desktopgame-runtime node+esbuild+Chrome端口 4320/9222 </b>
* <pre>
* // 基线量测仅打印成功率 + 存在性断言003-U1 第一步实测真基线
* NEWAPI_KEY=sk-... timeout 6000 mvn -pl ...aigc-server test \
* -Dtest=SaaFullGraphE2eTest -Dsaa.e2e=1 \
* -Dtest=SaaFullGraphE2eTest -Dsaa.e2e=1 -Dsaa.e2e.n=10 \
* -DGAME_RUNTIME_DIR=/root/game-staging/repo/game-runtime \
* -Dsurefire.failIfNoSpecifiedTests=false
* // flip 前置硬门成功率须80%否则测试 fail追加 -Dsaa.e2e.minSuccessRate=0.8
* </pre>
*
* @author 造梦AIU7 集成顶石
* @author 造梦AIU7 集成顶石003-U1 验收门工具化
*/
class SaaFullGraphE2eTest {
@ -77,15 +86,23 @@ class SaaFullGraphE2eTest {
Long.getLong("saa.e2e.perBriefSec", 900L);
/**
* N=5 条多样一句话 brief覆盖不同玩法品类避免单品类偶然过/不过 {@code -Dsaa.e2e.n=K} 取前 K
* 全部一次性投递后台真玩门 Semaphore(1) 串行逐条跑
* 内置 12 条多样一句话 brief覆盖不同玩法品类避免单品类偶然过/不过003-U1 扩到 10 以作产线门量测
* 偏向运行时/harness 支持的品类realtime/physics/tap-targets/clickable {@code -Dsaa.e2e.n=K} 取前 K
* {@code -Dsaa.e2e.briefFile=<路径>} 整组改用外部文件全部一次性投递后台真玩门 Semaphore(1) 串行逐条跑
*/
private static final List<String> ALL_BRIEFS = List.of(
"打砖块:挡板接球把上方砖块全部消除即胜,掉球则负。",
"贪吃蛇:操控小蛇吃食物变长,撞到自己身体或墙壁则游戏结束。",
"接金币:左右移动篮子接住从天上掉落的金币,漏接太多就失败。",
"躲障碍:角色不断前进,点击跳跃躲开迎面而来的障碍物,撞到就结束。",
"点击消除:点击三个或以上相连的同色方块把它们消掉,消满目标分数过关。"
"点击消除:点击三个或以上相连的同色方块把它们消掉,消满目标分数过关。",
"太空射击:飞船左右移动并发射子弹击落持续下降的敌机,被敌机撞到则失败。",
"打地鼠:地鼠随机从洞口冒头,点击敲中得分,限时结束时分数越高越好。",
"节奏敲击:方块下落到底部判定线的瞬间点击得分,连续错过三次则结束。",
"平台跳跃:左右移动并跳跃,踩着悬空平台抵达终点旗帜,掉出屏幕底部则失败。",
"太空躲陨石:上下左右移动飞船躲避飞来的陨石,坚持到计时归零即通关。",
"弹力打靶:点击屏幕给小球施加方向,把场上所有靶子撞掉即过关。",
"泡泡射手:瞄准并发射泡泡,三个同色相连则消除,清空全部泡泡即胜。"
);
/** 单条 brief 的 e2e 结果证据(回调按 traceId 精确归位)。 */
@ -99,6 +116,8 @@ class SaaFullGraphE2eTest {
volatile boolean dispatched = false; // dispatch 投递握手是否 true
volatile long callbackAtMs = 0; // 回调命中时刻相对 run 起点推单条墙钟用
volatile String failDetail = ""; // 失败时从 verdict.json/giveup-dump.json 摘的真因
volatile long totalTokens = -1; // 成本会计prompt+completion token成本代理-1=回调未带 trace token
volatile String modelTier = "stage1"; // 终态模型档stage1/stage2stage2=救场升强档成本关注质量-成本权衡
BriefResult(int idx, String brief, String traceId) {
this.idx = idx;
@ -136,9 +155,18 @@ class SaaFullGraphE2eTest {
r.engineBundleLen = reqVO.getEngineBundle() == null ? 0 : reqVO.getEngineBundle().length();
r.failureReason = reqVO.getFailureReason() == null ? "" : reqVO.getFailureReason();
r.callbackAtMs = System.currentTimeMillis() - runStartMs;
// 成本会计003-U1 trace best-effort token 总数 + 模型档/异常保持默认 -1/stage1不阻断归位
Object traceObj = reqVO.getTrace();
if (traceObj instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> tr = (Map<String, Object>) traceObj;
r.totalTokens = extractTotalTokens(tr);
r.modelTier = extractModelTier(tr);
}
System.out.println("[callback] #" + r.idx + " traceId=" + traceId
+ " status=" + r.status + " engineBundleLen=" + r.engineBundleLen
+ " reason=" + r.failureReason + " @+" + (r.callbackAtMs / 1000) + "s");
+ " reason=" + r.failureReason + " tokens=" + r.totalTokens + " tier=" + r.modelTier
+ " @+" + (r.callbackAtMs / 1000) + "s");
} else {
// 不应发生traceId 必在 map 留痕仍倒数避免 latch 永挂
System.out.println("[callback] 未知 traceId=" + traceId + "(不在 map 内,忽略归位仍倒数)");
@ -163,10 +191,12 @@ class SaaFullGraphE2eTest {
assertTrue(Files.exists(runtimeRoot.resolve("node_modules/esbuild")),
"缺 node_modules/esbuildbuild 节点依赖),请在 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);
// brief 003-U1-Dsaa.e2e.briefFile 给定且可读外部文件行式否则内置 12
// 再按 -Dsaa.e2e.n=K 取前 K 未设=全量串行耗时大调小便于在短 mvn timeout 内跑通若干条
List<String> sourceBriefs = resolveSourceBriefs();
List<String> briefs = clampToN(sourceBriefs, Integer.getInteger("saa.e2e.n"));
int n = briefs.size();
System.out.println("[briefs] 本次投递 n=" + n + " 条(源 " + sourceBriefs.size() + " 条)");
// 1) 构造真 AigcExecutorProperties@Data setter 全有
// checkpoint + DataSource null DB 依赖dispatcher=saa 走进程内全图派发
@ -250,8 +280,10 @@ class SaaFullGraphE2eTest {
System.out.println("------------------------------------------------------");
for (BriefResult r : results) {
System.out.println(String.format(
"#%d status=%-26s bundleLen=%-7d callback@+%-5s reason=%s",
"#%d status=%-26s bundleLen=%-7d tokens=%-7s tier=%-6s callback@+%-5s reason=%s",
r.idx, r.status, r.engineBundleLen,
(r.totalTokens >= 0 ? String.valueOf(r.totalTokens) : "-"),
r.modelTier,
(r.callbackAtMs > 0 ? (r.callbackAtMs / 1000) + "s" : "-"),
r.succeeded() ? "-" : (r.failureReason.isEmpty() ? "(无)" : r.failureReason)));
System.out.println(" brief: " + r.brief);
@ -259,12 +291,56 @@ class SaaFullGraphE2eTest {
System.out.println(" fail : " + r.failDetail);
}
}
// 成本会计003-U1·token 代理¥¥=follow-up F3SAA cost 省略
long totalTok = 0;
int tokCounted = 0;
int stage2Count = 0;
for (BriefResult r : results) {
if (r.totalTokens >= 0) {
totalTok += r.totalTokens;
tokCounted++;
}
if ("stage2".equals(r.modelTier)) {
stage2Count++;
}
}
System.out.println("------------------------------------------------------");
System.out.println("成本会计token 代理,非¥;折¥=follow-up F3");
System.out.println(" 总 token = " + totalTok + "(有数据 " + tokCounted + "/" + n + " 条)"
+ (tokCounted > 0 ? "" + (totalTok / tokCounted) + " token/款" : ""));
System.out.println(" 升 stage2强档救场= " + stage2Count + "/" + n + ""
+ (stage2Count > 0 ? "(强档↑成本,关注质量-成本权衡 KTD5" : ""));
// -Dsaa.e2e.maxTokensPerGame=N仅软警告超预算条目不硬阻断遵创始人质量优先允许临时破价记账对账
Long maxTok = Long.getLong("saa.e2e.maxTokensPerGame");
if (maxTok != null) {
for (BriefResult r : results) {
if (r.totalTokens > maxTok) {
System.out.println(" [warn] #" + r.idx + " token " + r.totalTokens
+ " 超预算 " + maxTok + "(软警告,不阻断——质量优先允许临时破价,见 KTD5");
}
}
}
System.out.println("######################################################");
// 7) 存在性断言首证至少 1 条真全图 e2e 跑通成功率作证据不硬断 80%留人判
// 7) 断言003-U1 验收门
// 7a) 存在性下限首证 / 基线量测模式至少 1 条跑通始终生效
assertTrue(succeeded >= 1,
"至少 1 条 SAA 真全图 e2e 应跑通(实得 " + succeeded + "/" + briefs.size()
"至少 1 条 SAA 真全图 e2e 应跑通(实得 " + succeeded + "/" + n
+ ");若全 0检查 new-api 可达/九门 harness/端口 4320-9222 占用/图终态——见上方逐条 failDetail");
// 7b) flip 前置硬门仅当显式 -Dsaa.e2e.minSuccessRate 设定时才硬断成功率门槛默认不设=仅存在性先实测真基线
double minSuccessRate = parseRate(System.getProperty("saa.e2e.minSuccessRate"));
if (minSuccessRate > 0.0) {
assertTrue(passesGate((int) succeeded, n, minSuccessRate),
String.format("成功率 %.1f%%%d/%d< flip 前置门槛 %.1f%% —— 硬门未过不可切默认产线003-U2"
+ "据根因分析按主杠杆(升模型档 + ReAct继续收敛",
100.0 * succeeded / n, succeeded, n, minSuccessRate * 100));
System.out.println("[gate] flip 前置硬门通过:成功率 "
+ String.format("%.1f%%", 100.0 * succeeded / n) + " ≥ 门槛 "
+ String.format("%.1f%%", minSuccessRate * 100));
} else {
System.out.println("[gate] 未设 -Dsaa.e2e.minSuccessRate仅存在性断言基线量测模式"
+ "flip 前置硬门请加 -Dsaa.e2e.minSuccessRate=0.8");
}
}
/**
@ -319,4 +395,160 @@ class SaaFullGraphE2eTest {
return "(读 failDetail 异常:" + e.getMessage() + ")";
}
}
// 003-U1 验收门工具化纯逻辑静态方法模型无关可单测 SaaFullGraphE2eGateLogicTest
/**
* 决定本次 brief 列表来源{@code -Dsaa.e2e.briefFile} 给定且可读 外部文件 {@link #parseBriefLines} 解析
* 否则用内置 {@link #ALL_BRIEFS}外部文件解析为空或读取失败 warn + 回退内置不抛不致测试无 brief 可投
*
* @return brief 列表外部文件 or 内置恒非空
*/
private static List<String> resolveSourceBriefs() {
String briefFile = System.getProperty("saa.e2e.briefFile");
if (briefFile != null && !briefFile.isBlank()) {
Path p = Paths.get(briefFile.trim());
try {
List<String> parsed = parseBriefLines(Files.readAllLines(p));
if (!parsed.isEmpty()) {
System.out.println("[briefs] 外部文件 " + p + " 载入 " + parsed.size() + "");
return parsed;
}
System.out.println("[briefs][warn] 外部文件 " + p + " 解析后为空,回退内置 " + ALL_BRIEFS.size() + "");
} catch (Exception e) {
System.out.println("[briefs][warn] 读外部文件失败(" + e.getMessage() + "),回退内置 " + ALL_BRIEFS.size() + "");
}
}
return ALL_BRIEFS;
}
/**
* 解析外部 brief 文件行纯函数逐行去首尾空白跳过空行与 {@code #} 注释行其余原样保留
*
* @param rawLines 原始行null 视作空
* @return 清洗后的 brief 列表恒非空对象可能 0 元素
*/
static List<String> parseBriefLines(List<String> rawLines) {
List<String> out = new ArrayList<>();
if (rawLines == null) {
return out;
}
for (String line : rawLines) {
if (line == null) {
continue;
}
String t = line.trim();
if (t.isEmpty() || t.startsWith("#")) {
continue; // 跳空行 / # 注释行
}
out.add(t);
}
return out;
}
/**
* 取前 N 纯函数{@code nOrNull} null 全量否则 clamp {@code [1, size]}空列表安全返回
*
* @param briefs 源列表
* @param nOrNull -Dsaa.e2e.n未设为 null
* @return N 条子列表
*/
static List<String> clampToN(List<String> briefs, Integer nOrNull) {
if (briefs == null) {
return new ArrayList<>();
}
if (briefs.isEmpty()) {
return briefs;
}
int n = (nOrNull == null) ? briefs.size() : nOrNull;
n = Math.max(1, Math.min(n, briefs.size()));
return briefs.subList(0, n);
}
/**
* 解析成功率门槛纯函数null//非法 0.0=不设硬门仅存在性断言否则 clamp {@code [0,1]}
*
* @param raw -Dsaa.e2e.minSuccessRate 原始串
* @return 门槛 [0,1]
*/
static double parseRate(String raw) {
if (raw == null || raw.isBlank()) {
return 0.0;
}
try {
double v = Double.parseDouble(raw.trim());
if (v < 0) {
return 0.0;
}
return Math.min(v, 1.0);
} catch (NumberFormatException e) {
return 0.0; // 非法值按不设门处理基线量测模式不致测试构造期抛
}
}
/**
* 成功率硬门判定纯函数{@code total<=0} false无样本不算过门否则 {@code succeeded/total >= minRate}
*
* @param succeeded 跑通条数
* @param total 总投递条数
* @param minRate 门槛 [0,1]
* @return 是否达门
*/
static boolean passesGate(int succeeded, int total, double minRate) {
if (total <= 0) {
return false;
}
return ((double) succeeded / total) >= minRate;
}
/**
* 从回调 {@code trace} map token 总数纯函数{@code trace.tokens.{prompt,completion}} 之和
* SAA 路恒落 tokens缺省 0trace null / tokens 子对象 {@code -1}表示无数据区别于真 0
*
* @param trace 回调 trace{@link DifyCallbackReqVO#getTrace()}
* @return prompt+completion token 总数无数据返 -1
*/
static long extractTotalTokens(Map<String, Object> trace) {
if (trace == null) {
return -1;
}
Object tk = trace.get("tokens");
if (!(tk instanceof Map)) {
return -1;
}
Map<?, ?> tokens = (Map<?, ?>) tk;
return asLong(tokens.get("prompt")) + asLong(tokens.get("completion"));
}
/**
* 从回调 {@code trace} map 抽模型档纯函数{@code trace.modelTier}"stage1"/"stage2"
* dispatcher 真升档态才落 modelTier trace=null stage1未升档
*
* @param trace 回调 trace
* @return "stage1" "stage2"
*/
static String extractModelTier(Map<String, Object> trace) {
if (trace == null) {
return "stage1";
}
Object mt = trace.get("modelTier");
return mt == null ? "stage1" : String.valueOf(mt);
}
/**
* 宽松转 longtrace token 取值专用Number longValue数字串 parse其余/null 0
*/
private static long asLong(Object o) {
if (o instanceof Number) {
return ((Number) o).longValue();
}
if (o instanceof String) {
try {
return Long.parseLong(((String) o).trim());
} catch (NumberFormatException e) {
return 0;
}
}
return 0;
}
}