补齐试玩回包审查要求的关键验收(#548)
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
绑定逐视口摘要中的状态、阻断原因与首个未通过断言 验证成功和失败结果均完整进入最终 MCP 回包 新增真实点击后未进入 playing 的浏览器场景并对照持久报告 收紧视觉未执行断言并补充长期验收要求
This commit is contained in:
@@ -4422,67 +4422,100 @@ pub(super) fn drain_analytics_test_writer(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn browser_gameplay_failure_reaches_model_with_both_viewports_and_screenshots() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let evidence =
|
||||
super::super::direct_validation::tests::failed_browser_evidence_fixture(temp.path());
|
||||
for viewport in &evidence.viewport_results {
|
||||
image::RgbaImage::new(2, 2)
|
||||
.save(&viewport.screenshot_path)
|
||||
.unwrap();
|
||||
}
|
||||
let receipt = super::super::direct_validation::browser_tool_result(
|
||||
temp.path(),
|
||||
&evidence,
|
||||
Some(crate::browser::BrowserPlaytestScenario::GenericV1),
|
||||
1,
|
||||
);
|
||||
assert!(evidence.viewport_results.iter().all(|result| result.passed));
|
||||
assert_eq!(receipt["passed"], false);
|
||||
let failure =
|
||||
bridge_validation_result(temp.path(), Ok(receipt.clone()), |report, screenshots| {
|
||||
BrowserPlaytestError::PlaytestNotPassed {
|
||||
fn browser_gameplay_results_reach_model_with_matching_viewports_and_screenshots() {
|
||||
for passed in [false, true] {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let evidence = if passed {
|
||||
super::super::direct_validation::tests::browser_evidence_fixture(temp.path())
|
||||
} else {
|
||||
super::super::direct_validation::tests::failed_browser_evidence_fixture(temp.path())
|
||||
};
|
||||
for viewport in &evidence.viewport_results {
|
||||
image::RgbaImage::new(2, 2)
|
||||
.save(&viewport.screenshot_path)
|
||||
.unwrap();
|
||||
}
|
||||
let receipt = super::super::direct_validation::browser_tool_result(
|
||||
temp.path(),
|
||||
&evidence,
|
||||
Some(crate::browser::BrowserPlaytestScenario::GenericV1),
|
||||
1,
|
||||
);
|
||||
assert!(evidence.viewport_results.iter().all(|result| result.passed));
|
||||
assert_eq!(receipt["passed"], passed);
|
||||
let outcome = bridge_validation_result(
|
||||
temp.path(),
|
||||
Ok(receipt.clone()),
|
||||
|report, screenshots| BrowserPlaytestError::PlaytestNotPassed {
|
||||
report,
|
||||
screenshots,
|
||||
}
|
||||
})
|
||||
.unwrap_err();
|
||||
let error = ToolCallError::from(&failure);
|
||||
assert_eq!(error.images.len(), 2);
|
||||
assert_eq!(error.error["PlaytestNotPassed"]["report"], receipt);
|
||||
assert!(error.error["PlaytestNotPassed"]
|
||||
.get("screenshots")
|
||||
.is_none());
|
||||
let state = direct_tool_bridge_state(temp.path().to_path_buf());
|
||||
let response =
|
||||
compose_direct_tool_outcome(&state, "agc_browser_playtest", "{}", false, Err(error));
|
||||
assert_eq!(response["isError"], true);
|
||||
let text = response["content"][0]["text"].as_str().unwrap();
|
||||
assert_eq!(
|
||||
response["content"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|block| block["type"] == "image")
|
||||
.count(),
|
||||
2
|
||||
);
|
||||
for (index, entry) in evidence.viewport_playtests.iter().enumerate() {
|
||||
let report = &receipt["gameplayResults"][index];
|
||||
},
|
||||
)
|
||||
.map_err(|failure| {
|
||||
let error = ToolCallError::from(&failure);
|
||||
assert_eq!(error.error["PlaytestNotPassed"]["report"], receipt);
|
||||
assert!(error.error["PlaytestNotPassed"]
|
||||
.get("screenshots")
|
||||
.is_none());
|
||||
error
|
||||
});
|
||||
let state = direct_tool_bridge_state(temp.path().to_path_buf());
|
||||
let response =
|
||||
compose_direct_tool_outcome(&state, "agc_browser_playtest", "{}", false, outcome);
|
||||
assert_eq!(response["isError"], !passed);
|
||||
let text = response["content"][0]["text"].as_str().unwrap();
|
||||
let model_receipt: Value =
|
||||
serde_json::from_str(&text[text.find('{').unwrap()..]).unwrap();
|
||||
assert_eq!(model_receipt, receipt);
|
||||
assert_eq!(
|
||||
report["assertions"],
|
||||
serde_json::to_value(&entry.result.assertions).unwrap()
|
||||
response["content"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|block| block["type"] == "image")
|
||||
.count(),
|
||||
2
|
||||
);
|
||||
assert_eq!(report["finalPhase"], "ready");
|
||||
assert_eq!(report["finalSequence"], index as u64);
|
||||
assert!(text.contains(&entry.result.diagnostics[0]));
|
||||
let summary = model_receipt["summary"].as_str().unwrap();
|
||||
for (index, entry) in evidence.viewport_playtests.iter().enumerate() {
|
||||
let report = &model_receipt["gameplayResults"][index];
|
||||
assert_eq!(
|
||||
report["viewport"],
|
||||
serde_json::to_value(entry.viewport).unwrap()
|
||||
);
|
||||
assert_eq!(report["passed"], entry.result.passed);
|
||||
assert_eq!(
|
||||
report["assertions"],
|
||||
serde_json::to_value(&entry.result.assertions).unwrap()
|
||||
);
|
||||
assert_eq!(report["finalPhase"], "ready");
|
||||
assert_eq!(
|
||||
report["finalSequence"],
|
||||
entry.result.final_sequence.unwrap()
|
||||
);
|
||||
let viewport = report["viewport"].as_str().unwrap();
|
||||
let line = summary
|
||||
.lines()
|
||||
.find(|line| line.starts_with(&format!("{viewport}: gameplayPassed=")))
|
||||
.unwrap();
|
||||
assert!(line.starts_with(&format!(
|
||||
"{viewport}: gameplayPassed={}",
|
||||
entry.result.passed
|
||||
)));
|
||||
if let Some(reason) = entry.result.diagnostics.first() {
|
||||
assert_eq!(report["diagnostics"][0], reason.as_str());
|
||||
assert!(line.contains(reason));
|
||||
}
|
||||
if let Some(assertion) = entry
|
||||
.result
|
||||
.assertions
|
||||
.iter()
|
||||
.find(|assertion| !assertion.passed)
|
||||
{
|
||||
assert!(line.contains(&assertion.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
let summary = receipt["summary"].as_str().unwrap();
|
||||
assert!(summary.contains("desktop: gameplayPassed=false"));
|
||||
assert!(summary.contains("mobile: gameplayPassed=false"));
|
||||
assert!(summary.contains("start-control-clicked"));
|
||||
assert!(summary.contains("start-phase-playing"));
|
||||
assert!(summary.contains("未执行"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -786,7 +786,9 @@ pub(super) mod tests {
|
||||
assert_eq!(receipt["passed"], mode != "missing-mobile");
|
||||
if mode == "visual" {
|
||||
assert_eq!(receipt["gameplayResults"], json!([]));
|
||||
assert!(summary.contains("未执行"));
|
||||
assert!(summary
|
||||
.lines()
|
||||
.any(|line| line.starts_with("玩法检查:未执行")));
|
||||
assert_eq!(receipt["coverage"], "visual-only");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1514,6 +1514,41 @@ async fn real_chrome_generic_playtest_pointer_events_keeps_unavailable_controls_
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires an installed Chrome/Chromium/Edge and explicit local browser execution"]
|
||||
async fn real_chrome_generic_playtest_reports_start_click_without_playing_phase() {
|
||||
let result = run_stable_generic_viewport_fixture(false, Some(
|
||||
r#"<button type="button" data-playtest-id="start" data-start-phase="ready">Start</button>"#,
|
||||
)).await;
|
||||
assert!(!result.passed);
|
||||
assert!(result
|
||||
.viewport_results
|
||||
.iter()
|
||||
.all(|viewport| viewport.passed));
|
||||
for entry in &result.viewport_playtests {
|
||||
let playtest = &entry.result;
|
||||
assert!(!playtest.passed);
|
||||
assert_eq!(playtest.final_phase, Some(BrowserPlaytestPhase::Ready));
|
||||
assert_eq!(playtest.final_sequence, Some(1));
|
||||
for (name, expected) in [
|
||||
("start-control-clicked", true),
|
||||
("start-phase-playing", false),
|
||||
] {
|
||||
assert_eq!(
|
||||
playtest
|
||||
.assertions
|
||||
.iter()
|
||||
.find(|assertion| assertion.name == name)
|
||||
.unwrap()
|
||||
.passed,
|
||||
expected
|
||||
);
|
||||
}
|
||||
let reason = &playtest.diagnostics[0];
|
||||
assert!(reason.contains("必须进入 playing"), "{reason}");
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_stable_generic_viewport_fixture(
|
||||
mobile_action_broken: bool,
|
||||
start_markup: Option<&str>,
|
||||
@@ -1556,7 +1591,7 @@ async fn run_stable_generic_viewport_fixture(
|
||||
};
|
||||
document.querySelector('[data-playtest-id="start"]').addEventListener('click', (event) => {
|
||||
if (event.isTrusted && state.phase === 'ready') {
|
||||
advance(() => { state.phase = 'playing'; });
|
||||
advance(() => { state.phase = event.currentTarget.dataset.startPhase || 'playing'; });
|
||||
}
|
||||
});
|
||||
const primary = document.querySelector('[data-playtest-id="primary-action"]');
|
||||
@@ -1656,6 +1691,17 @@ async fn run_stable_generic_viewport_fixture(
|
||||
{
|
||||
let projected = &receipt["gameplayResults"][index];
|
||||
assert_eq!(projected["viewport"], entry["viewport"]);
|
||||
let viewport = entry["viewport"].as_str().unwrap();
|
||||
let line = receipt["summary"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.lines()
|
||||
.find(|line| line.starts_with(&format!("{viewport}: gameplayPassed=")))
|
||||
.unwrap();
|
||||
assert!(line.starts_with(&format!(
|
||||
"{viewport}: gameplayPassed={}",
|
||||
entry["result"]["passed"]
|
||||
)));
|
||||
for field in [
|
||||
"passed",
|
||||
"assertions",
|
||||
@@ -1670,7 +1716,15 @@ async fn run_stable_generic_viewport_fixture(
|
||||
}
|
||||
if let Some(reason) = entry["result"]["diagnostics"][0].as_str() {
|
||||
assert_eq!(projected["diagnostics"][0], reason);
|
||||
assert!(receipt["summary"].as_str().unwrap().contains(reason));
|
||||
assert!(line.contains(reason));
|
||||
}
|
||||
if let Some(assertion) = entry["result"]["assertions"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|assertion| assertion["passed"] == false)
|
||||
{
|
||||
assert!(line.contains(assertion["name"].as_str().unwrap()));
|
||||
}
|
||||
}
|
||||
result
|
||||
|
||||
@@ -196,6 +196,7 @@ UI 编辑器的“分析参考图”步骤、Rust 命令 `suggest_ui_design_sema
|
||||
|
||||
- 复用客户端浏览器与现有固定玩法场景。视觉检查采集双端画面/布局/资源/诊断;玩法检查分别在 desktop/mobile 执行明确的固定场景和真实输入,按视口保存结果。旧报告缺少移动端玩法结果时保持未知,不补通过。报告必须声明检查层级;视觉通过不能宣称玩法通过,固定场景通过也不能宣称覆盖未执行的完整关卡。
|
||||
- 浏览器工具回执在 `mode=gameplay` 时新增 `gameplayResults` 双端有界投影,每个视口包含 `viewport`、`passed`、`diagnostics`、`assertions` 及 `initialPhase` / `initialSequence` / `initialLevel`、`finalPhase` / `finalSequence` / `finalLevel`。`assertions` 仅列当前固定场景的断言名称和通过状态;每端最多公开 4 条诊断,每条最多 512 字符。缺少某端结果时仍返回该视口 `passed=false`,诊断说明证据缺失,断言为空且六个状态字段为 `null`;断言的 `passed=false` 可包含未执行,不能据此断言该断言已独立执行且失败。`mode=visual` 的 `gameplayResults` 为空,并在摘要中明确玩法未执行。摘要从同一投影给出每端首条诊断和首个未通过断言,不能与结构化结果相矛盾。工具回执不得展开完整宿主报告;`reportPath` 只作宿主证据引用。宿主报告结构、验收门禁、持久化预算和私有路径保护保持原合同,旧回执不回填新字段。
|
||||
- 回执验收覆盖 start 禁用、点击后 phase 不符合和玩法成功;逐视口核对持久报告与摘要中的状态、首条诊断和首个未通过断言,并检查成功、失败两条最终 MCP 回包路径。
|
||||
- 输入或碰撞改变先做定点玩法检查;纯图像/颜色变化做视觉检查;首次交付和影响闭环的修改做所需玩法验证。新增失败或相关代码变化才重跑对应层,不因改说明文字重复完整验证。
|
||||
- 内置试玩和客户端托管的外部 Node/npm 验证共用当前 clientTurnId 的持久化预算。客户端分配递增执行序号,模型提供的旧 attempt 仅作兼容输入,不能减少计数或重置预算;同一轮错误反馈、工具切换和进程重启均不能刷新已消费次数。
|
||||
- 新增本地 validation.maxRuns(默认 3,正整数)独立于 llm.maxRetries;显式配置原样使用,不按角色或运行模式改写。超限直接返回已用/上限和最近证据,停止新的验证。预检与正常构建不计作重复试玩。
|
||||
|
||||
Reference in New Issue
Block a user