
























@@ -0,0 +1,113 @@
1+import { spawnSync } from "node:child_process";
2+import { chmodSync, readFileSync, writeFileSync } from "node:fs";
3+import { join } from "node:path";
4+import { describe, expect, it } from "vitest";
5+import { createScriptTestHarness } from "./test-helpers";
6+7+const SCRIPT_PATH = "scripts/install.ps1";
8+9+function extractFunctionBody(source: string, name: string): string {
10+const match = source.match(
11+new RegExp(`^function ${name} \\{\\r?\\n([\\s\\S]*?)^\\}\\r?\\n`, "m"),
12+);
13+expect(match?.[1]).toBeDefined();
14+return match![1];
15+}
16+17+function findPowerShell(): string | undefined {
18+for (const candidate of ["pwsh", "powershell"]) {
19+const result = spawnSync(
20+candidate,
21+["-NoLogo", "-NoProfile", "-Command", "$PSVersionTable.PSVersion"],
22+{
23+encoding: "utf8",
24+},
25+);
26+if (result.status === 0) {
27+return candidate;
28+}
29+}
30+return undefined;
31+}
32+33+function toPowerShellSingleQuotedLiteral(value: string): string {
34+return `'${value.replaceAll("'", "''")}'`;
35+}
36+37+function createFailingNodeFixture(source: string): string {
38+const scriptWithoutEntryPoint = source.replace(
39+/\r?\n\$installSucceeded = Main\r?\nComplete-Install -Succeeded:\$installSucceeded\s*$/m,
40+"",
41+);
42+expect(scriptWithoutEntryPoint).not.toBe(source);
43+44+return [
45+scriptWithoutEntryPoint,
46+"",
47+"function Write-Banner { }",
48+"function Ensure-ExecutionPolicy { return $true }",
49+"function Ensure-Node { return $false }",
50+"",
51+"$installSucceeded = Main",
52+"Complete-Install -Succeeded:$installSucceeded",
53+"",
54+].join("\n");
55+}
56+57+describe("install.ps1 failure handling", () => {
58+const harness = createScriptTestHarness();
59+const source = readFileSync(SCRIPT_PATH, "utf8");
60+const powershell = findPowerShell();
61+const runIfPowerShell = powershell ? it : it.skip;
62+63+it("does not exit directly from inside Main", () => {
64+const mainBody = extractFunctionBody(source, "Main");
65+expect(mainBody).not.toMatch(/\bexit\b/i);
66+expect(mainBody).toContain("return (Fail-Install)");
67+});
68+69+it("keeps failure termination in the top-level completion handler", () => {
70+const completeInstallBody = extractFunctionBody(source, "Complete-Install");
71+expect(completeInstallBody).toMatch(/\$PSCommandPath/);
72+expect(completeInstallBody).toMatch(/\bexit \$script:InstallExitCode\b/);
73+expect(completeInstallBody).toMatch(/\bthrow "OpenClaw installation failed with exit code/);
74+});
75+76+runIfPowerShell("exits non-zero when run as a script file", () => {
77+const tempDir = harness.createTempDir("openclaw-install-ps1-");
78+const scriptPath = join(tempDir, "install.ps1");
79+writeFileSync(scriptPath, createFailingNodeFixture(source));
80+chmodSync(scriptPath, 0o755);
81+82+const result = spawnSync(
83+powershell!,
84+["-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", scriptPath],
85+{ encoding: "utf8" },
86+);
87+88+expect(result.status).toBe(1);
89+});
90+91+runIfPowerShell("throws without killing the caller when run as a scriptblock", () => {
92+const tempDir = harness.createTempDir("openclaw-install-ps1-");
93+const scriptPath = join(tempDir, "install.ps1");
94+writeFileSync(scriptPath, createFailingNodeFixture(source));
95+chmodSync(scriptPath, 0o755);
96+97+const command = [
98+"try {",
99+` & ([scriptblock]::Create((Get-Content -LiteralPath ${toPowerShellSingleQuotedLiteral(scriptPath)} -Raw)))`,
100+"} catch {",
101+' Write-Output "caught=$($_.Exception.Message)"',
102+"}",
103+'Write-Output "alive-after-install"',
104+].join("\n");
105+const result = spawnSync(powershell!, ["-NoLogo", "-NoProfile", "-Command", command], {
106+encoding: "utf8",
107+});
108+109+expect(result.status).toBe(0);
110+expect(result.stdout).toContain("caught=OpenClaw installation failed with exit code 1.");
111+expect(result.stdout).toContain("alive-after-install");
112+});
113+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。