按未 pin 回退 lts/codename 避免静默选中错误版本

- version_matches_pin 对 lts/<codename>(如 lts/iron)返回 None,不再当作匹配任意已安装版本
- 补充单测:lts/iron 回退、lts/* 仍匹配,并加入不支持 pin 的回退用例
- decision-log 记录 lts/<codename> 需要 codename → 版本行映射,当前按未 pin 回退
This commit is contained in:
2026-10-07 11:43:00 +08:00
parent 7ed3696959
commit d905fa1c02
2 changed files with 11 additions and 6 deletions
@@ -562,11 +562,14 @@ fn version_matches_pin(version: (u64, u64, u64), pin: &str) -> Option<bool> {
// `.nvmrc` 两种写法都常见:`v22.23.3` 与 `22.23.3`。
let pin = pin.strip_prefix('v').unwrap_or(pin);
let lower = pin.to_ascii_lowercase();
if matches!(lower.as_str(), "*" | "x" | "node" | "latest" | "lts/*")
|| lower.starts_with("lts/")
{
if matches!(lower.as_str(), "*" | "x" | "node" | "latest" | "lts/*") {
return Some(true);
}
if lower.starts_with("lts/") {
// `lts/<codename>`(如 `lts/iron` = Node 20)需要 codename → 版本行映射,当前不维护;
// 按“未 pin”回退,绝不能声称匹配任意版本而静默选中已安装的最高版本。
return None;
}
if lower.starts_with("iojs") || lower.contains("||") {
return None;
}
@@ -1369,7 +1372,7 @@ mod tests {
let fnm_root = tempfile::tempdir().unwrap();
install_fnm_version(fnm_root.path(), "v22.23.3");
let project = tempfile::tempdir().unwrap();
for pin in ["iojs\n", ">20\n", "<=20\n"] {
for pin in ["iojs\n", ">20\n", "<=20\n", "lts/iron\n"] {
fs::write(project.path().join(".node-version"), pin).unwrap();
let runtime = resolve_at_with_managed_roots(
project.path(),
@@ -1394,7 +1397,9 @@ mod tests {
assert_eq!(version_matches_pin((20, 11, 0), "~20.11.0"), Some(true));
assert_eq!(version_matches_pin((20, 11, 0), ">=20 <23"), Some(true));
assert_eq!(version_matches_pin((20, 11, 0), "lts/*"), Some(true));
assert_eq!(version_matches_pin((20, 11, 0), "lts/iron"), Some(true));
// codename 未维护映射时必须按未 pin 回退,不能匹配任意版本。
assert_eq!(version_matches_pin((20, 11, 0), "lts/iron"), None);
assert_eq!(version_matches_pin((22, 23, 3), "lts/iron"), None);
assert_eq!(version_matches_pin((20, 11, 0), ">=22 || 20"), None);
assert_eq!(version_matches_pin((20, 11, 0), ">20"), None);
assert_eq!(version_matches_pin((20, 11, 0), "<=20"), None);
@@ -4,7 +4,7 @@
- 背景:开发构建里 AGC 让命令沙箱执行 `npm run build` / `npm install` 时,宿主 Node 由 fnm 托管,`node` / `npm` 实际是随 shell 会话变化的 fnm multishell 目录里的 shim;bwrap `--tmpfs /run` 会抹掉该路径,而只按单文件挂载 `<前缀>/bin/npm`(它软链到 `lib/node_modules/npm/bin/npm-cli.js`)会因 `Cannot find module '../lib/cli.js'` 失败。此前把宿主 `node` / `npm` / `npx` shim 指到 `/usr/bin/*` 是错误取舍:系统 Node 26 默认启用实验性 Web Storage,会顶掉 vitest 0.34 jsdom 的 localStorage,使 AGC 测试套件在 HEAD 即失败(见 `pitfalls.md` 2026-10-03 条)。
- 决策(宿主发现与沙箱挂载共用窄叶校验):新增 `validate_node_installation_prefix`,canonicalize 后拒绝 `/home`、`/root`、`/tmp`、`/var`、`/etc`、`/proc`、`/dev`、`/run`、`/sys`、`/boot`、`/srv` 根、HOME 及其祖先和相对路径,并要求前缀同时含 `bin/node` 与 `lib/node_modules/npm/bin/npm-cli.js`(bundle 形态为 `<前缀>/node` + `node_modules/npm/bin/npm-cli.js`)。宿主版本枚举与 Linux 沙箱只读挂载都调用它,避免两处信任口径漂移。
- 决策(版本选择):`.nvmrc` / `.node-version` 是权威 pin,能理解但未安装时返回 `node-version-pinned-not-installed` 失败关闭;`package.json` `engines.node` 只是偏好,永不阻塞;不支持的写法(`iojs`、`||`、部分 `>` / `<=`、hyphen range、prerelease)按未 pin 回退。整体顺序为 pin 命中 > PATH 可解析的可用 Node > 版本管理器回退链(`engines` 最高匹配 > 活动版本 > 默认别名 > 已安装最高版本),只实现文档化比较子集(精确三元组、major、`>=` / `>` / `<=` / `<`、`^`、`~`、`x` / `*`、`lts/*`);`v22.23.3` 这类带 `v` 的 `.nvmrc` 必须先剥前缀。nvm 的 `alias/default` 常只写主版本号(如 `22`),必须按同一 pin 子集在已安装版本里选最高匹配,不能当成完整三元组 `(22,0,0)`。
- 决策(版本选择):`.nvmrc` / `.node-version` 是权威 pin,能理解但未安装时返回 `node-version-pinned-not-installed` 失败关闭;`package.json` `engines.node` 只是偏好,永不阻塞;不支持的写法(`iojs`、`||`、部分 `>` / `<=`、hyphen range、prerelease)按未 pin 回退。整体顺序为 pin 命中 > PATH 可解析的可用 Node > 版本管理器回退链(`engines` 最高匹配 > 活动版本 > 默认别名 > 已安装最高版本),只实现文档化比较子集(精确三元组、major、`>=` / `>` / `<=` / `<`、`^`、`~`、`x` / `*`、`lts/*`;`lts/<codename>` 需要 codename → 版本行映射,当前按未 pin 回退而不猜);`v22.23.3` 这类带 `v` 的 `.nvmrc` 必须先剥前缀。nvm 的 `alias/default` 常只写主版本号(如 `22`),必须按同一 pin 子集在已安装版本里选最高匹配,不能当成完整三元组 `(22,0,0)`。
- 决策(沙箱内启动形态):Linux npm 改为 `node <npm-cli.js>`,因为单文件挂载 npm 软链必然丢 `../lib/cli.js`;只读挂载整棵已验证的安装前缀(不是整个 HOME、`FNM_DIR` 或 `NVM_DIR`)。`npm install` 的联网判定跟随真实启动形态(`node` + `npm-cli.js` + `install`),不因包装变化丢 `--share-net`。
- 决策(范围与非目标):托管版本管理器发现只在 `debug_assertions` / development 生效,发布构建继续只认随包 bundle;不把 fnm / nvm CLI 做成沙箱内工具;Windows 不变;不新增 fnm / nvm 之外的版本管理器。
- 影响范围:`apps/ai-game-creator-shell/src-tauri/src/environment_check.rs`(窄叶校验、托管安装枚举、pin / engines 解析、选择与回退)、`command_sandbox.rs`(Node 工具链挂载收集与合并、`command_sandbox_requests_npm_install`、`FNM_MULTISHELL_PATH` 清理)、`command_exec.rs`(Linux npm `node_launcher`、`project_command_actual_target`、非 Node 程序 PATH 前置工具链 bin)、`process_session_bridge.rs`(`ProcessSessionLaunchPlan::from_launch` 改用实际目标)。