Files
zero/internal/update/installmethod_test.go
PierrunoYTandVasanthdev2004 5f36349c18 feat(update): add zero upgrade command to apply self-updates (#461)
* feat(update): add zero upgrade command to apply self-updates

Extends the existing `zero update --check` (which only reported an
available release) with an actual install step:

- `zero update --apply` / `zero upgrade` download, sha256-verify, and
  install the latest release.
- npm installs delegate to `npm install -g @gitlawb/zero@latest`.
- Standalone installs download the release archive, verify its
  checksum, extract it (zip-slip guarded), and atomically replace the
  running binary plus any pre-existing optional sandbox helpers. On
  Windows, the running exe is renamed aside (NTFS allows this even
  while it's executing) since it can't be overwritten directly; the
  leftover ".old" file is cleaned up on a later invocation.

Closes #459.

* fix(update): correct macOS expectations in apply test

The optional-helper assertion in TestApplyStandaloneUpdateReplacesBinary
assumed every non-Windows platform has a linux-style "zero-seccomp"
helper to refresh, but applyStandaloneUpdate correctly ships no optional
helpers on macOS (matching scripts/postinstall.mjs). This made the test
fail on macOS CI. Only assert the helper refresh on linux/windows.

* fix(update): address CodeRabbit review findings on zero upgrade

- Reject --target when combined with --apply/upgrade: applying installs
  onto the current machine, so a mismatched --target could replace the
  running binary with an incompatible-arch build.
- Bound archive/checksum downloads with a dedicated timeout separate
  from the release-metadata check timeout, so a stalled connection
  can't hang zero upgrade forever.
- Surface a failed rollback in the Windows replaceBinary path instead
  of swallowing it, so a botched install is reported rather than
  silently leaving the binary missing.
- Run CleanupStaleBinary regardless of whether an update is available,
  so a leftover Windows ".old" file isn't stuck waiting for a future
  upgrade that may never come.
- Report optional sandbox-helper refresh failures as warnings on
  ApplyResult (surfaced in text and JSON output) instead of discarding
  them silently.

* fix(oauth): retry secret-lock on Windows delete-pending race to unblock CI

TestLoadOrCreateSecretConcurrentConverges was failing this PR's Windows
smoke job with "create token secret lock: ...Access is denied." — a
pre-existing flake in internal/oauth, not in the upgrade code. On Windows
a concurrent holder's os.Remove leaves the .lock in a delete-pending
state, so an O_EXCL create races it with ERROR_ACCESS_DENIED
(os.ErrPermission) rather than ErrExist; createSecretFile only treated
ErrExist as contention and hard-failed otherwise.

Treat os.ErrPermission as retryable too, mirroring acquireFileLock in
lock.go. This is the same root-cause fix as #445 (diagnosis by
@gnanam1990); folded in here to make this PR's CI green. Verified on
Windows: the concurrent-converge test passes 30/30.

---------

Co-authored-by: Vasanthdev2004 <[email protected]>
2026-07-04 08:52:07 +08:00

61 lines
2.0 KiB
Go

package update
import (
"os"
"path/filepath"
"testing"
)
func TestDetectInstallMethodStandaloneByDefault(t *testing.T) {
dir := t.TempDir()
exePath := filepath.Join(dir, "zero")
if err := os.WriteFile(exePath, []byte("binary"), 0o755); err != nil {
t.Fatalf("WriteFile: %v", err)
}
if method := DetectInstallMethod(exePath); method != InstallMethodStandalone {
t.Fatalf("DetectInstallMethod = %q, want standalone", method)
}
}
func TestDetectInstallMethodNpmViaMarkerFile(t *testing.T) {
dir := t.TempDir()
exePath := filepath.Join(dir, "zero")
if err := os.WriteFile(exePath, []byte("binary"), 0o755); err != nil {
t.Fatalf("WriteFile: %v", err)
}
if err := os.WriteFile(filepath.Join(dir, ".zero-binary-version"), []byte("0.1.0\n"), 0o644); err != nil {
t.Fatalf("WriteFile marker: %v", err)
}
if method := DetectInstallMethod(exePath); method != InstallMethodNpm {
t.Fatalf("DetectInstallMethod = %q, want npm", method)
}
}
func TestDetectInstallMethodNpmViaPackageJSON(t *testing.T) {
dir := t.TempDir()
exePath := filepath.Join(dir, "zero")
if err := os.WriteFile(exePath, []byte("binary"), 0o755); err != nil {
t.Fatalf("WriteFile: %v", err)
}
if err := os.WriteFile(filepath.Join(dir, "package.json"), []byte(`{"name":"@gitlawb/zero"}`), 0o644); err != nil {
t.Fatalf("WriteFile package.json: %v", err)
}
if method := DetectInstallMethod(exePath); method != InstallMethodNpm {
t.Fatalf("DetectInstallMethod = %q, want npm", method)
}
}
func TestDetectInstallMethodIgnoresUnrelatedPackageJSON(t *testing.T) {
dir := t.TempDir()
exePath := filepath.Join(dir, "zero")
if err := os.WriteFile(exePath, []byte("binary"), 0o755); err != nil {
t.Fatalf("WriteFile: %v", err)
}
if err := os.WriteFile(filepath.Join(dir, "package.json"), []byte(`{"name":"some-other-package"}`), 0o644); err != nil {
t.Fatalf("WriteFile package.json: %v", err)
}
if method := DetectInstallMethod(exePath); method != InstallMethodStandalone {
t.Fatalf("DetectInstallMethod = %q, want standalone", method)
}
}