脚本没启动归为宿主侧故障而不是命令失败

export/draft/xhs_minitool/build.rs:conclude 增加 script_launched 判据,用 target_exec != established 区分「根本没跑起来」与「跑了被信号杀掉」;前者报 exportUnavailable,由前端既有路径抛进错误池上报,后者维持 commandFailed。补对应单测。
This commit is contained in:
2026-10-06 21:44:17 +08:00
parent 1ae7ae41c9
commit 7703dcc1f4
@@ -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<NpmScript, XHSMiniToolExportBuildError> {
Ok(script)
}
/// 把宿主的构建结果收成导出结果:先判成功,再确认产物真的在。
/// 把宿主的构建结果收成导出结果:先判脚本有没有真的跑起来,再判成功,最后确认产物真的在。
///
/// 只吃三个字段而不是整份 `ProjectVerificationResult`:这条判据是导出链路里唯一"脚本跑完之后
/// 算不算数"的规则,抽成纯函数才能脱离"真的跑一遍 npm"逐条测(非零退出、超时、产物缺失、成功)。
/// 只吃四个字段而不是整份 `ProjectVerificationResult`:这条判据是导出链路里唯一"脚本跑完之后
/// 算不算数"的规则,抽成纯函数才能脱离"真的跑一遍 npm"逐条测(未启动、非零退出、超时、产物缺失、
/// 成功)。
fn conclude(
root: &Path,
exit_code: Option<i32>,
timed_out: bool,
script_launched: bool,
output: &str,
) -> Result<XHSMiniToolExportRunResult, XHSMiniToolExportBuildError> {
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` 与可用的命令沙箱,慢且依赖宿主环境,不进常规单测。