feat(saa): U3 gamedef 量测路 + 有界并发池 harness(默认 K=1 字节零变)

003-U1 U3「量对路 + 有界并发(量测用)」:
- AigcExecutorProperties: saaConcurrency(默认 1)/saaPlayPortBase(4320)/saaCdpPortBase(9222) — 默认全为生产字节零变值
- SaaGraphDispatcher: newSingleThreadExecutor→newFixedThreadPool(K) + Semaphore(1)→Semaphore(K) + K 个错开端口槽池(第 i 槽 playPort=base+2i/cdpPort=base+i);runGraphAndCallback 起手 take 槽 + acquire 许可、整 job 持槽、finally 必归还防泄漏;端口覆盖放 runGraphAndCallback(保 buildInputs 签名不变,反射测兼容);构造期 null-properties 兜底(K=1+默认端口,修 TraceTest NPE);allocPortSlots/normalizeConcurrency 抽为可单测纯函数 + 共享面审计结论入注释
- SaaFullGraphE2eTest: 加 -Dsaa.e2e.sourceMode(默认 factory,量 gamedef 须显式设=修发现②量错路) + -Dsaa.e2e.concurrency(默认 1)
- 新增 saa-e2e-briefs-gamedef.txt(34 条多品类≥30) + saa-e2e-briefs-holdout.txt(12 条留出集,与调优集及内置 ALL_BRIEFS 零重叠)

量对路(发现②修复):旧 e2e 只 setDispatcher("saa") 未 setSaaSourceMode → props 默认 factory(iife),量的是旧路;现 setSaaSourceMode(sourceMode),量 gamedef 真基线须 -Dsaa.e2e.sourceMode=gamedef。
K=1 字节零变:newFixedThreadPool(1)≡单线程串行 + Semaphore(1) + 仅槽 0=[4320,9222],与改造前逐字一致。共享面审计(只读 serve-and-play.sh,脚本未改):K>1 下端口为唯一真冲突点,净场 lsof|kill 只杀本槽端口、static-serve 只读共享根、evidence 按 gameId 隔离 → 跨 job 安全。
并发口径(plan 2026-06-19 创始人指令对齐):K 是量测用「真玩并发」上限,受 Mac 实时九门帧预算硬限(C_frame/E_live,~3-4),非 M3 API 限;生成端(网络限)的宽并发(gen≫play 时)留 U5 量 gen:play 占比后按需解耦,不在本单元。
验证:编译绿;GateLogic+TraceTest+NodesRegression+PickAnswer 指定集独立复跑 exit 0、零失败标记;agent -am 全 SAA 包 101/0/0/3skip。真跑 K 路校准(verdict 对等+结构隔离)延 U5 执行期。
默认 K=1/sourceMode=factory 现行字节零变;不碰 gd-runtime/build-from-source(Plan B 域)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
lili 2026-06-19 08:48:53 -07:00
parent 08aa0c13d0
commit 12abfbac4a
6 changed files with 352 additions and 25 deletions

View File

@ -28,8 +28,10 @@ import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.Semaphore;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
@ -51,9 +53,13 @@ import java.util.concurrent.atomic.AtomicInteger;
* 生成是数十秒级多步活不在 tick 线程内长挂任务留 RUNNING 等回调命中 deadline watchdog 收尸
* 投递前置失败图未就绪/线程池拒绝/game-runtime 不存在返回 false调用方按 failed(llm_error) 落终态
*
* <p><b>串行真玩门Semaphore(1)</b>play 节点用固定端口4320/9222+ headless Chrome job 并行真玩必撞端口
* 故图执行经一把全局 {@link Semaphore}(1) 串行化acquire跑图release Python worker {@code service.py:80}
* 串行锁同语义serve 4320/CDP 9222 不可并发后台线程池亦设单线程双保险
* <p><b>有界并发真玩门003-U1信号量(K) + 端口槽池</b>play 节点用真玩端口 + headless Chrome job
* <b>共用同一对</b>端口必撞故每 job 整生命期独占一对错开端口 i serve={@code base+2i}/CDP={@code base+i}
* 并发上限 K={@link AigcExecutorProperties#getSaaConcurrency()}线程池 size=K + {@link Semaphore}(K) + K 个端口槽三者同界
* <b>默认 K=1 = 生产字节零变</b>单线程池 + Semaphore(1) + 仅槽 0=[4320,9222]与改造前 {@code newSingleThreadExecutor +
* Semaphore(1) + buildInputs 常量 4320/9222} 逐字节等价 Python worker {@code service.py:80} 串行锁同语义
* K&gt;1 时各 job 端口对两两不撞serve-and-play.sh 净场只杀本槽两端口static-serve 只读共享根evidence gameId
* 隔离 job 安全共享面审计见 {@link #allocPortSlots} 注释
*
* <p><b>事务边界</b>图执行<b>不包大 {@code @Transactional}</b>数十秒级跑图占长事务=连接池杀手落库由
* {@code handleCallback DifyCallbackTxService} 内部独立短事务完成既有三表同事务写链不变
@ -95,22 +101,33 @@ public class SaaGraphDispatcher implements GenerationDispatcher {
private final SourceProjectApi sourceProjectApi;
/**
* 后台单线程池跑图阻塞活禁占 tick/Tomcat 线程daemon 线程命名便于排障
* 单线程 = 串行跑图 Semaphore 双保险单线程已天然串行Semaphore 未来若改多线程语义不破
* 后台跑图线程池跑图阻塞活禁占 tick/Tomcat 线程daemon 线程命名便于排障
* <b>有界 size=K</b>{@link #concurrency}003-U1K=1 时是单线程池与改造前 {@code newSingleThreadExecutor}
* 逐字节等价天然串行K&gt;1 K 路并行跑图并发上限由本池 + {@link #playLock}(K) + {@link #portSlots} 三者共同界定
* 构造期据 {@code properties.saaConcurrency} size见构造函数故非 inline 初始化
*/
private final ExecutorService graphExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() {
private final AtomicInteger seq = new AtomicInteger();
private final ExecutorService graphExecutor;
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, "saa-graph-dispatcher-" + seq.incrementAndGet());
t.setDaemon(true); // 守护线程进程退出不被它阻塞在飞 job watchdog 兜超时
return t;
}
});
/**
* 真玩门信号量play 用真玩端口 job 并行真玩须各占独立端口对故并发受限
* <b>容量=K</b>{@link #concurrency}003-U1K=1 时等价改造前 {@code Semaphore(1)}严格串行
* K&gt;1 时放行 K 路并发与端口槽 {@link #portSlots} K 个错开端口对一一配对每路 job 整生命期持一槽
* 杜绝撞端口线程池已隐含同样上限信号量为显式语义双保险与改造前单线程 + Semaphore 双保险同范式
*/
private final Semaphore playLock;
/** 真玩门串行锁play 用固定端口 4320/9222多 job 并行真玩必撞,故全局串行)。 */
private final Semaphore playLock = new Semaphore(1);
/**
* 端口槽池003-U1有界并发的核心{@code BlockingQueue<int[2]>} K 个错开端口对 {@code [playPort, cdpPort]}
* i playPort={@code saaPlayPortBase+2i} / cdpPort={@code saaCdpPortBase+i}{@link #allocPortSlots}
* <p><b> job 生命期持槽语义</b>{@code runGraphAndCallback} 起手 {@code take()} 拿一空闲槽无则阻塞与信号量同节流
* 用该槽端口覆盖 {@code inputs} port/cdpPort 后跑图<b>finally release 把同一槽 {@code put} 回池</b>
* 即便跑图超时/异常也归还防端口泄漏致后续 job 无槽永挂<b>K=1 时只有槽 0 = {@code [4320,9222]}</b>
* 覆盖值与改造前 buildInputs 常量逐字一致 字节零变
*/
private final BlockingQueue<int[]> portSlots;
/** 后台并发度 K{@code max(1, saaConcurrency)};线程池 size / 信号量容量 / 端口槽数三者同此值)。 */
private final int concurrency;
/**
* trace 抽取专用 mapperW-G1 组B · 闭合 SAA split-braindispatcher 原无 ObjectMapper{@code SaaStudioNodes.MAPPER}
@ -156,6 +173,32 @@ public class SaaGraphDispatcher implements GenerationDispatcher {
this.dataSource = dataSource;
this.observationRegistry = observationRegistry;
this.sourceProjectApi = sourceProjectApi;
// 003-U1 有界并发池 saaConcurrency K默认 1=生产字节零变三件套线程池/信号量/端口槽同此值
// null-properties 容错 trace 抽取单测SaaGraphDispatcherTraceTest 4-null 依赖构造本类抽取方法不碰 properties
// 改造前字段为 inline 初始化不触 properties故此处亦须对 properties==null 兜底K=1 + 默认端口基址保该单测路不破
Integer rawK = (properties == null) ? null : properties.getSaaConcurrency();
Integer playBase = (properties == null) ? null : properties.getSaaPlayPortBase();
Integer cdpBase = (properties == null) ? null : properties.getSaaCdpPortBase();
this.concurrency = normalizeConcurrency(rawK);
// 后台线程池K=1 newFixedThreadPool(1) 与改造前 newSingleThreadExecutor 行为等价 worker 串行无界队列K>1 K 路并行
this.graphExecutor = Executors.newFixedThreadPool(concurrency, new ThreadFactory() {
private final AtomicInteger seq = new AtomicInteger();
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, "saa-graph-dispatcher-" + seq.incrementAndGet());
t.setDaemon(true); // 守护线程进程退出不被它阻塞在飞 job watchdog 兜超时
return t;
}
});
// 真玩门信号量容量=KK=1 等价改造前 Semaphore(1)
this.playLock = new Semaphore(concurrency);
// 端口槽池K 个错开端口对入队 0 = [saaPlayPortBase, saaCdpPortBase] = 默认 [4320,9222]K=1 时字节零变allocPortSlots null 基址兜底 4320/9222
this.portSlots = new LinkedBlockingQueue<>();
for (int[] slot : allocPortSlots(playBase, cdpBase, concurrency)) {
this.portSlots.add(slot);
}
}
/**
@ -241,11 +284,12 @@ public class SaaGraphDispatcher implements GenerationDispatcher {
return false;
}
// 提交后台单线程跑图投递握手即返回图执行+回调全在后台线程tick 线程不长挂
// 提交后台跑图投递握手即返回图执行+回调全在后台线程tick 线程不长挂
// 后台池 size=KK=1 等价改造前单线程并发上限由池 + Semaphore(K) + 端口槽共同界定
try {
graphExecutor.submit(() -> runGraphAndCallback(graph, inputs, job, traceId));
} catch (Exception e) {
// 线程池拒绝理论不可达单线程无界队列投递失败留痕
// 线程池拒绝理论不可达固定池无界队列投递失败留痕
log.error("[saa-dispatch] 图执行提交后台线程失败,投递失败 traceId={}", traceId, e);
return false;
}
@ -257,17 +301,30 @@ public class SaaGraphDispatcher implements GenerationDispatcher {
// ============================== 私有图执行 + 进程内回调 ==============================
/**
* 后台线程体串行跑图Semaphore 守真玩端口 result state ReqVO 进程内调 handleCallback
* 任何异常都兜成 failed(llm_error) 回调绝不静默吞否则任务卡 RUNNING 直到 watchdog 超时收尸体验差
* 后台线程体取信号量许可 + 端口槽K=1 严格串行/K&gt;1 有界并发 job 独占端口对 跑图 result state ReqVO
* 进程内调 handleCallback任何异常都兜成 failed(llm_error) 回调绝不静默吞否则任务卡 RUNNING 直到 watchdog 超时收尸体验差
* finally 必归还端口槽 + 释放许可即便超时/异常防端口泄漏致后续 job 无槽永挂
*/
private void runGraphAndCallback(CompiledGraph graph, Map<String, Object> inputs,
Map<String, Object> job, String traceId) {
boolean acquired = false;
int[] slot = null; // job 占用的端口槽 [playPort, cdpPort]finally 必归还防泄漏
try {
// 串行真玩门阻塞获取单线程池下恒能拿到双保险防未来多线程改动
// 真玩门信号量阻塞获取K=1 下恒能拿到=严格串行K>1 放行 K 路并发与端口槽数一一对应
playLock.acquire();
acquired = true;
// 003-U1取一空闲端口槽无则阻塞与信号量同节流用该槽端口覆盖 inputs port/cdpPort
// per-job 端口隔离 job 整生命期独占一对端口serve 4320+2i / CDP 9222+i跑完 finally 归还
// K=1 时只有槽 0=[saaPlayPortBase, saaCdpPortBase]=默认[4320,9222]覆盖值与 buildInputs 常量逐字一致 字节零变
slot = portSlots.take();
inputs.put("port", slot[0]);
inputs.put("cdpPort", slot[1]);
if (concurrency > 1) {
// K>1 并发态打点K=1 不打保日志字节零变便于排障 job 用哪端口
log.info("[saa-dispatch] 取端口槽 playPort={}, cdpPort={}K={} 并发traceId={}", slot[0], slot[1], concurrency, traceId);
}
// threadId(traceId) 跑图checkpoint 开启时图已挂 MysqlSaver+releaseThread(false)崩溃后同
// traceId 二次 invoke 即从最后已落 checkpoint 的节点续跑step5 已接watchdog 兜超时checkpoint 兜续跑
RunnableConfig runConfig = RunnableConfig.builder().threadId(traceId).build();
@ -292,6 +349,11 @@ public class SaaGraphDispatcher implements GenerationDispatcher {
log.error("[saa-dispatch] 图执行异常,兜 failed(llm_error) 回调 traceId={}", traceId, e);
safeFailedCallback(job, traceId, "图执行异常:" + trunc(String.valueOf(e.getMessage()), 200));
} finally {
// 先归还端口槽即便超时/异常也归还防端口泄漏致后续 job 无槽永挂再释放信号量槽与permit成对不变量
// put 回的是 take 出的同一 int[]同一槽K=1 时即把槽 0 放回下个 job 再取仍是 [4320,9222]字节零变
if (slot != null) {
portSlots.add(slot); // 有界队列容量足够 K 个槽add 不会阻塞/拒绝
}
if (acquired) {
playLock.release();
}
@ -912,4 +974,60 @@ public class SaaGraphDispatcher implements GenerationDispatcher {
}
return s.length() > n ? s.substring(0, n) : s;
}
// ============================== 003-U1 有界并发端口分配纯逻辑模型无关可单测 ==============================
/**
* 规整并发度 K纯函数{@code null} {@code <1} 1退回单飞误配不崩不破生产字节零变否则原值
*
* @param raw {@link AigcExecutorProperties#getSaaConcurrency()}可能 null/非法
* @return K {@code >=1}
*/
static int normalizeConcurrency(Integer raw) {
if (raw == null || raw < 1) {
return 1;
}
return raw;
}
/**
* 端口槽分配纯函数003-U1 有界并发核心 K 路并发各算一对<b>错开</b>端口 {@code [playPort, cdpPort]}
* <p> i {@code 0..k-1}{@code playPort = httpBase + 2*i}步长 2 留头寸{@code cdpPort = cdpBase + i}步长 1
* <b> 0 = {@code [httpBase, cdpBase]}</b>默认 {@code [4320, 9222]} K=1 时与改造前 buildInputs 常量逐字一致字节零变
*
* <p><b>不撞端口的依据</b>{@code SaaFullGraphE2eGateLogicTest} 单测断言playPort {@code {httpBase, httpBase+2, ...}}
* cdpPort {@code {cdpBase, cdpBase+1, ...}} 默认区间天然不交叠4320 vs 9222 相距数千族内步长保互异
* K 2K 个端口两两不同默认值下成立若误配 base 致两族交叠属部署配置错超本单元 U5 真跑校准兜
*
* <p><b>K&gt;1 共享面安全审计只读核 {@code _shared/serve-and-play.sh} + {@code static-serve.cjs} + {@code play.cdp.cjs}
* 脚本禁改铁律下仅审计</b>结论同一对端口K 路并发的其余共享资源均隔离或读共享安全
* <ol>
* <li><b>端口唯一真冲突点</b> job 持独立端口对整生命期独占{@link #portSlots}serve-and-play.sh 净场
* {@code lsof -ti tcp:$PORT|kill} <b>只杀传入的本槽两端口</b>非全局 kill故跨 job 不误杀 端口隔离即足</li>
* <li><b>服务根 cwd共享只读</b>N {@code static-serve.cjs} {@code listen} 自己端口serve 同一 game-runtime
* 根但<b>纯只读</b> {@code fs.readFile} {@code ..} 穿越无写 并发只读同一根安全</li>
* <li><b>{@code _shared/} 脚本共享只读</b>serve-and-play.sh / play.cdp.cjs / static-serve.cjs 是共读脚本文件
* 运行期不自改 安全</li>
* <li><b>{@code /tmp/wg1-*-$PORT.log}按端口键隔离</b>serve/chrome 日志名含 {@code $PORT}端口对既隔离则日志路径天然不撞 安全</li>
* <li><b>Chrome user-data-dir按调用隔离</b>serve-and-play.sh {@code mktemp -d -t wg1-chrome-XXXX} 每次唯一临时目录 安全</li>
* <li><b>evidence 产物 gameId 隔离</b>play.cdp.cjs 只写 {@code games/_wg1-gen/<gameId>/evidence/}gameId={@code saa-<traceId>} job 唯一 写面不交叠</li>
* </ol>
* <b>遗留U5 真跑校准项</b>上述为静态审计结论K <b>真并发</b>verdict 对等K=1 vs K=N brief 判定一致+同款×N 结构隔离
* 须九门环境实跑验证 003-U5 执行期
*
* @param httpBase playPort 基址{@link AigcExecutorProperties#getSaaPlayPortBase()}默认 4320null 兜底 4320
* @param cdpBase cdpPort 基址{@link AigcExecutorProperties#getSaaCdpPortBase()}默认 9222null 兜底 9222
* @param k 并发度 {@code >=1} {@link #normalizeConcurrency} 保证
* @return K 个端口对 {@code [playPort, cdpPort]}按槽序 0 在首
*/
static java.util.List<int[]> allocPortSlots(Integer httpBase, Integer cdpBase, int k) {
int hb = (httpBase == null) ? 4320 : httpBase;
int cb = (cdpBase == null) ? 9222 : cdpBase;
int n = Math.max(1, k);
java.util.List<int[]> slots = new java.util.ArrayList<>(n);
for (int i = 0; i < n; i++) {
slots.add(new int[]{hb + 2 * i, cb + i});
}
return slots;
}
}

View File

@ -197,6 +197,31 @@ public class AigcExecutorProperties {
*/
private String saaSourceMode = "factory";
/**
* SAA 图后台执行并发度 Kplan 2026-06-19 003-U1 有界并发池P0-3/P1-5进程内后台跑图的<b>并行 job 上限</b>
* <b>默认 1 = 生产字节零变</b>单线程池 + Semaphore(1) + 端口槽 0 端口恒为 saaPlayPortBase/saaCdpPortBase
* 与改造前 {@code newSingleThreadExecutor + Semaphore(1) + 常量 4320/9222} 行为逐字节等价回归红线
* <p>K&gt;1 固定线程池 size=K + Semaphore(K) + K 个错开端口槽每槽 {@code playPort=base+2i / cdpPort=base+i}
* job 并行真玩各占独立端口对serve-and-play.sh 净场只杀本槽两端口static-serve 只读共享 game-runtime
* evidence gameId 隔离 job 安全详见 {@code SaaGraphDispatcher} 共享面审计注释
* <p><b> dispatcher=saa 生效</b>&lt;1 的误配按 1 兜底不崩退回单飞真跑 K 路并发校准verdict 对等 + 结构隔离
* 需九门环境 003-U5 执行期本字段先就位 + 纯端口分配逻辑可单测
*/
private Integer saaConcurrency = 1;
/**
* SAA 真玩 HTTP 端口基址端口池槽 0 playPortplan 003-U1 i playPort = {@code base + 2*i}步长 2 留头寸
* 默认 4320与改造前 buildInputs 常量serve-and-play.sh 默认逐字一致 K=1 字节零变内网地址按项目规则可入库
*/
private Integer saaPlayPortBase = 4320;
/**
* SAA 真玩 CDP 端口基址端口池槽 0 cdpPortplan 003-U1 i cdpPort = {@code base + i}步长 1
* 默认 9222与改造前 buildInputs 常量serve-and-play.sh 默认逐字一致 K=1 字节零变
* <p>两族端口区间天然不交叠4320 vs 9222 族内步长保互异 K 槽端口对两两不撞{@code SaaGraphDispatcher.allocPortSlots} 单测守
*/
private Integer saaCdpPortBase = 9222;
// ===== SAA 模型协议Plan A · U1用对 M3flag 旁挂默认 openai 字节零变=====
/**

View File

@ -1,14 +1,17 @@
package com.wanxiang.huijing.game.module.aigc.saa;
import com.wanxiang.huijing.game.module.aigc.service.executor.AigcExecutorProperties;
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.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@ -168,4 +171,79 @@ class SaaFullGraphE2eGateLogicTest {
assertEquals("stage1", SaaFullGraphE2eTest.extractModelTier(new HashMap<>()), "缺 modelTier → stage1");
assertEquals("stage1", SaaFullGraphE2eTest.extractModelTier(null), "trace=null → stage1");
}
// 003-U1 有界并发池端口分配 + 并发规整纯逻辑模型无关
// saaConcurrency 默认值回归红线默认 1 = 生产字节零变前提
@Test
@DisplayName("saaConcurrency 默认 = 1K=1 字节零变回归红线:默认即单飞,不改生产行为)")
void saaConcurrency_defaultsToOne() {
AigcExecutorProperties props = new AigcExecutorProperties();
assertEquals(1, props.getSaaConcurrency(), "saaConcurrency 默认必须是 1默认并发=1 才保 K=1 字节零变)");
// 端口基址默认亦须与改造前 buildInputs 常量一致4320/9222否则槽 0 端口漂移破字节零变
assertEquals(4320, props.getSaaPlayPortBase(), "saaPlayPortBase 默认必须 4320= 改造前 play 端口常量)");
assertEquals(9222, props.getSaaCdpPortBase(), "saaCdpPortBase 默认必须 9222= 改造前 cdp 端口常量)");
}
// normalizeConcurrencyK 规整null/<1 1
@Test
@DisplayName("normalizeConcurrencynull/0/负 → 1误配退回单飞不崩、不破字节零变正值原样")
void normalizeConcurrency_cases() {
assertEquals(1, SaaGraphDispatcher.normalizeConcurrency(null), "null → 1");
assertEquals(1, SaaGraphDispatcher.normalizeConcurrency(0), "0 → 1");
assertEquals(1, SaaGraphDispatcher.normalizeConcurrency(-3), "负 → 1");
assertEquals(1, SaaGraphDispatcher.normalizeConcurrency(1), "1 → 1");
assertEquals(4, SaaGraphDispatcher.normalizeConcurrency(4), "4 → 4正值原样");
}
// allocPortSlots端口槽分配 0 字节零变 + 步长 + 不撞 + null 兜底
@Test
@DisplayName("allocPortSlotsK=1 槽 0 = [4320,9222](字节零变铁证:与改造前 buildInputs 常量逐字一致)")
void allocPortSlots_k1_byteIdentical() {
List<int[]> slots = SaaGraphDispatcher.allocPortSlots(4320, 9222, 1);
assertEquals(1, slots.size(), "K=1 只有一个槽");
assertEquals(4320, slots.get(0)[0], "K=1 playPort 必须恒为 4320字节零变");
assertEquals(9222, slots.get(0)[1], "K=1 cdpPort 必须恒为 9222字节零变");
}
@Test
@DisplayName("allocPortSlots第 i 槽 playPort=base+2i / cdpPort=base+i步长 2 / 1")
void allocPortSlots_strideFormula() {
List<int[]> slots = SaaGraphDispatcher.allocPortSlots(4320, 9222, 4);
assertEquals(4, slots.size());
for (int i = 0; i < 4; i++) {
assertEquals(4320 + 2 * i, slots.get(i)[0], "" + i + " playPort = 4320+2*" + i);
assertEquals(9222 + i, slots.get(i)[1], "" + i + " cdpPort = 9222+" + i);
}
}
@Test
@DisplayName("allocPortSlotsK 槽 2K 个端口两两不撞play 族 vs cdp 族不交叠 + 族内互异)")
void allocPortSlots_noCollision() {
for (int k : new int[]{1, 2, 4, 8, 16}) {
List<int[]> slots = SaaGraphDispatcher.allocPortSlots(4320, 9222, k);
Set<Integer> seen = new HashSet<>();
for (int[] slot : slots) {
assertTrue(seen.add(slot[0]), "K=" + k + " playPort " + slot[0] + " 与已分配端口相撞");
assertTrue(seen.add(slot[1]), "K=" + k + " cdpPort " + slot[1] + " 与已分配端口相撞");
}
assertEquals(2 * k, seen.size(), "K=" + k + " 应有 2K=" + (2 * k) + " 个互异端口");
}
}
@Test
@DisplayName("allocPortSlotsnull 基址兜底 4320/9222k<1 兜底 1 槽(防御误配,不抛)")
void allocPortSlots_nullBaseAndClamp() {
List<int[]> nullBase = SaaGraphDispatcher.allocPortSlots(null, null, 2);
assertEquals(4320, nullBase.get(0)[0], "httpBase=null → 兜底 4320");
assertEquals(9222, nullBase.get(0)[1], "cdpBase=null → 兜底 9222");
assertEquals(4322, nullBase.get(1)[0], "槽 1 仍按步长");
List<int[]> clamp = SaaGraphDispatcher.allocPortSlots(4320, 9222, 0);
assertEquals(1, clamp.size(), "k<1 → 至少 1 槽(防御,不返空)");
assertEquals(4320, clamp.get(0)[0]);
}
}

View File

@ -57,17 +57,30 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
* <b></b>SAA 路只有 token 计量¥ follow-up F3{@code SaaGraphDispatcher} cost 整段省略§6.3 此处<b>不造假¥</b></li>
* </ol>
*
* <p><b>003-U1 量测 flags</b>
* <ul>
* <li>{@code -Dsaa.e2e.sourceMode}默认 {@code factory}量哪条生成路{@code factory}=iife 工厂旧路现行字节零变基线
* {@code gamedef}=真结构化 gameDefinition cutover 60% 量测对象<b>修发现</b>旧版未 setSaaSourceMode量的是 iife 旧路非 gamedef
* 故量 gamedef 真基线<b>必须</b> {@code -Dsaa.e2e.sourceMode=gamedef}</li>
* <li>{@code -Dsaa.e2e.concurrency}默认 1后台并发度 K1=串行生产字节零变基线&gt;1 job 并行真玩各占错开端口对
* K=1 vs K=N verdict 对等校准延 003-U5 真跑期</li>
* <li>{@code -Dsaa.e2e.briefFile}可选外部 brief 文件行式去空白/跳空行与 {@code #} 注释
* A/B 量测用 {@code src/test/resources/saa-e2e-briefs-gamedef.txt}30 条多品类出集留 {@code saa-e2e-briefs-holdout.txt} final R1</li>
* </ul>
*
* <p><b>运行mini-desktopgame-runtime node+esbuild+Chrome端口 4320/9222 </b>
* <pre>
* // 基线量测仅打印成功率 + 存在性断言003-U1 第一步实测真基线
* // gamedef 路基线量测003-U1 第一步实测真基线务必带 sourceMode=gamedef 才量对路
* NEWAPI_KEY=sk-... timeout 6000 mvn -pl ...aigc-server test \
* -Dtest=SaaFullGraphE2eTest -Dsaa.e2e=1 -Dsaa.e2e.n=10 \
* -Dtest=SaaFullGraphE2eTest -Dsaa.e2e=1 -Dsaa.e2e.sourceMode=gamedef -Dsaa.e2e.n=10 \
* -Dsaa.e2e.briefFile=src/test/resources/saa-e2e-briefs-gamedef.txt \
* -DGAME_RUNTIME_DIR=/root/game-staging/repo/game-runtime \
* -Dsurefire.failIfNoSpecifiedTests=false
* // flip 前置硬门成功率须80%否则测试 fail追加 -Dsaa.e2e.minSuccessRate=0.8
* // flip 前置硬门gamedef 成功率须60% iife 基线否则测试 fail追加 -Dsaa.e2e.minSuccessRate=0.6
* // 有界并发量测K=4 并行真玩端口对自动错开追加 -Dsaa.e2e.concurrency=4
* </pre>
*
* @author 造梦AIU7 集成顶石003-U1 验收门工具化
* @author 造梦AIU7 集成顶石003-U1 验收门工具化 + sourceMode/concurrency 量测 flags
*/
class SaaFullGraphE2eTest {
@ -198,6 +211,13 @@ class SaaFullGraphE2eTest {
int n = briefs.size();
System.out.println("[briefs] 本次投递 n=" + n + " 条(源 " + sourceBriefs.size() + " 条)");
// 量测路别003-U1 发现修复-Dsaa.e2e.sourceMode 决定量的是哪条生成路
// factory默认= iife 工厂旧路现行字节零变基线gamedef = 真结构化 gameDefinition cutover 60% 量测对象
// 不显式设 setSaaSourceMode props 默认 "factory"与现行一致故缺省= iife gamedef 基线须 -Dsaa.e2e.sourceMode=gamedef
String sourceMode = System.getProperty("saa.e2e.sourceMode", "factory").trim();
// 后台并发度003-U1-Dsaa.e2e.concurrency 默认 1串行=生产字节零变基线>1 时多 job 并行真玩各占错开端口对
int concurrency = Integer.getInteger("saa.e2e.concurrency", 1);
// 1) 构造真 AigcExecutorProperties@Data setter 全有
// checkpoint + DataSource null DB 依赖dispatcher=saa 走进程内全图派发
AigcExecutorProperties props = new AigcExecutorProperties();
@ -209,6 +229,9 @@ class SaaFullGraphE2eTest {
props.setSaaMaxPlayerRounds(1); // player 顾问轮上限
props.setSaaStage2ExtraRepairs(3); // 救场阶梯 stage2 额外修复轮5+3 阶梯
props.setSaaCheckpointEnabled(false); // checkpoint 不依赖 DataSource DB
props.setSaaSourceMode(sourceMode); // 003-U1 发现量对路gamedef 基线须显式 -Dsaa.e2e.sourceMode=gamedef
props.setSaaConcurrency(concurrency); // 003-U1后台并发度默认 1=串行基线>1 job 并行真玩各占错开端口
System.out.println("[config] sourceMode=" + sourceMode + "factory=iife旧路/gamedef=真结构化), concurrency=" + concurrency);
// 2) 结果容器 + stub 回调共享 latch(N) + byTrace 精确归位+ 派发器5 sourceProjectApi=null=create
long runStart = System.currentTimeMillis();

View File

@ -0,0 +1,53 @@
# saa-e2e-briefs-gamedef.txt —— 003-U1 gamedef 路 A/B 量测 brief 集≥30 条多品类)
# 用法:-Dsaa.e2e.sourceMode=gamedef -Dsaa.e2e.briefFile=src/test/resources/saa-e2e-briefs-gamedef.txt
# 行式:去首尾空白、跳空行与本行式 # 注释SaaFullGraphE2eTest.parseBriefLines
# 选品偏向运行时/九门 harness 支持的品类realtime/physics/tap-targets/clickable避免单品类偶过/偶不过。
# 留出集(不进本 A/B、供 final R1 量saa-e2e-briefs-holdout.txt——两文件 brief 不重叠。
#
# ── 挡板/接球类realtime + 碰撞)──
打砖块:挡板接球把上方砖块全部消除即胜,掉球则负。
弹球台:用底部挡板把弹球接住反弹,击碎顶部所有彩色方块过关,球落底则失败。
双挡板对打:上下各一块挡板来回击球,让球越过对方底线即得分,先得五分者胜。
# ── 蛇/贪吃类(网格 + tick 移动)──
贪吃蛇:操控小蛇吃食物变长,撞到自己身体或墙壁则游戏结束。
吃豆人:在迷宫里移动吃光所有豆子,碰到巡逻的幽灵则失败。
# ── 接物/收集类realtime + 左右移动)──
接金币:左右移动篮子接住从天上掉落的金币,漏接太多就失败。
接水果:晃动盘子接住掉落的水果,接到炸弹则扣命,命数耗尽结束。
雨中接伞:左右移动小人撑伞接住落下的雨滴,三滴落空则游戏结束。
# ── 跳跃/躲避类physics + 点击跳)──
躲障碍:角色不断前进,点击跳跃躲开迎面而来的障碍物,撞到就结束。
小恐龙跑酷:按键让恐龙跳过仙人掌和飞鸟,撞到障碍即结束,跑得越远分越高。
火柴人跳坑:连续点击控制跳跃跨过地面深坑,掉进坑里则失败。
直升机穿洞:长按上升松开下降,穿过上下起伏的山洞间隙,撞壁即结束。
# ── 消除/三连类clickable + 网格)──
点击消除:点击三个或以上相连的同色方块把它们消掉,消满目标分数过关。
方块连连看:点击两个图案相同且可直连的方块成对消除,全部清空即胜。
下落消除:交换相邻宝石凑成三连消除得分,步数用尽时达标过关。
# ── 射击/弹幕类realtime + 发射)──
太空射击:飞船左右移动并发射子弹击落持续下降的敌机,被敌机撞到则失败。
打飞碟:移动炮台瞄准并射击横向飞过的飞碟,限时内击落数量达标过关。
保卫基地:旋转底部炮塔射击四面来袭的入侵者,基地被攻破则失败。
# ── 点击反应/打地鼠类tap-targets + 限时)──
打地鼠:地鼠随机从洞口冒头,点击敲中得分,限时结束时分数越高越好。
戳泡泡:屏幕不断冒出彩色泡泡,快速点击戳破得分,漏掉太多则结束。
快速点靶:随机位置闪现靶心,限时内尽量多地点中,结束时统计命中数。
点亮方块:方块随机亮起,趁熄灭前点中得分,连续三次没点中则结束。
# ── 节奏/判定类realtime + 时机)──
节奏敲击:方块下落到底部判定线的瞬间点击得分,连续错过三次则结束。
钢琴块:只点黑色方块别碰白块,节奏越来越快,点错或漏点即结束。
# ── 平台/抵达类physics + 移动跳跃)──
平台跳跃:左右移动并跳跃,踩着悬空平台抵达终点旗帜,掉出屏幕底部则失败。
跳台阶登顶:不断向上跳到下一块平台,踩空掉落则结束,登得越高分越高。
# ── 飞行/穿缝类physics + 点击上升)──
像素鸟穿管:点击让小鸟扇翅上升,穿过一根根上下水管的缝隙,撞管或落地即结束。
气球升空:轻点给气球补气上升,避开横向飘来的尖刺,被扎破则游戏结束。
# ── 躲陨石/生存类realtime + 八向移动)──
太空躲陨石:上下左右移动飞船躲避飞来的陨石,坚持到计时归零即通关。
雪崩求生:左右移动滑雪者躲开滚落的石块与树木,被砸中即结束,撑得越久越好。
# ── 弹力/打靶类physics + 点击施力)──
弹力打靶:点击屏幕给小球施加方向,把场上所有靶子撞掉即过关。
愤怒投石:拖拽瞄准弹射石块,砸倒所有目标木箱即通关,弹药用尽未清场则失败。
# ── 泡泡射手类physics + 瞄准发射)──
泡泡射手:瞄准并发射泡泡,三个同色相连则消除,清空全部泡泡即胜。
祖玛吐珠:旋转中心炮台向滚动的珠链射入同色珠子凑三消,珠链到达终点则失败。

View File

@ -0,0 +1,30 @@
# saa-e2e-briefs-holdout.txt —— 003-U1 留出集hold-out不进 A/B 调参,供 final R1 量)
# 用法:-Dsaa.e2e.sourceMode=gamedef -Dsaa.e2e.briefFile=src/test/resources/saa-e2e-briefs-holdout.txt
# 纪律:本集 brief 与 saa-e2e-briefs-gamedef.txt 及 SaaFullGraphE2eTest.ALL_BRIEFS 均不重叠——
# A/B 阶段只用 gamedef.txt 调参/收敛避免对留出集过拟合final R1 验收才用本集量真泛化成功率。
# 选品同样偏向运行时/九门 harness 支持品类realtime/physics/tap-targets/clickable
#
# ── 挡板/接球类 ──
保龄反弹:底部挡板把弹珠弹向顶部砖墙,敲碎全部砖块即胜,球落底则败。
# ── 蛇/网格类 ──
吃星变长:操控小蛇在网格内吃星星变长,撞墙或咬到自己则结束。
# ── 接物类 ──
接落叶:左右移动竹篮接住飘落的树叶,连漏五片则游戏结束。
# ── 跳跃/躲避类 ──
忍者翻墙:连续点击让忍者起跳越过迎面飞来的飞镖,被击中即结束。
# ── 消除类 ──
同色爆破:点击成片相连的同色气球一次性引爆,达到目标分数过关。
# ── 射击类 ──
炮台轰机:左右移动炮台射击编队俯冲的战机,被撞或漏过太多则失败。
# ── 打地鼠/点击反应类 ──
拍蚊子:屏幕四处乱飞的蚊子,快速点中拍死得分,限时结束统计战果。
# ── 节奏类 ──
鼓点连击:圆圈缩到判定环瞬间点击得分,连续错失三次则结束。
# ── 平台类 ──
攀岩登顶:交替左右蹬跳攀上一块块岩点抵达山顶,踩空坠落则失败。
# ── 飞行/穿缝类 ──
潜艇穿礁:点击上浮松手下沉,驾潜艇穿过上下错落的暗礁缝隙,触礁即结束。
# ── 躲避/生存类 ──
雷区穿行:上下左右移动小车躲开不断落下的地雷,坚持到倒计时归零即通关。
# ── 弹力/打靶类 ──
台球清台:拖动球杆瞄准击打母球,把台面所有彩球撞进袋即清台过关。