browser4-cli 0.1.24 → 0.1.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,19 +16,20 @@
16
16
  # --version, -v TAG Release tag (e.g. "v4.11.0"). Default: latest.
17
17
  # --install-dir, -d DIR Install directory (default: ~/.local/bin).
18
18
  # --source SRC Force download source: "github" or "oss".
19
- # Default (auto): locale-aware OSS first for China mainland.
19
+ # Default (auto): locale-aware -- OSS first for China mainland.
20
20
  # --no-path Skip adding install dir to PATH.
21
21
  # --skip-local Skip checking for a locally-bundled binary.
22
22
  # --locate Print detection results and exit (no install).
23
23
  # --silent, -s Suppress non-error output.
24
24
  # --dry-run Print what would be done without doing it.
25
+ # --skip-if-installed Skip download if binary already exists at install path.
25
26
  # --help, -h Show this message.
26
27
 
27
28
  set -euo pipefail
28
29
 
29
- # ──────────────────────────────────────────────
30
+ # ----------------------------------------------
30
31
  # Globals
31
- # ──────────────────────────────────────────────
32
+ # ----------------------------------------------
32
33
 
33
34
  GITHUB_REPO="platonai/Browser4"
34
35
  OSS_BASE="https://browser4.oss-cn-beijing.aliyuncs.com"
@@ -39,19 +40,20 @@ SOURCE=""
39
40
  ADD_TO_PATH=true
40
41
  SILENT=false
41
42
  DRY_RUN=false
43
+ SKIP_IF_INSTALLED=false
42
44
  SKIP_LOCAL=false
43
45
  LOCATE_MODE=false
44
46
  CHINA_DETECTED=false
45
47
  SCRIPT_DIR=""
46
48
 
47
- # ──────────────────────────────────────────────
49
+ # ----------------------------------------------
48
50
  # Helpers
49
- # ──────────────────────────────────────────────
51
+ # ----------------------------------------------
50
52
 
51
53
  say() { if [[ "$SILENT" != true ]]; then echo -e "$*"; fi; }
52
- step() { say " $*"; }
53
- ok() { say " $*"; }
54
- warn() { say " $*" >&2; }
54
+ step() { say " -> $*"; }
55
+ ok() { say " [v] $*"; }
56
+ warn() { say " [!] $*" >&2; }
55
57
  die() { echo "ERROR: $*" >&2; exit 1; }
56
58
 
57
59
  color_cyan='\033[0;36m'
@@ -61,16 +63,16 @@ color_reset='\033[0m'
61
63
 
62
64
  header() {
63
65
  if [[ "$SILENT" != true ]]; then
64
- echo -e "${color_cyan}╔════════════════════════════════════════╗${color_reset}"
65
- echo -e "${color_cyan}browser4-cli Installer ║${color_reset}"
66
- echo -e "${color_cyan}╚════════════════════════════════════════╝${color_reset}"
66
+ echo -e "${color_cyan}==========================================${color_reset}"
67
+ echo -e "${color_cyan} browser4-cli Installer${color_reset}"
68
+ echo -e "${color_cyan}==========================================${color_reset}"
67
69
  echo ""
68
70
  fi
69
71
  }
70
72
 
71
- # ──────────────────────────────────────────────
72
- # Script location find ourselves on disk
73
- # ──────────────────────────────────────────────
73
+ # ----------------------------------------------
74
+ # Script location -- find ourselves on disk
75
+ # ----------------------------------------------
74
76
 
75
77
  detect_script_dir() {
76
78
  # BASH_SOURCE works even when sourced; prefer it over $0.
@@ -79,7 +81,7 @@ detect_script_dir() {
79
81
  elif [[ -n "${0:-}" ]] && [[ "$0" != "bash" ]] && [[ "$0" != "-bash" ]] && [[ -f "$0" ]]; then
80
82
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
81
83
  fi
82
- # If piped via curl | bash, SCRIPT_DIR stays empty no local binaries available.
84
+ # If piped via curl | bash, SCRIPT_DIR stays empty -- no local binaries available.
83
85
  }
84
86
 
85
87
  # Search for a pre-downloaded binary near the script (bundled/sideload install).
@@ -127,6 +129,7 @@ Options:
127
129
  --locate Print detection results and exit without installing.
128
130
  --silent, -s Suppress non-error output.
129
131
  --dry-run Print what would be done without doing it.
132
+ --skip-if-installed Skip download if binary already exists at install path.
130
133
  --help, -h Show this message.
131
134
 
132
135
  Examples:
@@ -137,12 +140,13 @@ Examples:
137
140
  $(basename "$0") --locate # Run diagnostics (no install)
138
141
  $(basename "$0") --skip-local # Force download, ignore bundled binary
139
142
  $(basename "$0") --source oss # Force Aliyun OSS (China mainland)
143
+ $(basename "$0") --skip-if-installed # Skip download if already installed
140
144
  EOF
141
145
  }
142
146
 
143
- # ──────────────────────────────────────────────
147
+ # ----------------------------------------------
144
148
  # Argument parsing
145
- # ──────────────────────────────────────────────
149
+ # ----------------------------------------------
146
150
 
147
151
  while [[ $# -gt 0 ]]; do
148
152
  case "$1" in
@@ -163,29 +167,30 @@ while [[ $# -gt 0 ]]; do
163
167
  --locate) LOCATE_MODE=true; shift ;;
164
168
  --silent|-s) SILENT=true; shift ;;
165
169
  --dry-run) DRY_RUN=true; shift ;;
170
+ --skip-if-installed) SKIP_IF_INSTALLED=true; shift ;;
166
171
  --help|-h) usage; exit 0 ;;
167
172
  *) die "Unknown argument: $1 (use --help)";;
168
173
  esac
169
174
  done
170
175
 
171
- # ──────────────────────────────────────────────
176
+ # ----------------------------------------------
172
177
  # China mainland locale detection (zero-network)
173
- # ──────────────────────────────────────────────
178
+ # ----------------------------------------------
174
179
 
175
180
  detect_china_locale() {
176
- # 1 Locale env vars
181
+ # 1 -- Locale env vars
177
182
  local lang
178
183
  lang="${LC_ALL:-${LANG:-${LC_CTYPE:-${LC_MESSAGES:-}}}}"
179
184
  case "$lang" in
180
185
  zh_CN*|zh-CN*|"Chinese (Simplified)_China"*) return 0 ;;
181
186
  esac
182
187
 
183
- # 2 TZ env var
188
+ # 2 -- TZ env var
184
189
  case "${TZ:-}" in
185
190
  Asia/Shanghai|Asia/Chongqing|Asia/Urumqi|Asia/Harbin) return 0 ;;
186
191
  esac
187
192
 
188
- # 3 /etc/timezone
193
+ # 3 -- /etc/timezone
189
194
  if [[ -f /etc/timezone ]]; then
190
195
  local tz
191
196
  tz=$(cat /etc/timezone 2>/dev/null || true)
@@ -197,9 +202,9 @@ detect_china_locale() {
197
202
  return 1
198
203
  }
199
204
 
200
- # ──────────────────────────────────────────────
205
+ # ----------------------------------------------
201
206
  # Platform detection
202
- # ──────────────────────────────────────────────
207
+ # ----------------------------------------------
203
208
 
204
209
  detect_os() {
205
210
  case "$(uname -s)" in
@@ -235,7 +240,7 @@ detect_libc() {
235
240
  fi
236
241
  fi
237
242
 
238
- # Check for musl loader covers common architectures
243
+ # Check for musl loader -- covers common architectures
239
244
  # x86_64, aarch64, armhf (32-bit ARM), i386, riscv64, s390x, ppc64le, mips64
240
245
  local musl_loader
241
246
  for musl_loader in \
@@ -298,9 +303,9 @@ get_default_install_dir() {
298
303
  echo "$dir"
299
304
  }
300
305
 
301
- # ──────────────────────────────────────────────
306
+ # ----------------------------------------------
302
307
  # Download
303
- # ──────────────────────────────────────────────
308
+ # ----------------------------------------------
304
309
 
305
310
  check_commands() {
306
311
  local missing=()
@@ -392,7 +397,7 @@ download_file() {
392
397
  return 0
393
398
  fi
394
399
 
395
- warn "Downloaded file too small (${size} bytes) may be an error page"
400
+ warn "Downloaded file too small (${size} bytes) -- may be an error page"
396
401
  rm -f "$dest"
397
402
  return 1
398
403
  fi
@@ -402,9 +407,9 @@ download_file() {
402
407
  return 1
403
408
  }
404
409
 
405
- # ──────────────────────────────────────────────
410
+ # ----------------------------------------------
406
411
  # PATH management
407
- # ──────────────────────────────────────────────
412
+ # ----------------------------------------------
408
413
 
409
414
  add_to_shell_rc() {
410
415
  local dir="$1"
@@ -468,9 +473,9 @@ add_to_shell_rc() {
468
473
  say " Reload with: source $rc_file"
469
474
  }
470
475
 
471
- # ──────────────────────────────────────────────
476
+ # ----------------------------------------------
472
477
  # Symlinks
473
- # ──────────────────────────────────────────────
478
+ # ----------------------------------------------
474
479
 
475
480
  create_symlinks() {
476
481
  local binary_name="$1"
@@ -514,9 +519,9 @@ create_symlinks() {
514
519
  fi
515
520
  }
516
521
 
517
- # ──────────────────────────────────────────────
522
+ # ----------------------------------------------
518
523
  # Main
519
- # ──────────────────────────────────────────────
524
+ # ----------------------------------------------
520
525
 
521
526
  main() {
522
527
  check_commands
@@ -538,11 +543,11 @@ main() {
538
543
  platform_key=$(get_platform_key)
539
544
  binary_name=$(get_binary_name "$platform_key")
540
545
 
541
- # ── Locate mode: print diagnostics and exit ──
546
+ # -- Locate mode: print diagnostics and exit --
542
547
  if [[ "$LOCATE_MODE" == true ]]; then
543
- echo -e "${color_cyan}─── Locate / diagnostics ───${color_reset}"
548
+ echo -e "${color_cyan}--- Locate / diagnostics ---${color_reset}"
544
549
  echo ""
545
- step "Script dir: ${SCRIPT_DIR:-'(not available piped via curl?)'}"
550
+ step "Script dir: ${SCRIPT_DIR:-'(not available -- piped via curl?)'}"
546
551
  step "Platform key: $platform_key"
547
552
  step "Binary name: $binary_name"
548
553
  step "Default install: $(get_default_install_dir)"
@@ -606,7 +611,7 @@ main() {
606
611
 
607
612
  local binary_path="${INSTALL_DIR}/${binary_name}"
608
613
 
609
- # ── Local binary discovery (bundled/sideload) ──
614
+ # -- Local binary discovery (bundled/sideload) --
610
615
  local use_local_binary=false
611
616
  local local_binary_path=""
612
617
  if [[ "$SKIP_LOCAL" != true ]]; then
@@ -616,7 +621,7 @@ main() {
616
621
  ok "Local binary verified (--version OK)"
617
622
  use_local_binary=true
618
623
  else
619
- warn "Local binary found but --version check failed will download instead"
624
+ warn "Local binary found but --version check failed -- will download instead"
620
625
  fi
621
626
  fi
622
627
  else
@@ -624,7 +629,7 @@ main() {
624
629
  fi
625
630
 
626
631
  # Install binary: local copy > already installed > download
627
- if [[ -f "$binary_path" ]] && [[ -z "$VERSION" ]] && [[ "$use_local_binary" != true ]]; then
632
+ if [[ -f "$binary_path" ]] && [[ -z "$VERSION" ]] && [[ "$SKIP_IF_INSTALLED" == true ]] && [[ "$use_local_binary" != true ]]; then
628
633
  ok "Binary already installed: $binary_path"
629
634
  elif [[ "$use_local_binary" == true ]]; then
630
635
  # Copy local binary to install dir
@@ -705,10 +710,10 @@ main() {
705
710
  say ""
706
711
  if [[ "$DRY_RUN" != true ]]; then
707
712
  if version_output=$("$binary_path" --version 2>&1); then
708
- echo -e "${color_green} browser4-cli installed successfully${color_reset}"
713
+ echo -e "${color_green}[v] browser4-cli installed successfully${color_reset}"
709
714
  say " Version: $version_output"
710
715
  else
711
- echo -e "${color_green} Binary installed at: $binary_path${color_reset}"
716
+ echo -e "${color_green}[v] Binary installed at: $binary_path${color_reset}"
712
717
  warn "Could not verify --version (this is normal on first install)"
713
718
  fi
714
719
  else
@@ -0,0 +1,366 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Tests for install-browser4-cli.ps1
4
+ PowerShell 5.1+ only — zero external dependencies.
5
+ Run: powershell -NoProfile -ExecutionPolicy Bypass -File install-browser4-cli.tests.ps1
6
+ #>
7
+
8
+ $ErrorActionPreference = "Stop"
9
+ $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
10
+ $installScript = Join-Path (Split-Path $scriptDir -Parent) "install-browser4-cli.ps1"
11
+
12
+ $pass = 0
13
+ $fail = 0
14
+
15
+ function Test($name, [ScriptBlock]$block) {
16
+ try {
17
+ $null = & $block
18
+ $script:pass++
19
+ Write-Host " PASS $name" -ForegroundColor Green
20
+ } catch {
21
+ $script:fail++
22
+ Write-Host " FAIL $name" -ForegroundColor Red
23
+ Write-Host " $($_.Exception.Message)" -ForegroundColor Red
24
+ }
25
+ }
26
+
27
+ function RunScript([string]$scriptArgs, [ref]$exitCode) {
28
+ # Use -Command to control stderr redirect (2> path must come before script args,
29
+ # which -File would pass as literal arguments to the script).
30
+ $tmpErr = [System.IO.Path]::GetTempFileName()
31
+ $cmd = "& '$installScript' $scriptArgs 2>'$tmpErr'"
32
+ $psi = New-Object System.Diagnostics.ProcessStartInfo
33
+ $psi.FileName = "powershell.exe"
34
+ $psi.Arguments = "-NoProfile -ExecutionPolicy Bypass -Command `"$cmd`""
35
+ $psi.RedirectStandardOutput = $true
36
+ $psi.UseShellExecute = $false
37
+ $psi.CreateNoWindow = $true
38
+ $proc = [System.Diagnostics.Process]::Start($psi)
39
+ $out = $proc.StandardOutput.ReadToEnd()
40
+ $proc.WaitForExit()
41
+ $exitCode.Value = $proc.ExitCode
42
+ $err = if (Test-Path $tmpErr) { Get-Content $tmpErr -Raw -ErrorAction SilentlyContinue; Remove-Item $tmpErr -Force -ErrorAction SilentlyContinue } else { "" }
43
+ return [PSCustomObject]@{ Output = $out; Error = $err; ExitCode = $proc.ExitCode }
44
+ }
45
+
46
+ Write-Host "============================================" -ForegroundColor Cyan
47
+ Write-Host " install-browser4-cli.ps1 Test Suite" -ForegroundColor Cyan
48
+ Write-Host "============================================" -ForegroundColor Cyan
49
+ Write-Host ""
50
+
51
+ # ── Pre-flight ──
52
+ Write-Host "--- Pre-flight ---" -ForegroundColor Cyan
53
+
54
+ Test "file exists" {
55
+ if (-not (Test-Path $installScript)) { throw "Not found: $installScript" }
56
+ }
57
+
58
+ Test "file is readable" {
59
+ $null = Get-Content $installScript -Raw -ErrorAction Stop
60
+ }
61
+
62
+ Test "no non-ASCII bytes" {
63
+ $bytes = [System.IO.File]::ReadAllBytes($installScript)
64
+ $nonAscii = @($bytes | Where-Object { $_ -gt 127 })
65
+ if ($nonAscii.Count -gt 0) {
66
+ throw "Found $($nonAscii.Count) non-ASCII bytes"
67
+ }
68
+ }
69
+
70
+ Test "AST parses without errors" {
71
+ $parseErrors = $null
72
+ $ast = [System.Management.Automation.Language.Parser]::ParseFile(
73
+ $installScript, [ref]$null, [ref]$parseErrors
74
+ )
75
+ if ($parseErrors.Count -gt 0) {
76
+ $msg = ($parseErrors | ForEach-Object { "L$($_.Extent.StartLineNumber): $($_.Message)" }) -join "; "
77
+ throw $msg
78
+ }
79
+ }
80
+
81
+ Write-Host ""
82
+
83
+ # ── Param block (via AST) ──
84
+ Write-Host "--- Param block ---" -ForegroundColor Cyan
85
+
86
+ $content = Get-Content $installScript -Raw
87
+ $parseErrors = $null
88
+ $ast = [System.Management.Automation.Language.Parser]::ParseInput($content, [ref]$null, [ref]$parseErrors)
89
+
90
+ Test "param block defines all expected parameters" {
91
+ $expected = @('Version', 'InstallDir', 'Source', 'AddToPath',
92
+ 'Silent', 'DryRun', 'SkipIfInstalled', 'SkipLocal', 'Locate')
93
+ # Extract param block text via AST extents
94
+ $paramAst = $ast.ParamBlock
95
+ if (-not $paramAst) { throw "Could not find param block AST" }
96
+ $txt = $content.Substring($paramAst.Extent.StartOffset, $paramAst.Extent.EndOffset - $paramAst.Extent.StartOffset)
97
+ foreach ($p in $expected) {
98
+ if ($txt -notmatch ('\$' + $p + '\b')) {
99
+ throw "Missing parameter: $p"
100
+ }
101
+ }
102
+ }
103
+
104
+ Test "Source has ValidateSet with empty string" {
105
+ if ($content -notmatch 'ValidateSet\("",\s*"github",\s*"oss"\)') {
106
+ throw "ValidateSet must include empty string for iex compatibility"
107
+ }
108
+ }
109
+
110
+ Write-Host ""
111
+
112
+ # ── Locate mode ──
113
+ Write-Host "--- Locate mode ---" -ForegroundColor Cyan
114
+
115
+ $ec = 0
116
+ $r = RunScript -scriptArgs "-Locate" -exitCode ([ref]$ec)
117
+
118
+ Test "-Locate exits with code 0" {
119
+ if ($ec -ne 0) { throw "Exit code: $ec" }
120
+ }
121
+
122
+ Test "-Locate shows platform key" {
123
+ if ($r.Output -notmatch 'Platform key') {
124
+ throw "Missing 'Platform key' in: $($r.Output.Substring(0, [Math]::Min(500, $r.Output.Length)))"
125
+ }
126
+ }
127
+
128
+ Test "-Locate shows binary name" {
129
+ if ($r.Output -notmatch 'Binary name') {
130
+ throw "Missing 'Binary name' in output (len=$($r.Output.Length))"
131
+ }
132
+ }
133
+
134
+ Test "-Locate shows download order" {
135
+ if ($r.Output -notmatch 'Download order') {
136
+ throw "Missing 'Download order' in output"
137
+ }
138
+ }
139
+
140
+ Write-Host ""
141
+
142
+ # ── Download URLs (via -Locate output) ──
143
+ Write-Host "--- Download URLs ---" -ForegroundColor Cyan
144
+
145
+ Test "locate shows correct GitHub latest/download URL" {
146
+ if ($r.Output -notmatch 'github\.com/platonai/Browser4/releases/latest/download/') {
147
+ $lines = ($r.Output -split '\n' | Where-Object { $_ -match 'ownload' }) -join '; '
148
+ throw "GitHub latest URL not found. Download lines: $lines"
149
+ }
150
+ }
151
+
152
+ Test "locate shows correct OSS download/latest URL" {
153
+ if ($r.Output -notmatch 'oss-cn-beijing.*?releases/download/latest/') {
154
+ $lines = ($r.Output -split '\n' | Where-Object { $_ -match 'ownload' }) -join '; '
155
+ throw "OSS latest URL not found. Download lines: $lines"
156
+ }
157
+ }
158
+
159
+ # Test versioned URLs
160
+ $ec2 = 0
161
+ $r2 = RunScript -scriptArgs "-Version v4.11.0 -Locate" -exitCode ([ref]$ec2)
162
+
163
+ Test "-Version shows tag-based URLs" {
164
+ if ($r2.Output -notmatch 'releases/download/v4\.11\.0/') {
165
+ throw "Versioned URL not in output"
166
+ }
167
+ }
168
+
169
+ Write-Host ""
170
+
171
+ # ── Parameter acceptance ──
172
+ Write-Host "--- Parameter acceptance ---" -ForegroundColor Cyan
173
+
174
+ Test "-SkipIfInstalled flag accepted" {
175
+ $ec3 = 0; $r3 = RunScript -scriptArgs "-SkipIfInstalled -DryRun" -exitCode ([ref]$ec3)
176
+ if ($ec3 -ne 0) { throw "Exit code: $ec3, output: $($r3.Output)" }
177
+ }
178
+
179
+ Test "-SkipLocal flag accepted" {
180
+ $ec4 = 0; $r4 = RunScript -scriptArgs "-SkipLocal -DryRun" -exitCode ([ref]$ec4)
181
+ if ($ec4 -ne 0) { throw "Exit code: $ec4, output: $($r4.Output)" }
182
+ }
183
+
184
+ Test "-Force rejected (replaced by -SkipIfInstalled)" {
185
+ $ec5 = 0; $r5 = RunScript -scriptArgs "-Force -DryRun" -exitCode ([ref]$ec5)
186
+ if ($r5.Error -notmatch 'Force') {
187
+ throw "-Force should be rejected, got: $($r5.Error)"
188
+ }
189
+ }
190
+
191
+ Test "-Source oss accepted" {
192
+ $ec6 = 0; $r6 = RunScript -scriptArgs "-Source oss -DryRun" -exitCode ([ref]$ec6)
193
+ if ($ec6 -ne 0) { throw "Exit code: $ec6, output: $($r6.Output)" }
194
+ }
195
+
196
+ Test "-Source github accepted" {
197
+ $ec7 = 0; $r7 = RunScript -scriptArgs "-Source github -DryRun" -exitCode ([ref]$ec7)
198
+ if ($ec7 -ne 0) { throw "Exit code: $ec7, output: $($r7.Output)" }
199
+ }
200
+
201
+ Test "-Source invalid rejected" {
202
+ $ec8 = 0; $r8 = RunScript -scriptArgs "-Source invalid -DryRun" -exitCode ([ref]$ec8)
203
+ if ($r8.Error -notmatch 'Source|invalid|parameter') {
204
+ throw "-Source invalid should be rejected, got: $($r8.Error)"
205
+ }
206
+ }
207
+
208
+ Test "-Silent flag accepted" {
209
+ $ec9 = 0; $r9 = RunScript -scriptArgs "-Silent -DryRun" -exitCode ([ref]$ec9)
210
+ if ($ec9 -ne 0) { throw "Exit code: $ec9, output: $($r9.Output)" }
211
+ }
212
+
213
+ Test "-Version flag accepted" {
214
+ $ec10 = 0; $r10 = RunScript -scriptArgs "-Version v4.11.0 -DryRun" -exitCode ([ref]$ec10)
215
+ if ($ec10 -ne 0) { throw "Exit code: $ec10, output: $($r10.Output)" }
216
+ }
217
+
218
+ Write-Host ""
219
+
220
+ # ── Functions via dot-source ──
221
+ Write-Host "--- Functions ---" -ForegroundColor Cyan
222
+
223
+ # Strip trailing Main call and dot-source for function-level tests
224
+ $scriptContent = Get-Content $installScript -Raw
225
+ $scriptContent = $scriptContent -replace '\r?\nMain\s*$', ''
226
+ $scriptContent = $scriptContent -replace '\$ErrorActionPreference\s*=\s*"Stop"', ''
227
+ $sb = [ScriptBlock]::Create($scriptContent)
228
+
229
+ & {
230
+ # Suppress output
231
+ $Silent = $true
232
+ $DryRun = $false
233
+ $SkipLocal = $false
234
+ $Locate = $false
235
+ $Source = ""
236
+ $Version = ""
237
+ $InstallDir = ""
238
+ $AddToPath = $true
239
+
240
+ . $sb
241
+
242
+ Test "Get-PlatformKey returns valid format" {
243
+ $key = Get-PlatformKey
244
+ if ($key -notmatch '^(win32|linux|darwin)-(x64|arm64)$' -and
245
+ $key -notmatch '^linux-musl-(x64|arm64)$') {
246
+ throw "Unexpected platform key: $key"
247
+ }
248
+ }
249
+
250
+ Test "Get-BinaryName includes .exe on win32" {
251
+ $name = Get-BinaryName -PlatformKey "win32-x64"
252
+ if ($name -ne "browser4-cli-win32-x64.exe") { throw "Got: $name" }
253
+ }
254
+
255
+ Test "Get-BinaryName excludes .exe on linux" {
256
+ $name = Get-BinaryName -PlatformKey "linux-x64"
257
+ if ($name -ne "browser4-cli-linux-x64") { throw "Got: $name" }
258
+ }
259
+
260
+ Test "Get-BinaryName excludes .exe on darwin" {
261
+ $name = Get-BinaryName -PlatformKey "darwin-arm64"
262
+ if ($name -ne "browser4-cli-darwin-arm64") { throw "Got: $name" }
263
+ }
264
+
265
+ Test "Get-DefaultInstallDir returns non-empty" {
266
+ $dir = Get-DefaultInstallDir
267
+ if ([string]::IsNullOrEmpty($dir)) { throw "Empty install dir" }
268
+ }
269
+
270
+ Test "Test-ChinaLocale returns [bool]" {
271
+ $result = Test-ChinaLocale
272
+ if ($result -isnot [bool]) { throw "Expected [bool], got $($result.GetType())" }
273
+ }
274
+
275
+ Test "Find-LocalBinary returns null for non-existent" {
276
+ $result = Find-LocalBinary -BinaryName "nonexistent-file-xyz.exe"
277
+ if ($result -ne $null) { throw "Expected null, got: $result" }
278
+ }
279
+
280
+ Test "Test-LocalBinary returns false for empty string" {
281
+ if (Test-LocalBinary -Path "") { throw "Should be false" }
282
+ }
283
+
284
+ Test "Test-LocalBinary returns false for null" {
285
+ if (Test-LocalBinary -Path $null) { throw "Should be false" }
286
+ }
287
+
288
+ # ── Set-BinaryFile (locked-binary-safe replacement) ──
289
+ Test "Set-BinaryFile creates new file (Copy)" {
290
+ $tmpDir = Join-Path $env:TEMP "b4-test-$(New-Guid)"
291
+ New-Item -ItemType Directory $tmpDir -Force | Out-Null
292
+ try {
293
+ $src = Join-Path $tmpDir "source.exe"
294
+ $dst = Join-Path $tmpDir "target.exe"
295
+ "new-content" | Out-File $src -Encoding ASCII
296
+ Set-BinaryFile -TargetPath $dst -SourcePath $src
297
+ if (-not (Test-Path $dst)) { throw "Target was not created" }
298
+ $actual = (Get-Content $dst -Raw).Trim()
299
+ if ($actual -ne "new-content") { throw "Wrong content: '$actual'" }
300
+ # Source still exists (Copy mode)
301
+ if (-not (Test-Path $src)) { throw "Source should still exist after copy" }
302
+ } finally {
303
+ Remove-Item $tmpDir -Recurse -Force -ErrorAction SilentlyContinue
304
+ }
305
+ }
306
+
307
+ Test "Set-BinaryFile creates new file (Move)" {
308
+ $tmpDir = Join-Path $env:TEMP "b4-test-$(New-Guid)"
309
+ New-Item -ItemType Directory $tmpDir -Force | Out-Null
310
+ try {
311
+ $src = Join-Path $tmpDir "source.exe"
312
+ $dst = Join-Path $tmpDir "target.exe"
313
+ "moved-content" | Out-File $src -Encoding ASCII
314
+ Set-BinaryFile -TargetPath $dst -SourcePath $src -Move
315
+ if (-not (Test-Path $dst)) { throw "Target was not created" }
316
+ $actual = (Get-Content $dst -Raw).Trim()
317
+ if ($actual -ne "moved-content") { throw "Wrong content: '$actual'" }
318
+ # Source should be gone (Move mode)
319
+ if (Test-Path $src) { throw "Source should not exist after move" }
320
+ } finally {
321
+ Remove-Item $tmpDir -Recurse -Force -ErrorAction SilentlyContinue
322
+ }
323
+ }
324
+
325
+ Test "Set-BinaryFile replaces existing file (Copy)" {
326
+ $tmpDir = Join-Path $env:TEMP "b4-test-$(New-Guid)"
327
+ New-Item -ItemType Directory $tmpDir -Force | Out-Null
328
+ try {
329
+ $src = Join-Path $tmpDir "source.exe"
330
+ $dst = Join-Path $tmpDir "target.exe"
331
+ "old" | Out-File $dst -Encoding ASCII
332
+ "new" | Out-File $src -Encoding ASCII
333
+ Set-BinaryFile -TargetPath $dst -SourcePath $src
334
+ if ((Get-Content $dst -Raw).Trim() -ne "new") { throw "Content not replaced" }
335
+ } finally {
336
+ Remove-Item $tmpDir -Recurse -Force -ErrorAction SilentlyContinue
337
+ }
338
+ }
339
+
340
+ Test "Set-BinaryFile replaces existing file (Move)" {
341
+ $tmpDir = Join-Path $env:TEMP "b4-test-$(New-Guid)"
342
+ New-Item -ItemType Directory $tmpDir -Force | Out-Null
343
+ try {
344
+ $src = Join-Path $tmpDir "source.exe"
345
+ $dst = Join-Path $tmpDir "target.exe"
346
+ "old" | Out-File $dst -Encoding ASCII
347
+ "moved" | Out-File $src -Encoding ASCII
348
+ Set-BinaryFile -TargetPath $dst -SourcePath $src -Move
349
+ if ((Get-Content $dst -Raw).Trim() -ne "moved") { throw "Content not replaced" }
350
+ if (Test-Path $src) { throw "Source should not exist after move" }
351
+ } finally {
352
+ Remove-Item $tmpDir -Recurse -Force -ErrorAction SilentlyContinue
353
+ }
354
+ }
355
+ }
356
+
357
+ Write-Host ""
358
+
359
+ # ── Summary ──
360
+ Write-Host "============================================" -ForegroundColor Cyan
361
+ $total = $pass + $fail
362
+ Write-Host " Results: $pass / $total passed" -ForegroundColor $(if ($fail -eq 0) { "Green" } else { "Red" })
363
+ Write-Host "============================================" -ForegroundColor Cyan
364
+
365
+ if ($fail -gt 0) { exit 1 }
366
+ exit 0