485ed50b26
Project CI / AI game creator shell Rust smoke (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (push) Has been cancelled
Project CI / AI game creator shell Rust shard 1/4 (push) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (push) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (push) Has been cancelled
## 背景 净月潭案例(2026-09-19 21:53 → 09-20 01:26,3 小时 33 分)的问题不是模型慢,而是环境未就绪、验收靠模型自述、预算只约束单个工具入口、工具调用被 SDK 全局串行闸门卡住。本 PR 落地确认后的七项交付效率合同,并把捆绑 Codex 升到当前 npm latest。 ## 结果 - 新建 Web 游戏在正式生成前由宿主自动预检:捆绑 Node/npm、真实 Vite 构建、受限浏览器桌面/移动截图;失败不启动生成或付费素材。 - 首个副作用前冻结交付合同,宿主保存权威证据与预算;证据齐全后先封口、排空并取得执行器完整退出证明,再产出交付报告,模型回复不再当作验收。 - 原生 shell、内置浏览器与托管命令、第三方 MCP 共用同一执行许可和累计执行时间;`validation.maxRuns` 改为执行/返修批次语义,另设 `maxTurnSeconds` 墙钟上限。 - 所有工具可并发:移除 SDK 全局串行的 `apply_patch`/`update_plan` 注册,改由宿主 `agc_apply_patch`(官方 parser + 当前回合写许可 + 受控进程树 + 短项目事务)和 `agc_update_plan` 提供等价能力;真实请求目录里已无串行注册,长 MCP 与补丁/计划实测在同一响应内重叠。 - 付费提交与本地写入绑定原回合原租约:容量与同动作锁等待可取消,封口、取消或预算耗尽后零新增提交;已越过提交边界的请求保留 operation ID 走 GET 对账,不自动重放。 - 新增请求与工具分段计时、有界并行批读和首轮上下文预取,未知耗时不补零。 - 捆绑 Codex 0.147.0 → 0.155.1:固定版本收敛到 `build_support/codex_bundle.rs` 单一声明,vendor 解析源码按 `rust-v0.155.1` 逐字节重取并更新 UPSTREAM 证据,适配 0.155 统一 exec(`exec_command`/`write_stdin`);macOS 侧车最小系统版本仍为 15.0。 ## 验证 - 生产 CLI → 捆绑 Codex 0.155.1 → 本地 Responses/MCP 夹具 9/9:补丁、完成收尾、批次、只读/可写 MCP 并发、原生命令并发、原生资源并发、统一 exec 会话、期限终止(真实重叠 775 / 764 / 999 ms)。 - 真实模型目录 2/2;vendor 上游库 111 项;宿主定向与回归 106 项;真实 Windows 进程与取消 21 项;Node 侧门禁 52 项。 - 发行载荷:artifact-only 重建 NSIS,解包验证侧车清单 `codex-cli 0.155.1`、6 个组件哈希、打包后 `bin/codex.exe --version`、Node 运行时 2130 文件与双端 PNG、`--environment-check` ready。 - `check-config`、TypeScript、编码(4991 文件)、文档索引、`git diff --check`、`cargo fmt --check` 全部通过。 ## 未覆盖 - 真实陶泥儿登录态 Provider 生成尚未执行(本机 `--llm-status` 返回 `authentication-required`)。 - macOS 侧车只在本机做静态 Mach-O 与清单解析,未在 macOS 上跑 `check-macos-bundle.mjs`。 - 全仓库聚合套件在本机仍因负载敏感的后台 mock-provider 用例而红,与本次改动无关:改动前的旧二进制同样失败,换单线程后相关用例 3/3 通过。 --------- Co-authored-by: kdletters <61648117+kdletters@users.noreply.github.com> Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/439
312 lines
9.4 KiB
Rust
312 lines
9.4 KiB
Rust
use std::collections::HashSet;
|
|
use std::path::PathBuf;
|
|
use std::time::Duration;
|
|
|
|
use serde::{Deserialize, Deserializer, Serialize};
|
|
|
|
pub(super) const RESULT_SCHEMA_VERSION: &str = "browser-validation.v1";
|
|
pub(super) const DEFAULT_SETTLE_MS: u64 = 800;
|
|
pub(super) const MAX_SETTLE_MS: u64 = 30_000;
|
|
pub(super) const MAX_EXPECTED_TEXT_ITEMS: usize = 32;
|
|
pub(super) const MAX_EXPECTED_TEXT_CHARS: usize = 512;
|
|
pub(super) const MAX_URL_CHARS: usize = 2_048;
|
|
pub(super) const BROWSER_TIMEOUT: Duration = Duration::from_secs(30);
|
|
|
|
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum BrowserValidationViewport {
|
|
Desktop,
|
|
Mobile,
|
|
}
|
|
|
|
pub(super) const REQUIRED_VIEWPORTS: [BrowserValidationViewport; 2] = [
|
|
BrowserValidationViewport::Desktop,
|
|
BrowserValidationViewport::Mobile,
|
|
];
|
|
|
|
impl BrowserValidationViewport {
|
|
pub(super) fn dimensions(self) -> (u32, u32, bool) {
|
|
match self {
|
|
Self::Desktop => (1280, 720, false),
|
|
Self::Mobile => (390, 844, true),
|
|
}
|
|
}
|
|
|
|
pub(super) fn file_stem(self) -> &'static str {
|
|
match self {
|
|
Self::Desktop => "desktop",
|
|
Self::Mobile => "mobile",
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
pub enum BrowserPlaytestScenario {
|
|
GenericV1,
|
|
TetrisV1,
|
|
LaneDefenseV1,
|
|
RunnerV1,
|
|
}
|
|
|
|
impl BrowserPlaytestScenario {
|
|
pub(super) fn as_str(self) -> &'static str {
|
|
match self {
|
|
Self::GenericV1 => "generic-v1",
|
|
Self::TetrisV1 => "tetris-v1",
|
|
Self::LaneDefenseV1 => "lane-defense-v1",
|
|
Self::RunnerV1 => "runner-v1",
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
|
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
|
pub struct BrowserValidationInput {
|
|
pub url: String,
|
|
#[serde(deserialize_with = "deserialize_fixed_viewports")]
|
|
pub viewports: Vec<BrowserValidationViewport>,
|
|
#[serde(default)]
|
|
pub expected_text: Vec<String>,
|
|
#[serde(default = "default_settle_ms")]
|
|
pub settle_ms: u64,
|
|
#[serde(default = "default_fail_on_console_error")]
|
|
pub fail_on_console_error: bool,
|
|
#[serde(default)]
|
|
pub playtest_scenario: Option<BrowserPlaytestScenario>,
|
|
pub evidence_root: PathBuf,
|
|
}
|
|
|
|
fn default_settle_ms() -> u64 {
|
|
DEFAULT_SETTLE_MS
|
|
}
|
|
|
|
fn default_fail_on_console_error() -> bool {
|
|
true
|
|
}
|
|
|
|
pub(super) fn validate_fixed_viewports(
|
|
viewports: &[BrowserValidationViewport],
|
|
) -> Result<(), String> {
|
|
if viewports.len() != REQUIRED_VIEWPORTS.len() {
|
|
return Err("viewports 必须且只能同时包含 desktop 和 mobile".to_string());
|
|
}
|
|
let mut unique = HashSet::new();
|
|
if viewports.iter().any(|viewport| !unique.insert(*viewport)) {
|
|
return Err("viewports 不能重复".to_string());
|
|
}
|
|
if REQUIRED_VIEWPORTS
|
|
.iter()
|
|
.any(|required| !unique.contains(required))
|
|
{
|
|
return Err("viewports 只能包含 desktop 和 mobile".to_string());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn deserialize_fixed_viewports<'de, D>(
|
|
deserializer: D,
|
|
) -> Result<Vec<BrowserValidationViewport>, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
{
|
|
let viewports = Vec::<BrowserValidationViewport>::deserialize(deserializer)?;
|
|
validate_fixed_viewports(&viewports).map_err(serde::de::Error::custom)?;
|
|
Ok(viewports)
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
pub enum DiscoveredBrowserKind {
|
|
Chrome,
|
|
Edge,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct DiscoveredBrowser {
|
|
pub kind: DiscoveredBrowserKind,
|
|
pub executable_path: PathBuf,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct BrowserIdentity {
|
|
pub kind: DiscoveredBrowserKind,
|
|
pub product: String,
|
|
pub protocol_version: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct BrowserValidationEvidencePaths {
|
|
pub root: PathBuf,
|
|
pub report_path: PathBuf,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct BrowserExpectedTextMatch {
|
|
pub text: String,
|
|
pub found: bool,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum BrowserPlaytestPhase {
|
|
Ready,
|
|
Playing,
|
|
Won,
|
|
Lost,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
|
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
|
pub struct BrowserPlaytestAssertion {
|
|
pub name: String,
|
|
pub passed: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
|
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
|
pub struct BrowserPlaytestResult {
|
|
pub scenario: BrowserPlaytestScenario,
|
|
pub scenario_fingerprint: String,
|
|
pub passed: bool,
|
|
pub initial_sequence: Option<u64>,
|
|
pub initial_phase: Option<BrowserPlaytestPhase>,
|
|
pub initial_level: Option<u64>,
|
|
pub final_sequence: Option<u64>,
|
|
pub final_phase: Option<BrowserPlaytestPhase>,
|
|
pub final_level: Option<u64>,
|
|
pub assertions: Vec<BrowserPlaytestAssertion>,
|
|
pub diagnostics: Vec<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub runner_evidence: Option<BrowserRunnerEvidence>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
|
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
|
pub struct BrowserRunnerEvidence {
|
|
pub seed: u32,
|
|
pub course_fingerprint: String,
|
|
pub input_kind: String,
|
|
pub short_apex_milli_px: u64,
|
|
pub long_apex_milli_px: u64,
|
|
pub short_jump_cut_count: u64,
|
|
pub fair_window_ms: i64,
|
|
pub sampled_ticks: u64,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
|
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
|
pub struct BrowserViewportPlaytestResult {
|
|
pub viewport: BrowserValidationViewport,
|
|
pub result: BrowserPlaytestResult,
|
|
}
|
|
|
|
pub(crate) fn required_viewport_playtests_passed(
|
|
scenario: BrowserPlaytestScenario,
|
|
results: &[BrowserViewportPlaytestResult],
|
|
) -> bool {
|
|
results.len() == REQUIRED_VIEWPORTS.len()
|
|
&& REQUIRED_VIEWPORTS.iter().all(|viewport| {
|
|
let mut matching = results.iter().filter(|entry| entry.viewport == *viewport);
|
|
matching
|
|
.next()
|
|
.is_some_and(|entry| entry.result.scenario == scenario && entry.result.passed)
|
|
&& matching.next().is_none()
|
|
})
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct BrowserConsoleMessage {
|
|
pub level: String,
|
|
pub text: String,
|
|
pub source_url: Option<String>,
|
|
pub line_number: Option<u32>,
|
|
pub column_number: Option<u32>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct BrowserException {
|
|
pub text: String,
|
|
pub source_url: Option<String>,
|
|
pub line_number: u32,
|
|
pub column_number: u32,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct BrowserFailedRequest {
|
|
pub url: String,
|
|
pub method: String,
|
|
pub resource_type: String,
|
|
pub error_text: String,
|
|
pub status_code: Option<u16>,
|
|
pub canceled: bool,
|
|
pub blocked_by_policy: bool,
|
|
pub fatal: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct BrowserCanvasProbe {
|
|
pub width: u32,
|
|
pub height: u32,
|
|
pub css_width: f64,
|
|
pub css_height: f64,
|
|
pub visible_area: f64,
|
|
pub sample_count: u32,
|
|
pub non_empty_pixel_count: u32,
|
|
pub non_empty: Option<bool>,
|
|
pub probe_error: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct BrowserViewportValidationResult {
|
|
pub viewport: BrowserValidationViewport,
|
|
pub width: u32,
|
|
pub height: u32,
|
|
pub final_url: String,
|
|
pub title: String,
|
|
pub ready_state: String,
|
|
pub visible_text_summary: String,
|
|
pub visible_text_character_count: usize,
|
|
pub dom_character_count: usize,
|
|
pub expected_text: Vec<BrowserExpectedTextMatch>,
|
|
pub console_errors: Vec<BrowserConsoleMessage>,
|
|
pub console_warnings: Vec<BrowserConsoleMessage>,
|
|
pub exceptions: Vec<BrowserException>,
|
|
pub failed_requests: Vec<BrowserFailedRequest>,
|
|
pub canvases: Vec<BrowserCanvasProbe>,
|
|
pub blocked_popup_count: u32,
|
|
pub blocked_dialog_count: u32,
|
|
pub blocked_download_count: u32,
|
|
pub blocked_permission_count: u32,
|
|
pub blocked_service_worker_count: u32,
|
|
pub screenshot_path: PathBuf,
|
|
pub passed: bool,
|
|
pub diagnostics: Vec<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct BrowserValidationResult {
|
|
pub schema_version: String,
|
|
pub url: String,
|
|
pub browser: BrowserIdentity,
|
|
pub passed: bool,
|
|
pub viewport_results: Vec<BrowserViewportValidationResult>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub playtest: Option<BrowserPlaytestResult>,
|
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
pub viewport_playtests: Vec<BrowserViewportPlaytestResult>,
|
|
pub diagnostics: Vec<String>,
|
|
pub evidence: BrowserValidationEvidencePaths,
|
|
pub completed_at_unix_ms: u64,
|
|
}
|