From 5e2426c8a7cf9c3d6902953da99dd2d3ceaef34d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Mon, 5 Oct 2026 16:58:40 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=BC=E5=87=BA=E6=94=B6=E5=B0=BE=E5=88=A4?= =?UTF-8?q?=E6=8D=AE=E6=8A=BD=E6=88=90=E5=8F=AF=E6=B5=8B=E5=87=BD=E6=95=B0?= =?UTF-8?q?=EF=BC=8C=E6=88=90=E5=8A=9F=E7=BB=93=E6=9E=9C=E5=8E=BB=E6=8E=89?= =?UTF-8?q?=E6=81=92=E4=B8=BA=200=20=E7=9A=84=E6=AD=BB=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - build.rs 把「脚本跑完之后算不算数」抽成 conclude(root, exit_code, timed_out, output):非零退出与超时收成 commandFailed(带退出码与输出尾部),退出码 0 但产物不在或为空文件收成 artifactMissing,产物在才算成功。 - 补 5 例确定性用例覆盖这条判据(非零退出、超时无退出码、产物缺失、空产物不算、成功),不再只靠"真的跑一遍 npm"才能碰到这些分支。 - XHSMiniToolExportRunResult 去掉 exit_code:能走到成功出口的退出码必然是 0,带回来只是死字段;失败侧 CommandFailed 自己带退出码。成功时改为回构建输出尾部(打包脚本会把 zip 条目数与体积打在那里)。 - 同步重生成 XHSMiniToolExportRunResult.ts。 --- .../src/export/draft/xhs_minitool/build.rs | 117 +++++++++++++++--- .../src/export/draft/xhs_minitool/dto.rs | 6 +- .../generated/XHSMiniToolExportRunResult.ts | 7 +- 3 files changed, 112 insertions(+), 18 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/export/draft/xhs_minitool/build.rs b/apps/ai-game-creator-shell/src-tauri/src/export/draft/xhs_minitool/build.rs index 7109c024a..1f9226ef5 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/export/draft/xhs_minitool/build.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/export/draft/xhs_minitool/build.rs @@ -41,10 +41,23 @@ pub(crate) async fn run(root: &Path) -> Result, + timed_out: bool, + output: &str, +) -> Result { + if timed_out || exit_code != Some(0) { return Err(XHSMiniToolExportError::CommandFailed(CommandFailed { - exit_code: result.exit_code, - output_tail: tail(&result.output), + exit_code, + output_tail: tail(output), })); } @@ -60,9 +73,9 @@ pub(crate) async fn run(root: &Path) -> Result String { mod tests { use super::*; + /// 每次都新建一个临时项目根:产物判据是文件系统事实,测试之间不能共享目录。 + fn fixture_root(label: &str) -> std::path::PathBuf { + let root = std::env::temp_dir().join(format!( + "genarrative-xhs-build-{label}-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + let _ = std::fs::remove_dir_all(&root); + std::fs::create_dir_all(&root).unwrap(); + root + } + + fn write_artifact(root: &Path, bytes: &[u8]) { + let path = artifact_path(root); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + std::fs::write(&path, bytes).unwrap(); + } + #[test] fn keeps_short_output_as_is() { assert_eq!(tail(" 构建失败\n"), "构建失败"); @@ -97,15 +131,7 @@ mod tests { #[test] fn reports_a_missing_script_before_touching_the_project() { - let root = std::env::temp_dir().join(format!( - "genarrative-xhs-build-{}-{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_nanos() - )); - std::fs::create_dir_all(&root).unwrap(); + let root = fixture_root("missing-script"); let error = tauri::async_runtime::block_on(run(&root)) .expect_err("no package.json means no script"); @@ -114,4 +140,67 @@ mod tests { XHSMiniToolExportError::BuildScriptMissing(_) )); } + + #[test] + fn a_non_zero_exit_keeps_the_code_and_the_output_tail() { + let root = fixture_root("non-zero"); + + let error = conclude(&root, Some(1), false, "vite 构建失败\n入口不存在") + .expect_err("non-zero exit must fail"); + let XHSMiniToolExportError::CommandFailed(failure) = error else { + panic!("非零退出必须映射成 commandFailed"); + }; + assert_eq!(failure.exit_code, Some(1)); + assert_eq!(failure.output_tail, "vite 构建失败\n入口不存在"); + } + + #[test] + fn a_timeout_still_reports_command_failed_without_an_exit_code() { + let root = fixture_root("timeout"); + + let error = conclude(&root, None, true, "已取消") + .expect_err("timeout must fail even with exit code 0"); + let XHSMiniToolExportError::CommandFailed(failure) = error else { + panic!("超时必须映射成 commandFailed"); + }; + assert_eq!(failure.exit_code, None); + assert_eq!(failure.output_tail, "已取消"); + } + + #[test] + fn a_successful_script_without_the_artifact_is_reported_as_missing() { + let root = fixture_root("no-artifact"); + + let error = conclude(&root, Some(0), false, "构建完成").expect_err("产物缺失必须失败"); + let XHSMiniToolExportError::ArtifactMissing(missing) = error else { + panic!("没有 zip 必须映射成 artifactMissing"); + }; + assert_eq!(missing.path, ".export/xhs-minitool.zip"); + } + + #[test] + fn an_empty_artifact_file_does_not_count() { + let root = fixture_root("empty-artifact"); + write_artifact(&root, b""); + + assert!(matches!( + conclude(&root, Some(0), false, "构建完成"), + Err(XHSMiniToolExportError::ArtifactMissing(_)) + )); + } + + #[test] + fn a_successful_script_with_a_non_empty_artifact_returns_the_output_tail() { + let root = fixture_root("ok"); + write_artifact(&root, b"PK\x03\x04zip"); + + let result = conclude( + &root, + Some(0), + false, + "zip 已生成:.export/xhs-minitool.zip(3 个条目)", + ) + .expect("产物在就应当成功"); + assert!(result.output_tail.contains("3 个条目")); + } } diff --git a/apps/ai-game-creator-shell/src-tauri/src/export/draft/xhs_minitool/dto.rs b/apps/ai-game-creator-shell/src-tauri/src/export/draft/xhs_minitool/dto.rs index 4d49d1dfe..578531675 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/export/draft/xhs_minitool/dto.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/export/draft/xhs_minitool/dto.rs @@ -68,11 +68,13 @@ pub(crate) struct XHSMiniToolExportState { } /// 一次成功导出的结果。产物路径是常量(`.export/xhs-minitool.zip`),不进 DTO。 +/// +/// 不带 `exit_code`:走到这里的出口码必然是 0,带回来只是一个恒为 0 的死字段;失败侧 +/// (`CommandFailed`)自己带退出码。 #[derive(Clone, Debug, PartialEq, Eq, Serialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/export/generated/"))] pub(crate) struct XHSMiniToolExportRunResult { - pub(crate) exit_code: Option, - /// 成功时为空;失败时是被截断的输出尾部(stdout 与 stderr 合并),够定位就行。 + /// 构建输出尾部(stdout 与 stderr 合并)。打包脚本会把 zip 的条目数与体积打在这里。 pub(crate) output_tail: String, } diff --git a/apps/ai-game-creator-shell/src/view/project-development/export/generated/XHSMiniToolExportRunResult.ts b/apps/ai-game-creator-shell/src/view/project-development/export/generated/XHSMiniToolExportRunResult.ts index edcc30c11..ec9a4b976 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/export/generated/XHSMiniToolExportRunResult.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/export/generated/XHSMiniToolExportRunResult.ts @@ -2,9 +2,12 @@ /** * 一次成功导出的结果。产物路径是常量(`.export/xhs-minitool.zip`),不进 DTO。 + * + * 不带 `exit_code`:走到这里的出口码必然是 0,带回来只是一个恒为 0 的死字段;失败侧 + * (`CommandFailed`)自己带退出码。 */ -export type XHSMiniToolExportRunResult = { exitCode: number | null, +export type XHSMiniToolExportRunResult = { /** - * 成功时为空;失败时是被截断的输出尾部(stdout 与 stderr 合并),够定位就行。 + * 构建输出尾部(stdout 与 stderr 合并)。打包脚本会把 zip 的条目数与体积打在这里。 */ outputTail: string, };