|
1 | 1 | // Install Ps1 tests cover install ps1 script behavior. |
2 | | -import { spawnSync } from "node:child_process"; |
3 | | -import { chmodSync, readFileSync, writeFileSync } from "node:fs"; |
| 2 | +import { spawn, spawnSync } from "node:child_process"; |
| 3 | +import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; |
| 4 | +import { tmpdir } from "node:os"; |
4 | 5 | import { join } from "node:path"; |
5 | 6 | import { beforeAll, describe, expect, it } from "vitest"; |
6 | 7 | import { createScriptTestHarness } from "./test-helpers"; |
@@ -66,12 +67,35 @@ describe("install.ps1 failure handling", () => { |
66 | 67 | const source = readFileSync(SCRIPT_PATH, "utf8"); |
67 | 68 | const powershell = findPowerShell(); |
68 | 69 | const runIfPowerShell = powershell ? it : it.skip; |
| 70 | + const runConcurrentIfPowerShell = powershell ? it.concurrent : it.skip; |
69 | 71 | const runPowerShell = (args: string[]) => { |
70 | 72 | if (!powershell) { |
71 | 73 | throw new Error("PowerShell is not available"); |
72 | 74 | } |
73 | 75 | return spawnSync(powershell, args, { encoding: "utf8" }); |
74 | 76 | }; |
| 77 | + const runPowerShellAsync = (args: string[]) => { |
| 78 | + if (!powershell) { |
| 79 | + throw new Error("PowerShell is not available"); |
| 80 | + } |
| 81 | + return new Promise<{ status: number | null; stderr: string; stdout: string }>( |
| 82 | + (resolve, reject) => { |
| 83 | + const child = spawn(powershell, args, { stdio: ["ignore", "pipe", "pipe"] }); |
| 84 | + let stdout = ""; |
| 85 | + let stderr = ""; |
| 86 | + child.stdout.setEncoding("utf8"); |
| 87 | + child.stderr.setEncoding("utf8"); |
| 88 | + child.stdout.on("data", (chunk: string) => { |
| 89 | + stdout += chunk; |
| 90 | + }); |
| 91 | + child.stderr.on("data", (chunk: string) => { |
| 92 | + stderr += chunk; |
| 93 | + }); |
| 94 | + child.once("error", reject); |
| 95 | + child.once("close", (status) => resolve({ status, stderr, stdout })); |
| 96 | + }, |
| 97 | + ); |
| 98 | + }; |
75 | 99 | const batchedPowerShellResults = new Map<string, { error: string; ok: boolean }>(); |
76 | 100 |
|
77 | 101 | beforeAll(() => { |
@@ -640,68 +664,79 @@ describe("install.ps1 failure handling", () => { |
640 | 664 | expect(mainBody).toContain("Invoke-InteractiveOpenClawCommand onboard"); |
641 | 665 | }); |
642 | 666 |
|
643 | | - runIfPowerShell("fails install when interactive onboarding exits non-zero", () => { |
644 | | - const tempDir = harness.createTempDir("openclaw-install-ps1-"); |
645 | | - const scriptPath = join(tempDir, "install.ps1"); |
646 | | - const scriptWithoutEntryPoint = source.replace(ENTRYPOINT_RE, ""); |
647 | | - writeFileSync( |
648 | | - scriptPath, |
649 | | - [ |
650 | | - scriptWithoutEntryPoint, |
651 | | - "", |
652 | | - "function Write-Banner { }", |
653 | | - "function Ensure-ExecutionPolicy { return $true }", |
654 | | - "function Check-Node { return $true }", |
655 | | - "function Check-ExistingOpenClaw { return $false }", |
656 | | - "function Get-NpmCommandPath { return 'npm.cmd' }", |
657 | | - "function Install-OpenClaw { return $true }", |
658 | | - "function Ensure-OpenClawOnPath { return $true }", |
659 | | - "function Add-ToUserPath { param([string]$Path) }", |
660 | | - "function Get-OpenClawCommandPath { return 'cmd.exe' }", |
661 | | - "function Start-Process {", |
662 | | - " param([string]$FilePath, [string[]]$ArgumentList, [switch]$NoNewWindow, [switch]$Wait, [switch]$PassThru)", |
663 | | - " [pscustomobject]@{ ExitCode = 17 }", |
664 | | - "}", |
665 | | - "$InstallMethod = 'npm'", |
666 | | - "$NoOnboard = $false", |
667 | | - "", |
668 | | - ...ENTRYPOINT_LINES, |
669 | | - "", |
670 | | - ].join("\n"), |
671 | | - ); |
672 | | - chmodSync(scriptPath, 0o755); |
673 | | - |
674 | | - const result = runPowerShell([ |
675 | | - "-NoLogo", |
676 | | - "-NoProfile", |
677 | | - "-ExecutionPolicy", |
678 | | - "Bypass", |
679 | | - "-File", |
680 | | - scriptPath, |
681 | | - ]); |
682 | | - |
683 | | - expect(result.status).toBe(1); |
684 | | - expect(`${result.stdout}\n${result.stderr}`).toContain( |
685 | | - "openclaw onboard failed with exit code 17", |
686 | | - ); |
687 | | - }); |
| 667 | + runConcurrentIfPowerShell( |
| 668 | + "fails install when interactive onboarding exits non-zero", |
| 669 | + async () => { |
| 670 | + const tempDir = mkdtempSync(join(tmpdir(), "openclaw-install-ps1-")); |
| 671 | + const scriptPath = join(tempDir, "install.ps1"); |
| 672 | + try { |
| 673 | + const scriptWithoutEntryPoint = source.replace(ENTRYPOINT_RE, ""); |
| 674 | + writeFileSync( |
| 675 | + scriptPath, |
| 676 | + [ |
| 677 | + scriptWithoutEntryPoint, |
| 678 | + "", |
| 679 | + "function Write-Banner { }", |
| 680 | + "function Ensure-ExecutionPolicy { return $true }", |
| 681 | + "function Check-Node { return $true }", |
| 682 | + "function Check-ExistingOpenClaw { return $false }", |
| 683 | + "function Get-NpmCommandPath { return 'npm.cmd' }", |
| 684 | + "function Install-OpenClaw { return $true }", |
| 685 | + "function Ensure-OpenClawOnPath { return $true }", |
| 686 | + "function Add-ToUserPath { param([string]$Path) }", |
| 687 | + "function Get-OpenClawCommandPath { return 'cmd.exe' }", |
| 688 | + "function Start-Process {", |
| 689 | + " param([string]$FilePath, [string[]]$ArgumentList, [switch]$NoNewWindow, [switch]$Wait, [switch]$PassThru)", |
| 690 | + " [pscustomobject]@{ ExitCode = 17 }", |
| 691 | + "}", |
| 692 | + "$InstallMethod = 'npm'", |
| 693 | + "$NoOnboard = $false", |
| 694 | + "", |
| 695 | + ...ENTRYPOINT_LINES, |
| 696 | + "", |
| 697 | + ].join("\n"), |
| 698 | + ); |
| 699 | + chmodSync(scriptPath, 0o755); |
| 700 | + |
| 701 | + const result = await runPowerShellAsync([ |
| 702 | + "-NoLogo", |
| 703 | + "-NoProfile", |
| 704 | + "-ExecutionPolicy", |
| 705 | + "Bypass", |
| 706 | + "-File", |
| 707 | + scriptPath, |
| 708 | + ]); |
| 709 | + |
| 710 | + expect(result.status).toBe(1); |
| 711 | + expect(`${result.stdout}\n${result.stderr}`).toContain( |
| 712 | + "openclaw onboard failed with exit code 17", |
| 713 | + ); |
| 714 | + } finally { |
| 715 | + rmSync(tempDir, { force: true, recursive: true }); |
| 716 | + } |
| 717 | + }, |
| 718 | + ); |
688 | 719 |
|
689 | | - runIfPowerShell("exits non-zero when run as a script file", () => { |
690 | | - const tempDir = harness.createTempDir("openclaw-install-ps1-"); |
| 720 | + runConcurrentIfPowerShell("exits non-zero when run as a script file", async () => { |
| 721 | + const tempDir = mkdtempSync(join(tmpdir(), "openclaw-install-ps1-")); |
691 | 722 | const scriptPath = join(tempDir, "install.ps1"); |
692 | | - writeFileSync(scriptPath, createFailingNodeFixture(source)); |
693 | | - chmodSync(scriptPath, 0o755); |
694 | | - |
695 | | - const result = runPowerShell([ |
696 | | - "-NoLogo", |
697 | | - "-NoProfile", |
698 | | - "-ExecutionPolicy", |
699 | | - "Bypass", |
700 | | - "-File", |
701 | | - scriptPath, |
702 | | - ]); |
703 | | - |
704 | | - expect(result.status).toBe(1); |
| 723 | + try { |
| 724 | + writeFileSync(scriptPath, createFailingNodeFixture(source)); |
| 725 | + chmodSync(scriptPath, 0o755); |
| 726 | + |
| 727 | + const result = await runPowerShellAsync([ |
| 728 | + "-NoLogo", |
| 729 | + "-NoProfile", |
| 730 | + "-ExecutionPolicy", |
| 731 | + "Bypass", |
| 732 | + "-File", |
| 733 | + scriptPath, |
| 734 | + ]); |
| 735 | + |
| 736 | + expect(result.status).toBe(1); |
| 737 | + } finally { |
| 738 | + rmSync(tempDir, { force: true, recursive: true }); |
| 739 | + } |
705 | 740 | }); |
706 | 741 |
|
707 | 742 | runIfPowerShell("throws without killing the caller when run as a scriptblock", () => { |
|
0 commit comments