use super::*; #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct WebProjectFileRecord { pub path: String, pub content: String, pub media_type: String, pub encoding: String, pub size_bytes: u32, } #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct WebProjectRecord { pub project_id: String, pub owner_user_id: String, pub title: String, pub template_key: String, pub active_snapshot_id: String, pub active_preview_build_id: Option, pub created_at: String, pub updated_at: String, } #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct WebProjectSnapshotRecord { pub snapshot_id: String, pub project_id: String, pub owner_user_id: String, pub parent_snapshot_id: Option, pub template_key: String, pub files: Vec, pub patch_summary: String, pub created_by: String, pub created_at: String, } #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct WebProjectPreviewBuildRecord { pub job_id: String, pub project_id: String, pub snapshot_id: String, pub owner_user_id: String, pub status: String, pub logs: Vec, pub artifact_id: Option, pub preview_token_id: Option, pub preview_url: Option, pub error_summary: Option, pub created_at: String, pub started_at: Option, pub finished_at: Option, pub updated_at: String, } #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct WebProjectRuntimeJobRecord { pub job_id: String, pub project_id: String, pub snapshot_id: String, pub owner_user_id: String, pub job_kind: String, pub status: String, pub attempt: u32, pub worker_id: Option, pub lease_token: Option, pub lease_expires_at: Option, pub cancel_requested_at: Option, pub stale_reason: Option, pub artifact_id: Option, pub preview_build_id: Option, pub error_summary: Option, pub created_at: String, pub started_at: Option, pub finished_at: Option, pub updated_at: String, } #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct WebProjectRuntimeJobLogRecord { pub log_id: String, pub job_id: String, pub project_id: String, pub owner_user_id: String, pub sequence: u64, pub level: String, pub message: String, pub created_at: String, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct WebProjectCreateRecordInput { pub project_id: String, pub snapshot_id: String, pub owner_user_id: String, pub title: String, pub initial_files: Vec, pub now_micros: i64, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct WebProjectGetRecordInput { pub project_id: String, pub owner_user_id: String, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct WebProjectSnapshotGetRecordInput { pub project_id: String, pub snapshot_id: Option, pub owner_user_id: String, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct WebProjectSnapshotSaveRecordInput { pub snapshot_id: String, pub project_id: String, pub owner_user_id: String, pub parent_snapshot_id: Option, pub files: Vec, pub patch_summary: String, pub created_by: String, pub now_micros: i64, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct WebProjectPreviewBuildCreateRecordInput { pub job_id: String, pub project_id: String, pub snapshot_id: String, pub owner_user_id: String, pub now_micros: i64, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct WebProjectPreviewBuildGetRecordInput { pub job_id: String, pub owner_user_id: String, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct WebProjectPreviewBuildTokenGetRecordInput { pub preview_token_id: String, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct WebProjectPreviewBuildUpdateRecordInput { pub job_id: String, pub owner_user_id: String, pub status: String, pub logs: Vec, pub artifact_id: Option, pub preview_token_id: Option, pub preview_url: Option, pub error_summary: Option, pub started_at_micros: Option, pub finished_at_micros: Option, pub updated_at_micros: i64, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct WebProjectRuntimeJobCreateRecordInput { pub job_id: String, pub project_id: String, pub snapshot_id: String, pub owner_user_id: String, pub job_kind: String, pub preview_build_id: Option, pub now_micros: i64, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct WebProjectRuntimeJobGetRecordInput { pub job_id: String, pub owner_user_id: String, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct WebProjectRuntimeJobListOpenRecordInput { pub project_id: String, pub owner_user_id: String, pub limit: u32, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct WebProjectRuntimeJobClaimRecordInput { pub worker_id: String, pub limit: u32, pub lease_expires_at_micros: i64, pub claimed_at_micros: i64, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct WebProjectRuntimeJobRenewLeaseRecordInput { pub job_id: String, pub worker_id: String, pub lease_token: String, pub lease_expires_at_micros: i64, pub renewed_at_micros: i64, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct WebProjectRuntimeJobCompleteRecordInput { pub job_id: String, pub worker_id: String, pub lease_token: String, pub artifact_id: Option, pub preview_build_id: Option, pub completed_at_micros: i64, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct WebProjectRuntimeJobCompletePreviewBuildRecordInput { pub job_id: String, pub worker_id: String, pub lease_token: String, pub preview_build_id: String, pub artifact_id: String, pub preview_token_id: String, pub preview_url: String, pub logs: Vec, pub started_at_micros: Option, pub finished_at_micros: i64, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct WebProjectRuntimeJobFailRecordInput { pub job_id: String, pub worker_id: String, pub lease_token: String, pub error_summary: String, pub failed_at_micros: i64, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct WebProjectRuntimeJobCancelRecordInput { pub job_id: String, pub owner_user_id: String, pub cancelled_at_micros: i64, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct WebProjectRuntimeJobStaleRecordInput { pub job_id: String, pub owner_user_id: String, pub stale_reason: String, pub stale_at_micros: i64, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct WebProjectRuntimeJobExpireRecordInput { pub job_id: String, pub owner_user_id: String, pub expired_at_micros: i64, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct WebProjectRuntimeJobAppendLogRecordInput { pub log_id: String, pub job_id: String, pub owner_user_id: String, pub sequence: u64, pub level: String, pub message: String, pub worker_id: Option, pub lease_token: Option, pub created_at_micros: i64, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct WebProjectRuntimeJobListLogsRecordInput { pub job_id: String, pub owner_user_id: String, pub after_sequence: Option, pub limit: u32, } #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct WebProjectSnapshotMutationRecord { pub project: WebProjectRecord, pub snapshot: WebProjectSnapshotRecord, } #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct WebProjectPreviewBuildMutationRecord { pub project: WebProjectRecord, pub build: WebProjectPreviewBuildRecord, } #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct WebProjectRuntimeJobPreviewBuildMutationRecord { pub project: WebProjectRecord, pub build: WebProjectPreviewBuildRecord, pub job: WebProjectRuntimeJobRecord, } impl From for crate::module_bindings::WebProjectFileSnapshot { fn from(input: WebProjectFileRecord) -> Self { Self { path: input.path, content: input.content, media_type: input.media_type, encoding: input.encoding, size_bytes: input.size_bytes, } } } impl From for WebProjectFileRecord { fn from(snapshot: crate::module_bindings::WebProjectFileSnapshot) -> Self { Self { path: snapshot.path, content: snapshot.content, media_type: snapshot.media_type, encoding: snapshot.encoding, size_bytes: snapshot.size_bytes, } } } impl From for crate::module_bindings::WebProjectCreateInput { fn from(input: WebProjectCreateRecordInput) -> Self { Self { project_id: input.project_id, snapshot_id: input.snapshot_id, owner_user_id: input.owner_user_id, title: input.title, initial_files: input.initial_files.into_iter().map(Into::into).collect(), now_micros: input.now_micros, } } } impl From for crate::module_bindings::WebProjectGetInput { fn from(input: WebProjectGetRecordInput) -> Self { Self { project_id: input.project_id, owner_user_id: input.owner_user_id, } } } impl From for crate::module_bindings::WebProjectSnapshotGetInput { fn from(input: WebProjectSnapshotGetRecordInput) -> Self { Self { project_id: input.project_id, snapshot_id: input.snapshot_id, owner_user_id: input.owner_user_id, } } } impl From for crate::module_bindings::WebProjectSnapshotSaveInput { fn from(input: WebProjectSnapshotSaveRecordInput) -> Self { Self { snapshot_id: input.snapshot_id, project_id: input.project_id, owner_user_id: input.owner_user_id, parent_snapshot_id: input.parent_snapshot_id, files: input.files.into_iter().map(Into::into).collect(), patch_summary: input.patch_summary, created_by: input.created_by, now_micros: input.now_micros, } } } impl From for crate::module_bindings::WebProjectPreviewBuildCreateInput { fn from(input: WebProjectPreviewBuildCreateRecordInput) -> Self { Self { job_id: input.job_id, project_id: input.project_id, snapshot_id: input.snapshot_id, owner_user_id: input.owner_user_id, now_micros: input.now_micros, } } } impl From for crate::module_bindings::WebProjectPreviewBuildGetInput { fn from(input: WebProjectPreviewBuildGetRecordInput) -> Self { Self { job_id: input.job_id, owner_user_id: input.owner_user_id, } } } impl From for crate::module_bindings::WebProjectPreviewBuildTokenGetInput { fn from(input: WebProjectPreviewBuildTokenGetRecordInput) -> Self { Self { preview_token_id: input.preview_token_id, } } } impl From for crate::module_bindings::WebProjectPreviewBuildUpdateInput { fn from(input: WebProjectPreviewBuildUpdateRecordInput) -> Self { Self { job_id: input.job_id, owner_user_id: input.owner_user_id, status: input.status, logs: input.logs, artifact_id: input.artifact_id, preview_token_id: input.preview_token_id, preview_url: input.preview_url, error_summary: input.error_summary, started_at_micros: input.started_at_micros, finished_at_micros: input.finished_at_micros, updated_at_micros: input.updated_at_micros, } } } impl From for crate::module_bindings::WebProjectRuntimeJobCreateInput { fn from(input: WebProjectRuntimeJobCreateRecordInput) -> Self { Self { job_id: input.job_id, project_id: input.project_id, snapshot_id: input.snapshot_id, owner_user_id: input.owner_user_id, job_kind: input.job_kind, preview_build_id: input.preview_build_id, now_micros: input.now_micros, } } } impl From for crate::module_bindings::WebProjectRuntimeJobGetInput { fn from(input: WebProjectRuntimeJobGetRecordInput) -> Self { Self { job_id: input.job_id, owner_user_id: input.owner_user_id, } } } impl From for crate::module_bindings::WebProjectRuntimeJobListOpenInput { fn from(input: WebProjectRuntimeJobListOpenRecordInput) -> Self { Self { project_id: input.project_id, owner_user_id: input.owner_user_id, limit: input.limit, } } } impl From for crate::module_bindings::WebProjectRuntimeJobClaimInput { fn from(input: WebProjectRuntimeJobClaimRecordInput) -> Self { Self { worker_id: input.worker_id, limit: input.limit, lease_expires_at_micros: input.lease_expires_at_micros, claimed_at_micros: input.claimed_at_micros, } } } impl From for crate::module_bindings::WebProjectRuntimeJobRenewLeaseInput { fn from(input: WebProjectRuntimeJobRenewLeaseRecordInput) -> Self { Self { job_id: input.job_id, worker_id: input.worker_id, lease_token: input.lease_token, lease_expires_at_micros: input.lease_expires_at_micros, renewed_at_micros: input.renewed_at_micros, } } } impl From for crate::module_bindings::WebProjectRuntimeJobCompleteInput { fn from(input: WebProjectRuntimeJobCompleteRecordInput) -> Self { Self { job_id: input.job_id, worker_id: input.worker_id, lease_token: input.lease_token, artifact_id: input.artifact_id, preview_build_id: input.preview_build_id, completed_at_micros: input.completed_at_micros, } } } impl From for crate::module_bindings::WebProjectRuntimeJobCompletePreviewBuildInput { fn from(input: WebProjectRuntimeJobCompletePreviewBuildRecordInput) -> Self { Self { job_id: input.job_id, worker_id: input.worker_id, lease_token: input.lease_token, preview_build_id: input.preview_build_id, artifact_id: input.artifact_id, preview_token_id: input.preview_token_id, preview_url: input.preview_url, logs: input.logs, started_at_micros: input.started_at_micros, finished_at_micros: input.finished_at_micros, } } } impl From for crate::module_bindings::WebProjectRuntimeJobFailInput { fn from(input: WebProjectRuntimeJobFailRecordInput) -> Self { Self { job_id: input.job_id, worker_id: input.worker_id, lease_token: input.lease_token, error_summary: input.error_summary, failed_at_micros: input.failed_at_micros, } } } impl From for crate::module_bindings::WebProjectRuntimeJobCancelInput { fn from(input: WebProjectRuntimeJobCancelRecordInput) -> Self { Self { job_id: input.job_id, owner_user_id: input.owner_user_id, cancelled_at_micros: input.cancelled_at_micros, } } } impl From for crate::module_bindings::WebProjectRuntimeJobStaleInput { fn from(input: WebProjectRuntimeJobStaleRecordInput) -> Self { Self { job_id: input.job_id, owner_user_id: input.owner_user_id, stale_reason: input.stale_reason, stale_at_micros: input.stale_at_micros, } } } impl From for crate::module_bindings::WebProjectRuntimeJobExpireInput { fn from(input: WebProjectRuntimeJobExpireRecordInput) -> Self { Self { job_id: input.job_id, owner_user_id: input.owner_user_id, expired_at_micros: input.expired_at_micros, } } } impl From for crate::module_bindings::WebProjectRuntimeJobAppendLogInput { fn from(input: WebProjectRuntimeJobAppendLogRecordInput) -> Self { Self { log_id: input.log_id, job_id: input.job_id, owner_user_id: input.owner_user_id, sequence: input.sequence, level: input.level, message: input.message, worker_id: input.worker_id, lease_token: input.lease_token, created_at_micros: input.created_at_micros, } } } impl From for crate::module_bindings::WebProjectRuntimeJobListLogsInput { fn from(input: WebProjectRuntimeJobListLogsRecordInput) -> Self { Self { job_id: input.job_id, owner_user_id: input.owner_user_id, after_sequence: input.after_sequence, limit: input.limit, } } } pub(crate) fn map_web_project_procedure_result( result: WebProjectProcedureResult, ) -> Result { if !result.ok { return Err(SpacetimeClientError::procedure_failed(result.error_message)); } result .project .map(map_web_project_snapshot) .ok_or_else(|| SpacetimeClientError::missing_snapshot("Web 工程")) } pub(crate) fn map_web_project_snapshot_procedure_result( result: WebProjectSnapshotProcedureResult, ) -> Result { if !result.ok { return Err(SpacetimeClientError::procedure_failed(result.error_message)); } let project = result .project .map(map_web_project_snapshot) .ok_or_else(|| SpacetimeClientError::missing_snapshot("Web 工程"))?; let snapshot = result .snapshot .map(map_web_project_source_snapshot) .ok_or_else(|| SpacetimeClientError::missing_snapshot("Web 工程快照"))?; Ok(WebProjectSnapshotMutationRecord { project, snapshot }) } pub(crate) fn map_web_project_preview_build_procedure_result( result: WebProjectPreviewBuildProcedureResult, ) -> Result { if !result.ok { return Err(SpacetimeClientError::procedure_failed(result.error_message)); } let project = result .project .map(map_web_project_snapshot) .ok_or_else(|| SpacetimeClientError::missing_snapshot("Web 工程"))?; let build = result .build .map(map_web_project_preview_build_snapshot) .ok_or_else(|| SpacetimeClientError::missing_snapshot("Web 工程预览构建"))?; Ok(WebProjectPreviewBuildMutationRecord { project, build }) } pub(crate) fn map_web_project_runtime_job_procedure_result( result: WebProjectRuntimeJobProcedureResult, ) -> Result { if !result.ok { return Err(SpacetimeClientError::procedure_failed(result.error_message)); } result .job .map(map_web_project_runtime_job_snapshot) .ok_or_else(|| SpacetimeClientError::missing_snapshot("Web Project runtime job")) } pub(crate) fn map_web_project_runtime_job_preview_build_procedure_result( result: WebProjectRuntimeJobPreviewBuildProcedureResult, ) -> Result { if !result.ok { return Err(SpacetimeClientError::procedure_failed(result.error_message)); } let project = result .project .map(map_web_project_snapshot) .ok_or_else(|| SpacetimeClientError::missing_snapshot("Web 工程"))?; let build = result .build .map(map_web_project_preview_build_snapshot) .ok_or_else(|| SpacetimeClientError::missing_snapshot("Web 工程预览构建"))?; let job = result .job .map(map_web_project_runtime_job_snapshot) .ok_or_else(|| SpacetimeClientError::missing_snapshot("Web Project runtime job"))?; Ok(WebProjectRuntimeJobPreviewBuildMutationRecord { project, build, job, }) } pub(crate) fn map_web_project_runtime_job_list_result( result: WebProjectRuntimeJobProcedureResult, ) -> Result, SpacetimeClientError> { if !result.ok { return Err(SpacetimeClientError::procedure_failed(result.error_message)); } Ok(result .jobs .into_iter() .map(map_web_project_runtime_job_snapshot) .collect()) } pub(crate) fn map_web_project_runtime_job_log_procedure_result( result: WebProjectRuntimeJobProcedureResult, ) -> Result { if !result.ok { return Err(SpacetimeClientError::procedure_failed(result.error_message)); } result .log .map(map_web_project_runtime_job_log_snapshot) .ok_or_else(|| SpacetimeClientError::missing_snapshot("Web Project runtime job 日志")) } pub(crate) fn map_web_project_runtime_job_log_list_result( result: WebProjectRuntimeJobProcedureResult, ) -> Result, SpacetimeClientError> { if !result.ok { return Err(SpacetimeClientError::procedure_failed(result.error_message)); } Ok(result .logs .into_iter() .map(map_web_project_runtime_job_log_snapshot) .collect()) } fn map_web_project_snapshot(snapshot: WebProjectProjectSnapshot) -> WebProjectRecord { WebProjectRecord { project_id: snapshot.project_id, owner_user_id: snapshot.owner_user_id, title: snapshot.title, template_key: snapshot.template_key, active_snapshot_id: snapshot.active_snapshot_id, active_preview_build_id: snapshot.active_preview_build_id, created_at: format_timestamp_micros(snapshot.created_at_micros), updated_at: format_timestamp_micros(snapshot.updated_at_micros), } } fn map_web_project_source_snapshot(snapshot: WebProjectSnapshot) -> WebProjectSnapshotRecord { WebProjectSnapshotRecord { snapshot_id: snapshot.snapshot_id, project_id: snapshot.project_id, owner_user_id: snapshot.owner_user_id, parent_snapshot_id: snapshot.parent_snapshot_id, template_key: snapshot.template_key, files: snapshot.files.into_iter().map(Into::into).collect(), patch_summary: snapshot.patch_summary, created_by: snapshot.created_by, created_at: format_timestamp_micros(snapshot.created_at_micros), } } fn map_web_project_preview_build_snapshot( snapshot: WebProjectPreviewBuildSnapshot, ) -> WebProjectPreviewBuildRecord { WebProjectPreviewBuildRecord { job_id: snapshot.job_id, project_id: snapshot.project_id, snapshot_id: snapshot.snapshot_id, owner_user_id: snapshot.owner_user_id, status: snapshot.status, logs: snapshot.logs, artifact_id: snapshot.artifact_id, preview_token_id: snapshot.preview_token_id, preview_url: snapshot.preview_url, error_summary: snapshot.error_summary, created_at: format_timestamp_micros(snapshot.created_at_micros), started_at: snapshot.started_at_micros.map(format_timestamp_micros), finished_at: snapshot.finished_at_micros.map(format_timestamp_micros), updated_at: format_timestamp_micros(snapshot.updated_at_micros), } } fn map_web_project_runtime_job_snapshot( snapshot: WebProjectRuntimeJobSnapshot, ) -> WebProjectRuntimeJobRecord { WebProjectRuntimeJobRecord { job_id: snapshot.job_id, project_id: snapshot.project_id, snapshot_id: snapshot.snapshot_id, owner_user_id: snapshot.owner_user_id, job_kind: snapshot.job_kind, status: snapshot.status, attempt: snapshot.attempt, worker_id: snapshot.worker_id, lease_token: snapshot.lease_token, lease_expires_at: snapshot .lease_expires_at_micros .map(format_timestamp_micros), cancel_requested_at: snapshot .cancel_requested_at_micros .map(format_timestamp_micros), stale_reason: snapshot.stale_reason, artifact_id: snapshot.artifact_id, preview_build_id: snapshot.preview_build_id, error_summary: snapshot.error_summary, created_at: format_timestamp_micros(snapshot.created_at_micros), started_at: snapshot.started_at_micros.map(format_timestamp_micros), finished_at: snapshot.finished_at_micros.map(format_timestamp_micros), updated_at: format_timestamp_micros(snapshot.updated_at_micros), } } fn map_web_project_runtime_job_log_snapshot( snapshot: WebProjectRuntimeJobLogSnapshot, ) -> WebProjectRuntimeJobLogRecord { WebProjectRuntimeJobLogRecord { log_id: snapshot.log_id, job_id: snapshot.job_id, project_id: snapshot.project_id, owner_user_id: snapshot.owner_user_id, sequence: snapshot.sequence, level: snapshot.level, message: snapshot.message, created_at: format_timestamp_micros(snapshot.created_at_micros), } } #[cfg(test)] mod tests { use super::*; #[test] fn maps_web_project_procedure_error() { let result = WebProjectProcedureResult { ok: false, project: None, error_message: Some("无权访问该 Web 工程".to_string()), }; assert_eq!( map_web_project_procedure_result(result) .expect_err("procedure error should map") .to_string(), "无权访问该 Web 工程" ); } #[test] fn maps_runtime_job_log_list_in_sequence_order_from_module_result() { let result = WebProjectRuntimeJobProcedureResult { ok: true, job: None, jobs: Vec::new(), log: None, logs: vec![WebProjectRuntimeJobLogSnapshot { log_id: "log-1".to_string(), job_id: "job-1".to_string(), project_id: "project-1".to_string(), owner_user_id: "user-1".to_string(), sequence: 2, level: "info".to_string(), message: "构建完成".to_string(), created_at_micros: 1_700_000_000_000_000, }], error_message: None, }; let logs = map_web_project_runtime_job_log_list_result(result) .expect("runtime job logs should map"); assert_eq!(logs.len(), 1); assert_eq!(logs[0].sequence, 2); assert_eq!(logs[0].message, "构建完成"); } #[test] fn maps_runtime_job_preview_build_combo_result() { let result = WebProjectRuntimeJobPreviewBuildProcedureResult { ok: true, project: Some(WebProjectProjectSnapshot { project_id: "project-1".to_string(), owner_user_id: "user-1".to_string(), title: "测试工程".to_string(), template_key: "react-vite-ts-static".to_string(), active_snapshot_id: "snapshot-2".to_string(), active_preview_build_id: Some("build-2".to_string()), created_at_micros: 1_700_000_000_000_000, updated_at_micros: 1_700_000_010_000_000, }), build: Some(WebProjectPreviewBuildSnapshot { job_id: "build-1".to_string(), project_id: "project-1".to_string(), snapshot_id: "snapshot-1".to_string(), owner_user_id: "user-1".to_string(), status: "stale".to_string(), logs: vec!["runtime worker: 构建完成,预览地址已生成".to_string()], artifact_id: None, preview_token_id: None, preview_url: None, error_summary: Some("job snapshot 已不是项目 active snapshot".to_string()), created_at_micros: 1_700_000_000_000_000, started_at_micros: Some(1_700_000_005_000_000), finished_at_micros: Some(1_700_000_010_000_000), updated_at_micros: 1_700_000_010_000_000, }), job: Some(WebProjectRuntimeJobSnapshot { job_id: "job-1".to_string(), project_id: "project-1".to_string(), snapshot_id: "snapshot-1".to_string(), owner_user_id: "user-1".to_string(), job_kind: "preview_build".to_string(), status: "stale".to_string(), attempt: 1, worker_id: None, lease_token: None, lease_expires_at_micros: None, cancel_requested_at_micros: None, stale_reason: Some("job snapshot 已不是项目 active snapshot".to_string()), artifact_id: None, preview_build_id: Some("build-1".to_string()), error_summary: None, created_at_micros: 1_700_000_000_000_000, started_at_micros: Some(1_700_000_005_000_000), finished_at_micros: Some(1_700_000_010_000_000), updated_at_micros: 1_700_000_010_000_000, }), error_message: None, }; let mapped = map_web_project_runtime_job_preview_build_procedure_result(result) .expect("combo result should map"); assert_eq!( mapped.project.active_preview_build_id.as_deref(), Some("build-2") ); assert_eq!(mapped.build.status, "stale"); assert_eq!(mapped.build.preview_url, None); assert_eq!(mapped.job.status, "stale"); assert_eq!( mapped.job.stale_reason.as_deref(), Some("job snapshot 已不是项目 active snapshot") ); } }