Files

461 lines
18 KiB
Go

package catalog
import (
"bytes"
"encoding/json"
"errors"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
)
func TestCacheRoundTripStoresRawJSONValueAndReparsesSnapshot(t *testing.T) {
path := filepath.Join(t.TempDir(), "models.dev.catalog.json")
result := validSyncResult(t)
if err := StoreCache(path, result); err != nil {
t.Fatalf("StoreCache() error = %v", err)
}
contents, err := os.ReadFile(path)
if err != nil {
t.Fatalf("ReadFile(cache) error = %v", err)
}
if bytes.Contains(contents, []byte(`"raw":"`)) || !bytes.Contains(contents, []byte(`"raw":{`)) {
t.Fatalf("cache raw payload is not an embedded JSON value: %s", contents)
}
var document map[string]json.RawMessage
if err := json.Unmarshal(contents, &document); err != nil {
t.Fatalf("cache document JSON error = %v", err)
}
if string(document["version"]) != "1" {
t.Fatalf("cache version = %s, want 1", document["version"])
}
loaded, err := LoadCache(path)
if err != nil {
t.Fatalf("LoadCache() error = %v", err)
}
if loaded.Metadata != result.Metadata || string(loaded.RawJSON) != string(result.RawJSON) || !reflect.DeepEqual(loaded.Snapshot, result.Snapshot) {
t.Fatalf("loaded cache = %#v, want result %#v", loaded, result)
}
loadedProvider := loaded.Snapshot.Providers["openai"]
loadedModel := loadedProvider.Models["gpt-x"]
*loadedModel.Metadata.Capabilities.Attachment = false
loadedModel.Metadata.Modalities.Input[0] = "audio"
*loadedModel.Metadata.Limits.Context = 99
loadedProvider.Models["gpt-x"] = loadedModel
loaded.Snapshot.Providers["openai"] = loadedProvider
sourceMetadata := result.Snapshot.Providers["openai"].Models["gpt-x"].Metadata
if sourceMetadata.Capabilities.Attachment == nil || !*sourceMetadata.Capabilities.Attachment ||
sourceMetadata.Modalities.Input[0] != "text" ||
sourceMetadata.Limits.Context == nil || *sourceMetadata.Limits.Context != 1_000_000 {
t.Fatalf("loaded cache shares snapshot metadata with source: %#v", sourceMetadata)
}
reloaded, err := LoadCache(path)
if err != nil {
t.Fatalf("LoadCache() after caller mutation error = %v", err)
}
reloadedMetadata := reloaded.Snapshot.Providers["openai"].Models["gpt-x"].Metadata
if reloadedMetadata.Capabilities.Attachment == nil || !*reloadedMetadata.Capabilities.Attachment ||
reloadedMetadata.Modalities.Input[0] != "text" ||
reloadedMetadata.Limits.Context == nil || *reloadedMetadata.Limits.Context != 1_000_000 {
t.Fatalf("caller mutation changed cached metadata: %#v", reloadedMetadata)
}
if runtime.GOOS != "windows" {
info, err := os.Stat(path)
if err != nil {
t.Fatalf("Stat(cache) error = %v", err)
}
if info.Mode().Perm() != 0o600 {
t.Fatalf("cache mode = %o, want 600", info.Mode().Perm())
}
}
}
func TestLoadCacheRejectsUnsupportedCorruptAndOversizeDocuments(t *testing.T) {
validRaw := `{"openai":{"id":"openai","name":"OpenAI","models":{}}}`
tests := []struct {
name string
contents string
}{
{name: "duplicate version", contents: `{"version":2,"version":1,"successful_fetch_at_ms":1,"raw":` + validRaw + `}`},
{name: "duplicate raw", contents: `{"version":1,"successful_fetch_at_ms":1,"raw":{},"raw":` + validRaw + `}`},
{name: "duplicate etag", contents: `{"version":1,"etag":"old","etag":"new","successful_fetch_at_ms":1,"raw":` + validRaw + `}`},
{name: "duplicate last modified", contents: `{"version":1,"last_modified":"old","last_modified":"new","successful_fetch_at_ms":1,"raw":` + validRaw + `}`},
{name: "duplicate checked timestamp", contents: `{"version":1,"checked_at_ms":2,"checked_at_ms":3,"successful_fetch_at_ms":1,"raw":` + validRaw + `}`},
{name: "duplicate successful timestamp", contents: `{"version":1,"successful_fetch_at_ms":0,"successful_fetch_at_ms":1,"raw":` + validRaw + `}`},
{name: "version case alias", contents: `{"version":2,"Version":1,"successful_fetch_at_ms":1,"raw":` + validRaw + `}`},
{name: "raw case alias", contents: `{"version":1,"successful_fetch_at_ms":1,"raw":{},"Raw":` + validRaw + `}`},
{name: "etag case alias", contents: `{"version":1,"etag":"old","ETag":"new","successful_fetch_at_ms":1,"raw":` + validRaw + `}`},
{name: "last modified case alias", contents: `{"version":1,"last_modified":"old","Last_Modified":"new","successful_fetch_at_ms":1,"raw":` + validRaw + `}`},
{name: "checked timestamp case alias", contents: `{"version":1,"checked_at_ms":2,"Checked_At_MS":3,"successful_fetch_at_ms":1,"raw":` + validRaw + `}`},
{name: "successful timestamp case alias", contents: `{"version":1,"successful_fetch_at_ms":0,"Successful_Fetch_At_MS":1,"raw":` + validRaw + `}`},
{name: "unsupported version", contents: `{"version":2,"successful_fetch_at_ms":1,"raw":` + validRaw + `}`},
{name: "zero successful fetch time", contents: `{"version":1,"successful_fetch_at_ms":0,"raw":` + validRaw + `}`},
{name: "negative successful fetch time", contents: `{"version":1,"successful_fetch_at_ms":-1,"raw":` + validRaw + `}`},
{name: "invalid etag control", contents: `{"version":1,"etag":"bad\nvalue","successful_fetch_at_ms":1,"raw":` + validRaw + `}`},
{name: "invalid last modified control", contents: `{"version":1,"last_modified":"bad\nvalue","successful_fetch_at_ms":1,"raw":` + validRaw + `}`},
{name: "missing raw", contents: `{"version":1,"successful_fetch_at_ms":1}`},
{name: "raw string instead object", contents: `{"version":1,"successful_fetch_at_ms":1,"raw":"` + strings.ReplaceAll(validRaw, `"`, `\"`) + `"}`},
{name: "invalid raw snapshot", contents: `{"version":1,"successful_fetch_at_ms":1,"raw":{"openai":{"id":"wrong","name":"OpenAI","models":{}}}}`},
{name: "duplicate raw provider", contents: `{"version":1,"successful_fetch_at_ms":1,"raw":{"openai":{"id":"openai","name":"OpenAI","models":{}},"openai":{"id":"openai","name":"Other","models":{}}}}`},
{name: "trailing cache JSON", contents: `{"version":1,"successful_fetch_at_ms":1,"raw":` + validRaw + `}{}`},
{name: "malformed cache JSON", contents: `{"version":1`},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
path := filepath.Join(t.TempDir(), "models.dev.catalog.json")
if err := os.WriteFile(path, []byte(test.contents), 0o600); err != nil {
t.Fatalf("WriteFile() error = %v", err)
}
if _, err := LoadCache(path); err == nil {
t.Fatalf("LoadCache() accepted invalid document: %s", test.contents)
}
})
}
t.Run("oversize file", func(t *testing.T) {
path := filepath.Join(t.TempDir(), "models.dev.catalog.json")
file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0o600)
if err != nil {
t.Fatalf("OpenFile() error = %v", err)
}
if err := file.Truncate(maxCacheFileBytes + 1); err != nil {
_ = file.Close()
t.Fatalf("Truncate() error = %v", err)
}
if err := file.Close(); err != nil {
t.Fatalf("Close() error = %v", err)
}
if _, err := LoadCache(path); err == nil {
t.Fatal("LoadCache() accepted oversize file")
}
})
}
func TestStoreCacheAcceptsOnlyInternallyConsistentValidated200Result(t *testing.T) {
valid := validSyncResult(t)
tests := []struct {
name string
mutate func(*SyncResult)
}{
{name: "not modified", mutate: func(result *SyncResult) { result.NotModified = true }},
{name: "missing raw", mutate: func(result *SyncResult) { result.RawJSON = nil }},
{name: "missing snapshot", mutate: func(result *SyncResult) { result.Snapshot = nil }},
{name: "invalid raw", mutate: func(result *SyncResult) { result.RawJSON = json.RawMessage(`{}` + `{}`) }},
{name: "snapshot mismatch", mutate: func(result *SyncResult) {
result.Snapshot = &Snapshot{Providers: map[string]Provider{}}
}},
{name: "missing successful fetch time", mutate: func(result *SyncResult) {
result.Metadata.SuccessfulFetchAtMillis = 0
}},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := valid
test.mutate(&result)
path := filepath.Join(t.TempDir(), "models.dev.catalog.json")
if err := StoreCache(path, result); err == nil {
t.Fatal("StoreCache() accepted invalid result")
}
if _, err := os.Stat(path); !os.IsNotExist(err) {
t.Fatalf("invalid result created cache file: %v", err)
}
})
}
}
func TestCacheFailedReplacementKeepsPreviousFileAndRemovesTemporary(t *testing.T) {
directory := t.TempDir()
path := filepath.Join(directory, "models.dev.catalog.json")
previous := []byte("previous last known good bytes\n")
if err := os.WriteFile(path, previous, 0o600); err != nil {
t.Fatalf("WriteFile(previous) error = %v", err)
}
replaceErr := errors.New("injected atomic replacement failure")
replaceCalls := 0
syncCalls := 0
err := storeCache(
path,
validSyncResult(t),
func(temporaryPath, finalPath string) error {
replaceCalls++
if finalPath != path || filepath.Dir(temporaryPath) != directory {
t.Fatalf("replace paths = %q -> %q", temporaryPath, finalPath)
}
if replaceCalls == 1 {
return replaceErr
}
return os.Rename(temporaryPath, finalPath)
},
func(string) error {
syncCalls++
return nil
},
)
if !errors.Is(err, replaceErr) || replaceCalls != 2 || syncCalls != 1 {
t.Fatalf("storeCache() error/replace/sync calls = %v/%d/%d, want injected error/2/1", err, replaceCalls, syncCalls)
}
got, err := os.ReadFile(path)
if err != nil {
t.Fatalf("ReadFile(previous) error = %v", err)
}
if !bytes.Equal(got, previous) {
t.Fatalf("previous LKG changed: got %q, want %q", got, previous)
}
temporaryFiles, err := filepath.Glob(filepath.Join(directory, ".models.dev.catalog.json.*.tmp"))
if err != nil {
t.Fatalf("Glob(temp files) error = %v", err)
}
if len(temporaryFiles) != 0 {
t.Fatalf("temporary files remain after failure: %#v", temporaryFiles)
}
}
func TestCacheReplaceErrorAfterChangingFinalRestoresPreviousFile(t *testing.T) {
directory := t.TempDir()
path := filepath.Join(directory, "models.dev.catalog.json")
previous := []byte("previous LKG before uncertain replacement\n")
if err := os.WriteFile(path, previous, 0o600); err != nil {
t.Fatalf("WriteFile(previous) error = %v", err)
}
replaceErr := errors.New("replace reported failure after changing final")
replaceCalls := 0
syncCalls := 0
err := storeCache(
path,
validSyncResult(t),
func(temporaryPath, finalPath string) error {
replaceCalls++
if replaceCalls == 1 {
if err := os.Rename(temporaryPath, finalPath); err != nil {
t.Fatalf("simulate uncertain replacement: %v", err)
}
return replaceErr
}
return os.Rename(temporaryPath, finalPath)
},
func(string) error {
syncCalls++
return nil
},
)
if !errors.Is(err, replaceErr) || replaceCalls != 2 || syncCalls != 1 {
t.Fatalf("storeCache() error/replace/sync calls = %v/%d/%d, want original error/2/1", err, replaceCalls, syncCalls)
}
got, err := os.ReadFile(path)
if err != nil {
t.Fatalf("ReadFile(restored final) error = %v", err)
}
if !bytes.Equal(got, previous) {
t.Fatalf("restored final = %q, want previous bytes %q", got, previous)
}
assertNoCatalogTemporaryFiles(t, directory)
}
func TestCacheReplaceErrorAndRollbackFailureRetainsPreviousBackupPath(t *testing.T) {
directory := t.TempDir()
path := filepath.Join(directory, "models.dev.catalog.json")
previous := []byte("previous LKG retained after uncertain replacement\n")
if err := os.WriteFile(path, previous, 0o600); err != nil {
t.Fatalf("WriteFile(previous) error = %v", err)
}
replaceErr := errors.New("replace reported failure after changing final")
rollbackErr := errors.New("rollback primitive failed")
replaceCalls := 0
storeErr := storeCache(
path,
validSyncResult(t),
func(temporaryPath, finalPath string) error {
replaceCalls++
if replaceCalls == 1 {
if err := os.Rename(temporaryPath, finalPath); err != nil {
t.Fatalf("simulate uncertain replacement: %v", err)
}
return replaceErr
}
if err := os.Rename(temporaryPath, finalPath); err != nil {
t.Fatalf("simulate uncertain rollback replacement: %v", err)
}
return rollbackErr
},
func(string) error {
t.Fatal("directory sync called after failed rollback primitive")
return nil
},
)
if !errors.Is(storeErr, replaceErr) || !errors.Is(storeErr, rollbackErr) || replaceCalls != 2 {
t.Fatalf("storeCache() error/calls = %v/%d, want both errors and two replacements", storeErr, replaceCalls)
}
backupFiles, err := filepath.Glob(filepath.Join(directory, ".models.dev.catalog.json.*.tmp"))
if err != nil {
t.Fatalf("Glob(backup files) error = %v", err)
}
if len(backupFiles) != 1 || !strings.Contains(storeErr.Error(), backupFiles[0]) {
t.Fatalf("retained backups/error = %#v/%q, want one explicit recovery path", backupFiles, storeErr)
}
backup, err := os.ReadFile(backupFiles[0])
if err != nil {
t.Fatalf("ReadFile(retained backup) error = %v", err)
}
if !bytes.Equal(backup, previous) {
t.Fatalf("retained backup = %q, want previous bytes %q", backup, previous)
}
}
func TestCacheReplaceErrorWithoutPreviousFileRemovesPossibleFinal(t *testing.T) {
directory := t.TempDir()
path := filepath.Join(directory, "models.dev.catalog.json")
replaceErr := errors.New("first publication reported failure after changing final")
syncCalls := 0
err := storeCache(
path,
validSyncResult(t),
func(temporaryPath, finalPath string) error {
if err := os.Rename(temporaryPath, finalPath); err != nil {
t.Fatalf("simulate uncertain first publication: %v", err)
}
return replaceErr
},
func(string) error {
syncCalls++
return nil
},
)
if !errors.Is(err, replaceErr) || syncCalls != 1 {
t.Fatalf("storeCache() error/sync calls = %v/%d, want original error/1", err, syncCalls)
}
if _, err := os.Stat(path); !os.IsNotExist(err) {
t.Fatalf("uncertain first publication final still exists: %v", err)
}
assertNoCatalogTemporaryFiles(t, directory)
}
func TestCacheDirectorySyncFailureAfterReplacementRestoresPreviousFile(t *testing.T) {
directory := t.TempDir()
path := filepath.Join(directory, "models.dev.catalog.json")
previous := []byte("previous durable last known good bytes\n")
if err := os.WriteFile(path, previous, 0o600); err != nil {
t.Fatalf("WriteFile(previous) error = %v", err)
}
syncErr := errors.New("injected parent directory sync failure")
syncCalls := 0
err := storeCache(
path,
validSyncResult(t),
os.Rename,
func(string) error {
syncCalls++
if syncCalls == 1 {
return syncErr
}
return nil
},
)
if !errors.Is(err, syncErr) {
t.Fatalf("storeCache() error = %v, want injected directory sync error", err)
}
if syncCalls < 2 {
t.Fatalf("directory sync calls = %d, want replacement failure plus restored-LKG sync", syncCalls)
}
got, err := os.ReadFile(path)
if err != nil {
t.Fatalf("ReadFile(restored LKG) error = %v", err)
}
if !bytes.Equal(got, previous) {
t.Fatalf("directory sync failure left changed LKG: got %q, want %q", got, previous)
}
temporaryFiles, err := filepath.Glob(filepath.Join(directory, ".models.dev.catalog.json.*.tmp"))
if err != nil {
t.Fatalf("Glob(temp files) error = %v", err)
}
if len(temporaryFiles) != 0 {
t.Fatalf("temporary/backup files remain after restore: %#v", temporaryFiles)
}
}
func TestCacheRollbackReplaceFailureKeepsRecoverablePreviousBackup(t *testing.T) {
directory := t.TempDir()
path := filepath.Join(directory, "models.dev.catalog.json")
previous := []byte("only previous durable last known good bytes\n")
if err := os.WriteFile(path, previous, 0o600); err != nil {
t.Fatalf("WriteFile(previous) error = %v", err)
}
syncErr := errors.New("injected parent directory sync failure")
rollbackErr := errors.New("injected rollback replace failure")
replaceCalls := 0
storeErr := storeCache(
path,
validSyncResult(t),
func(temporaryPath, finalPath string) error {
replaceCalls++
if replaceCalls == 1 {
return os.Rename(temporaryPath, finalPath)
}
return rollbackErr
},
func(string) error { return syncErr },
)
if !errors.Is(storeErr, syncErr) || !errors.Is(storeErr, rollbackErr) || replaceCalls != 2 {
t.Fatalf("storeCache() error/calls = %v/%d, want both injected errors and two replacements", storeErr, replaceCalls)
}
backupFiles, err := filepath.Glob(filepath.Join(directory, ".models.dev.catalog.json.*.tmp"))
if err != nil {
t.Fatalf("Glob(backup files) error = %v", err)
}
if len(backupFiles) != 1 {
t.Fatalf("recoverable backup files = %#v, want exactly one retained backup", backupFiles)
}
if !strings.Contains(storeErr.Error(), backupFiles[0]) {
t.Fatalf("storeCache() error = %q, want explicit recoverable backup path %q", storeErr, backupFiles[0])
}
backup, err := os.ReadFile(backupFiles[0])
if err != nil {
t.Fatalf("ReadFile(retained backup) error = %v", err)
}
if !bytes.Equal(backup, previous) {
t.Fatalf("retained backup = %q, want previous LKG bytes %q", backup, previous)
}
finalContents, err := os.ReadFile(path)
if err != nil {
t.Fatalf("ReadFile(final after failed rollback) error = %v", err)
}
if bytes.Equal(finalContents, previous) {
t.Fatal("failed rollback unexpectedly reports the final path as restored; recovery guarantee belongs to the sibling backup")
}
}
func assertNoCatalogTemporaryFiles(t *testing.T, directory string) {
t.Helper()
temporaryFiles, err := filepath.Glob(filepath.Join(directory, ".models.dev.catalog.json.*.tmp"))
if err != nil {
t.Fatalf("Glob(temp files) error = %v", err)
}
if len(temporaryFiles) != 0 {
t.Fatalf("temporary files remain: %#v", temporaryFiles)
}
}
func validSyncResult(t *testing.T) SyncResult {
t.Helper()
raw := json.RawMessage(`{"openai":{"id":"openai","name":"OpenAI","models":{"gpt-x":{"id":"gpt-x","name":"GPT X","description":"General model","attachment":true,"modalities":{"input":["text","image"],"output":["text"]},"limit":{"context":1000000,"output":100000},"cost":{"input":0.000000001}}}}}`)
snapshot, err := MergeOfficial(mustParse(t, string(raw)))
if err != nil {
t.Fatalf("MergeOfficial() error = %v", err)
}
return SyncResult{
Metadata: Metadata{
ETag: `"catalog-v1"`,
LastModified: "Mon, 03 Aug 2026 01:00:00 GMT",
CheckedAtMillis: 1_754_180_400_123,
SuccessfulFetchAtMillis: 1_754_180_400_123,
},
RawJSON: raw,
Snapshot: snapshot,
}
}