e424c5539e
为九个固定图集合同路径补齐持久事务、原子标记、锁守卫和崩溃恢复。 统一校验主图与切片的资源、资产和任务身份,避免远端请求期间占用项目写锁。 收紧俄罗斯方块静态语义、浏览器动作因果和固定场景合同,拒绝注释与伪 telemetry 绕过。 迁移旧 Generic 俄罗斯方块根合同和试玩子任务合同,并失效旧场景回执。 补齐事务故障、恶意脚本、真实 Chrome、game-chat 修复和合同迁移回归测试。 同步 AI 游戏创作技术方案与共享决策记录。
272 lines
8.0 KiB
Rust
272 lines
8.0 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,
|
|
}
|
|
|
|
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",
|
|
}
|
|
}
|
|
}
|
|
|
|
#[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>,
|
|
}
|
|
|
|
#[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>,
|
|
pub diagnostics: Vec<String>,
|
|
pub evidence: BrowserValidationEvidencePaths,
|
|
pub completed_at_unix_ms: u64,
|
|
}
|