From 7703dcc1f48c41df006efb34be0a067097902e2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Tue, 6 Oct 2026 21:44:17 +0800 Subject: [PATCH] =?UTF-8?q?=E8=84=9A=E6=9C=AC=E6=B2=A1=E5=90=AF=E5=8A=A8?= =?UTF-8?q?=E5=BD=92=E4=B8=BA=E5=AE=BF=E4=B8=BB=E4=BE=A7=E6=95=85=E9=9A=9C?= =?UTF-8?q?=E8=80=8C=E4=B8=8D=E6=98=AF=E5=91=BD=E4=BB=A4=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit export/draft/xhs_minitool/build.rs:conclude 增加 script_launched 判据,用 target_exec != established 区分「根本没跑起来」与「跑了被信号杀掉」;前者报 exportUnavailable,由前端既有路径抛进错误池上报,后者维持 commandFailed。补对应单测。 --- .../src/export/draft/xhs_minitool/build.rs | 61 +++++++++++++------ 1 file changed, 44 insertions(+), 17 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 011748199..fbcaf793a 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 @@ -49,16 +49,16 @@ pub(crate) async fn run( .await .map_err(ExportUnavailable::new)?; + // `target_exec != "established"` 就是「脚本根本没跑起来」:沙箱建不起来(`not-attempted`)或目标 + // 可执行文件执行失败(`failed`)。这跟「脚本跑了但被信号杀掉」(`exit_code` 同样是 None,但 + // `target_exec` 是 `established`)不是一回事,所以判据用它,而不是 `exit_code.is_none()`。 + let script_launched = result.target_exec == "established"; + let exit_code = result.exit_code; + let timed_out = result.timed_out; + let output = result.output; + // 收尾判产物同样是同步 IO(symlink_metadata),一样搬去阻塞线程池。 - offload(move || { - conclude( - &root, - result.exit_code, - result.timed_out, - &result.output, - ) - }) - .await + offload(move || conclude(&root, exit_code, timed_out, script_launched, &output)).await } /// 构建前置(同步):找到脚本、确认权限点位没被拒、清掉上一次的产物。 @@ -99,16 +99,28 @@ fn prepare(root: &Path) -> Result { Ok(script) } -/// 把宿主的构建结果收成导出结果:先判成功,再确认产物真的在。 +/// 把宿主的构建结果收成导出结果:先判脚本有没有真的跑起来,再判成功,最后确认产物真的在。 /// -/// 只吃三个字段而不是整份 `ProjectVerificationResult`:这条判据是导出链路里唯一"脚本跑完之后 -/// 算不算数"的规则,抽成纯函数才能脱离"真的跑一遍 npm"逐条测(非零退出、超时、产物缺失、成功)。 +/// 只吃四个字段而不是整份 `ProjectVerificationResult`:这条判据是导出链路里唯一"脚本跑完之后 +/// 算不算数"的规则,抽成纯函数才能脱离"真的跑一遍 npm"逐条测(未启动、非零退出、超时、产物缺失、 +/// 成功)。 fn conclude( root: &Path, exit_code: Option, timed_out: bool, + script_launched: bool, output: &str, ) -> Result { + if !script_launched { + // 脚本根本没跑起来(沙箱建不起来 / 目标可执行文件执行失败):这是宿主侧事实故障, + // 不是项目代码的毛病。报 `exportUnavailable` 让前端按宿主故障处理(并抛进错误池上报), + // 而不是 `commandFailed` 去引导 code agent 改一份它改不动的宿主环境。 + let (output_tail, _) = tail(output); + return Err(XHSMiniToolExportBuildError::ExportUnavailable( + ExportUnavailable::new(output_tail), + )); + } + if timed_out || exit_code != Some(0) { let (output_tail, omitted_characters) = tail(output); return Err(XHSMiniToolExportBuildError::CommandFailed(CommandFailed { @@ -216,7 +228,7 @@ mod tests { 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入口不存在") + let error = conclude(&root, Some(1), false, true, "vite 构建失败\n入口不存在") .expect_err("non-zero exit must fail"); let XHSMiniToolExportBuildError::CommandFailed(failure) = error else { panic!("非零退出必须映射成 commandFailed"); @@ -230,7 +242,7 @@ mod tests { fn a_timeout_still_reports_command_failed_without_an_exit_code() { let root = fixture_root("timeout"); - let error = conclude(&root, None, true, "已取消") + let error = conclude(&root, None, true, true, "已取消") .expect_err("timeout must fail even with exit code 0"); let XHSMiniToolExportBuildError::CommandFailed(failure) = error else { panic!("超时必须映射成 commandFailed"); @@ -244,7 +256,7 @@ mod tests { 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 error = conclude(&root, Some(0), false, true, "构建完成").expect_err("产物缺失必须失败"); let XHSMiniToolExportBuildError::ArtifactMissing(missing) = error else { panic!("没有 zip 必须映射成 artifactMissing"); }; @@ -257,7 +269,7 @@ mod tests { write_artifact(&root, b""); assert!(matches!( - conclude(&root, Some(0), false, "构建完成"), + conclude(&root, Some(0), false, true, "构建完成"), Err(XHSMiniToolExportBuildError::ArtifactMissing(_)) )); } @@ -274,7 +286,7 @@ mod tests { assert!( matches!( - conclude(&root, Some(0), false, "构建完成"), + conclude(&root, Some(0), false, true, "构建完成"), Err(XHSMiniToolExportBuildError::ArtifactMissing(_)) ), "指向项目外非空文件的链接不能算产出成功" @@ -290,6 +302,7 @@ mod tests { &root, Some(0), false, + true, "zip 已生成:.export/xhs-minitool.zip(3 个条目)", ) .expect("产物在就应当成功"); @@ -297,6 +310,20 @@ mod tests { assert_eq!(result.omitted_characters, 0); } + #[test] + fn a_script_that_never_launched_is_host_unavailable_not_command_failed() { + let root = fixture_root("not-launched"); + + // 沙箱建不起来或目标可执行文件执行失败:`exit_code` 同样是 None,但脚本根本没跑, + // 必须报宿主侧故障,不能让「让陶泥儿修」的按钮出现在一个它改不动的环境问题上。 + let error = conclude(&root, None, false, false, "命令沙箱不可用") + .expect_err("脚本没跑起来必须失败"); + let XHSMiniToolExportBuildError::ExportUnavailable(unavailable) = error else { + panic!("脚本未启动必须映射成 exportUnavailable"); + }; + assert_eq!(unavailable.cause, "命令沙箱不可用"); + } + /// 真起进程跑一遍宿主链路:解析 npm 包、走 `command.exec` 边界执行项目脚本、再按产物判据收尾。 /// /// 默认 `#[ignore]`:它要本机 `node` / `npm` 与可用的命令沙箱,慢且依赖宿主环境,不进常规单测。