fix(qwen): gadfly round 3 — stop guarding a duplicate, delete it
Build & push image / build-and-push (pull_request) Successful in 5s
Build & push image / test (pull_request) Canceled after 7m27s

Twelve findings, all real, and the two that matter are about the pre-flight I
added rather than about qwen.

The credential check had a false pass in the OTHER direction from round 2's: on
the GADFLY_BASE_URL override path, resolveModel builds the client with
GADFLY_API_KEY and never reads QWEN_API_KEY/KIMI_API_KEY, so treating the
provider's own key as sufficient there let a doomed run proceed. Having now
been wrong about these rules in both directions, the check no longer tries to
model both paths: it covers the REGISTRY path, whose rules it can state
exactly, and says nothing about the override path — which is hand-configured by
definition, while the registry path is the one you hit by adding a model id to
a var and forgetting the secret.

The logic moves to scripts/preflight.sh, sourced by both run.sh and the test.
The previous answer to "this test duplicates production logic" was a regex
drift-guard, and that guard compared only the provider table — not the decision
logic, which is precisely the half that carried the bug. A duplicate you guard
is still a duplicate; this deletes it, and the test now runs under `set -u`
like production does.

Also: the test that pins the shared provider slice held its own copy of the
list (now ranges the slice); endpointProviderNames had nothing tying it to the
switches it describes, which is how it shipped without "gemini" (a new test
asserts every advertised name resolves); two godoc lists had drifted; and the
"sanity" line that asserted nothing is gone.

And the repo had NO test job — `go test` and the pre-flight table both existed
and neither was ever executed by CI, which reads as coverage while providing
none. Added one (build/vet/gofmt/test/pre-flight), running alongside the image
build rather than gating it, so red is loud without standing between a push and
a rebuild.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
2026-08-12 17:20:44 -04:00
co-authored by Claude Opus 5
parent 1d6eaa08c5
commit 67a73616e1
6 changed files with 212 additions and 119 deletions
+10 -6
View File
@@ -62,10 +62,12 @@ const endpointProviderNames = "openai/openai-compatible/kimi/qwen/ollama/ollama-
// GADFLY_BASE_URL override the backend endpoint (OpenAI/Ollama-compatible
// servers, a remote Ollama, an OpenRouter-style gateway…).
// When set, the provider is constructed directly at that URL.
// GADFLY_API_KEY bearer/API key for the chosen provider. Optional; when
// unset the provider falls back to its standard env var
// (OLLAMA_API_KEY / OPENAI_API_KEY / ANTHROPIC_API_KEY /
// GOOGLE_API_KEY|GEMINI_API_KEY). Local Ollama needs none.
// GADFLY_API_KEY bearer/API key for the chosen provider, used ONLY on the
// GADFLY_BASE_URL override path. With no base URL the
// provider reads its own standard variable and this is never
// consulted: OLLAMA_API_KEY / OPENAI_API_KEY /
// QWEN_API_KEY / KIMI_API_KEY / ANTHROPIC_API_KEY /
// GOOGLE_API_KEY|GEMINI_API_KEY. Local Ollama needs none.
//
// With GADFLY_BASE_URL unset, resolution goes through majordomo's registry, so
// LLM_* env DSNs and registered aliases/tiers work too.
@@ -223,8 +225,10 @@ func modelProvider() string {
// plaintext local Ollama (or foreman queue) works:
// GADFLY_ENDPOINT_BIGBOX="ollama|http://192.168.1.50:11434"
// GADFLY_MODEL=bigbox/qwen2.5-coder:7b
// provider is one of endpointProviderNames; "foreman"
// targets a foreman daemon (native Ollama on the wire):
// provider is ollama/openai/anthropic/google/foreman/llama-swap(s) or an
// openai-compat built-in (kimi, qwen) — endpointProviderNames is the
// authoritative list. "foreman" targets a foreman daemon (native Ollama
// on the wire):
// GADFLY_ENDPOINT_M1="foreman|http://foreman-m1:8080|tok"
//
// GADFLY_ALIAS_<NAME> = "<majordomo spec>"
+26 -2
View File
@@ -1,6 +1,9 @@
package main
import "testing"
import (
"strings"
"testing"
)
func TestEndpointProvider(t *testing.T) {
t.Run("ollama http endpoint registers under its name", func(t *testing.T) {
@@ -77,7 +80,9 @@ func TestEndpointProvider(t *testing.T) {
// reason a user could guess — which is exactly what happened here on the first
// pass. Asserting both in one table is what makes the pair fail together.
func TestOpenAICompatProvidersResolveOnBothPaths(t *testing.T) {
for _, provider := range []string{"openai", "openai-compatible", "kimi", "qwen"} {
// Ranges the SHARED slice rather than a fourth copy of the names: 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 {
@@ -99,6 +104,25 @@ func TestOpenAICompatProvidersResolveOnBothPaths(t *testing.T) {
}
}
// TestEndpointProviderNamesAreAllAccepted keeps the operator-facing list
// honest. endpointProviderNames exists to stop two error messages drifting
// apart, but nothing tied it to the switches it describes — and its first
// version had already dropped the "gemini" alias, so the anti-drift list was
// itself drifted. Every name it advertises must actually resolve.
func TestEndpointProviderNamesAreAllAccepted(t *testing.T) {
for _, name := range strings.Split(endpointProviderNames, "/") {
name = strings.TrimSpace(name)
if name == "" {
continue
}
t.Run(name, 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)
}
})
}
}
func TestBuildSpec(t *testing.T) {
tests := []struct {
name string