From b7b066e5ca0e8a16b13c37a308419bc475cd459b Mon Sep 17 00:00:00 2001 From: LIghtJUNction Date: Mon, 21 Sep 2026 14:16:46 +0800 Subject: [PATCH 1/4] fix: delegate Pi setup to official cross-platform installers Keep upstream dependency, permission, PATH and cancellation handling intact. Run the LMM plugin follow-up only for its tested host, without downgrading Pi. Retain native Termux dependencies and add installer-delegation regressions. --- README.md | 14 +++++----- pi.ps1 | 9 +++++-- pi.sh | 14 +++++++--- test.ps1 | 36 +++++++++++++++++++++---- test.py | 78 ++++++++++++++++++++++++++++++++++++++++++++++--------- 5 files changed, 122 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index bc55879..1fe37bd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # LMM 安装脚本 -根目录直接维护脚本,不生成代码。没有代理配置、测速、镜像、私有 Node、缓存或回滚框架。 +根目录直接维护脚本,不生成代码。没有代理配置、测速、镜像、私有 Node、缓存或回滚框架;官方安装器自己的依赖安装和检查保持原样。 ```sh curl -fsSLo menu.sh https://raw.githubusercontent.com/TokenNotIncluded/lmm-scripts/main/menu.sh @@ -15,7 +15,7 @@ irm https://raw.githubusercontent.com/TokenNotIncluded/lmm-scripts/main/menu.ps1 | 文件名(`.sh` / `.ps1`) | 安装方式 | 启动 | |---|---|---| -| `pi` | 官方 npm 命令,加 LMM 插件 | `pi`,然后 `/login` | +| `pi` | `pi.dev/install.sh` / `install.ps1`,保留官方交互和依赖处理 | `pi`,然后 `/login` | | `dsh` | npm + pnpm,加 LMM 插件;默认 web profile | `dsh web` | | `codex` | 官方原生安装器,参数直接传给官方 | `codex` | | `claude-code` | 官方原生安装器,参数直接传给官方 | `claude` | @@ -23,11 +23,13 @@ irm https://raw.githubusercontent.com/TokenNotIncluded/lmm-scripts/main/menu.ps1 | `clash-verge-rev` | Linux deb/rpm/AUR,macOS Homebrew,Windows WinGet | 桌面应用 | | `lmm` | 0.1.0 预览版发行包;另支持 `--from-source` / `-FromSource` | 安装结束打印的完整路径 | -Pi/DSH 的宿主和插件保留已适配版本;其余工具交给上游维护版本。只安装软件,不写供应商配置、不登录账号、不启用系统代理或 TUN。 +Pi 的版本、Node、安装位置、权限和 Windows Git Bash 交给官方安装器。官方正常安装后,才从它选出的安装路径接着装 LMM 插件;取消或卸载不继续。现有 LMM alpha 只验证过 Pi 0.85.1,其他版本会明确跳过插件,不偷偷降级宿主。DSH 的宿主和插件继续固定已适配版本;其当前官方 README 使用 npx,没有现行的独立安装脚本,不能拿归档记录中的旧脚本替代。 + +不写供应商配置、不登录账号、不启用系统代理或 TUN。 ## 环境 -Unix 入口需要 Bash、curl。Pi/DSH 另需 Node 22.19+(22.x)或 24+、npm;Windows 的 Pi 需要 Git Bash。脚本不代装 Node,不修改 npm registry。 +Unix 入口需要 Bash、curl。Pi 的依赖提示由官方处理;DSH 另需 Node 22.19+(22.x)或 24+、npm。不修改 npm registry。 Codex/Claude 的 Linux、macOS、Windows 安装都用官方脚本,不要求 Node,也不限定 apt 发行版。先满足上游的系统和运行库要求。Alpine 的 Claude 需要 `bash curl libgcc libstdc++ ripgrep`,运行时使用 `USE_BUILTIN_RIPGREP=0 claude`。NixOS 请使用 Nix 包环境,不保证通用二进制可运行。 @@ -37,7 +39,7 @@ LMM 预览包仅提供 Linux x64(glibc 2.39+)、macOS arm64、Windows x64, ## Termux -Pi 使用 Termux 的原生 Node/npm:`pkg install nodejs npm git`,然后 `bash pi.sh`。DSH 使用同一 npm 安装方式,Android 原生依赖尚未真机验证。 +Pi 先运行 `pkg install nodejs npm git` 准备 Android 原生依赖,再调用同一个官方安装器;没有 `TMPDIR` 时使用 `$PREFIX/tmp`。DSH 使用同一 npm 安装方式,Android 原生依赖尚未真机验证。 Codex/Claude 使用**已有的 PRoot Linux 环境**,不是 Android 原生二进制。先准备 `proot-distro` 和 Ubuntu guest,并在 guest 中安装 Bash、curl、CA 证书,再运行相应脚本。其他 guest 用 `LMM_DISTRO=名称 bash codex.sh`。安装后进入同一 guest 运行 `codex` / `claude`;不再生成转发启动器,不创建或重置 guest,不关闭沙箱。两个桌面工具不支持 Termux,菜单会隐藏它们。 @@ -49,6 +51,6 @@ Codex/Claude 使用**已有的 PRoot Linux 环境**,不是 Android 原生二 ## 依据与测试 -2026-09-21 核查:[Codex](https://learn.chatgpt.com/docs/codex/cli) · [Claude Code](https://code.claude.com/docs/en/setup) · [Pi](https://pi.dev/docs/latest/quickstart) · [Pi Termux](https://pi.dev/docs/latest/termux) · [DSH](https://github.com/deepseek-ai/deepseek-harness) · [DSH 插件](https://deepseek-harness.github.io/deepseek-harness/en/develop/basic/publish) · [CC Switch](https://github.com/farion1231/cc-switch#download--installation) · [Clash Verge Rev](https://www.clashverge.dev/install.html)。LMM 插件适配版本见 [Pi 插件](https://github.com/TokenNotIncluded/pi-lmm-provider) 和 [DSH 插件](https://github.com/TokenNotIncluded/dsh-lmm-provider)。 +2026-09-21 核查:[Pi 官网安装入口](https://pi.dev/)([Shell](https://pi.dev/install.sh) / [PowerShell](https://pi.dev/install.ps1))· [Codex](https://learn.chatgpt.com/docs/codex/cli) · [Claude Code](https://code.claude.com/docs/en/setup) · [Pi Termux](https://pi.dev/docs/latest/termux) · [DSH 当前 README](https://github.com/deepseek-ai/deepseek-harness/blob/master/README.md) · [DSH 插件](https://deepseek-harness.github.io/deepseek-harness/en/develop/basic/publish) · [CC Switch](https://github.com/farion1231/cc-switch#download--installation) · [Clash Verge Rev](https://www.clashverge.dev/install.html)。LMM 插件适配版本见 [Pi 插件](https://github.com/TokenNotIncluded/pi-lmm-provider) 和 [DSH 插件](https://github.com/TokenNotIncluded/dsh-lmm-provider)。 本地检查:`python3 test.py`、`pwsh -NoProfile -File test.ps1`、`shellcheck *.sh`。CI 另做三平台 CLI 实装和 Debian/Alpine 实装;不把模拟测试当作桌面 GUI、Termux 真机、账号登录或代理功能实测。 diff --git a/pi.ps1 b/pi.ps1 index 37155c4..22ac019 100644 --- a/pi.ps1 +++ b/pi.ps1 @@ -1,6 +1,11 @@ $ErrorActionPreference = 'Stop' if ($args.Count) { throw 'Usage: .\pi.ps1' } -npm.cmd install -g --ignore-scripts @earendil-works/pi-coding-agent@0.85.1 +$installer = Invoke-RestMethod https://pi.dev/install.ps1 +& ([scriptblock]::Create($installer + "`n" + @' +$pi = Join-Path (Get-PiBinDir) 'pi.cmd' +$version = & $pi --version if ($LASTEXITCODE) { exit $LASTEXITCODE } -pi.cmd install npm:@tokennotincluded/pi-lmm-provider@0.1.0-alpha.1 +if ($version -ne '0.85.1') { Write-Warning "Pi $version installed; LMM alpha requires 0.85.1, plugin skipped."; exit 0 } +& $pi install npm:@tokennotincluded/pi-lmm-provider@0.1.0-alpha.1 exit $LASTEXITCODE +'@)) diff --git a/pi.sh b/pi.sh index 55f1220..2a6a0d1 100755 --- a/pi.sh +++ b/pi.sh @@ -1,6 +1,12 @@ #!/usr/bin/env bash -set -euo pipefail +set -e [[ $# = 0 ]] || { echo 'Usage: bash pi.sh' >&2; exit 2; } -# Pin the host version required by the LMM alpha plugin. -npm install -g --ignore-scripts @earendil-works/pi-coding-agent@0.85.1 -pi install npm:@tokennotincluded/pi-lmm-provider@0.1.0-alpha.1 +if [[ -n ${TERMUX_VERSION:-} || ${PREFIX:-} == */com.termux/files/usr ]]; then + pkg install nodejs npm git + export TMPDIR="${TMPDIR:-${PREFIX:-/data/data/com.termux/files/usr}/tmp}" +fi +installer=$(curl -fsSL https://pi.dev/install.sh) +exec sh -c "$installer"' +pi=$(pi_installed_path); version=$("$pi" --version) +[ "$version" = 0.85.1 ] || { echo "Pi $version installed; LMM alpha requires 0.85.1, plugin skipped." >&2; exit 0; } +"$pi" install npm:@tokennotincluded/pi-lmm-provider@0.1.0-alpha.1' diff --git a/test.ps1 b/test.ps1 index 66c91be..0378f9c 100644 --- a/test.ps1 +++ b/test.ps1 @@ -5,6 +5,7 @@ foreach ($file in Get-ChildItem $PSScriptRoot -Filter '*.ps1') { if ($errors.Count) { throw ($errors | Out-String) } } $engine = (Get-Process -Id $PID).Path +$checks = 0 function Check([string]$File,[string]$Setup,[int]$Code,[string]$Pattern,[string]$Arguments='') { $ErrorActionPreference = 'Continue' $path=(Join-Path $PSScriptRoot $File).Replace("'","''") @@ -12,20 +13,45 @@ function Check([string]$File,[string]$Setup,[int]$Code,[string]$Pattern,[string] $encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($codeText)) $output = & $engine -NoProfile -NonInteractive -EncodedCommand $encoded 2>&1 | Out-String if ($LASTEXITCODE -ne $Code -or $output -notmatch $Pattern) { throw "$File expected exit $Code / $Pattern, received $LASTEXITCODE : $output" } + $script:checks++ } +$piSetup = @' +$env:PI_TEST_INSTALLER = 'function Get-PiBinDir { "official-bin" }; $env:PI_VENDOR_ENV="ready"' +$env:PI_TEST_VERSION = '0.85.1' +function Invoke-RestMethod { + if ($args[0] -ne 'https://pi.dev/install.ps1') { throw 'wrong installer URL' } + return $env:PI_TEST_INSTALLER +} +function Join-Path { + param($Path,$ChildPath) + if ($Path -ne 'official-bin' -or $ChildPath -ne 'pi.cmd') { throw 'wrong installed path' } + return 'official-pi' +} +function official-pi { + if ($env:PI_VENDOR_ENV -ne 'ready') { throw 'official environment lost' } + $global:LASTEXITCODE=0 + if ($args[0] -eq '--version') { $env:PI_TEST_VERSION } + else { Write-Output ($args -join '|') } +} +function npm.cmd { throw 'must not replace the official installer with npm' } +function pi.cmd { throw 'must not invoke stale pi on PATH' } +'@ +Check 'pi.ps1' $piSetup 0 'install\|npm:@tokennotincluded/pi-lmm-provider' +Check 'pi.ps1' ($piSetup+"`n`$env:PI_TEST_INSTALLER='Write-Output cancelled; exit 0'") 0 'cancelled' +Check 'pi.ps1' ($piSetup+"`n`$env:PI_TEST_INSTALLER='Write-Output failed; exit 9'") 9 'failed' +Check 'pi.ps1' ($piSetup+"`n`$env:PI_TEST_VERSION='0.86.1'; function official-pi { if (`$args[0] -ne '--version') { throw 'unsupported plugin must not run' }; `$env:PI_TEST_VERSION }") 0 'plugin skipped' +Check 'pi.ps1' ($piSetup+"`nfunction official-pi { Write-Output broken; `$global:LASTEXITCODE=8 }") 8 'broken' +Check 'pi.ps1' ($piSetup+"`nfunction official-pi { if (`$args[0] -eq '--version') { '0.85.1'; `$global:LASTEXITCODE=0 } else { Write-Output plugin-failed; `$global:LASTEXITCODE=7 } }") 7 'plugin-failed' +Check 'pi.ps1' "function Invoke-RestMethod { throw 'download-failed' }" 1 'download-failed' $npmFailure = @' function npm.cmd { Write-Output 'npm failed'; $global:LASTEXITCODE=9 } -function pi.cmd { throw 'plugin must not run' } function dsh.cmd { throw 'plugin must not run' } '@ -Check 'pi.ps1' $npmFailure 9 'npm failed' Check 'dsh.ps1' $npmFailure 9 'npm failed' $npmSuccess = @' function npm.cmd { Write-Output ($args -join '|'); $global:LASTEXITCODE=0 } -function pi.cmd { Write-Output ($args -join '|'); $global:LASTEXITCODE=0 } function dsh.cmd { Write-Output ($args -join '|'); $global:LASTEXITCODE=0 } '@ -Check 'pi.ps1' $npmSuccess 0 'install\|npm:@tokennotincluded/pi-lmm-provider' Check 'dsh.ps1' $npmSuccess 0 'plugin\|--profile\|headless' '-Profile headless' $upstream = @' function Invoke-RestMethod { return 'param([string]$Version) Write-Output "upstream:$Version"' } @@ -53,4 +79,4 @@ function Start-Process { '@ Check 'cc-switch.ps1' ($msi+"`n`$env:PROCESSOR_ARCHITECTURE='AMD64'") 0 'https://test.invalid/x64' Check 'cc-switch.ps1' ($msi+"`n`$env:PROCESSOR_ARCHITECTURE='ARM64'") 0 'https://test.invalid/arm64' -Write-Output 'PowerShell syntax and 11 command-contract checks passed.' +Write-Output "PowerShell syntax and $checks command-contract checks passed." diff --git a/test.py b/test.py index dcbc8d0..898bc50 100644 --- a/test.py +++ b/test.py @@ -21,7 +21,21 @@ print('touch "$HOME/should-not-exist"') sys.exit(18) url = next(a for a in args if a.startswith('https://')) - if '/codex/install.sh' in url or url == 'https://claude.ai/install.sh': + if url == 'https://pi.dev/install.sh': + # A new prefix outside PATH: never fall back to the old `pi` stub. + entry = Path(os.environ['HOME'], "official pi's bin", 'pi') + entry.parent.mkdir() + entry.write_text('#!' + sys.executable + '\n' + "import json, os, sys\n" + + "assert os.environ.get('PI_VENDOR_ENV') == 'ready'\n" + + "with open(os.environ['LOG'], 'a') as f: f.write(json.dumps(['official-pi', sys.argv[1:]])+'\\n')\n" + + "if sys.argv[1:] == ['--version']:\n" + + " print(os.environ.get('PI_VERSION', '0.85.1')); sys.exit(int(os.environ.get('PI_VERSION_EXIT', '0')))\n" + + "sys.exit(int(os.environ.get('COMMAND_EXIT', '0')))\n") + entry.chmod(0o755) + print('set -eu\nexport PI_VENDOR_ENV=ready\npi_installed_path() { printf "%s" "$HOME/official pi\'s bin/pi"; }') + if 'PI_INSTALLER_EXIT' in os.environ: + print('exit ' + os.environ['PI_INSTALLER_EXIT']) + elif '/codex/install.sh' in url or url == 'https://claude.ai/install.sh': print('printf "%s\\n" "$@" > "$HOME/received-args"') elif url.endswith('/desktop.sh'): print(Path(os.environ['PROJECT'], 'desktop.sh').read_text()) @@ -48,6 +62,7 @@ # Record the actual guest invocation; do not fake an Android binary. sys.exit(0) elif name == 'npm': sys.exit(int(os.environ.get('NPM_EXIT','0'))) +elif name == 'pkg': sys.exit(int(os.environ.get('PKG_EXIT','0'))) elif name == 'sudo': os.execvp(args[0],args) else: sys.exit(int(os.environ.get('COMMAND_EXIT','0'))) ''' @@ -63,10 +78,10 @@ def setUp(self): # Keep host package managers out of simulated platform tests. for name in ('bash','sh','dirname','mktemp','rm','mkdir','install'): os.symlink(shutil.which(name), self.bin/name) - for name in ('curl','npm','pi','dsh','uname','jq','tar','proot-distro','brew','cargo','sudo'): + for name in ('curl','npm','pi','dsh','uname','jq','tar','proot-distro','brew','cargo','sudo','pkg'): self.stub(name) self.env = dict(os.environ, HOME=str(self.home), PATH=str(self.bin), LOG=str(self.log), PROJECT=str(ROOT), REAL_JQ=shutil.which('jq')) - for key in ('TERMUX_VERSION','PREFIX','LMM_DISTRO'): + for key in ('TERMUX_VERSION','PREFIX','LMM_DISTRO','PI_VENDOR_ENV'): self.env.pop(key,None) def tearDown(self): @@ -83,17 +98,52 @@ def run_script(self,name,*args,**env): def calls(self): return [json.loads(line) for line in self.log.read_text().splitlines()] - def test_pi_and_dsh_keep_official_commands_and_lmm_plugins(self): - self.assertEqual(self.run_script('pi.sh').returncode,0) - calls=self.calls() - self.assertIn('--ignore-scripts', calls[0][1]) - self.assertEqual(calls[1],['pi',['install','npm:@tokennotincluded/pi-lmm-provider@0.1.0-alpha.1']]) + def test_pi_delegates_to_official_installer_and_uses_its_environment(self): + result=self.run_script('pi.sh') + self.assertEqual(result.returncode,0,result.stderr) + self.assertEqual(self.calls(),[ + ['curl',['-fsSL','https://pi.dev/install.sh']], + ['official-pi',['--version']], + ['official-pi',['install','npm:@tokennotincluded/pi-lmm-provider@0.1.0-alpha.1']], + ]) + + def test_pi_official_failure_or_cancellation_stops_plugin(self): + for code in ('9','0','130'): + with self.subTest(code=code): + result=self.run_script('pi.sh',PI_INSTALLER_EXIT=code) + self.assertEqual(result.returncode,int(code),result.stderr) + self.assertTrue(all(name=='curl' for name,_ in self.calls())) + shutil.rmtree(self.home/"official pi's bin") + + def test_pi_new_host_is_not_downgraded_for_old_plugin(self): + result=self.run_script('pi.sh',PI_VERSION='0.86.1') + self.assertEqual(result.returncode,0,result.stderr) + self.assertIn('plugin skipped',result.stderr) + self.assertEqual([name for name,_ in self.calls()],['curl','official-pi']) + + def test_pi_version_and_plugin_failures_propagate(self): + for env in ({'PI_VERSION_EXIT':'8'},{'COMMAND_EXIT':'7'}): + result=self.run_script('pi.sh',**env) + self.assertEqual(result.returncode,int(next(iter(env.values()))),result.stderr) + shutil.rmtree(self.home/"official pi's bin") + + def test_pi_termux_prepares_native_dependencies_then_delegates(self): + result=self.run_script('pi.sh',TERMUX_VERSION='test',PREFIX='/data/data/com.termux/files/usr') + self.assertEqual(result.returncode,0,result.stderr) + self.assertEqual(self.calls()[0],['pkg',['install','nodejs','npm','git']]) + self.assertEqual(self.calls()[1],['curl',['-fsSL','https://pi.dev/install.sh']]) + + def test_pi_termux_dependency_failure_stops_before_official_bootstrap(self): + result=self.run_script('pi.sh',PREFIX='/data/data/com.termux/files/usr',PKG_EXIT='7') + self.assertEqual(result.returncode,7,result.stderr) + self.assertEqual(self.calls(),[['pkg',['install','nodejs','npm','git']]]) + + def test_dsh_keeps_documented_commands_and_lmm_plugin(self): self.assertEqual(self.run_script('dsh.sh','headless').returncode,0) self.assertEqual(self.calls()[-1][1][:3],['plugin','--profile','headless']) def test_npm_failure_never_installs_plugin(self): - for script in ('pi.sh','dsh.sh'): - self.assertEqual(self.run_script(script,NPM_EXIT='9').returncode,9) + self.assertEqual(self.run_script('dsh.sh',NPM_EXIT='9').returncode,9) self.assertTrue(all(name=='npm' for name,_ in self.calls())) def test_removed_flags_fail_instead_of_silently_installing(self): @@ -109,7 +159,7 @@ def test_native_cli_delegates_and_preserves_arguments(self): self.assertTrue(all(name=='curl' for name,_ in self.calls())) def test_failed_download_does_not_execute_partial_script(self): - for name in ('codex.sh','claude-code.sh'): + for name in ('pi.sh','codex.sh','claude-code.sh'): self.assertNotEqual(self.run_script(name,FAIL_DOWNLOAD='1').returncode,0) self.assertFalse((self.home/'should-not-exist').exists()) @@ -190,7 +240,11 @@ def test_flat_layout_and_size_budget(self): self.assertNotIn('@MENU_REV@',text) self.assertNotIn('@DESKTOP_REV@',text) for name in ('codex','claude-code','pi','dsh'): - self.assertLessEqual(len((ROOT/f'{name}.sh').read_text().splitlines()),10) + self.assertLessEqual(len((ROOT/f'{name}.sh').read_text().splitlines()),12 if name=='pi' else 10) + for ext in ('sh','ps1'): + text=(ROOT/f'pi.{ext}').read_text() + self.assertIn(f'https://pi.dev/install.{ext}',text) + self.assertNotIn('@earendil-works/pi-coding-agent',text) if __name__=='__main__': unittest.main(verbosity=2) From 751ac3c404e97b951f8a4c71c7ddc9ab299e6940 Mon Sep 17 00:00:00 2001 From: LIghtJUNction Date: Mon, 21 Sep 2026 14:17:09 +0800 Subject: [PATCH 2/4] fix: point menus to Pi's official-installer entry --- menu.ps1 | 2 +- menu.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/menu.ps1 b/menu.ps1 index 6ad408e..56c5324 100644 --- a/menu.ps1 +++ b/menu.ps1 @@ -16,7 +16,7 @@ try { if ($PSScriptRoot -and (Test-Path (Join-Path $PSScriptRoot $name))) { $path = Join-Path $PSScriptRoot $name } else { - Invoke-WebRequest -UseBasicParsing "https://raw.githubusercontent.com/TokenNotIncluded/lmm-scripts/848c82c253b5d35712943e5a9990606e4613e7b9/$name" -OutFile $path + Invoke-WebRequest -UseBasicParsing "https://raw.githubusercontent.com/TokenNotIncluded/lmm-scripts/b7b066e5ca0e8a16b13c37a308419bc475cd459b/$name" -OutFile $path } & (Get-Process -Id $PID).Path -NoProfile -ExecutionPolicy Bypass -File $path if ($LASTEXITCODE) { Write-Warning "Installer exited with $LASTEXITCODE" } diff --git a/menu.sh b/menu.sh index f4da78e..1ddee00 100755 --- a/menu.sh +++ b/menu.sh @@ -16,7 +16,7 @@ while true; do if [[ -n ${BASH_SOURCE[0]:-} && -f $dir/$tool.sh ]]; then bash "$dir/$tool.sh" &2 else - script=$(curl -fsSL "https://raw.githubusercontent.com/TokenNotIncluded/lmm-scripts/848c82c253b5d35712943e5a9990606e4613e7b9/$tool.sh") || continue + script=$(curl -fsSL "https://raw.githubusercontent.com/TokenNotIncluded/lmm-scripts/b7b066e5ca0e8a16b13c37a308419bc475cd459b/$tool.sh") || continue bash -c "$script" &2 fi done From 75e97be1c45253f8012fb2da8ecabdd59b9b0d8e Mon Sep 17 00:00:00 2001 From: LIghtJUNction Date: Mon, 21 Sep 2026 14:19:29 +0800 Subject: [PATCH 3/4] test: capture PowerShell warning streams in installer contracts --- test.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test.ps1 b/test.ps1 index 0378f9c..08d28de 100644 --- a/test.ps1 +++ b/test.ps1 @@ -11,7 +11,7 @@ function Check([string]$File,[string]$Setup,[int]$Code,[string]$Pattern,[string] $path=(Join-Path $PSScriptRoot $File).Replace("'","''") $codeText = "`$ErrorActionPreference='Stop'; `$LASTEXITCODE=0; $Setup`n& '$path' $Arguments; exit `$LASTEXITCODE" $encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($codeText)) - $output = & $engine -NoProfile -NonInteractive -EncodedCommand $encoded 2>&1 | Out-String + $output = & $engine -NoProfile -NonInteractive -EncodedCommand $encoded *>&1 | Out-String if ($LASTEXITCODE -ne $Code -or $output -notmatch $Pattern) { throw "$File expected exit $Code / $Pattern, received $LASTEXITCODE : $output" } $script:checks++ } From 132f9cb57e79b1605f300180c98ba6d6a62eeb3b Mon Sep 17 00:00:00 2001 From: LIghtJUNction Date: Mon, 21 Sep 2026 14:21:52 +0800 Subject: [PATCH 4/4] test: assert Pi warning calls and failed version probes directly --- test.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test.ps1 b/test.ps1 index 08d28de..3fcdd27 100644 --- a/test.ps1 +++ b/test.ps1 @@ -33,6 +33,7 @@ function official-pi { if ($args[0] -eq '--version') { $env:PI_TEST_VERSION } else { Write-Output ($args -join '|') } } +function Write-Warning { param($Message) Write-Output $Message } function npm.cmd { throw 'must not replace the official installer with npm' } function pi.cmd { throw 'must not invoke stale pi on PATH' } '@ @@ -40,7 +41,7 @@ Check 'pi.ps1' $piSetup 0 'install\|npm:@tokennotincluded/pi-lmm-provider' Check 'pi.ps1' ($piSetup+"`n`$env:PI_TEST_INSTALLER='Write-Output cancelled; exit 0'") 0 'cancelled' Check 'pi.ps1' ($piSetup+"`n`$env:PI_TEST_INSTALLER='Write-Output failed; exit 9'") 9 'failed' Check 'pi.ps1' ($piSetup+"`n`$env:PI_TEST_VERSION='0.86.1'; function official-pi { if (`$args[0] -ne '--version') { throw 'unsupported plugin must not run' }; `$env:PI_TEST_VERSION }") 0 'plugin skipped' -Check 'pi.ps1' ($piSetup+"`nfunction official-pi { Write-Output broken; `$global:LASTEXITCODE=8 }") 8 'broken' +Check 'pi.ps1' ($piSetup+"`nfunction official-pi { if (`$args[0] -ne '--version') { throw 'plugin must not run' }; `$global:LASTEXITCODE=8 }") 8 '' Check 'pi.ps1' ($piSetup+"`nfunction official-pi { if (`$args[0] -eq '--version') { '0.85.1'; `$global:LASTEXITCODE=0 } else { Write-Output plugin-failed; `$global:LASTEXITCODE=7 } }") 7 'plugin-failed' Check 'pi.ps1' "function Invoke-RestMethod { throw 'download-failed' }" 1 'download-failed' $npmFailure = @'