Round 5, and the best findings are again about the fix from round 4. The scrub only ran on success. `set -e` aborts the step when `go mod download` fails, so the cleanup line after it never executed — leaving a push-capable credential on a long-lived self-hosted runner for whatever job landed there next. It is now a `trap ... EXIT`, verified against a simulated failure. It also scrubbed the wrong file in principle: `git config --global` writes to GIT_CONFIG_GLOBAL, else $XDG_CONFIG_HOME/git/config when that exists, else ~/.gitconfig — so deleting ~/.gitconfig can scrub a path the credential was never in. The step now names GIT_CONFIG_GLOBAL itself, leaving exactly one file to remove. And the verification failed open in the case that matters most: `grep -F ""` matches every file, so a run WITHOUT the secret — a fork PR, the threat model — failed the check with a message accusing it of leaking a credential it never had. Guarded on a non-empty secret. Credentials move to an Authorization header instead of being embedded in the URL, so a password containing @ : / or # can no longer break URL parsing in a way that reads as a bad password. Two list-drift holes closed with one test that reads across languages: TestOpenAICompatProvidersAreFullyWired asserts every openAICompatProviders entry is both advertised in endpointProviderNames and has a credential arm in scripts/preflight.sh. Adding a compat provider touches three places in two languages and nothing connected them. Break-checked in both directions. Finally, a whitespace-only GADFLY_BASE_URL disagreed across the boundary: Go TrimSpaces it and takes the registry path, bash called it "set" and skipped the pre-flight, so the missing key arrived as a bare 401 with no notice. Both now agree on what unset means. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
196 lines
7.6 KiB
Go
196 lines
7.6 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestEndpointProvider(t *testing.T) {
|
|
t.Run("ollama http endpoint registers under its name", func(t *testing.T) {
|
|
p, err := endpointProvider("bigbox", "ollama|http://192.168.1.50:11434")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if p.Name() != "bigbox" {
|
|
t.Errorf("Name() = %q, want %q", p.Name(), "bigbox")
|
|
}
|
|
})
|
|
t.Run("openai compatible with key", func(t *testing.T) {
|
|
if _, err := endpointProvider("gpu", "openai|http://gpu.lan:8000/v1|sk-x"); err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
})
|
|
t.Run("foreman queue registers under its name", func(t *testing.T) {
|
|
p, err := endpointProvider("m1", "foreman|http://foreman-m1:8080|tok")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
// WithName(name) must win over the Foreman preset's default "foreman".
|
|
if p.Name() != "m1" {
|
|
t.Errorf("Name() = %q, want %q", p.Name(), "m1")
|
|
}
|
|
})
|
|
t.Run("foreman without token", func(t *testing.T) {
|
|
if _, err := endpointProvider("m5", "foreman|http://foreman-m5:8080"); err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
})
|
|
t.Run("llamaswap registers under its name", func(t *testing.T) {
|
|
p, err := endpointProvider("ls", "llamaswap|http://swap.lan:8080|tok")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if p.Name() != "ls" {
|
|
t.Errorf("Name() = %q, want %q", p.Name(), "ls")
|
|
}
|
|
})
|
|
t.Run("llamaswap without token", func(t *testing.T) {
|
|
if _, err := endpointProvider("ls2", "llamaswap|http://swap.lan:8080"); err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
})
|
|
// All llama-swap spellings (hyphenated/TLS variants mirror majordomo's DSN
|
|
// schemes) must resolve to the llamaswap provider.
|
|
for _, name := range []string{"llama-swap", "llama-swaps", "llamaswaps"} {
|
|
t.Run(name+" alias", func(t *testing.T) {
|
|
p, err := endpointProvider("ls", name+"|https://swap.lan:8080|tok")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if p.Name() != "ls" {
|
|
t.Errorf("Name() = %q, want %q", p.Name(), "ls")
|
|
}
|
|
})
|
|
}
|
|
for _, bad := range []string{"", "ollama", "noprovider-no-pipe", "mystery|http://x"} {
|
|
t.Run("rejects "+bad, func(t *testing.T) {
|
|
if _, err := endpointProvider("n", bad); err == nil {
|
|
t.Errorf("endpointProvider(%q) = nil error, want error", bad)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestOpenAICompatProvidersResolveOnBothPaths pins the two provider switches
|
|
// together. kimi and qwen are majordomo built-ins that ARE the openai client at
|
|
// a different base URL, and two independent places have to know it:
|
|
// resolveModel's GADFLY_BASE_URL override, and endpointProvider's
|
|
// GADFLY_ENDPOINT_* parser. A name accepted by one and rejected by the other is
|
|
// a provider that works when configured one way and errors the other, for no
|
|
// reason a user could guess. Asserting both from one table makes the pair fail
|
|
// together.
|
|
func TestOpenAICompatProvidersResolveOnBothPaths(t *testing.T) {
|
|
// Ranges the SHARED slice: a test that pins a list against drift must not
|
|
// be able to drift from it.
|
|
for _, provider := range openAICompatProviders {
|
|
t.Run(provider+" via GADFLY_ENDPOINT_*", func(t *testing.T) {
|
|
p, err := endpointProvider("ep", provider+"|https://host.example/v1|sk-x")
|
|
if err != nil {
|
|
t.Fatalf("endpointProvider(%q): %v", provider, err)
|
|
}
|
|
if p.Name() != "ep" {
|
|
t.Errorf("Name() = %q, want %q", p.Name(), "ep")
|
|
}
|
|
})
|
|
t.Run(provider+" via GADFLY_BASE_URL", func(t *testing.T) {
|
|
t.Setenv("GADFLY_PROVIDER", provider)
|
|
t.Setenv("GADFLY_BASE_URL", "https://host.example/v1")
|
|
t.Setenv("GADFLY_API_KEY", "sk-x")
|
|
t.Setenv("GADFLY_MODEL", "some-model")
|
|
if _, err := resolveModel(); err != nil {
|
|
t.Fatalf("resolveModel with GADFLY_PROVIDER=%q: %v", provider, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestEndpointProviderNamesAreAllAccepted keeps the operator-facing list
|
|
// honest: every name endpointProviderNames advertises must actually resolve.
|
|
// The constant is read by somebody whose config just failed, so a name listed
|
|
// there and rejected by the code sends them to debug a spelling that was never
|
|
// going to work.
|
|
func TestEndpointProviderNamesAreAllAccepted(t *testing.T) {
|
|
for _, name := range strings.Split(endpointProviderNames, "/") {
|
|
name = strings.TrimSpace(name)
|
|
if name == "" {
|
|
continue
|
|
}
|
|
// Both switches, not one: this constant is the error text for BOTH
|
|
// GADFLY_ENDPOINT_* and GADFLY_BASE_URL, so a name accepted by only
|
|
// half of them still misleads whichever operator hits the other path.
|
|
t.Run(name+" via GADFLY_ENDPOINT_*", func(t *testing.T) {
|
|
if _, err := endpointProvider("ep", name+"|https://host.example/v1|sk-x"); err != nil {
|
|
t.Errorf("endpointProviderNames advertises %q but endpointProvider rejects it: %v", name, err)
|
|
}
|
|
})
|
|
t.Run(name+" via GADFLY_BASE_URL", func(t *testing.T) {
|
|
t.Setenv("GADFLY_PROVIDER", name)
|
|
t.Setenv("GADFLY_BASE_URL", "https://host.example/v1")
|
|
t.Setenv("GADFLY_API_KEY", "sk-x")
|
|
t.Setenv("GADFLY_MODEL", "some-model")
|
|
if _, err := resolveModel(); err != nil {
|
|
t.Errorf("endpointProviderNames advertises %q but resolveModel rejects it: %v", name, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildSpec(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
provider string
|
|
model string
|
|
want string
|
|
}{
|
|
{"bare id gets provider prefix", "ollama-cloud", "qwen3-coder:480b-cloud", "ollama-cloud/qwen3-coder:480b-cloud"},
|
|
{"bare id local ollama", "ollama", "llama3.1", "ollama/llama3.1"},
|
|
{"already has provider passes through", "ollama-cloud", "openai/gpt-4o", "openai/gpt-4o"},
|
|
{"slashed model name passes through verbatim", "openai", "openai/meta-llama/Llama-3.1", "openai/meta-llama/Llama-3.1"},
|
|
{"failover chain passes through", "ollama-cloud", "anthropic/opus-4.8,ollama-cloud/qwen3-coder:480b-cloud", "anthropic/opus-4.8,ollama-cloud/qwen3-coder:480b-cloud"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := buildSpec(tt.provider, tt.model); got != tt.want {
|
|
t.Errorf("buildSpec(%q, %q) = %q, want %q", tt.provider, tt.model, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestOpenAICompatProvidersAreFullyWired closes the two remaining ways this
|
|
// provider family can be half-added. Adding one means touching three places in
|
|
// two languages, and nothing but this test connects them:
|
|
//
|
|
// - endpointProviderNames is the operator-facing list. A provider the code
|
|
// accepts but the list omits sends someone debugging a name that works.
|
|
// - scripts/preflight.sh needs a credential arm, or a missing key for that
|
|
// provider skips the pre-flight and arrives as five unexplained per-lens
|
|
// failures — the exact thing the pre-flight exists to replace.
|
|
func TestOpenAICompatProvidersAreFullyWired(t *testing.T) {
|
|
advertised := make(map[string]bool)
|
|
for _, n := range strings.Split(endpointProviderNames, "/") {
|
|
advertised[strings.TrimSpace(n)] = true
|
|
}
|
|
|
|
preflight, err := os.ReadFile(filepath.Join("..", "..", "scripts", "preflight.sh"))
|
|
if err != nil {
|
|
t.Fatalf("read preflight.sh: %v", err)
|
|
}
|
|
|
|
for _, p := range openAICompatProviders {
|
|
if !advertised[p] {
|
|
t.Errorf("openAICompatProviders has %q but endpointProviderNames does not list it — "+
|
|
"the error message operators read would omit a name that works", p)
|
|
}
|
|
// The arm may be shared ("openai|openai-compatible)"), so match the
|
|
// bare name as a case alternative rather than a whole line.
|
|
if !regexp.MustCompile(`(?m)^\s*(\w[\w-]*\|)*` + regexp.QuoteMeta(p) + `(\|[\w-]+)*\)`).Match(preflight) {
|
|
t.Errorf("openAICompatProviders has %q but scripts/preflight.sh has no credential arm for it — "+
|
|
"a missing key for %s would skip the pre-flight and surface as unexplained lens failures", p, p)
|
|
}
|
|
}
|
|
}
|