26f897c1ec
release / release-please (push) Failing after 1m49s
docs / build (push) Failing after 6m34s
release / build-and-upload (arm64, linux) (push) Has been cancelled
release / build-and-upload (arm64, windows) (push) Has been cancelled
release / build-darwin (amd64, darwin) (push) Has been cancelled
release / checksums (push) Has been cancelled
release / finalize (push) Has been cancelled
release / build-darwin (arm64, darwin) (push) Has been cancelled
release / build-and-upload (amd64, linux) (push) Has been cancelled
release / build-and-upload (amd64, windows) (push) Has been cancelled
docs / deploy (push) Has been cancelled
1157 lines
36 KiB
Go
1157 lines
36 KiB
Go
package update
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/kunchenguid/no-mistakes/internal/db"
|
|
"github.com/kunchenguid/no-mistakes/internal/paths"
|
|
"github.com/kunchenguid/no-mistakes/internal/types"
|
|
)
|
|
|
|
func TestUpdaterCheckLatestAndRefreshCache(t *testing.T) {
|
|
allowInsecureDownloads = true
|
|
t.Cleanup(func() { allowInsecureDownloads = false })
|
|
|
|
tests := []struct {
|
|
name string
|
|
platform platformSpec
|
|
archiveName string
|
|
}{
|
|
{
|
|
name: "darwin tarball",
|
|
platform: platformSpec{GOOS: "darwin", GOARCH: "arm64"},
|
|
archiveName: "no-mistakes-v1.2.3-darwin-arm64.tar.gz",
|
|
},
|
|
{
|
|
name: "windows zip",
|
|
platform: platformSpec{GOOS: "windows", GOARCH: "amd64"},
|
|
archiveName: "no-mistakes-v1.2.3-windows-amd64.zip",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/repos/kunchenguid/no-mistakes/releases/latest" {
|
|
t.Fatalf("unexpected path %q", r.URL.Path)
|
|
}
|
|
fmt.Fprintf(w, `{"tag_name":"v1.2.3","assets":[{"name":%q,"browser_download_url":"http://example.com/archive"},{"name":"checksums.txt","browser_download_url":"http://example.com/checksums"}]}`,
|
|
tt.archiveName,
|
|
)
|
|
}))
|
|
defer server.Close()
|
|
|
|
cachePath := filepath.Join(t.TempDir(), "update-check.json")
|
|
u := &updater{
|
|
appName: "no-mistakes",
|
|
repo: "kunchenguid/no-mistakes",
|
|
currentVersion: "v1.2.2",
|
|
platform: tt.platform,
|
|
apiBaseURL: server.URL,
|
|
httpClient: server.Client(),
|
|
cachePath: cachePath,
|
|
now: func() time.Time { return time.Date(2026, 4, 9, 12, 0, 0, 0, time.UTC) },
|
|
}
|
|
|
|
plan, err := u.checkLatest(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("checkLatest error = %v", err)
|
|
}
|
|
if !plan.UpdateAvailable {
|
|
t.Fatal("expected update to be available")
|
|
}
|
|
if plan.LatestVersion != "v1.2.3" {
|
|
t.Fatalf("LatestVersion = %q", plan.LatestVersion)
|
|
}
|
|
if plan.ArchiveName != tt.archiveName {
|
|
t.Fatalf("ArchiveName = %q, want %q", plan.ArchiveName, tt.archiveName)
|
|
}
|
|
if plan.Archive.Name != tt.archiveName {
|
|
t.Fatalf("Archive.Name = %q, want %q", plan.Archive.Name, tt.archiveName)
|
|
}
|
|
|
|
if err := u.refreshCache(context.Background()); err != nil {
|
|
t.Fatalf("refreshCache error = %v", err)
|
|
}
|
|
cache := readCache(cachePath)
|
|
if cache == nil || cache.LatestVersion != "v1.2.3" {
|
|
t.Fatalf("cache = %#v", cache)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUpdaterRunReplacesExecutable(t *testing.T) {
|
|
allowInsecureDownloads = true
|
|
t.Cleanup(func() { allowInsecureDownloads = false })
|
|
|
|
archiveName := "no-mistakes-v1.2.3-darwin-arm64.tar.gz"
|
|
archive := makeTarGz(t, map[string][]byte{
|
|
"bin/no-mistakes": []byte("new-binary"),
|
|
})
|
|
sum := sha256.Sum256(archive)
|
|
checksums := fmt.Sprintf("%s %s\n", hex.EncodeToString(sum[:]), archiveName)
|
|
|
|
var server *httptest.Server
|
|
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/repos/kunchenguid/no-mistakes/releases/latest":
|
|
fmt.Fprintf(w, `{"tag_name":"v1.2.3","assets":[{"name":%q,"browser_download_url":%q},{"name":"checksums.txt","browser_download_url":%q}]}`,
|
|
archiveName,
|
|
server.URL+"/archive",
|
|
server.URL+"/checksums",
|
|
)
|
|
case "/archive":
|
|
w.Write(archive)
|
|
case "/checksums":
|
|
fmt.Fprint(w, checksums)
|
|
default:
|
|
t.Fatalf("unexpected path %q", r.URL.Path)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
execPath := filepath.Join(t.TempDir(), "no-mistakes")
|
|
if err := os.WriteFile(execPath, []byte("old-binary"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
stdout := new(bytes.Buffer)
|
|
u := &updater{
|
|
appName: "no-mistakes",
|
|
repo: "kunchenguid/no-mistakes",
|
|
currentVersion: "v1.2.2",
|
|
platform: platformSpec{GOOS: "darwin", GOARCH: "arm64"},
|
|
apiBaseURL: server.URL,
|
|
httpClient: server.Client(),
|
|
executablePath: execPath,
|
|
stdout: stdout,
|
|
now: func() time.Time { return time.Date(2026, 4, 9, 12, 0, 0, 0, time.UTC) },
|
|
}
|
|
|
|
if err := u.run(context.Background()); err != nil {
|
|
t.Fatalf("run error = %v", err)
|
|
}
|
|
content, err := os.ReadFile(execPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(content) != "new-binary" {
|
|
t.Fatalf("executable content = %q", string(content))
|
|
}
|
|
if !strings.Contains(stdout.String(), "updated no-mistakes") {
|
|
t.Fatalf("stdout = %q", stdout.String())
|
|
}
|
|
}
|
|
|
|
func TestUpdaterRunResetsDaemonAfterUpdate(t *testing.T) {
|
|
allowInsecureDownloads = true
|
|
t.Cleanup(func() { allowInsecureDownloads = false })
|
|
|
|
archiveName := "no-mistakes-v1.2.3-darwin-arm64.tar.gz"
|
|
archive := makeTarGz(t, map[string][]byte{
|
|
"bin/no-mistakes": []byte("new-binary"),
|
|
})
|
|
sum := sha256.Sum256(archive)
|
|
checksums := fmt.Sprintf("%s %s\n", hex.EncodeToString(sum[:]), archiveName)
|
|
|
|
var server *httptest.Server
|
|
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/repos/kunchenguid/no-mistakes/releases/latest":
|
|
fmt.Fprintf(w, `{"tag_name":"v1.2.3","assets":[{"name":%q,"browser_download_url":%q},{"name":"checksums.txt","browser_download_url":%q}]}`,
|
|
archiveName,
|
|
server.URL+"/archive",
|
|
server.URL+"/checksums",
|
|
)
|
|
case "/archive":
|
|
w.Write(archive)
|
|
case "/checksums":
|
|
fmt.Fprint(w, checksums)
|
|
default:
|
|
t.Fatalf("unexpected path %q", r.URL.Path)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
execPath := filepath.Join(t.TempDir(), "no-mistakes")
|
|
if err := os.WriteFile(execPath, []byte("old-binary"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
resetCalled := false
|
|
u := &updater{
|
|
appName: "no-mistakes",
|
|
repo: "kunchenguid/no-mistakes",
|
|
currentVersion: "v1.2.2",
|
|
platform: platformSpec{GOOS: "darwin", GOARCH: "arm64"},
|
|
apiBaseURL: server.URL,
|
|
httpClient: server.Client(),
|
|
executablePath: execPath,
|
|
now: func() time.Time { return time.Date(2026, 4, 9, 12, 0, 0, 0, time.UTC) },
|
|
resetDaemon: func() error {
|
|
resetCalled = true
|
|
return nil
|
|
},
|
|
}
|
|
|
|
if err := u.run(context.Background()); err != nil {
|
|
t.Fatalf("run error = %v", err)
|
|
}
|
|
if !resetCalled {
|
|
t.Fatal("expected daemon reset after successful update")
|
|
}
|
|
}
|
|
|
|
func TestUpdaterRunRefusesWithActiveRunsAndListsThem(t *testing.T) {
|
|
allowInsecureDownloads = true
|
|
t.Cleanup(func() { allowInsecureDownloads = false })
|
|
|
|
archiveName := "no-mistakes-v1.2.3-darwin-arm64.tar.gz"
|
|
archive := makeTarGz(t, map[string][]byte{
|
|
"bin/no-mistakes": []byte("new-binary"),
|
|
})
|
|
sum := sha256.Sum256(archive)
|
|
checksums := fmt.Sprintf("%s %s\n", hex.EncodeToString(sum[:]), archiveName)
|
|
|
|
var server *httptest.Server
|
|
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/repos/kunchenguid/no-mistakes/releases/latest":
|
|
fmt.Fprintf(w, `{"tag_name":"v1.2.3","assets":[{"name":%q,"browser_download_url":%q},{"name":"checksums.txt","browser_download_url":%q}]}`,
|
|
archiveName,
|
|
server.URL+"/archive",
|
|
server.URL+"/checksums",
|
|
)
|
|
case "/archive":
|
|
w.Write(archive)
|
|
case "/checksums":
|
|
fmt.Fprint(w, checksums)
|
|
default:
|
|
t.Fatalf("unexpected path %q", r.URL.Path)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
p := paths.WithRoot(t.TempDir())
|
|
if err := p.EnsureDirs(); err != nil {
|
|
t.Fatalf("ensure paths: %v", err)
|
|
}
|
|
database, err := db.Open(p.DB())
|
|
if err != nil {
|
|
t.Fatalf("open db: %v", err)
|
|
}
|
|
repo, err := database.InsertRepo("/home/user/project", "git@github.com:user/project.git", "main")
|
|
if err != nil {
|
|
t.Fatalf("insert repo: %v", err)
|
|
}
|
|
pendingRun, err := database.InsertRun(repo.ID, "feature-a", "aaa111", "000")
|
|
if err != nil {
|
|
t.Fatalf("insert pending run: %v", err)
|
|
}
|
|
runningRun, err := database.InsertRun(repo.ID, "feature-b", "bbb222", "000")
|
|
if err != nil {
|
|
t.Fatalf("insert running run: %v", err)
|
|
}
|
|
if err := database.UpdateRunStatus(runningRun.ID, types.RunRunning); err != nil {
|
|
t.Fatalf("mark running: %v", err)
|
|
}
|
|
if err := database.Close(); err != nil {
|
|
t.Fatalf("close db: %v", err)
|
|
}
|
|
|
|
execPath := filepath.Join(t.TempDir(), "no-mistakes")
|
|
if err := os.WriteFile(execPath, []byte("old-binary"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
stdout := new(bytes.Buffer)
|
|
stderr := new(bytes.Buffer)
|
|
resetCalled := false
|
|
u := &updater{
|
|
appName: "no-mistakes",
|
|
repo: "kunchenguid/no-mistakes",
|
|
currentVersion: "v1.2.2",
|
|
platform: platformSpec{GOOS: "darwin", GOARCH: "arm64"},
|
|
apiBaseURL: server.URL,
|
|
httpClient: server.Client(),
|
|
executablePath: execPath,
|
|
stdout: stdout,
|
|
stderr: stderr,
|
|
stdin: strings.NewReader("y\n"),
|
|
now: func() time.Time { return time.Date(2026, 4, 9, 12, 0, 0, 0, time.UTC) },
|
|
resetDaemon: func() error {
|
|
resetCalled = true
|
|
return nil
|
|
},
|
|
paths: p,
|
|
}
|
|
|
|
err = u.run(context.Background())
|
|
if err == nil {
|
|
t.Fatal("run should fail when active run warning is rejected")
|
|
}
|
|
if !strings.Contains(err.Error(), "refusing update") {
|
|
t.Fatalf("run error = %v", err)
|
|
}
|
|
transcript := stderr.String()
|
|
for _, want := range []string{
|
|
"2 active pipeline runs",
|
|
pendingRun.ID,
|
|
"feature-a",
|
|
runningRun.ID,
|
|
"feature-b",
|
|
"continuing can cause these pipelines to fail",
|
|
} {
|
|
if !strings.Contains(transcript, want) {
|
|
t.Fatalf("stderr should contain %q, got %q", want, transcript)
|
|
}
|
|
}
|
|
content, readErr := os.ReadFile(execPath)
|
|
if readErr != nil {
|
|
t.Fatal(readErr)
|
|
}
|
|
if string(content) != "old-binary" {
|
|
t.Fatalf("executable content = %q", string(content))
|
|
}
|
|
if resetCalled {
|
|
t.Fatal("reset daemon should not run after rejecting active run warning")
|
|
}
|
|
if stdout.Len() != 0 {
|
|
t.Fatalf("stdout = %q", stdout.String())
|
|
}
|
|
}
|
|
|
|
func TestUpdaterActiveRunGuardAllowsForce(t *testing.T) {
|
|
p := paths.WithRoot(t.TempDir())
|
|
if err := p.EnsureDirs(); err != nil {
|
|
t.Fatalf("ensure paths: %v", err)
|
|
}
|
|
database, err := db.Open(p.DB())
|
|
if err != nil {
|
|
t.Fatalf("open db: %v", err)
|
|
}
|
|
repo, err := database.InsertRepo("/home/user/project", "git@github.com:user/project.git", "main")
|
|
if err != nil {
|
|
t.Fatalf("insert repo: %v", err)
|
|
}
|
|
if _, err := database.InsertRun(repo.ID, "feature-a", "aaa111", "000"); err != nil {
|
|
t.Fatalf("insert run: %v", err)
|
|
}
|
|
if err := database.Close(); err != nil {
|
|
t.Fatalf("close db: %v", err)
|
|
}
|
|
|
|
stderr := new(bytes.Buffer)
|
|
u := &updater{paths: p, stderr: stderr, force: true}
|
|
if err := u.confirmActiveRunsBeforeUpdate(); err != nil {
|
|
t.Fatalf("confirmActiveRunsBeforeUpdate with force error = %v", err)
|
|
}
|
|
transcript := stderr.String()
|
|
for _, want := range []string{
|
|
"FORCE: continuing update",
|
|
"feature-a",
|
|
"aaa111",
|
|
} {
|
|
if !strings.Contains(transcript, want) {
|
|
t.Fatalf("stderr should contain %q, got %q", want, transcript)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUpdaterRunFailsWhenDaemonResetFails(t *testing.T) {
|
|
allowInsecureDownloads = true
|
|
t.Cleanup(func() { allowInsecureDownloads = false })
|
|
|
|
archiveName := "no-mistakes-v1.2.3-darwin-arm64.tar.gz"
|
|
archive := makeTarGz(t, map[string][]byte{
|
|
"bin/no-mistakes": []byte("new-binary"),
|
|
})
|
|
sum := sha256.Sum256(archive)
|
|
checksums := fmt.Sprintf("%s %s\n", hex.EncodeToString(sum[:]), archiveName)
|
|
|
|
var server *httptest.Server
|
|
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/repos/kunchenguid/no-mistakes/releases/latest":
|
|
fmt.Fprintf(w, `{"tag_name":"v1.2.3","assets":[{"name":%q,"browser_download_url":%q},{"name":"checksums.txt","browser_download_url":%q}]}`,
|
|
archiveName,
|
|
server.URL+"/archive",
|
|
server.URL+"/checksums",
|
|
)
|
|
case "/archive":
|
|
w.Write(archive)
|
|
case "/checksums":
|
|
fmt.Fprint(w, checksums)
|
|
default:
|
|
t.Fatalf("unexpected path %q", r.URL.Path)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
execPath := filepath.Join(t.TempDir(), "no-mistakes")
|
|
if err := os.WriteFile(execPath, []byte("old-binary"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
stdout := new(bytes.Buffer)
|
|
stderr := new(bytes.Buffer)
|
|
u := &updater{
|
|
appName: "no-mistakes",
|
|
repo: "kunchenguid/no-mistakes",
|
|
currentVersion: "v1.2.2",
|
|
platform: platformSpec{GOOS: "darwin", GOARCH: "arm64"},
|
|
apiBaseURL: server.URL,
|
|
httpClient: server.Client(),
|
|
executablePath: execPath,
|
|
stdout: stdout,
|
|
stderr: stderr,
|
|
now: func() time.Time { return time.Date(2026, 4, 9, 12, 0, 0, 0, time.UTC) },
|
|
resetDaemon: func() error {
|
|
return fmt.Errorf("boom")
|
|
},
|
|
}
|
|
|
|
err := u.run(context.Background())
|
|
if err == nil {
|
|
t.Fatal("run should fail when daemon reset fails")
|
|
}
|
|
if !strings.Contains(err.Error(), "failed to reset daemon") {
|
|
t.Fatalf("run error = %v", err)
|
|
}
|
|
content, err := os.ReadFile(execPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(content) != "new-binary" {
|
|
t.Fatalf("executable content = %q", string(content))
|
|
}
|
|
if stdout.Len() != 0 {
|
|
t.Fatalf("stdout = %q", stdout.String())
|
|
}
|
|
if stderr.Len() != 0 {
|
|
t.Fatalf("stderr = %q", stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestUpdaterRunFailsWhenDaemonResetLeavesDaemonOffline(t *testing.T) {
|
|
allowInsecureDownloads = true
|
|
t.Cleanup(func() { allowInsecureDownloads = false })
|
|
|
|
archiveName := "no-mistakes-v1.2.3-darwin-arm64.tar.gz"
|
|
archive := makeTarGz(t, map[string][]byte{
|
|
"bin/no-mistakes": []byte("new-binary"),
|
|
})
|
|
sum := sha256.Sum256(archive)
|
|
checksums := fmt.Sprintf("%s %s\n", hex.EncodeToString(sum[:]), archiveName)
|
|
|
|
var server *httptest.Server
|
|
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/repos/kunchenguid/no-mistakes/releases/latest":
|
|
fmt.Fprintf(w, `{"tag_name":"v1.2.3","assets":[{"name":%q,"browser_download_url":%q},{"name":"checksums.txt","browser_download_url":%q}]}`,
|
|
archiveName,
|
|
server.URL+"/archive",
|
|
server.URL+"/checksums",
|
|
)
|
|
case "/archive":
|
|
w.Write(archive)
|
|
case "/checksums":
|
|
fmt.Fprint(w, checksums)
|
|
default:
|
|
t.Fatalf("unexpected path %q", r.URL.Path)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
execPath := filepath.Join(t.TempDir(), "no-mistakes")
|
|
if err := os.WriteFile(execPath, []byte("old-binary"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
stdout := new(bytes.Buffer)
|
|
u := &updater{
|
|
appName: "no-mistakes",
|
|
repo: "kunchenguid/no-mistakes",
|
|
currentVersion: "v1.2.2",
|
|
platform: platformSpec{GOOS: "darwin", GOARCH: "arm64"},
|
|
apiBaseURL: server.URL,
|
|
httpClient: server.Client(),
|
|
executablePath: execPath,
|
|
stdout: stdout,
|
|
now: func() time.Time { return time.Date(2026, 4, 9, 12, 0, 0, 0, time.UTC) },
|
|
resetDaemon: func() error {
|
|
return &daemonResetError{err: errors.New("start daemon: boom"), daemonOffline: true}
|
|
},
|
|
}
|
|
|
|
err := u.run(context.Background())
|
|
if err == nil {
|
|
t.Fatal("run should fail when daemon reset leaves daemon offline")
|
|
}
|
|
if !strings.Contains(err.Error(), "daemon is offline") {
|
|
t.Fatalf("run error = %v", err)
|
|
}
|
|
content, readErr := os.ReadFile(execPath)
|
|
if readErr != nil {
|
|
t.Fatal(readErr)
|
|
}
|
|
if string(content) != "new-binary" {
|
|
t.Fatalf("executable content = %q", string(content))
|
|
}
|
|
if stdout.Len() != 0 {
|
|
t.Fatalf("stdout = %q", stdout.String())
|
|
}
|
|
}
|
|
|
|
func TestUpdaterRunFailsWhenDaemonUsesDifferentExecutable(t *testing.T) {
|
|
allowInsecureDownloads = true
|
|
t.Cleanup(func() { allowInsecureDownloads = false })
|
|
|
|
archiveName := "no-mistakes-v1.2.3-darwin-arm64.tar.gz"
|
|
archive := makeTarGz(t, map[string][]byte{
|
|
"bin/no-mistakes": []byte("new-binary"),
|
|
})
|
|
sum := sha256.Sum256(archive)
|
|
checksums := fmt.Sprintf("%s %s\n", hex.EncodeToString(sum[:]), archiveName)
|
|
|
|
var server *httptest.Server
|
|
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/repos/kunchenguid/no-mistakes/releases/latest":
|
|
fmt.Fprintf(w, `{"tag_name":"v1.2.3","assets":[{"name":%q,"browser_download_url":%q},{"name":"checksums.txt","browser_download_url":%q}]}`,
|
|
archiveName,
|
|
server.URL+"/archive",
|
|
server.URL+"/checksums",
|
|
)
|
|
case "/archive":
|
|
w.Write(archive)
|
|
case "/checksums":
|
|
fmt.Fprint(w, checksums)
|
|
default:
|
|
t.Fatalf("unexpected path %q", r.URL.Path)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
execDir := t.TempDir()
|
|
execPath := filepath.Join(execDir, "no-mistakes")
|
|
if err := os.WriteFile(execPath, []byte("old-binary"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
otherExecPath := filepath.Join(execDir, "other-no-mistakes")
|
|
if err := os.WriteFile(otherExecPath, []byte("other-binary"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
origDaemonIsRunning := daemonIsRunning
|
|
origDaemonExecutablePath := daemonExecutablePath
|
|
t.Cleanup(func() {
|
|
daemonIsRunning = origDaemonIsRunning
|
|
daemonExecutablePath = origDaemonExecutablePath
|
|
})
|
|
|
|
checks := 0
|
|
daemonIsRunning = func(*paths.Paths) (bool, error) {
|
|
checks++
|
|
return true, nil
|
|
}
|
|
daemonExecutablePath = func(*paths.Paths) (string, error) {
|
|
return otherExecPath, nil
|
|
}
|
|
|
|
resetCalled := false
|
|
u := &updater{
|
|
appName: "no-mistakes",
|
|
repo: "kunchenguid/no-mistakes",
|
|
currentVersion: "v1.2.2",
|
|
platform: platformSpec{GOOS: "darwin", GOARCH: "arm64"},
|
|
apiBaseURL: server.URL,
|
|
httpClient: server.Client(),
|
|
executablePath: execPath,
|
|
now: func() time.Time { return time.Date(2026, 4, 9, 12, 0, 0, 0, time.UTC) },
|
|
resetDaemon: func() error {
|
|
resetCalled = true
|
|
return nil
|
|
},
|
|
paths: paths.WithRoot(t.TempDir()),
|
|
}
|
|
|
|
err := u.run(context.Background())
|
|
if err == nil {
|
|
t.Fatal("run should fail when daemon uses a different executable")
|
|
}
|
|
if !strings.Contains(err.Error(), "daemon is running from") {
|
|
t.Fatalf("run error = %v", err)
|
|
}
|
|
if checks == 0 {
|
|
t.Fatal("expected daemon health check before update")
|
|
}
|
|
if resetCalled {
|
|
t.Fatal("reset daemon should not run when executables mismatch")
|
|
}
|
|
content, readErr := os.ReadFile(execPath)
|
|
if readErr != nil {
|
|
t.Fatal(readErr)
|
|
}
|
|
if string(content) != "old-binary" {
|
|
t.Fatalf("executable content = %q", string(content))
|
|
}
|
|
}
|
|
|
|
func TestUpdaterRunReplacesDaemonWhenDifferentExecutableConfirmed(t *testing.T) {
|
|
allowInsecureDownloads = true
|
|
t.Cleanup(func() { allowInsecureDownloads = false })
|
|
|
|
archiveName := "no-mistakes-v1.2.3-darwin-arm64.tar.gz"
|
|
archive := makeTarGz(t, map[string][]byte{
|
|
"bin/no-mistakes": []byte("new-binary"),
|
|
})
|
|
sum := sha256.Sum256(archive)
|
|
checksums := fmt.Sprintf("%s %s\n", hex.EncodeToString(sum[:]), archiveName)
|
|
|
|
var server *httptest.Server
|
|
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/repos/kunchenguid/no-mistakes/releases/latest":
|
|
fmt.Fprintf(w, `{"tag_name":"v1.2.3","assets":[{"name":%q,"browser_download_url":%q},{"name":"checksums.txt","browser_download_url":%q}]}`,
|
|
archiveName,
|
|
server.URL+"/archive",
|
|
server.URL+"/checksums",
|
|
)
|
|
case "/archive":
|
|
w.Write(archive)
|
|
case "/checksums":
|
|
fmt.Fprint(w, checksums)
|
|
default:
|
|
t.Fatalf("unexpected path %q", r.URL.Path)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
origDaemonIsRunning := daemonIsRunning
|
|
origDaemonExecutablePath := daemonExecutablePath
|
|
t.Cleanup(func() {
|
|
daemonIsRunning = origDaemonIsRunning
|
|
daemonExecutablePath = origDaemonExecutablePath
|
|
})
|
|
|
|
tests := []struct {
|
|
name string
|
|
stdin string
|
|
assumeYes bool
|
|
want string
|
|
}{
|
|
{name: "prompt", stdin: "y\n", want: "Replace the running daemon"},
|
|
{name: "assume yes", assumeYes: true, want: "-y was provided"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
execDir := t.TempDir()
|
|
execPath := filepath.Join(execDir, "no-mistakes")
|
|
if err := os.WriteFile(execPath, []byte("old-binary"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
otherExecPath := filepath.Join(execDir, "other-no-mistakes")
|
|
if err := os.WriteFile(otherExecPath, []byte("other-binary"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
daemonIsRunning = func(*paths.Paths) (bool, error) {
|
|
return true, nil
|
|
}
|
|
daemonExecutablePath = func(*paths.Paths) (string, error) {
|
|
return otherExecPath, nil
|
|
}
|
|
|
|
resetCalled := false
|
|
stdout := new(bytes.Buffer)
|
|
stderr := new(bytes.Buffer)
|
|
u := &updater{
|
|
appName: "no-mistakes",
|
|
repo: "kunchenguid/no-mistakes",
|
|
currentVersion: "v1.2.2",
|
|
platform: platformSpec{GOOS: "darwin", GOARCH: "arm64"},
|
|
apiBaseURL: server.URL,
|
|
httpClient: server.Client(),
|
|
executablePath: execPath,
|
|
stdout: stdout,
|
|
stderr: stderr,
|
|
stdin: strings.NewReader(tt.stdin),
|
|
now: func() time.Time { return time.Date(2026, 4, 9, 12, 0, 0, 0, time.UTC) },
|
|
resetDaemon: func() error {
|
|
resetCalled = true
|
|
return nil
|
|
},
|
|
paths: paths.WithRoot(t.TempDir()),
|
|
assumeYes: tt.assumeYes,
|
|
}
|
|
|
|
if err := u.run(context.Background()); err != nil {
|
|
t.Fatalf("run error = %v", err)
|
|
}
|
|
if !resetCalled {
|
|
t.Fatal("expected daemon reset after confirming executable takeover")
|
|
}
|
|
content, readErr := os.ReadFile(execPath)
|
|
if readErr != nil {
|
|
t.Fatal(readErr)
|
|
}
|
|
if string(content) != "new-binary" {
|
|
t.Fatalf("executable content = %q", string(content))
|
|
}
|
|
if !strings.Contains(stdout.String(), "updated no-mistakes from v1.2.2 to v1.2.3") {
|
|
t.Fatalf("stdout should report successful update, got %q", stdout.String())
|
|
}
|
|
for _, want := range []string{resolveExecutablePath(otherExecPath), resolveExecutablePath(execPath), tt.want} {
|
|
if !strings.Contains(stderr.String(), want) {
|
|
t.Fatalf("stderr should contain %q, got %q", want, stderr.String())
|
|
}
|
|
}
|
|
t.Logf("stderr transcript:\n%s", stderr.String())
|
|
t.Logf("stdout transcript:\n%s", stdout.String())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUpdaterRunFailsWhenDaemonExecutableCannotBeResolved(t *testing.T) {
|
|
allowInsecureDownloads = true
|
|
t.Cleanup(func() { allowInsecureDownloads = false })
|
|
|
|
archiveName := "no-mistakes-v1.2.3-darwin-arm64.tar.gz"
|
|
archive := makeTarGz(t, map[string][]byte{
|
|
"bin/no-mistakes": []byte("new-binary"),
|
|
})
|
|
sum := sha256.Sum256(archive)
|
|
checksums := fmt.Sprintf("%s %s\n", hex.EncodeToString(sum[:]), archiveName)
|
|
|
|
var server *httptest.Server
|
|
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/repos/kunchenguid/no-mistakes/releases/latest":
|
|
fmt.Fprintf(w, `{"tag_name":"v1.2.3","assets":[{"name":%q,"browser_download_url":%q},{"name":"checksums.txt","browser_download_url":%q}]}`,
|
|
archiveName,
|
|
server.URL+"/archive",
|
|
server.URL+"/checksums",
|
|
)
|
|
case "/archive":
|
|
w.Write(archive)
|
|
case "/checksums":
|
|
fmt.Fprint(w, checksums)
|
|
default:
|
|
t.Fatalf("unexpected path %q", r.URL.Path)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
execDir := t.TempDir()
|
|
execPath := filepath.Join(execDir, "no-mistakes")
|
|
if err := os.WriteFile(execPath, []byte("old-binary"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
origDaemonIsRunning := daemonIsRunning
|
|
origDaemonExecutablePath := daemonExecutablePath
|
|
t.Cleanup(func() {
|
|
daemonIsRunning = origDaemonIsRunning
|
|
daemonExecutablePath = origDaemonExecutablePath
|
|
})
|
|
|
|
checks := 0
|
|
daemonIsRunning = func(*paths.Paths) (bool, error) {
|
|
checks++
|
|
return true, nil
|
|
}
|
|
daemonExecutablePath = func(*paths.Paths) (string, error) {
|
|
return "", errors.New("pid lookup failed")
|
|
}
|
|
|
|
resetCalled := false
|
|
u := &updater{
|
|
appName: "no-mistakes",
|
|
repo: "kunchenguid/no-mistakes",
|
|
currentVersion: "v1.2.2",
|
|
platform: platformSpec{GOOS: "darwin", GOARCH: "arm64"},
|
|
apiBaseURL: server.URL,
|
|
httpClient: server.Client(),
|
|
executablePath: execPath,
|
|
now: func() time.Time { return time.Date(2026, 4, 9, 12, 0, 0, 0, time.UTC) },
|
|
resetDaemon: func() error {
|
|
resetCalled = true
|
|
return nil
|
|
},
|
|
paths: paths.WithRoot(t.TempDir()),
|
|
}
|
|
|
|
err := u.run(context.Background())
|
|
if err == nil {
|
|
t.Fatal("run should fail when daemon executable cannot be resolved")
|
|
}
|
|
if !strings.Contains(err.Error(), "cannot determine daemon executable path") {
|
|
t.Fatalf("run error = %v", err)
|
|
}
|
|
if checks == 0 {
|
|
t.Fatal("expected daemon health check before update")
|
|
}
|
|
if resetCalled {
|
|
t.Fatal("reset daemon should not run when daemon executable cannot be resolved")
|
|
}
|
|
}
|
|
|
|
func TestUpdaterRunSkipsDaemonExecutableCheckWhenAlreadyUpToDate(t *testing.T) {
|
|
allowInsecureDownloads = true
|
|
t.Cleanup(func() { allowInsecureDownloads = false })
|
|
|
|
var server *httptest.Server
|
|
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/repos/kunchenguid/no-mistakes/releases/latest":
|
|
fmt.Fprint(w, `{"tag_name":"v1.2.2","assets":[]}`)
|
|
default:
|
|
t.Fatalf("unexpected path %q", r.URL.Path)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
execPath := filepath.Join(t.TempDir(), "no-mistakes")
|
|
if err := os.WriteFile(execPath, []byte("current-binary"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
origDaemonIsRunning := daemonIsRunning
|
|
origDaemonExecutablePath := daemonExecutablePath
|
|
t.Cleanup(func() {
|
|
daemonIsRunning = origDaemonIsRunning
|
|
daemonExecutablePath = origDaemonExecutablePath
|
|
})
|
|
|
|
checks := 0
|
|
daemonIsRunning = func(*paths.Paths) (bool, error) {
|
|
checks++
|
|
return true, nil
|
|
}
|
|
daemonExecutablePath = func(*paths.Paths) (string, error) {
|
|
return "", errors.New("pid lookup failed")
|
|
}
|
|
|
|
stdout := new(bytes.Buffer)
|
|
u := &updater{
|
|
appName: "no-mistakes",
|
|
repo: "kunchenguid/no-mistakes",
|
|
currentVersion: "v1.2.2",
|
|
platform: platformSpec{GOOS: "darwin", GOARCH: "arm64"},
|
|
apiBaseURL: server.URL,
|
|
httpClient: server.Client(),
|
|
executablePath: execPath,
|
|
stdout: stdout,
|
|
now: func() time.Time { return time.Date(2026, 4, 9, 12, 0, 0, 0, time.UTC) },
|
|
paths: paths.WithRoot(t.TempDir()),
|
|
}
|
|
|
|
if err := u.run(context.Background()); err != nil {
|
|
t.Fatalf("run error = %v", err)
|
|
}
|
|
if checks != 0 {
|
|
t.Fatalf("expected no daemon executable check when already up to date, got %d checks", checks)
|
|
}
|
|
if !strings.Contains(stdout.String(), "already up to date") {
|
|
t.Fatalf("stdout = %q", stdout.String())
|
|
}
|
|
}
|
|
|
|
func TestUpdaterMaybeNotifyAndCheck(t *testing.T) {
|
|
cachePath := filepath.Join(t.TempDir(), "update-check.json")
|
|
if err := writeCache(cachePath, &checkCache{
|
|
CheckedAt: time.Date(2026, 4, 8, 12, 0, 0, 0, time.UTC),
|
|
LatestVersion: "v1.2.3",
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
stderr := new(bytes.Buffer)
|
|
spawned := false
|
|
u := &updater{
|
|
appName: "no-mistakes",
|
|
currentVersion: "v1.2.2",
|
|
cachePath: cachePath,
|
|
stderr: stderr,
|
|
now: func() time.Time { return time.Date(2026, 4, 9, 13, 0, 0, 0, time.UTC) },
|
|
spawnBackground: func(currentVersion string) error {
|
|
spawned = true
|
|
if currentVersion != "v1.2.2" {
|
|
t.Fatalf("currentVersion = %q", currentVersion)
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
|
|
u.maybeNotifyAndCheck([]string{"status"})
|
|
|
|
if !strings.Contains(stderr.String(), "A new version of no-mistakes is available: v1.2.2 -> v1.2.3") {
|
|
t.Fatalf("stderr = %q", stderr.String())
|
|
}
|
|
if !spawned {
|
|
t.Fatal("expected stale cache to trigger background refresh")
|
|
}
|
|
|
|
stderr.Reset()
|
|
spawned = false
|
|
u.maybeNotifyAndCheck([]string{"update"})
|
|
if stderr.Len() != 0 {
|
|
t.Fatalf("update command should not notify, got %q", stderr.String())
|
|
}
|
|
if spawned {
|
|
t.Fatal("update command should not spawn background refresh")
|
|
}
|
|
|
|
// Version queries are informational health checks: they must stay
|
|
// side-effect-free even with a stale cache and a newer version available
|
|
// (never print a notice, never spawn a background refresh) so callers can
|
|
// probe `--version` without turning it into an execution engine (#401).
|
|
for _, versionArgs := range [][]string{{"--version"}, {"-v"}} {
|
|
stderr.Reset()
|
|
spawned = false
|
|
u.maybeNotifyAndCheck(versionArgs)
|
|
if stderr.Len() != 0 {
|
|
t.Fatalf("%v should not notify, got %q", versionArgs, stderr.String())
|
|
}
|
|
if spawned {
|
|
t.Fatalf("%v should not spawn background refresh", versionArgs)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUpdaterCheckLatestBetaUsesReleasesList(t *testing.T) {
|
|
allowInsecureDownloads = true
|
|
t.Cleanup(func() { allowInsecureDownloads = false })
|
|
|
|
archiveName := "no-mistakes-v1.3.0-beta.1-darwin-arm64.tar.gz"
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/repos/kunchenguid/no-mistakes/releases":
|
|
fmt.Fprintf(w, `[
|
|
{"tag_name":"v1.3.0-beta.1","draft":false,"prerelease":true,"assets":[{"name":%q,"browser_download_url":"http://example.com/archive"},{"name":"checksums.txt","browser_download_url":"http://example.com/checksums"}]},
|
|
{"tag_name":"v1.2.3","draft":false,"prerelease":false,"assets":[]},
|
|
{"tag_name":"v1.4.0-draft","draft":true,"prerelease":true,"assets":[]}
|
|
]`, archiveName)
|
|
case "/repos/kunchenguid/no-mistakes/tags":
|
|
fmt.Fprint(w, `[{"name":"v1.3.0-beta.1"},{"name":"v1.2.3"}]`)
|
|
default:
|
|
t.Fatalf("unexpected path %q", r.URL.Path)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
u := &updater{
|
|
appName: "no-mistakes",
|
|
repo: "kunchenguid/no-mistakes",
|
|
currentVersion: "v1.2.3",
|
|
platform: platformSpec{GOOS: "darwin", GOARCH: "arm64"},
|
|
apiBaseURL: server.URL,
|
|
httpClient: server.Client(),
|
|
cachePath: filepath.Join(t.TempDir(), "update-check.json"),
|
|
now: func() time.Time { return time.Date(2026, 4, 22, 12, 0, 0, 0, time.UTC) },
|
|
includePrereleases: true,
|
|
}
|
|
|
|
plan, err := u.checkLatest(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("checkLatest error = %v", err)
|
|
}
|
|
if !plan.UpdateAvailable {
|
|
t.Fatal("expected update to be available")
|
|
}
|
|
if plan.LatestVersion != "v1.3.0-beta.1" {
|
|
t.Fatalf("LatestVersion = %q", plan.LatestVersion)
|
|
}
|
|
if plan.ArchiveName != archiveName {
|
|
t.Fatalf("ArchiveName = %q", plan.ArchiveName)
|
|
}
|
|
}
|
|
|
|
func TestUpdaterCheckLatestBetaPicksHighestSemver(t *testing.T) {
|
|
allowInsecureDownloads = true
|
|
t.Cleanup(func() { allowInsecureDownloads = false })
|
|
|
|
archiveName := "no-mistakes-v1.3.0-beta.2-darwin-arm64.tar.gz"
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/repos/kunchenguid/no-mistakes/releases":
|
|
fmt.Fprintf(w, `[
|
|
{"tag_name":"v1.3.0-beta.1","draft":false,"prerelease":true,"assets":[]},
|
|
{"tag_name":"v1.3.0-beta.2","draft":false,"prerelease":true,"assets":[{"name":%q,"browser_download_url":"http://example.com/archive"},{"name":"checksums.txt","browser_download_url":"http://example.com/checksums"}]},
|
|
{"tag_name":"v1.2.3","draft":false,"prerelease":false,"assets":[]}
|
|
]`, archiveName)
|
|
case "/repos/kunchenguid/no-mistakes/tags":
|
|
fmt.Fprint(w, `[{"name":"v1.3.0-beta.2"},{"name":"v1.3.0-beta.1"},{"name":"v1.2.3"}]`)
|
|
default:
|
|
t.Fatalf("unexpected path %q", r.URL.Path)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
u := &updater{
|
|
appName: "no-mistakes",
|
|
repo: "kunchenguid/no-mistakes",
|
|
currentVersion: "v1.2.3",
|
|
platform: platformSpec{GOOS: "darwin", GOARCH: "arm64"},
|
|
apiBaseURL: server.URL,
|
|
httpClient: server.Client(),
|
|
cachePath: filepath.Join(t.TempDir(), "update-check.json"),
|
|
now: func() time.Time { return time.Date(2026, 4, 22, 12, 0, 0, 0, time.UTC) },
|
|
includePrereleases: true,
|
|
}
|
|
|
|
plan, err := u.checkLatest(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("checkLatest error = %v", err)
|
|
}
|
|
if plan.LatestVersion != "v1.3.0-beta.2" {
|
|
t.Fatalf("LatestVersion = %q", plan.LatestVersion)
|
|
}
|
|
}
|
|
|
|
func TestUpdaterCheckLatestBetaFallsBackToTagsWhenListingStale(t *testing.T) {
|
|
allowInsecureDownloads = true
|
|
t.Cleanup(func() { allowInsecureDownloads = false })
|
|
|
|
archiveName := "no-mistakes-v1.3.0-beta.1-darwin-arm64.tar.gz"
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/repos/kunchenguid/no-mistakes/releases":
|
|
fmt.Fprint(w, `[
|
|
{"tag_name":"v1.2.3","draft":false,"prerelease":false,"assets":[]},
|
|
{"tag_name":"v1.2.2","draft":false,"prerelease":false,"assets":[]}
|
|
]`)
|
|
case "/repos/kunchenguid/no-mistakes/tags":
|
|
fmt.Fprint(w, `[{"name":"v1.3.0-beta.1"},{"name":"v1.2.3"},{"name":"v1.2.2"}]`)
|
|
case "/repos/kunchenguid/no-mistakes/releases/tags/v1.3.0-beta.1":
|
|
fmt.Fprintf(w, `{"tag_name":"v1.3.0-beta.1","draft":false,"prerelease":true,"assets":[{"name":%q,"browser_download_url":"http://example.com/archive"},{"name":"checksums.txt","browser_download_url":"http://example.com/checksums"}]}`, archiveName)
|
|
default:
|
|
t.Fatalf("unexpected path %q", r.URL.Path)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
u := &updater{
|
|
appName: "no-mistakes",
|
|
repo: "kunchenguid/no-mistakes",
|
|
currentVersion: "v1.2.3",
|
|
platform: platformSpec{GOOS: "darwin", GOARCH: "arm64"},
|
|
apiBaseURL: server.URL,
|
|
httpClient: server.Client(),
|
|
cachePath: filepath.Join(t.TempDir(), "update-check.json"),
|
|
now: func() time.Time { return time.Date(2026, 4, 22, 12, 0, 0, 0, time.UTC) },
|
|
includePrereleases: true,
|
|
}
|
|
|
|
plan, err := u.checkLatest(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("checkLatest error = %v", err)
|
|
}
|
|
if !plan.UpdateAvailable {
|
|
t.Fatal("expected update to be available")
|
|
}
|
|
if plan.LatestVersion != "v1.3.0-beta.1" {
|
|
t.Fatalf("LatestVersion = %q", plan.LatestVersion)
|
|
}
|
|
if plan.ArchiveName != archiveName {
|
|
t.Fatalf("ArchiveName = %q", plan.ArchiveName)
|
|
}
|
|
}
|
|
|
|
func TestUpdaterCheckLatestBetaChecksListedReleaseAfterMissingTags(t *testing.T) {
|
|
allowInsecureDownloads = true
|
|
t.Cleanup(func() { allowInsecureDownloads = false })
|
|
|
|
archiveName := "no-mistakes-v1.3.0-beta.1-darwin-arm64.tar.gz"
|
|
tagFetches := 0
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/repos/kunchenguid/no-mistakes/releases":
|
|
fmt.Fprintf(w, `[
|
|
{"tag_name":"v1.3.0-beta.1","draft":false,"prerelease":true,"assets":[{"name":%q,"browser_download_url":"http://example.com/archive"},{"name":"checksums.txt","browser_download_url":"http://example.com/checksums"}]},
|
|
{"tag_name":"v1.2.3","draft":false,"prerelease":false,"assets":[]}
|
|
]`, archiveName)
|
|
case "/repos/kunchenguid/no-mistakes/tags":
|
|
fmt.Fprint(w, `[
|
|
{"name":"v1.3.0-beta.6"},
|
|
{"name":"v1.3.0-beta.5"},
|
|
{"name":"v1.3.0-beta.4"},
|
|
{"name":"v1.3.0-beta.3"},
|
|
{"name":"v1.3.0-beta.2"},
|
|
{"name":"v1.3.0-beta.1"},
|
|
{"name":"v1.2.3"}
|
|
]`)
|
|
default:
|
|
if strings.HasPrefix(r.URL.Path, "/repos/kunchenguid/no-mistakes/releases/tags/") {
|
|
tagFetches++
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
t.Fatalf("unexpected path %q", r.URL.Path)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
u := &updater{
|
|
appName: "no-mistakes",
|
|
repo: "kunchenguid/no-mistakes",
|
|
currentVersion: "v1.2.3",
|
|
platform: platformSpec{GOOS: "darwin", GOARCH: "arm64"},
|
|
apiBaseURL: server.URL,
|
|
httpClient: server.Client(),
|
|
cachePath: filepath.Join(t.TempDir(), "update-check.json"),
|
|
now: func() time.Time { return time.Date(2026, 4, 22, 12, 0, 0, 0, time.UTC) },
|
|
includePrereleases: true,
|
|
}
|
|
|
|
plan, err := u.checkLatest(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("checkLatest error = %v", err)
|
|
}
|
|
if plan.LatestVersion != "v1.3.0-beta.1" {
|
|
t.Fatalf("LatestVersion = %q", plan.LatestVersion)
|
|
}
|
|
if tagFetches != 5 {
|
|
t.Fatalf("tagFetches = %d, want 5", tagFetches)
|
|
}
|
|
}
|
|
|
|
func TestUpdaterCachedLatestVersion(t *testing.T) {
|
|
cachePath := filepath.Join(t.TempDir(), "update-check.json")
|
|
if err := writeCache(cachePath, &checkCache{
|
|
CheckedAt: time.Date(2026, 4, 8, 12, 0, 0, 0, time.UTC),
|
|
LatestVersion: "v1.2.3",
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
u := &updater{
|
|
currentVersion: "v1.2.2",
|
|
cachePath: cachePath,
|
|
}
|
|
|
|
if got := u.cachedLatestVersion(); got != "v1.2.3" {
|
|
t.Fatalf("cachedLatestVersion() = %q, want %q", got, "v1.2.3")
|
|
}
|
|
|
|
u.currentVersion = "v1.2.3"
|
|
if got := u.cachedLatestVersion(); got != "" {
|
|
t.Fatalf("cachedLatestVersion() = %q, want empty when already current", got)
|
|
}
|
|
}
|