Files
openclaw/test/scripts/install-ps1.test.ts
2026-05-02 05:49:45 +01:00

230 lines
8.4 KiB
TypeScript

import { spawnSync } from "node:child_process";
import { chmodSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { createScriptTestHarness } from "./test-helpers";
const SCRIPT_PATH = "scripts/install.ps1";
const ENTRYPOINT_RE =
/\r?\n\$mainResults = @\(Main\)\r?\n\$installSucceeded = \$mainResults\.Count -gt 0 -and \$mainResults\[-1\] -eq \$true\r?\nComplete-Install -Succeeded:\$installSucceeded\s*$/m;
function extractFunctionBody(source: string, name: string): string {
const match = source.match(
new RegExp(`^function ${name} \\{\\r?\\n([\\s\\S]*?)^\\}\\r?\\n`, "m"),
);
expect(match?.[1]).toBeDefined();
return match![1];
}
function findPowerShell(): string | undefined {
for (const candidate of ["pwsh", "powershell"]) {
const result = spawnSync(
candidate,
["-NoLogo", "-NoProfile", "-Command", "$PSVersionTable.PSVersion"],
{
encoding: "utf8",
},
);
if (result.status === 0) {
return candidate;
}
}
return undefined;
}
function toPowerShellSingleQuotedLiteral(value: string): string {
return `'${value.replaceAll("'", "''")}'`;
}
function createFailingNodeFixture(source: string): string {
const scriptWithoutEntryPoint = source.replace(ENTRYPOINT_RE, "");
expect(scriptWithoutEntryPoint).not.toBe(source);
return [
scriptWithoutEntryPoint,
"",
"function Write-Banner { }",
"function Ensure-ExecutionPolicy { return $true }",
"function Ensure-Node { return $false }",
"",
"$mainResults = @(Main)",
"$installSucceeded = $mainResults.Count -gt 0 -and $mainResults[-1] -eq $true",
"Complete-Install -Succeeded:$installSucceeded",
"",
].join("\n");
}
describe("install.ps1 failure handling", () => {
const harness = createScriptTestHarness();
const source = readFileSync(SCRIPT_PATH, "utf8");
const powershell = findPowerShell();
const runIfPowerShell = powershell ? it : it.skip;
it("does not exit directly from inside Main", () => {
const mainBody = extractFunctionBody(source, "Main");
expect(mainBody).not.toMatch(/\bexit\b/i);
expect(mainBody).toContain("return (Fail-Install)");
});
it("keeps failure termination in the top-level completion handler", () => {
const completeInstallBody = extractFunctionBody(source, "Complete-Install");
expect(completeInstallBody).toMatch(/\$PSCommandPath/);
expect(completeInstallBody).toMatch(/\bexit \$script:InstallExitCode\b/);
expect(completeInstallBody).toMatch(/\bthrow "OpenClaw installation failed with exit code/);
});
it("runs npm capture commands from a writable installer temp directory", () => {
const nativeCaptureBody = extractFunctionBody(source, "Invoke-NativeCommandCapture");
const npmInstallBody = extractFunctionBody(source, "Install-OpenClawNpm");
const mainBody = extractFunctionBody(source, "Main");
expect(source).toContain("function Get-NpmWorkingDirectory {");
expect(nativeCaptureBody).toContain('[string]$WorkingDirectory = ""');
expect(nativeCaptureBody).toContain("$startProcessArgs.WorkingDirectory = $WorkingDirectory");
expect(npmInstallBody).toContain("-WorkingDirectory (Get-NpmWorkingDirectory)");
expect(mainBody).toContain("-WorkingDirectory (Get-NpmWorkingDirectory)");
});
runIfPowerShell("creates a temp npm working directory", () => {
const tempDir = harness.createTempDir("openclaw-install-ps1-");
const scriptPath = join(tempDir, "install.ps1");
const scriptWithoutEntryPoint = source.replace(ENTRYPOINT_RE, "");
writeFileSync(
scriptPath,
[
scriptWithoutEntryPoint,
"",
"$result = Get-NpmWorkingDirectory",
'if (!(Test-Path -LiteralPath $result)) { throw "missing $result" }',
'if ($result -notmatch "openclaw-installer") { throw "unexpected $result" }',
"",
].join("\n"),
);
chmodSync(scriptPath, 0o755);
const result = spawnSync(
powershell!,
["-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", scriptPath],
{ encoding: "utf8" },
);
expect(result.status).toBe(0);
expect(result.stderr).toBe("");
});
runIfPowerShell("exits non-zero when run as a script file", () => {
const tempDir = harness.createTempDir("openclaw-install-ps1-");
const scriptPath = join(tempDir, "install.ps1");
writeFileSync(scriptPath, createFailingNodeFixture(source));
chmodSync(scriptPath, 0o755);
const result = spawnSync(
powershell!,
["-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", scriptPath],
{ encoding: "utf8" },
);
expect(result.status).toBe(1);
});
runIfPowerShell("throws without killing the caller when run as a scriptblock", () => {
const tempDir = harness.createTempDir("openclaw-install-ps1-");
const scriptPath = join(tempDir, "install.ps1");
writeFileSync(scriptPath, createFailingNodeFixture(source));
chmodSync(scriptPath, 0o755);
const command = [
"try {",
` & ([scriptblock]::Create((Get-Content -LiteralPath ${toPowerShellSingleQuotedLiteral(scriptPath)} -Raw)))`,
"} catch {",
' Write-Output "caught=$($_.Exception.Message)"',
"}",
'Write-Output "alive-after-install"',
].join("\n");
const result = spawnSync(powershell!, ["-NoLogo", "-NoProfile", "-Command", command], {
encoding: "utf8",
});
expect(result.status).toBe(0);
expect(result.stdout).toContain("caught=OpenClaw installation failed with exit code 1.");
expect(result.stdout).toContain("alive-after-install");
});
runIfPowerShell("keeps npm chatter out of Main's success return value", () => {
const tempDir = harness.createTempDir("openclaw-install-ps1-");
const scriptPath = join(tempDir, "install.ps1");
const scriptWithoutEntryPoint = source.replace(ENTRYPOINT_RE, "");
writeFileSync(
scriptPath,
[
scriptWithoutEntryPoint,
"",
"function Write-Banner { }",
"function Ensure-ExecutionPolicy { return $true }",
"function Ensure-Node { return $true }",
"function Add-ToPath { param([string]$Path) }",
"function Invoke-NativeCommandCapture {",
" param([string]$FilePath, [string[]]$Arguments, [string]$WorkingDirectory = '')",
" return @{ ExitCode = 0; Stdout = 'npm stdout'; Stderr = 'npm stderr' }",
"}",
"$NoOnboard = $true",
"$result = Main",
"if ($result -is [array]) { throw 'Main returned an array' }",
'if ($result -ne $true) { throw "Main returned $result" }',
"",
].join("\n"),
);
chmodSync(scriptPath, 0o755);
const result = spawnSync(
powershell!,
["-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", scriptPath],
{ encoding: "utf8" },
);
expect(result.status).toBe(0);
expect(result.stderr).toBe("");
});
runIfPowerShell("uses Main's final boolean result when helper output precedes success", () => {
const tempDir = harness.createTempDir("openclaw-install-ps1-");
const scriptPath = join(tempDir, "install.ps1");
const scriptWithoutEntryPoint = source.replace(ENTRYPOINT_RE, "");
writeFileSync(
scriptPath,
[
scriptWithoutEntryPoint,
"",
"function Write-Banner { }",
"function Ensure-ExecutionPolicy { return $true }",
"function Ensure-Node { return $true }",
"function Ensure-Git { return $true }",
"function Add-ToPath { param([string]$Path) }",
"function Install-OpenClawNpm {",
" param([string]$Target = 'latest')",
" Write-Output 'native chatter'",
" return $true",
"}",
"function Invoke-NativeCommandCapture {",
" param([string]$FilePath, [string[]]$Arguments, [string]$WorkingDirectory = '')",
" return @{ ExitCode = 0; Stdout = 'npm prefix'; Stderr = '' }",
"}",
"$NoOnboard = $true",
"$mainResults = @(Main)",
"$installSucceeded = $mainResults.Count -gt 0 -and $mainResults[-1] -eq $true",
"Complete-Install -Succeeded:$installSucceeded",
"",
].join("\n"),
);
chmodSync(scriptPath, 0o755);
const result = spawnSync(
powershell!,
["-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", scriptPath],
{ encoding: "utf8" },
);
expect(result.status).toBe(0);
expect(result.stderr).toBe("");
});
});