mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-09 18:08:31 -10:00
* docs(readme): document /new, zero upgrade, and add Discord link Add the /new slash command and zero upgrade command to the README's command references, expand docs/UPDATE.md to cover the --apply install flow, and link the project Discord server. * docs(update): escape pipe in --endpoint table cell CodeRabbit flagged the unescaped | in the --endpoint placeholder breaking the Markdown table layout. * docs(readme-zh): bring /new and zero upgrade in line with README.md README_ZH.md had picked up the Discord link but was still missing the /new slash command and zero upgrade entries added to README.md, letting the translation drift out of sync. * cli: add upgrade to root help output * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <[email protected]> * docs: make required update flag explicit in common commands zero update errors without --check or --apply, so the bare form in the common-commands examples was misleading. Addresses the Copilot finding from the PR #489 review. * docs: use a permanent (non-expiring) Discord invite in the READMEs * docs(cli): match upgrade help summary to download/verify/install wording Copilot flagged the root zero --help summary for upgrade as less specific than the download/verify/install phrasing already used in README.md and docs/UPDATE.md for the same command. Co-Authored-By: Claude Sonnet 5 <[email protected]> * docs: use discord.gg short link for the community invite discord.com/invite/<code> and discord.gg/<code> resolve to the same invite; discord.gg is the canonical short form to publish. Co-Authored-By: Claude Sonnet 5 <[email protected]> * fix(update,readme-zh): keep npm --json stdout clean, add ZH Discussions route `zero upgrade --json` (and `zero update --apply --json`) on an npm-managed install wired npm's own stdout directly to the process stdout, so its progress output landed ahead of the JSON result and made it unparseable. Route npm's stdout into a buffer when JSON output is requested, only surfacing it in the error message if the install fails. README_ZH.md's Community section only pointed to Discord, unlike the English README, which reserves Discord for real-time chat and routes Q&A/ideas/sharing to GitHub Discussions per SUPPORT.md/CONTRIBUTING.md policy. Bring the Chinese section in line with the same split. Addresses the outstanding findings from jatmn's review on PR #489. Co-Authored-By: Claude Sonnet 5 <[email protected]> * docs(update): qualify npm-managed JSON output Document the npm subprocess output caveat instead of promising a single parseable JSON document for apply operations. Drop the JSON-specific updater plumbing and tests so the PR remains focused on documentation and help discovery. Amp-Thread-ID: https://ampcode.com/threads/T-019fa55a-8bcd-71bc-9bde-9b4c28b78b38 Co-authored-by: Pierre Bruno <[email protected]> * docs(update): align upgrade guidance Amp-Thread-ID: https://ampcode.com/threads/T-019fa7a0-1223-701d-9529-48ba5d7cf8c8 Co-authored-by: Pierre Bruno <[email protected]> * fix(update): keep the upgrade recommendation about this machine `zero update --check --target windows-x64` answers a question about a different machine, but the new call to action told the user to run `zero upgrade`, which only ever installs onto this one — replacing the target-specific download guidance the flag used to print. The recommendation is now conditional: a current-platform check keeps it, and a cross-target check gets the manual instruction plus a note naming the machine `zero upgrade` would have touched instead. Also align two flag contracts with what the code does: `zero update --help` described --repo/--endpoint as check-only although apply honors them, and UPDATE.md did not mention that both are ignored on an npm-managed apply, which takes its release from the npm registry rather than GitHub. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> * fix(update): do not send a custom-source check to the default source `zero upgrade` is a fresh invocation: it does not inherit the --repo/--endpoint of the check that suggested it, so recommending it bare after a custom-source check told the user to install from a repository they never asked about. The check now records the flag it was given and the guidance repeats it, naming where a bare upgrade would install from instead. ZERO_UPDATE_RELEASE_URL is deliberately not recorded: every Check reads it from the environment, including the one inside Apply, so a bare upgrade already follows it and telling the user to repeat it would be wrong. Two smaller edges from the same review: a cross-target check on a host with no published release target no longer ends by describing what `zero upgrade` does there, since it does not work there at all; and the flag table and help text now carry the npm caveat the prose bullet already had, so docs and help agree. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> * fix(update): keep source guidance accurate Amp-Thread-ID: https://ampcode.com/threads/T-019fafa2-7d9d-75bc-8801-6d0efc8db1df Co-authored-by: Pierre Bruno <[email protected]> --------- Co-authored-by: Copilot Autofix powered by AI <[email protected]> Co-authored-by: Vasanthdev2004 <[email protected]> Co-authored-by: Claude Sonnet 5 <[email protected]> Co-authored-by: Amp <[email protected]>
687 lines
23 KiB
Go
687 lines
23 KiB
Go
package update
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestNormalizeVersionTagAndCompare(t *testing.T) {
|
|
got, err := NormalizeVersionTag("v1.2.3+build.4")
|
|
if err != nil {
|
|
t.Fatalf("NormalizeVersionTag returned error: %v", err)
|
|
}
|
|
if got != "1.2.3" {
|
|
t.Fatalf("NormalizeVersionTag = %q, want 1.2.3", got)
|
|
}
|
|
|
|
comparison, err := CompareSemver("0.2.0", "0.1.9")
|
|
if err != nil {
|
|
t.Fatalf("CompareSemver returned error: %v", err)
|
|
}
|
|
if comparison <= 0 {
|
|
t.Fatal("0.2.0 should be newer than 0.1.9")
|
|
}
|
|
|
|
comparison, err = CompareSemver("v0.1.0", "0.1.0")
|
|
if err != nil {
|
|
t.Fatalf("CompareSemver returned error: %v", err)
|
|
}
|
|
if comparison != 0 {
|
|
t.Fatal("v0.1.0 should match 0.1.0")
|
|
}
|
|
}
|
|
|
|
func TestNormalizeVersionTagAndCompareReportInvalidInput(t *testing.T) {
|
|
if _, err := NormalizeVersionTag("nightly"); err == nil {
|
|
t.Fatal("NormalizeVersionTag should reject invalid versions")
|
|
}
|
|
if _, err := NormalizeVersionTag("v999999999999999999999.0.0"); err == nil {
|
|
t.Fatal("NormalizeVersionTag should reject oversized version components")
|
|
}
|
|
if _, err := CompareSemver("0.2.0", "nightly"); err == nil {
|
|
t.Fatal("CompareSemver should reject invalid versions")
|
|
}
|
|
if _, err := CompareSemver("v999999999999999999999.0.0", "0.1.0"); err == nil {
|
|
t.Fatal("CompareSemver should reject oversized version components")
|
|
}
|
|
}
|
|
|
|
func TestCheckReportsAvailableUpdate(t *testing.T) {
|
|
result, err := Check(context.Background(), Options{
|
|
CurrentVersion: "0.1.0",
|
|
GOOS: "linux",
|
|
GOARCH: "amd64",
|
|
Fetch: func(_ context.Context, endpoint string) (Release, error) {
|
|
if endpoint != Endpoint(DefaultRepository) {
|
|
t.Fatalf("endpoint = %q, want default", endpoint)
|
|
}
|
|
return releaseForTarget(t, "v0.2.0", "linux", "amd64"), nil
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Check returned error: %v", err)
|
|
}
|
|
if !result.UpdateAvailable || result.LatestVersion != "0.2.0" {
|
|
t.Fatalf("unexpected update result: %#v", result)
|
|
}
|
|
if !result.ReleaseAsset.Verified || result.ReleaseAsset.ArchiveName != "zero-v0.2.0-linux-x64.tar.gz" || result.ReleaseAsset.ChecksumName != "zero-v0.2.0-linux-x64.tar.gz.sha256" {
|
|
t.Fatalf("unexpected release asset check: %#v", result.ReleaseAsset)
|
|
}
|
|
}
|
|
|
|
func TestResolveTarget(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
name string
|
|
goos string
|
|
goarch string
|
|
platform string
|
|
arch string
|
|
}{
|
|
{input: "linux-x64", name: "linux-x64", goos: "linux", goarch: "amd64", platform: "linux", arch: "x64"},
|
|
{input: " Linux-X64 ", name: "linux-x64", goos: "linux", goarch: "amd64", platform: "linux", arch: "x64"},
|
|
{input: "linux-arm64", name: "linux-arm64", goos: "linux", goarch: "arm64", platform: "linux", arch: "arm64"},
|
|
{input: "macos-x64", name: "macos-x64", goos: "darwin", goarch: "amd64", platform: "macos", arch: "x64"},
|
|
{input: "MacOS-Arm64", name: "macos-arm64", goos: "darwin", goarch: "arm64", platform: "macos", arch: "arm64"},
|
|
{input: "windows-x64", name: "windows-x64", goos: "windows", goarch: "amd64", platform: "windows", arch: "x64"},
|
|
{input: " WINDOWS-ARM64 ", name: "windows-arm64", goos: "windows", goarch: "arm64", platform: "windows", arch: "arm64"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.input, func(t *testing.T) {
|
|
target, err := ResolveTarget(tt.input)
|
|
if err != nil {
|
|
t.Fatalf("ResolveTarget returned error: %v", err)
|
|
}
|
|
if target.Name != tt.name || target.GOOS != tt.goos || target.GOARCH != tt.goarch || target.Platform != tt.platform || target.Arch != tt.arch {
|
|
t.Fatalf("ResolveTarget(%q) = %#v", tt.input, target)
|
|
}
|
|
})
|
|
}
|
|
|
|
if _, err := ResolveTarget("solaris-sparc"); err == nil || !strings.Contains(err.Error(), "unsupported update target") {
|
|
t.Fatalf("ResolveTarget invalid error = %v, want unsupported target", err)
|
|
}
|
|
}
|
|
|
|
func TestCheckReturnsFetchError(t *testing.T) {
|
|
wantErr := errors.New("network failure")
|
|
|
|
_, err := Check(context.Background(), Options{
|
|
CurrentVersion: "0.1.0",
|
|
Fetch: func(context.Context, string) (Release, error) {
|
|
return Release{}, wantErr
|
|
},
|
|
})
|
|
|
|
if !errors.Is(err, wantErr) {
|
|
t.Fatalf("Check error = %v, want %v", err, wantErr)
|
|
}
|
|
}
|
|
|
|
func TestCheckRespectsContextCancellation(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
|
|
defer cancel()
|
|
|
|
_, err := Check(ctx, Options{
|
|
CurrentVersion: "0.1.0",
|
|
Fetch: func(ctx context.Context, _ string) (Release, error) {
|
|
<-ctx.Done()
|
|
return Release{}, ctx.Err()
|
|
},
|
|
})
|
|
|
|
if !errors.Is(err, context.DeadlineExceeded) {
|
|
t.Fatalf("Check error = %v, want context deadline", err)
|
|
}
|
|
}
|
|
|
|
func TestCheckRejectsNegativeTimeout(t *testing.T) {
|
|
_, err := Check(context.Background(), Options{
|
|
CurrentVersion: "0.1.0",
|
|
Timeout: -time.Second,
|
|
Fetch: func(context.Context, string) (Release, error) {
|
|
t.Fatal("Fetch should not run for invalid timeout")
|
|
return Release{}, nil
|
|
},
|
|
})
|
|
|
|
if err == nil || !strings.Contains(err.Error(), "timeout must be non-negative") {
|
|
t.Fatalf("Check error = %v, want non-negative timeout error", err)
|
|
}
|
|
}
|
|
|
|
func TestCheckRejectsMissingTagName(t *testing.T) {
|
|
_, err := Check(context.Background(), Options{
|
|
CurrentVersion: "0.1.0",
|
|
Fetch: func(context.Context, string) (Release, error) {
|
|
return Release{HTMLURL: "https://example.test/release"}, nil
|
|
},
|
|
})
|
|
|
|
if err == nil || !strings.Contains(err.Error(), "tag_name") {
|
|
t.Fatalf("Check error = %v, want missing tag_name error", err)
|
|
}
|
|
}
|
|
|
|
func TestCheckRejectsInvalidLatestVersion(t *testing.T) {
|
|
_, err := Check(context.Background(), Options{
|
|
CurrentVersion: "0.1.0",
|
|
Fetch: func(context.Context, string) (Release, error) {
|
|
return Release{TagName: "nightly"}, nil
|
|
},
|
|
})
|
|
|
|
if err == nil || !strings.Contains(err.Error(), "invalid semantic version") {
|
|
t.Fatalf("Check error = %v, want invalid version error", err)
|
|
}
|
|
}
|
|
|
|
func TestCheckFallsBackReleaseURL(t *testing.T) {
|
|
result, err := Check(context.Background(), Options{
|
|
CurrentVersion: "0.1.0",
|
|
Repository: "Gitlawb/zero",
|
|
GOOS: "linux",
|
|
GOARCH: "amd64",
|
|
Fetch: func(context.Context, string) (Release, error) {
|
|
release := releaseForTarget(t, "v0.2.0", "linux", "amd64")
|
|
release.HTMLURL = ""
|
|
return release, nil
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Check returned error: %v", err)
|
|
}
|
|
|
|
wantURL := "https://github.com/Gitlawb/zero/releases/tag/v0.2.0"
|
|
if result.ReleaseURL != wantURL {
|
|
t.Fatalf("ReleaseURL = %q, want %q", result.ReleaseURL, wantURL)
|
|
}
|
|
}
|
|
|
|
func TestCheckFetchesDataEndpoint(t *testing.T) {
|
|
payload := url.QueryEscape(`{"tag_name":"v0.2.0","html_url":"https://example.test/release","assets":[{"name":"zero-v0.2.0-linux-x64.tar.gz","browser_download_url":"https://example.test/zero-v0.2.0-linux-x64.tar.gz"},{"name":"zero-v0.2.0-linux-x64.tar.gz.sha256","browser_download_url":"https://example.test/zero-v0.2.0-linux-x64.tar.gz.sha256"}]}`)
|
|
|
|
result, err := Check(context.Background(), Options{
|
|
CurrentVersion: "0.1.0",
|
|
Endpoint: "data:application/json," + payload,
|
|
GOOS: "linux",
|
|
GOARCH: "amd64",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Check returned error: %v", err)
|
|
}
|
|
if !result.UpdateAvailable || result.ReleaseURL != "https://example.test/release" || !result.ReleaseAsset.Verified {
|
|
t.Fatalf("unexpected data endpoint result: %#v", result)
|
|
}
|
|
}
|
|
|
|
func TestCheckResolvesEndpointPrecedence(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
options Options
|
|
env string
|
|
want string
|
|
clearEnv bool
|
|
}{
|
|
{
|
|
name: "endpoint option wins",
|
|
options: Options{Endpoint: "Gitlawb/option-zero", Repository: "Gitlawb/repo-zero"},
|
|
env: "Gitlawb/env-zero",
|
|
want: Endpoint("Gitlawb/option-zero"),
|
|
},
|
|
{
|
|
name: "environment wins over repository",
|
|
options: Options{Repository: "Gitlawb/repo-zero"},
|
|
env: "Gitlawb/env-zero",
|
|
want: Endpoint("Gitlawb/env-zero"),
|
|
},
|
|
{
|
|
name: "repository wins over default",
|
|
options: Options{Repository: "Gitlawb/repo-zero"},
|
|
want: Endpoint("Gitlawb/repo-zero"),
|
|
clearEnv: true,
|
|
},
|
|
{
|
|
name: "default repository last",
|
|
want: Endpoint(DefaultRepository),
|
|
clearEnv: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if tt.clearEnv {
|
|
t.Setenv("ZERO_UPDATE_RELEASE_URL", "")
|
|
} else {
|
|
t.Setenv("ZERO_UPDATE_RELEASE_URL", tt.env)
|
|
}
|
|
options := tt.options
|
|
options.CurrentVersion = "0.1.0"
|
|
options.GOOS = "linux"
|
|
options.GOARCH = "amd64"
|
|
options.Fetch = func(_ context.Context, endpoint string) (Release, error) {
|
|
if endpoint != tt.want {
|
|
t.Fatalf("endpoint = %q, want %q", endpoint, tt.want)
|
|
}
|
|
return releaseForTarget(t, "v0.2.0", "linux", "amd64"), nil
|
|
}
|
|
|
|
if _, err := Check(context.Background(), options); err != nil {
|
|
t.Fatalf("Check returned error: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCheckRejectsMissingReleaseAssets(t *testing.T) {
|
|
_, err := Check(context.Background(), Options{
|
|
CurrentVersion: "0.1.0",
|
|
GOOS: "linux",
|
|
GOARCH: "amd64",
|
|
Fetch: func(context.Context, string) (Release, error) {
|
|
return Release{
|
|
TagName: "v0.2.0",
|
|
Assets: []Asset{
|
|
{Name: "zero-v0.2.0-linux-arm64.tar.gz"},
|
|
},
|
|
}, nil
|
|
},
|
|
})
|
|
|
|
if err == nil {
|
|
t.Fatal("Check should reject release metadata without expected assets")
|
|
}
|
|
if !strings.Contains(err.Error(), "zero-v0.2.0-linux-x64.tar.gz") || !strings.Contains(err.Error(), "zero-v0.2.0-linux-x64.tar.gz.sha256") {
|
|
t.Fatalf("Check error = %v, want missing archive and checksum names", err)
|
|
}
|
|
}
|
|
|
|
func TestExpectedAssetCheckUsesInstallerArchiveNames(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
version string
|
|
goos string
|
|
goarch string
|
|
archiveName string
|
|
platform string
|
|
arch string
|
|
}{
|
|
{
|
|
name: "linux amd64",
|
|
version: "0.2.0",
|
|
goos: "linux",
|
|
goarch: "amd64",
|
|
archiveName: "zero-v0.2.0-linux-x64.tar.gz",
|
|
platform: "linux",
|
|
arch: "x64",
|
|
},
|
|
{
|
|
name: "macos arm64",
|
|
version: "0.2.0",
|
|
goos: "darwin",
|
|
goarch: "arm64",
|
|
archiveName: "zero-v0.2.0-macos-arm64.tar.gz",
|
|
platform: "macos",
|
|
arch: "arm64",
|
|
},
|
|
{
|
|
name: "windows amd64",
|
|
version: "0.2.0",
|
|
goos: "windows",
|
|
goarch: "amd64",
|
|
archiveName: "zero-v0.2.0-windows-x64.zip",
|
|
platform: "windows",
|
|
arch: "x64",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
check, err := expectedAssetCheck(tt.version, tt.goos, tt.goarch)
|
|
if err != nil {
|
|
t.Fatalf("expectedAssetCheck returned error: %v", err)
|
|
}
|
|
if check.ArchiveName != tt.archiveName || check.ChecksumName != tt.archiveName+".sha256" || check.Platform != tt.platform || check.Arch != tt.arch {
|
|
t.Fatalf("expectedAssetCheck = %#v, want archive %s", check, tt.archiveName)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCheckReportsHTTPError(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
http.Error(w, "nope", http.StatusInternalServerError)
|
|
}))
|
|
defer server.Close()
|
|
|
|
_, err := Check(context.Background(), Options{
|
|
CurrentVersion: "0.1.0",
|
|
Endpoint: server.URL,
|
|
})
|
|
|
|
if err == nil || !strings.Contains(err.Error(), "github release check failed") {
|
|
t.Fatalf("Check error = %v, want HTTP status error", err)
|
|
}
|
|
}
|
|
|
|
func TestCheckReportsInvalidHTTPJSON(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte("{"))
|
|
}))
|
|
defer server.Close()
|
|
|
|
_, err := Check(context.Background(), Options{
|
|
CurrentVersion: "0.1.0",
|
|
Endpoint: server.URL,
|
|
})
|
|
|
|
if err == nil {
|
|
t.Fatal("Check should reject invalid JSON")
|
|
}
|
|
}
|
|
|
|
func TestResolveEndpointAcceptsURLAndRepositorySlug(t *testing.T) {
|
|
got, err := ResolveEndpoint("Gitlawb/alt-zero", DefaultRepository)
|
|
if err != nil {
|
|
t.Fatalf("ResolveEndpoint returned error: %v", err)
|
|
}
|
|
if got != Endpoint("Gitlawb/alt-zero") {
|
|
t.Fatalf("slug endpoint = %q", got)
|
|
}
|
|
|
|
got, err = ResolveEndpoint("https://example.test/latest", DefaultRepository)
|
|
if err != nil {
|
|
t.Fatalf("ResolveEndpoint returned error: %v", err)
|
|
}
|
|
if got != "https://example.test/latest" {
|
|
t.Fatalf("URL endpoint = %q", got)
|
|
}
|
|
|
|
got, err = ResolveEndpoint("", "Gitlawb/fallback")
|
|
if err != nil {
|
|
t.Fatalf("ResolveEndpoint returned error: %v", err)
|
|
}
|
|
if got != Endpoint("Gitlawb/fallback") {
|
|
t.Fatalf("fallback endpoint = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestResolveEndpointRejectsInvalidInput(t *testing.T) {
|
|
_, err := ResolveEndpoint("not a url", DefaultRepository)
|
|
|
|
if err == nil || !strings.Contains(err.Error(), "invalid update endpoint") {
|
|
t.Fatalf("ResolveEndpoint error = %v, want invalid endpoint error", err)
|
|
}
|
|
}
|
|
|
|
func TestFormatResult(t *testing.T) {
|
|
output := Format(Result{
|
|
CurrentVersion: "0.1.0",
|
|
LatestVersion: "0.2.0",
|
|
ReleaseURL: "https://github.com/Gitlawb/zero/releases/tag/v0.2.0",
|
|
TagName: "v0.2.0",
|
|
ReleaseAsset: assetCheckForTest(t, "v0.2.0", "linux", "amd64"),
|
|
UpdateAvailable: true,
|
|
})
|
|
if !strings.Contains(output, "Update available: 0.1.0 -> 0.2.0") {
|
|
t.Fatalf("unexpected update output: %q", output)
|
|
}
|
|
if !strings.Contains(output, "Release asset: zero-v0.2.0-linux-x64.tar.gz") || !strings.Contains(output, "Checksum asset: zero-v0.2.0-linux-x64.tar.gz.sha256") {
|
|
t.Fatalf("update output did not include release assets: %q", output)
|
|
}
|
|
if !strings.Contains(output, "Release target: linux-x64") {
|
|
t.Fatalf("update output did not include target-specific guidance: %q", output)
|
|
}
|
|
if strings.Contains(output, "your platform") {
|
|
t.Fatalf("update output should not use ambiguous platform wording: %q", output)
|
|
}
|
|
|
|
// The upgrade recommendation is about THIS machine, so it depends on whether
|
|
// the checked target is this machine — assert it against the local target
|
|
// rather than the fixed linux-x64 fixture above, which is a cross-target
|
|
// check anywhere but linux/amd64.
|
|
local := Format(Result{
|
|
CurrentVersion: "0.1.0",
|
|
LatestVersion: "0.2.0",
|
|
ReleaseURL: "https://github.com/Gitlawb/zero/releases/tag/v0.2.0",
|
|
TagName: "v0.2.0",
|
|
ReleaseAsset: assetCheckForTest(t, "v0.2.0", runtime.GOOS, runtime.GOARCH),
|
|
UpdateAvailable: true,
|
|
})
|
|
if !strings.Contains(local, "Run `zero upgrade` to download, verify, and install") {
|
|
t.Fatalf("current-platform check did not recommend zero upgrade: %q", local)
|
|
}
|
|
|
|
output = Format(Result{
|
|
CurrentVersion: "0.2.0",
|
|
LatestVersion: "0.2.0",
|
|
ReleaseURL: "https://github.com/Gitlawb/zero/releases/tag/v0.2.0",
|
|
TagName: "v0.2.0",
|
|
ReleaseAsset: assetCheckForTest(t, "v0.2.0", "linux", "amd64"),
|
|
UpdateAvailable: false,
|
|
})
|
|
if !strings.Contains(output, "up to date") {
|
|
t.Fatalf("unexpected up-to-date output: %q", output)
|
|
}
|
|
if !strings.Contains(output, "Release target: linux-x64") {
|
|
t.Fatalf("up-to-date output did not include release target: %q", output)
|
|
}
|
|
}
|
|
|
|
// TestFormatCustomSourceCheckRepeatsTheSourceFlag covers jatmn's #489 finding:
|
|
// `zero upgrade` is a fresh invocation that reads from the DEFAULT source, so
|
|
// recommending it bare after `--check --repo <fork>` sends the user to install
|
|
// from a repository they never asked about. The pre-PR text pointed at the
|
|
// printed asset URLs instead; this keeps a runnable command by naming the flag
|
|
// to repeat.
|
|
func TestFormatCustomSourceCheckRepeatsTheSourceFlag(t *testing.T) {
|
|
local := assetCheckForTest(t, "v0.2.0", runtime.GOOS, runtime.GOARCH)
|
|
for _, tc := range []struct {
|
|
name string
|
|
options Options
|
|
want string
|
|
}{
|
|
{name: "repo flag", options: Options{Repository: "someone/fork"}, want: "zero upgrade --repo someone/fork"},
|
|
{name: "endpoint flag", options: Options{Endpoint: "https://example.test/releases/latest?channel=$preview"}, want: "zero upgrade --endpoint 'https://example.test/releases/latest?channel=$preview'"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
output := Format(Result{
|
|
CurrentVersion: "0.1.0",
|
|
LatestVersion: "0.2.0",
|
|
ReleaseURL: "https://example.test/release",
|
|
TagName: "v0.2.0",
|
|
ReleaseAsset: local,
|
|
UpdateAvailable: true,
|
|
SourceFlag: upgradeSourceFlag(tc.options),
|
|
})
|
|
if !strings.Contains(output, tc.want) {
|
|
t.Fatalf("output = %q, want it to repeat %q", output, tc.want)
|
|
}
|
|
if !strings.Contains(output, "bare `zero upgrade` does not repeat") {
|
|
t.Fatalf("output = %q, want it to distinguish a bare upgrade", output)
|
|
}
|
|
})
|
|
}
|
|
|
|
// The default source keeps the plain call to action, including when --repo
|
|
// names the default repository explicitly.
|
|
for _, options := range []Options{{}, {Repository: DefaultRepository}} {
|
|
output := Format(Result{
|
|
CurrentVersion: "0.1.0",
|
|
LatestVersion: "0.2.0",
|
|
ReleaseURL: "https://example.test/release",
|
|
TagName: "v0.2.0",
|
|
ReleaseAsset: local,
|
|
UpdateAvailable: true,
|
|
SourceFlag: upgradeSourceFlag(options),
|
|
})
|
|
if !strings.Contains(output, "Run `zero upgrade` to download") {
|
|
t.Fatalf("default-source output = %q, want the plain upgrade recommendation", output)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestCheckRecordsTheSourceFlagItWasGiven pins where SourceFlag comes from: the
|
|
// per-invocation flags, and not ZERO_UPDATE_RELEASE_URL — the env var is read by
|
|
// every Check including the one inside Apply, so a bare `zero upgrade` already
|
|
// follows it and telling the user to repeat it would be wrong.
|
|
func TestCheckRecordsTheSourceFlagItWasGiven(t *testing.T) {
|
|
t.Setenv("ZERO_UPDATE_RELEASE_URL", "")
|
|
fetch := func(context.Context, string) (Release, error) {
|
|
return releaseForTarget(t, "v0.2.0", "linux", "amd64"), nil
|
|
}
|
|
base := Options{CurrentVersion: "0.1.0", GOOS: "linux", GOARCH: "amd64", Fetch: fetch}
|
|
|
|
withRepo := base
|
|
withRepo.Repository = "someone/fork"
|
|
result, err := Check(context.Background(), withRepo)
|
|
if err != nil {
|
|
t.Fatalf("Check: %v", err)
|
|
}
|
|
if result.SourceFlag != "--repo someone/fork" {
|
|
t.Fatalf("SourceFlag = %q, want the repo flag recorded", result.SourceFlag)
|
|
}
|
|
|
|
result, err = Check(context.Background(), base)
|
|
if err != nil {
|
|
t.Fatalf("Check: %v", err)
|
|
}
|
|
if result.SourceFlag != "" {
|
|
t.Fatalf("SourceFlag = %q, want empty for the default source", result.SourceFlag)
|
|
}
|
|
|
|
t.Setenv("ZERO_UPDATE_RELEASE_URL", "https://example.test/releases/latest")
|
|
for _, options := range []Options{base, withRepo} {
|
|
result, err = Check(context.Background(), options)
|
|
if err != nil {
|
|
t.Fatalf("Check: %v", err)
|
|
}
|
|
if result.SourceFlag != "" {
|
|
t.Fatalf("SourceFlag = %q, want empty: the env var wins over repo and carries over on its own", result.SourceFlag)
|
|
}
|
|
}
|
|
|
|
withEndpoint := withRepo
|
|
withEndpoint.Endpoint = "https://option.test/releases/latest"
|
|
result, err = Check(context.Background(), withEndpoint)
|
|
if err != nil {
|
|
t.Fatalf("Check: %v", err)
|
|
}
|
|
if result.SourceFlag != "--endpoint 'https://option.test/releases/latest'" {
|
|
t.Fatalf("SourceFlag = %q, want explicit endpoint recorded", result.SourceFlag)
|
|
}
|
|
}
|
|
|
|
func TestFormatNpmCustomSourceExplainsInstallSource(t *testing.T) {
|
|
result := Result{
|
|
CurrentVersion: "0.1.0", LatestVersion: "0.2.0", ReleaseURL: "https://example.test/release",
|
|
ReleaseAsset: assetCheckForTest(t, "v0.2.0", runtime.GOOS, runtime.GOARCH), UpdateAvailable: true,
|
|
SourceFlag: "--repo someone/fork", installMethod: InstallMethodNpm,
|
|
}
|
|
output := Format(result)
|
|
if strings.Contains(output, "zero upgrade --repo") || !strings.Contains(output, "npm install -g @gitlawb/zero@latest") || !strings.Contains(output, "only affects the release check and update gating") {
|
|
t.Fatalf("npm custom-source guidance is misleading: %q", output)
|
|
}
|
|
}
|
|
|
|
func TestFormatCrossTargetCustomSourceKeepsAccurateGuidance(t *testing.T) {
|
|
goos, goarch := "linux", "amd64"
|
|
if runtime.GOOS == goos && runtime.GOARCH == goarch {
|
|
goos, goarch = "darwin", "arm64"
|
|
}
|
|
output := Format(Result{
|
|
CurrentVersion: "0.1.0", LatestVersion: "0.2.0", ReleaseURL: "https://example.test/release",
|
|
ReleaseAsset: assetCheckForTest(t, "v0.2.0", goos, goarch), UpdateAvailable: true,
|
|
SourceFlag: "--repo someone/fork",
|
|
})
|
|
if strings.Contains(output, "Run `zero upgrade") || !strings.Contains(output, "Download the verified") || !strings.Contains(output, "custom source selected by `--repo someone/fork`") || !strings.Contains(output, "bare `zero upgrade` does not repeat that source") {
|
|
t.Fatalf("cross-target custom-source guidance is misleading: %q", output)
|
|
}
|
|
}
|
|
|
|
// TestFormatCrossTargetCheckDoesNotRecommendLocalUpgrade is the regression test
|
|
// for jatmn's #489 finding: `zero update --check --target <other>` answers a
|
|
// question about a different machine, but the output recommended `zero upgrade`,
|
|
// which only ever installs onto this one. That replaced the target-specific
|
|
// manual-download guidance the flag used to print.
|
|
func TestFormatCrossTargetCheckDoesNotRecommendLocalUpgrade(t *testing.T) {
|
|
// Pick a target that is definitely not this machine.
|
|
goos, goarch := "linux", "amd64"
|
|
if runtime.GOOS == "linux" && runtime.GOARCH == "amd64" {
|
|
goos, goarch = "darwin", "arm64"
|
|
}
|
|
other := assetCheckForTest(t, "v0.2.0", goos, goarch)
|
|
output := Format(Result{
|
|
CurrentVersion: "0.1.0",
|
|
LatestVersion: "0.2.0",
|
|
ReleaseURL: "https://github.com/Gitlawb/zero/releases/tag/v0.2.0",
|
|
TagName: "v0.2.0",
|
|
ReleaseAsset: other,
|
|
UpdateAvailable: true,
|
|
})
|
|
if strings.Contains(output, "Run `zero upgrade`") {
|
|
t.Fatalf("a cross-target check must not recommend the local upgrade: %q", output)
|
|
}
|
|
target := other.Platform + "-" + other.Arch
|
|
if !strings.Contains(output, "Download the verified "+target+" release asset") {
|
|
t.Fatalf("cross-target check lost its target-specific guidance: %q", output)
|
|
}
|
|
if localReleaseTarget() != "" && !strings.Contains(output, "installs onto this machine") {
|
|
t.Fatalf("cross-target check should say which machine zero upgrade would touch: %q", output)
|
|
}
|
|
}
|
|
|
|
func TestPublishedReleaseTargetRejectsUnsupportedCombinations(t *testing.T) {
|
|
tests := []struct {
|
|
goos, goarch, want string
|
|
}{
|
|
{goos: "linux", goarch: "amd64", want: "linux-x64"},
|
|
{goos: "darwin", goarch: "arm64", want: "macos-arm64"},
|
|
{goos: "windows", goarch: "amd64", want: "windows-x64"},
|
|
{goos: "windows", goarch: "arm64", want: ""},
|
|
{goos: "android", goarch: "arm64", want: ""},
|
|
}
|
|
for _, test := range tests {
|
|
if got := publishedReleaseTarget(test.goos, test.goarch); got != test.want {
|
|
t.Errorf("publishedReleaseTarget(%q, %q) = %q, want %q", test.goos, test.goarch, got, test.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func releaseForTarget(t *testing.T, tag string, goos string, goarch string) Release {
|
|
t.Helper()
|
|
check := assetCheckForTest(t, tag, goos, goarch)
|
|
return Release{
|
|
TagName: tag,
|
|
HTMLURL: "https://github.com/Gitlawb/zero/releases/tag/" + tag,
|
|
Assets: []Asset{
|
|
{Name: check.ArchiveName, BrowserDownloadURL: "https://example.test/" + check.ArchiveName},
|
|
{Name: check.ChecksumName, BrowserDownloadURL: "https://example.test/" + check.ChecksumName},
|
|
},
|
|
}
|
|
}
|
|
|
|
func assetCheckForTest(t *testing.T, tag string, goos string, goarch string) AssetCheck {
|
|
t.Helper()
|
|
version, err := NormalizeVersionTag(tag)
|
|
if err != nil {
|
|
t.Fatalf("NormalizeVersionTag(%q): %v", tag, err)
|
|
}
|
|
check, err := expectedAssetCheck(version, goos, goarch)
|
|
if err != nil {
|
|
t.Fatalf("expectedAssetCheck(%q, %q, %q): %v", version, goos, goarch, err)
|
|
}
|
|
check.ArchiveFound = true
|
|
check.ChecksumFound = true
|
|
check.Verified = true
|
|
return check
|
|
}
|