Files
ThothII/tools/tht/internal/pi/latest_test.go
T

217 lines
8.6 KiB
Go

package pi
import (
"context"
"errors"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
type registryFunc func(context.Context, string) (string, error)
func (f registryFunc) LatestStable(ctx context.Context, packageName string) (string, error) {
return f(ctx, packageName)
}
type roundTripFunc func(*http.Request) (*http.Response, error)
func (f roundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) {
return f(request)
}
func TestNPMRegistryClientSelectsHighestStableVersionAndIgnoresPrereleases(t *testing.T) {
client := newNPMRegistryClient(&http.Client{Transport: roundTripFunc(func(request *http.Request) (*http.Response, error) {
if request.URL.Scheme != "https" || request.URL.Host != "registry.npmjs.org" {
t.Fatalf("registry request = %s; want authoritative HTTPS registry", request.URL)
}
if request.URL.Path != "/@earendil-works%2Fpi-coding-agent" && request.URL.EscapedPath() != "/@earendil-works%2Fpi-coding-agent" {
t.Fatalf("registry path = %q", request.URL.EscapedPath())
}
return registryResponse(http.StatusOK, `{"versions":{"0.80.3":{},"0.81.0-rc.1":{},"0.81.0":{},"1.0.0-beta.2":{}}}`), nil
})})
got, err := client.LatestStable(context.Background(), "@earendil-works/pi-coding-agent")
if err != nil {
t.Fatal(err)
}
if got != "0.81.0" {
t.Fatalf("LatestStable() = %q, want 0.81.0", got)
}
}
func TestNPMRegistryClientRejectsInvalidResponses(t *testing.T) {
for _, test := range []struct {
name string
status int
body string
wantText string
}{
{name: "prerelease only", status: http.StatusOK, body: `{"versions":{"1.0.0-rc.1":{}}}`, wantText: "no stable"},
{name: "malformed JSON", status: http.StatusOK, body: `{`, wantText: "invalid"},
{name: "malformed semantic version key", status: http.StatusOK, body: `{"versions":{"0.81.0":{},"latest":{}}}`, wantText: "invalid"},
{name: "no version data", status: http.StatusOK, body: `{"versions":{}}`, wantText: "no stable"},
{name: "package not found", status: http.StatusNotFound, body: `{"error":"not_found"}`, wantText: "not found"},
} {
t.Run(test.name, func(t *testing.T) {
client := newNPMRegistryClient(&http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
return registryResponse(test.status, test.body), nil
})})
_, err := client.LatestStable(context.Background(), "@earendil-works/pi-coding-agent")
if err == nil || !strings.Contains(strings.ToLower(err.Error()), test.wantText) {
t.Fatalf("LatestStable() error = %v, want %q", err, test.wantText)
}
})
}
}
func TestNPMRegistryClientRejectsRedirectsOutsideTheAuthoritativePackageURL(t *testing.T) {
for _, destination := range []string{
"http://registry.npmjs.org/@earendil-works%2Fpi-coding-agent",
"https://mirror.example.invalid/@earendil-works%2Fpi-coding-agent",
} {
t.Run(destination, func(t *testing.T) {
calls := 0
client := newNPMRegistryClient(&http.Client{Transport: roundTripFunc(func(request *http.Request) (*http.Response, error) {
calls++
response := registryResponse(http.StatusFound, "")
response.Header.Set("Location", destination)
response.Request = request
return response, nil
})})
_, err := client.LatestStable(context.Background(), "@earendil-works/pi-coding-agent")
if err == nil || !strings.Contains(strings.ToLower(err.Error()), "redirect") {
t.Fatalf("LatestStable() redirect error = %v, want rejected redirect", err)
}
if calls != 1 {
t.Fatalf("LatestStable() followed rejected redirect %d times", calls-1)
}
})
}
}
func TestNPMRegistryClientRejectsAnOversizedResponse(t *testing.T) {
client := newNPMRegistryClient(&http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
return registryResponse(http.StatusOK, `{"versions":{"1.2.3":{}}}`+strings.Repeat(" ", registryBodyLimit+1)), nil
})})
_, err := client.LatestStable(context.Background(), "@earendil-works/pi-coding-agent")
if err == nil || !strings.Contains(err.Error(), "too large") {
t.Fatalf("LatestStable() error = %v, want bounded-response error", err)
}
}
func TestNPMRegistryClientHonorsContextTimeout(t *testing.T) {
client := newNPMRegistryClient(&http.Client{Transport: roundTripFunc(func(request *http.Request) (*http.Response, error) {
<-request.Context().Done()
return nil, request.Context().Err()
})})
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
defer cancel()
_, err := client.LatestStable(ctx, "@earendil-works/pi-coding-agent")
if !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("LatestStable() error = %v, want deadline exceeded", err)
}
}
func TestResolveRequestedVersionBypassesRegistryForExplicitVersion(t *testing.T) {
called := false
got, err := ResolveRequestedVersion(context.Background(), "1.2.3-rc.1", "@earendil-works/pi-coding-agent", registryFunc(func(context.Context, string) (string, error) {
called = true
return "", errors.New("registry must not be called")
}))
if err != nil {
t.Fatal(err)
}
if got != "1.2.3-rc.1" || called {
t.Fatalf("ResolveRequestedVersion() = %q, called=%t; want explicit prerelease without lookup", got, called)
}
}
func TestResolveRequestedVersionRejectsNonStrictSemver(t *testing.T) {
for _, version := range []string{"1.2", "01.2.3", "v1.2.3", "1.2.3.4", "1.2.3-01"} {
_, err := ResolveRequestedVersion(context.Background(), version, "@earendil-works/pi-coding-agent", nil)
if err == nil || !strings.Contains(err.Error(), "semantic version") {
t.Fatalf("ResolveRequestedVersion(%q) error = %v, want strict semver error", version, err)
}
}
}
func TestUpdateWithResolvedVersionDoesNotMutateWhenDiscoveryFails(t *testing.T) {
for _, test := range []struct {
name string
err error
}{
{name: "timeout", err: context.DeadlineExceeded},
{name: "package missing", err: errors.New("package not found")},
{name: "malformed", err: errors.New("registry response is invalid")},
{name: "no stable version", err: errors.New("registry contains no stable version")},
{name: "malformed semantic version key", err: errors.New("registry version key is invalid")},
} {
t.Run(test.name, func(t *testing.T) {
fake := newFakeRunner()
statePath := t.TempDir() + "/state/update.json"
_, err := UpdateWithResolvedVersion(context.Background(), fake, Request{StatePath: statePath, Source: BuildSource, Confirm: true, Drain: true}, "@earendil-works/pi-coding-agent", registryFunc(func(context.Context, string) (string, error) {
return "", test.err
}))
if !errors.Is(err, test.err) {
t.Fatalf("UpdateWithResolvedVersion() error = %v, want %v", err, test.err)
}
if len(fake.calls) != 0 {
t.Fatalf("discovery failure invoked Docker or lifecycle commands: %v", fake.calls)
}
if _, statErr := os.Stat(filepath.Dir(statePath)); !errors.Is(statErr, os.ErrNotExist) {
t.Fatalf("discovery failure created update state directory: %v", statErr)
}
})
}
}
func TestUpdateWithResolvedVersionDoesNotMutateForMalformedRegistryVersionKeys(t *testing.T) {
fake := newFakeRunner()
statePath := t.TempDir() + "/state/update.json"
registry := newNPMRegistryClient(&http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
return registryResponse(http.StatusOK, `{"versions":{"0.81.0":{},"not-a-semver":{}}}`), nil
})})
_, err := UpdateWithResolvedVersion(context.Background(), fake, Request{StatePath: statePath, Source: BuildSource, Confirm: true, Drain: true}, "@earendil-works/pi-coding-agent", registry)
if err == nil || !strings.Contains(err.Error(), "invalid") {
t.Fatalf("UpdateWithResolvedVersion() error = %v, want malformed registry version rejection", err)
}
if len(fake.calls) != 0 {
t.Fatalf("malformed registry version invoked Docker or lifecycle commands: %v", fake.calls)
}
if _, statErr := os.Stat(filepath.Dir(statePath)); !errors.Is(statErr, os.ErrNotExist) {
t.Fatalf("malformed registry version created update state directory: %v", statErr)
}
}
func TestReadRuntimePackageNameReadsThePiDependency(t *testing.T) {
root := t.TempDir()
path := filepath.Join(root, "docker", "pi-runtime")
if err := os.MkdirAll(path, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(path, "package.json"), []byte(`{"dependencies":{"@earendil-works/pi-coding-agent":"0.80.3"}}`), 0o600); err != nil {
t.Fatal(err)
}
got, err := ReadRuntimePackageName(root)
if err != nil {
t.Fatal(err)
}
if got != "@earendil-works/pi-coding-agent" {
t.Fatalf("ReadRuntimePackageName() = %q", got)
}
}
func registryResponse(status int, body string) *http.Response {
return &http.Response{
StatusCode: status,
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader(body)),
}
}