Files
Genarrative/server-rs/crates/spacetime-client/src/mapper/web_project.rs
Linghong b19b76af56 完成 Editor Agent P2 持久任务与运行时收口
新增 Web Project runtime job、持久日志、lease、取消、expired、stale 和 active preview guard 状态机

接入 api-server Web Project runtime worker 与 TempDirBuildRuntime 构建执行链路

补齐 SpacetimeDB procedure、spacetime-client facade、shared contracts 和前端 web-project client 契约

更新 /editor/agent 的 runtime job 恢复、日志回填、SSE 重连、取消按钮和 active preview 刷新恢复

新增 P2 dev smoke 脚本,并让完整 npm run dev 默认以 all 角色启动 P2 worker

补充 P2 自动化测试、浏览器 smoke 验收记录、开发运维文档和 Hermes 踩坑记忆
2026-06-17 21:22:41 +08:00

949 lines
31 KiB
Rust

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<String>,
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<String>,
pub template_key: String,
pub files: Vec<WebProjectFileRecord>,
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<String>,
pub artifact_id: Option<String>,
pub preview_token_id: Option<String>,
pub preview_url: Option<String>,
pub error_summary: Option<String>,
pub created_at: String,
pub started_at: Option<String>,
pub finished_at: Option<String>,
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<String>,
pub lease_token: Option<String>,
pub lease_expires_at: Option<String>,
pub cancel_requested_at: Option<String>,
pub stale_reason: Option<String>,
pub artifact_id: Option<String>,
pub preview_build_id: Option<String>,
pub error_summary: Option<String>,
pub created_at: String,
pub started_at: Option<String>,
pub finished_at: Option<String>,
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<WebProjectFileRecord>,
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<String>,
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<String>,
pub files: Vec<WebProjectFileRecord>,
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<String>,
pub artifact_id: Option<String>,
pub preview_token_id: Option<String>,
pub preview_url: Option<String>,
pub error_summary: Option<String>,
pub started_at_micros: Option<i64>,
pub finished_at_micros: Option<i64>,
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<String>,
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<String>,
pub preview_build_id: Option<String>,
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<String>,
pub started_at_micros: Option<i64>,
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<String>,
pub lease_token: Option<String>,
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<u64>,
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<WebProjectFileRecord> 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<crate::module_bindings::WebProjectFileSnapshot> 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<WebProjectCreateRecordInput> 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<WebProjectGetRecordInput> 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<WebProjectSnapshotGetRecordInput> 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<WebProjectSnapshotSaveRecordInput>
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<WebProjectPreviewBuildCreateRecordInput>
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<WebProjectPreviewBuildGetRecordInput>
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<WebProjectPreviewBuildTokenGetRecordInput>
for crate::module_bindings::WebProjectPreviewBuildTokenGetInput
{
fn from(input: WebProjectPreviewBuildTokenGetRecordInput) -> Self {
Self {
preview_token_id: input.preview_token_id,
}
}
}
impl From<WebProjectPreviewBuildUpdateRecordInput>
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<WebProjectRuntimeJobCreateRecordInput>
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<WebProjectRuntimeJobGetRecordInput>
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<WebProjectRuntimeJobListOpenRecordInput>
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<WebProjectRuntimeJobClaimRecordInput>
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<WebProjectRuntimeJobRenewLeaseRecordInput>
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<WebProjectRuntimeJobCompleteRecordInput>
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<WebProjectRuntimeJobCompletePreviewBuildRecordInput>
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<WebProjectRuntimeJobFailRecordInput>
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<WebProjectRuntimeJobCancelRecordInput>
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<WebProjectRuntimeJobStaleRecordInput>
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<WebProjectRuntimeJobExpireRecordInput>
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<WebProjectRuntimeJobAppendLogRecordInput>
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<WebProjectRuntimeJobListLogsRecordInput>
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<WebProjectRecord, SpacetimeClientError> {
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<WebProjectSnapshotMutationRecord, SpacetimeClientError> {
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<WebProjectPreviewBuildMutationRecord, SpacetimeClientError> {
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<WebProjectRuntimeJobRecord, SpacetimeClientError> {
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<WebProjectRuntimeJobPreviewBuildMutationRecord, SpacetimeClientError> {
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<Vec<WebProjectRuntimeJobRecord>, 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<WebProjectRuntimeJobLogRecord, SpacetimeClientError> {
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<Vec<WebProjectRuntimeJobLogRecord>, 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")
);
}
}