补齐 External v1 场景路由的客户端请求构造用例并勾选该验收项
- 新增 background_stage_posts_the_dedicated_scene_route_and_its_ledger_matches:用规范图夹具驱动真实生成链路,断言背景阶段只发一次 scenes 路由 POST、正文是结构化场景意图(sceneContent/stylePreset=custom/customStyle/referenceImageSrcs,且没有 assetKind/prompt),成功后不留在途账本 - 变异验证:把场景分支的路由改回通用图片路由后该用例变红,改回后恢复绿 - External v1 场景路由里程碑勾选「客户端定向测试」一项,逐条记录账本、恢复与 reuse/regenerate 的实跑证据;剩余仅本地真实栈受理 smoke
This commit is contained in:
@@ -15894,4 +15894,325 @@ mod canvas_generation_tests {
|
||||
b"old-image"
|
||||
);
|
||||
}
|
||||
|
||||
/// 场景(背景)阶段的请求夹具:只接受专用场景路由,记录原始请求,并按队列 → 换签 → 下载链路服务完。
|
||||
fn serve_scene_route_fixture(
|
||||
stream: &mut std::net::TcpStream,
|
||||
base_url: &str,
|
||||
posts: &std::sync::Mutex<Vec<String>>,
|
||||
others: &std::sync::Mutex<Vec<String>>,
|
||||
) {
|
||||
let request = read_test_http_request(stream);
|
||||
if request.starts_with("POST /api/external/v1/editor/scenes/generations ") {
|
||||
posts
|
||||
.lock()
|
||||
.expect("record scene generation posts")
|
||||
.push(request);
|
||||
write_test_json_response(
|
||||
stream,
|
||||
"202 Accepted",
|
||||
&serde_json::json!({
|
||||
"data": {
|
||||
"operationId": "scene-route-operation",
|
||||
"status": "queued",
|
||||
"pollAfterMs": 0,
|
||||
}
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
others
|
||||
.lock()
|
||||
.expect("record scene fixture requests")
|
||||
.push(request.clone());
|
||||
if request.starts_with("GET /api/external/v1/generations/scene-route-operation ") {
|
||||
write_test_json_response(
|
||||
stream,
|
||||
"200 OK",
|
||||
&serde_json::json!({
|
||||
"data": {
|
||||
"operationId": "scene-route-operation",
|
||||
"status": "completed",
|
||||
"pollAfterMs": 0,
|
||||
"result": {
|
||||
"resource": {
|
||||
"resourceId": "scene-route-resource",
|
||||
"objectKey": "scene-route.png",
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if request.starts_with("GET /api/external/v1/assets/read-url?") {
|
||||
write_test_json_response(
|
||||
stream,
|
||||
"200 OK",
|
||||
&serde_json::json!({
|
||||
"read": {"signedUrl": format!("{base_url}/scene-route.png")}
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if request.starts_with("GET /api/external/v1/editor/projects?view=summary ") {
|
||||
// 项目绑定已由 `install_test_external_project_binding` 预置,远端只需回认同一组身份。
|
||||
write_test_json_response(
|
||||
stream,
|
||||
"200 OK",
|
||||
&serde_json::json!({
|
||||
"data": {"projects": [{
|
||||
"projectId": "manual-test-canvas",
|
||||
"title": "场景生成画布",
|
||||
}]}
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if request.starts_with("GET /api/external/v1/editor/assets/library ") {
|
||||
write_test_json_response(
|
||||
stream,
|
||||
"200 OK",
|
||||
&serde_json::json!({
|
||||
"data": {"library": {"folders": [{
|
||||
"folderId": "manual-test-assets",
|
||||
"label": "场景生成素材",
|
||||
}]}}
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if request.starts_with("GET /scene-route.png ") {
|
||||
let png = rgba_test_png(u8::MAX).bytes;
|
||||
write!(
|
||||
stream,
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: image/png\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
||||
png.len()
|
||||
)
|
||||
.expect("write scene fixture png headers");
|
||||
stream.write_all(&png).expect("write scene fixture png");
|
||||
return;
|
||||
}
|
||||
panic!("unexpected scene route fixture request: {request}");
|
||||
}
|
||||
|
||||
/// 背景阶段(Scene)必须命中 external v1 的专用场景路由并以结构化场景字段提交,而且这一轮
|
||||
/// 真正写下的账本必须被「保留账本」判据按 Scene 认下来。
|
||||
///
|
||||
/// 这条配对正是 2026-09-23 之前出过错的地方:客户端向通用图片路由提交 `assetKind = scene`
|
||||
/// 被平台参数校验拒绝(HTTP 400),而保留账本匹配器又期望 `game-background`。两侧任一侧回流,
|
||||
/// 本条用例都会红。
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn background_stage_posts_the_dedicated_scene_route_and_its_ledger_matches() {
|
||||
let temporary = tempfile::tempdir().expect("create scene route project");
|
||||
let root = temporary.path();
|
||||
init_local_game_project_at(root, "manual-scene-route", "场景路由测试")
|
||||
.expect("init scene route project");
|
||||
write_project_permission_policy_at(
|
||||
root,
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: Vec::new(),
|
||||
confirm_commands: Vec::new(),
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("allow standalone scene generation");
|
||||
// `Scene` 属于「必须带规范图引用」的 kind:先种一张 assets/art-spec.png 并登记成 Canvas 来源。
|
||||
fs::write(
|
||||
root.join(AGENT_RUNTIME_ART_SPEC_PATH),
|
||||
rgba_test_png(u8::MAX).bytes,
|
||||
)
|
||||
.expect("write canonical art spec");
|
||||
register_local_asset_at(
|
||||
root,
|
||||
AGENT_RUNTIME_ART_SPEC_PATH,
|
||||
GameCreationAppAssetKind::IconSpec,
|
||||
"image/png",
|
||||
"scene-route-test",
|
||||
GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Canvas,
|
||||
canvas_project_id: Some("manual-test-canvas".to_string()),
|
||||
resource_id: Some("scene-route-art-spec".to_string()),
|
||||
asset_object_id: Some("scene-route-art-spec-object".to_string()),
|
||||
task_id: Some("scene-route-art-spec-task".to_string()),
|
||||
prompt: None,
|
||||
model: None,
|
||||
generation_route: Some("/api/external/v1/editor/images/generations".to_string()),
|
||||
generation_kind: Some("spec".to_string()),
|
||||
reference_resource_ids: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("register canonical art spec");
|
||||
|
||||
let listener =
|
||||
std::net::TcpListener::bind("127.0.0.1:0").expect("bind scene route fixture");
|
||||
listener
|
||||
.set_nonblocking(true)
|
||||
.expect("set scene route fixture nonblocking");
|
||||
let base_url = format!("http://{}", listener.local_addr().expect("fixture address"));
|
||||
let api_key = "scene-route-key";
|
||||
install_test_external_project_binding(root, &base_url, api_key);
|
||||
// 规范图的远端引用走 binding 缓存命中:预置绑定后不需要额外的上传夹具端点。
|
||||
{
|
||||
let access = ExternalEditorBindingAccess::new(&base_url, api_key, None)
|
||||
.expect("prepare scene route binding access");
|
||||
let manifest = read_manifest_for_project(root).expect("read scene route manifest");
|
||||
let source = manifest
|
||||
.assets
|
||||
.iter()
|
||||
.find(|asset| asset.local_path == AGENT_RUNTIME_ART_SPEC_PATH)
|
||||
.expect("registered canonical art spec");
|
||||
let source_bytes =
|
||||
fs::read(root.join(&source.local_path)).expect("read art spec bytes");
|
||||
let principal =
|
||||
external_editor_binding_principal(&access).expect("derive scene route principal");
|
||||
let source_identity = new_external_editor_source_identity(
|
||||
&source.id,
|
||||
&format!("{:x}", Sha256::digest(&source_bytes)),
|
||||
&source.media_type,
|
||||
&source.kind,
|
||||
)
|
||||
.expect("derive scene route source identity");
|
||||
let binding = new_external_editor_resource_binding(
|
||||
&manifest.project_id,
|
||||
&principal,
|
||||
"manual-test-canvas",
|
||||
&source_identity,
|
||||
Some("scene-route-art-spec-resource"),
|
||||
"generated/scene-route-art-spec.png",
|
||||
"scene-route-art-spec-object",
|
||||
Some(1),
|
||||
Some(1),
|
||||
unix_timestamp(),
|
||||
)
|
||||
.expect("create canonical resource binding");
|
||||
write_external_editor_resource_binding_at(root, &binding)
|
||||
.expect("persist canonical resource binding");
|
||||
}
|
||||
|
||||
let posts = std::sync::Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
|
||||
let others = std::sync::Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
|
||||
let (stop_sender, stop_receiver) = std::sync::mpsc::channel();
|
||||
let server_posts = posts.clone();
|
||||
let server_others = others.clone();
|
||||
let server_base_url = base_url.clone();
|
||||
let server = std::thread::spawn(move || {
|
||||
let mut handlers = Vec::new();
|
||||
loop {
|
||||
if stop_receiver.try_recv().is_ok() {
|
||||
break;
|
||||
}
|
||||
let (mut stream, _) = match listener.accept() {
|
||||
Ok(connection) => connection,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
|
||||
std::thread::sleep(Duration::from_millis(2));
|
||||
continue;
|
||||
}
|
||||
Err(error) => panic!("accept scene route request: {error}"),
|
||||
};
|
||||
let posts = server_posts.clone();
|
||||
let others = server_others.clone();
|
||||
let base_url = server_base_url.clone();
|
||||
handlers.push(std::thread::spawn(move || {
|
||||
serve_scene_route_fixture(&mut stream, &base_url, &posts, &others);
|
||||
}));
|
||||
}
|
||||
for handler in handlers {
|
||||
handler.join().expect("join scene route fixture handler");
|
||||
}
|
||||
});
|
||||
|
||||
let options = PlatformArtAssetGenerationOptions {
|
||||
output_path: Some("assets/scene-route-background.png".to_string()),
|
||||
aspect_ratio: "16:9".to_string(),
|
||||
image_size: "1K".to_string(),
|
||||
asset_kind: GameCreationAppAssetKind::Scene,
|
||||
asset_label: "雨夜天台".to_string(),
|
||||
replace_existing: false,
|
||||
slice_count: None,
|
||||
slice_mode: None,
|
||||
grid_x: None,
|
||||
grid_y: None,
|
||||
reference_asset_ids: Vec::new(),
|
||||
target_category: None,
|
||||
screen_color: None,
|
||||
};
|
||||
let prompt = "雨夜天台眺望霓虹城市的横屏背景";
|
||||
let credentials = crate::assets::external_editor_api_credentials_for_test(
|
||||
base_url.clone(),
|
||||
api_key.to_string(),
|
||||
);
|
||||
let generated = crate::assets::with_external_editor_api_credentials(credentials, async {
|
||||
match generate_platform_art_asset_with_options_at(root, prompt, &[], &options).await {
|
||||
Ok(generated) => generated,
|
||||
Err(error) => panic!("scene generation must succeed: {error}"),
|
||||
}
|
||||
})
|
||||
.await;
|
||||
stop_sender.send(()).expect("stop scene route fixture");
|
||||
server.join().expect("join scene route fixture");
|
||||
|
||||
assert_eq!(
|
||||
generated.resource_id.as_deref(),
|
||||
Some("scene-route-resource"),
|
||||
"场景生成必须复用夹具返回的远端资源身份"
|
||||
);
|
||||
let captured = posts.lock().expect("read captured scene posts").clone();
|
||||
assert_eq!(
|
||||
captured.len(),
|
||||
1,
|
||||
"背景阶段只能有一次生成 POST:{captured:#?}"
|
||||
);
|
||||
let post = &captured[0];
|
||||
assert!(
|
||||
post.starts_with("POST /api/external/v1/editor/scenes/generations "),
|
||||
"背景阶段必须走专用场景路由:{post}"
|
||||
);
|
||||
let body_text = post
|
||||
.split("\r\n\r\n")
|
||||
.nth(1)
|
||||
.expect("scene generation POST must carry a body");
|
||||
let body: serde_json::Value =
|
||||
serde_json::from_str(body_text).expect("scene generation body must be JSON");
|
||||
assert!(
|
||||
body["sceneContent"]
|
||||
.as_str()
|
||||
.is_some_and(|value| value.contains("雨夜天台眺望霓虹城市")),
|
||||
"结构化场景意图必须放在 sceneContent:{body}"
|
||||
);
|
||||
assert_eq!(body["stylePreset"], "custom", "{body}");
|
||||
assert!(
|
||||
body["customStyle"]
|
||||
.as_str()
|
||||
.is_some_and(|value| !value.trim().is_empty()),
|
||||
"自定义风格描述不能为空:{body}"
|
||||
);
|
||||
assert!(
|
||||
body.get("assetKind").is_none(),
|
||||
"结构化场景请求不得再带通用 assetKind:{body}"
|
||||
);
|
||||
assert!(
|
||||
body.get("prompt").is_none(),
|
||||
"结构化场景请求不得再带通用 prompt:{body}"
|
||||
);
|
||||
assert!(body["referenceImageSrcs"].is_array(), "{body}");
|
||||
assert_eq!(body["assetLabel"], "雨夜天台", "{body}");
|
||||
|
||||
// standalone 成功安装后不留「在途账本」:同一条精确动作的恢复账本必须已被清理,
|
||||
// 否则下一次同一动作会被当成恢复而不是新提交(场景侧的账本判据本身由
|
||||
// `retained_stage_recovery_accepts_the_same_reference_contract_as_submission` 覆盖)。
|
||||
let generation_prompt = build_platform_art_asset_prompt(prompt, &options);
|
||||
let runtime_context =
|
||||
standalone_platform_art_generation_runtime_context(&generation_prompt, &options, false)
|
||||
.expect("scene runtime context");
|
||||
assert!(
|
||||
super::external_generation_state::read_platform_art_generation_runtime_state(
|
||||
root,
|
||||
&runtime_context,
|
||||
)
|
||||
.expect("read scene generation ledger")
|
||||
.is_none(),
|
||||
"成功安装后不得保留在途生成账本"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,9 @@
|
||||
- 客户端:`canvas_generation.rs` 的 Scene 专属分支(新路由 + `sceneContent`)、保留账本匹配器(`canvas_generation.rs` 的 endpoint/`sceneContent` 判定)、`external_generation_state.rs`、`runtime_driver/recovery_scan.rs`、`direct_runtime/mod.rs` 的背景身份常量都与线上口径一致。
|
||||
- 本轮实测:`cargo test -p api-server --manifest-path server-rs/Cargo.toml -- scene --test-threads=1` → **18 passed / 0 failed**(覆盖站内拒绝 scene 绕过、外部场景鉴权与非法入参、非法幂等键、共享场景契约与可下载队列结果、OpenAPI 契约)。
|
||||
- 客户端账本与身份契约:`cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --offline -- external_generation_state --test-threads=1` → **10 passed**(覆盖生成账本身份、复用与并发认领口径;场景路由的账本匹配器就在这个模块里)。
|
||||
- 仍未取得(勾选状态保持未完成):客户端「背景阶段请求命中新场景路由且为结构化字段」这一条**没有现成用例**。`canvas_generation.rs` 的场景分支是内联在生成链路里的 `(endpoint, body)` 字面量(`/api/external/v1/editor/scenes/generations` + `sceneContent` / `stylePreset=custom`),要单独把它钉住,测试里得先种一张规范图——`Scene` 属于「必须带规范图引用」的 kind(`platform_art_asset_kind_requires_canonical_spec_reference`),所以还需要补规范图上传的夹具端点;本轮没有做,也不打算用「把同一段字面量抄进断言」的镜像用例凑数。
|
||||
- `reuse-or-create` / `regenerate` 的端到端回归与「本地真实栈 + 测试 Key 受理」smoke 同样未执行。
|
||||
- 本机限制:客户端 `-- background` 这个过滤词会匹配到一大片 `background_agent_runtime` 家族(本轮实测 138 条:47 passed / 91 failed,失败几乎都是等待 mock LLM 请求的 `Timeout` 或状态竞争),**不能用它作为本里程碑的判据**。已在 `pitfalls` 记录该家族的抖动特征与 A/B 方法:同一条用例在合并前的分支尖(`74b83aa1a`)上也会失败,安静状态下又能 3/3 通过。
|
||||
- 客户端定向测试(本轮补齐):新增 `background_stage_posts_the_dedicated_scene_route_and_its_ledger_matches`。它用项目内夹具驱动**真实生成链路**,先种一张 `assets/art-spec.png` 并预置其账号绑定(`Scene` 属于必须带规范图引用的 kind),然后断言背景阶段只发一次 `POST /api/external/v1/editor/scenes/generations`,正文是结构化场景意图(`sceneContent` 带原始意图、`stylePreset=custom`、非空 `customStyle`、`referenceImageSrcs` 数组、没有 `assetKind` / `prompt`),并在成功安装后确认在途账本已被清理。
|
||||
- 变异验证:把场景分支的路由改回 `/api/external/v1/editor/images/generations` 后该用例立刻变红(夹具拒绝通用路由、生成失败),改回后恢复绿(1.59s)。因此这一条勾选不是镜像断言。
|
||||
- 账本与恢复口径:`retained_stage_recovery_accepts_the_same_reference_contract_as_submission`(Scene 账本必须按新路由 + `sceneContent` + `stylePreset=custom` 被认下)与 `legacy_route_background_registration_still_counts_as_valid_base`(旧路由登记仍可读)单跑通过。
|
||||
- `reuse-or-create` / `regenerate` 回归:`direct_output_sync_accepts_trusted_spec_and_background_without_a_historical_spritesheet`(复用已登记背景)、`failed_background_stage_after_local_commit_restores_recorded_spec_and_background` 与 `failed_complete_package_regeneration_restores_replaced_spec_and_background`(失败后按记录恢复 spec / 背景)单跑通过。
|
||||
- 仍未取得:**本地真实栈 + 测试 Key 的受理 smoke**(`npm run dev:api-server` 后用真实 API Key POST 新路由校验受理与重放),需要本地 dev 栈与可用 Key。
|
||||
- 本机限制:客户端 `-- background` 过滤会匹配一大片 `background_agent_runtime` 家族并大量 `Timeout`(本轮实测 138 条:47 passed / 91 failed),不能当判据;该家族抖动特征与 A/B 方法记在 `pitfalls`。
|
||||
@@ -36,7 +36,7 @@
|
||||
- [x] 新路由契约测试通过:路由矩阵与鉴权、参数 400、同键重放返回原任务、同键不同请求 409。
|
||||
- [x] 外部场景路由与站内路由对相同输入产出相同的后端 Prompt 与入队 payload(共享实现单测对照)。
|
||||
- [x] `docs/openapi/genarrative-external-v1.openapi.json` 与实现一致,相关契约检查通过。
|
||||
- [ ] 客户端定向测试:背景阶段请求命中新路由且为结构化字段;保留账本匹配器与实际请求口径一致;`reuse-or-create` 与 `regenerate` 路径回归通过。
|
||||
- [x] 客户端定向测试:背景阶段请求命中新路由且为结构化字段;保留账本匹配器与实际请求口径一致;`reuse-or-create` 与 `regenerate` 路径回归通过。
|
||||
- [ ] 本地真实栈 smoke:`npm run dev:api-server` + 测试 Key 下美术包背景阶段受理成功(Provider 出图按环境可用性记录为已验证或未验证)。
|
||||
|
||||
## 证据要求
|
||||
@@ -54,6 +54,9 @@
|
||||
- 客户端:`canvas_generation.rs` 的 Scene 专属分支(新路由 + `sceneContent`)、保留账本匹配器(`canvas_generation.rs` 的 endpoint/`sceneContent` 判定)、`external_generation_state.rs`、`runtime_driver/recovery_scan.rs`、`direct_runtime/mod.rs` 的背景身份常量都与线上口径一致。
|
||||
- 本轮实测:`cargo test -p api-server --manifest-path server-rs/Cargo.toml -- scene --test-threads=1` → **18 passed / 0 failed**(覆盖站内拒绝 scene 绕过、外部场景鉴权与非法入参、非法幂等键、共享场景契约与可下载队列结果、OpenAPI 契约)。
|
||||
- 客户端账本与身份契约:`cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --offline -- external_generation_state --test-threads=1` → **10 passed**(覆盖生成账本身份、复用与并发认领口径;场景路由的账本匹配器就在这个模块里)。
|
||||
- 仍未取得(勾选状态保持未完成):客户端「背景阶段请求命中新场景路由且为结构化字段」这一条**没有现成用例**。`canvas_generation.rs` 的场景分支是内联在生成链路里的 `(endpoint, body)` 字面量(`/api/external/v1/editor/scenes/generations` + `sceneContent` / `stylePreset=custom`),要单独把它钉住,测试里得先种一张规范图——`Scene` 属于「必须带规范图引用」的 kind(`platform_art_asset_kind_requires_canonical_spec_reference`),所以还需要补规范图上传的夹具端点;本轮没有做,也不打算用「把同一段字面量抄进断言」的镜像用例凑数。
|
||||
- `reuse-or-create` / `regenerate` 的端到端回归与「本地真实栈 + 测试 Key 受理」smoke 同样未执行。
|
||||
- 本机限制:客户端 `-- background` 这个过滤词会匹配到一大片 `background_agent_runtime` 家族(本轮实测 138 条:47 passed / 91 failed,失败几乎都是等待 mock LLM 请求的 `Timeout` 或状态竞争),**不能用它作为本里程碑的判据**。已在 `pitfalls` 记录该家族的抖动特征与 A/B 方法:同一条用例在合并前的分支尖(`74b83aa1a`)上也会失败,安静状态下又能 3/3 通过。
|
||||
- 客户端定向测试(本轮补齐):新增 `background_stage_posts_the_dedicated_scene_route_and_its_ledger_matches`。它用项目内夹具驱动**真实生成链路**,先种一张 `assets/art-spec.png` 并预置其账号绑定(`Scene` 属于必须带规范图引用的 kind),然后断言背景阶段只发一次 `POST /api/external/v1/editor/scenes/generations`,正文是结构化场景意图(`sceneContent` 带原始意图、`stylePreset=custom`、非空 `customStyle`、`referenceImageSrcs` 数组、没有 `assetKind` / `prompt`),并在成功安装后确认在途账本已被清理。
|
||||
- 变异验证:把场景分支的路由改回 `/api/external/v1/editor/images/generations` 后该用例立刻变红(夹具拒绝通用路由、生成失败),改回后恢复绿(1.59s)。因此这一条勾选不是镜像断言。
|
||||
- 账本与恢复口径:`retained_stage_recovery_accepts_the_same_reference_contract_as_submission`(Scene 账本必须按新路由 + `sceneContent` + `stylePreset=custom` 被认下)与 `legacy_route_background_registration_still_counts_as_valid_base`(旧路由登记仍可读)单跑通过。
|
||||
- `reuse-or-create` / `regenerate` 回归:`direct_output_sync_accepts_trusted_spec_and_background_without_a_historical_spritesheet`(复用已登记背景)、`failed_background_stage_after_local_commit_restores_recorded_spec_and_background` 与 `failed_complete_package_regeneration_restores_replaced_spec_and_background`(失败后按记录恢复 spec / 背景)单跑通过。
|
||||
- 仍未取得:**本地真实栈 + 测试 Key 的受理 smoke**(`npm run dev:api-server` 后用真实 API Key POST 新路由校验受理与重放),需要本地 dev 栈与可用 Key。
|
||||
- 本机限制:客户端 `-- background` 过滤会匹配一大片 `background_agent_runtime` 家族并大量 `Timeout`(本轮实测 138 条:47 passed / 91 failed),不能当判据;该家族抖动特征与 A/B 方法记在 `pitfalls`。
|
||||
Reference in New Issue
Block a user