Files
gnoma/internal/router/discovery_test.go
vikingowl 0adf118675 fix(router): discovery loop removes forced arm, breaking routing
The discovery loop's reconcileArms removed the CLI-forced arm
(llamacpp/default) because the llama.cpp server reports the real model
name (e.g. gemma-26b), creating a mismatch. After 30s the forced arm
disappeared and all subsequent requests failed.

Three-layer fix:
- Eager: query the specific provider at startup to resolve the real
  model name before registering the forced arm
- Lazy: reconcileArms detects placeholder "default" arm names and
  atomically renames them when discovery reveals the real identity,
  with an onReconcile callback to update the session and TUI
- Guard: the forced arm is never garbage-collected by the removal loop

Also fixes misleading /init error messaging — failed inits now show
"loaded from disk (init failed)" instead of "AGENTS.md written to".
2026-04-12 17:51:30 +02:00

215 lines
5.9 KiB
Go

package router
import (
"context"
"log/slog"
"testing"
"somegit.dev/Owlibou/gnoma/internal/provider"
"somegit.dev/Owlibou/gnoma/internal/stream"
)
// --- ArmID helpers ---
func TestArmID_Provider(t *testing.T) {
tests := []struct {
id ArmID
want string
}{
{"llamacpp/gemma-26b", "llamacpp"},
{"anthropic/claude-sonnet", "anthropic"},
{"single", "single"},
}
for _, tt := range tests {
if got := tt.id.Provider(); got != tt.want {
t.Errorf("ArmID(%q).Provider() = %q, want %q", tt.id, got, tt.want)
}
}
}
func TestArmID_Model(t *testing.T) {
tests := []struct {
id ArmID
want string
}{
{"llamacpp/gemma-26b", "gemma-26b"},
{"anthropic/claude-sonnet", "claude-sonnet"},
{"single", "single"},
}
for _, tt := range tests {
if got := tt.id.Model(); got != tt.want {
t.Errorf("ArmID(%q).Model() = %q, want %q", tt.id, got, tt.want)
}
}
}
// --- reconcileArms ---
func noopFactory(name, model string) provider.Provider { return nil }
func dummyArm(id ArmID, local bool) *Arm {
return &Arm{
ID: id,
ModelName: id.Model(),
IsLocal: local,
Capabilities: provider.Capabilities{ToolUse: true, ContextWindow: 8192},
}
}
func TestReconcileArms_ForcedDefaultArm_ReconciledToDiscovered(t *testing.T) {
r := New(Config{})
r.RegisterArm(dummyArm("llamacpp/default", true))
r.ForceArm("llamacpp/default")
discovered := []DiscoveredModel{
{ID: "gemma-26b", Provider: "llamacpp", SupportsTools: true},
}
var reconciled ArmID
onReconcile := func(id ArmID) { reconciled = id }
reconcileArms(r, discovered, noopFactory, slog.Default(), onReconcile)
if got := r.ForcedArm(); got != "llamacpp/gemma-26b" {
t.Errorf("ForcedArm() = %q, want %q", got, "llamacpp/gemma-26b")
}
if reconciled != "llamacpp/gemma-26b" {
t.Errorf("onReconcile called with %q, want %q", reconciled, "llamacpp/gemma-26b")
}
// Select should succeed with the reconciled arm
decision := r.Select(Task{Type: TaskGeneration})
if decision.Error != nil {
t.Fatalf("Select after reconcile: %v", decision.Error)
}
if decision.Arm.ID != "llamacpp/gemma-26b" {
t.Errorf("Select returned %q, want %q", decision.Arm.ID, "llamacpp/gemma-26b")
}
}
func TestReconcileArms_ForcedArm_AlreadyCorrect(t *testing.T) {
r := New(Config{})
r.RegisterArm(dummyArm("llamacpp/gemma-26b", true))
r.ForceArm("llamacpp/gemma-26b")
discovered := []DiscoveredModel{
{ID: "gemma-26b", Provider: "llamacpp", SupportsTools: true},
}
var called bool
onReconcile := func(id ArmID) { called = true }
reconcileArms(r, discovered, noopFactory, slog.Default(), onReconcile)
if got := r.ForcedArm(); got != "llamacpp/gemma-26b" {
t.Errorf("ForcedArm() = %q, want %q", got, "llamacpp/gemma-26b")
}
if called {
t.Error("onReconcile should not be called when arm is already correct")
}
decision := r.Select(Task{Type: TaskGeneration})
if decision.Error != nil {
t.Fatalf("Select: %v", decision.Error)
}
}
func TestReconcileArms_ForcedArm_NonLocal(t *testing.T) {
r := New(Config{})
r.RegisterArm(dummyArm("anthropic/claude", false))
r.ForceArm("anthropic/claude")
discovered := []DiscoveredModel{
{ID: "gemma-26b", Provider: "llamacpp", SupportsTools: true},
}
reconcileArms(r, discovered, noopFactory, slog.Default(), nil)
if got := r.ForcedArm(); got != "anthropic/claude" {
t.Errorf("ForcedArm() = %q, want %q (non-local forced arm should be untouched)", got, "anthropic/claude")
}
}
func TestReconcileArms_NoForcedArm(t *testing.T) {
r := New(Config{})
existing := dummyArm("llamacpp/old-model", true)
r.RegisterArm(existing)
discovered := []DiscoveredModel{
{ID: "gemma-26b", Provider: "llamacpp", SupportsTools: true},
}
factory := func(name, model string) provider.Provider {
return &stubProvider{name: name, model: model}
}
reconcileArms(r, discovered, factory, slog.Default(), nil)
// Old arm should be removed (disappeared)
if _, ok := r.LookupArm("llamacpp/old-model"); ok {
t.Error("disappeared arm should be removed")
}
// New arm should be registered
if _, ok := r.LookupArm("llamacpp/gemma-26b"); !ok {
t.Error("discovered arm should be registered")
}
}
func TestReconcileArms_MultipleModelsForForcedProvider(t *testing.T) {
r := New(Config{})
r.RegisterArm(dummyArm("llamacpp/default", true))
r.ForceArm("llamacpp/default")
discovered := []DiscoveredModel{
{ID: "gemma-26b", Provider: "llamacpp", SupportsTools: true},
{ID: "phi-3", Provider: "llamacpp", SupportsTools: false},
}
var reconciled ArmID
onReconcile := func(id ArmID) { reconciled = id }
reconcileArms(r, discovered, noopFactory, slog.Default(), onReconcile)
// Should reconcile to the first match
if got := r.ForcedArm(); got != "llamacpp/gemma-26b" {
t.Errorf("ForcedArm() = %q, want %q", got, "llamacpp/gemma-26b")
}
if reconciled != "llamacpp/gemma-26b" {
t.Errorf("onReconcile = %q, want %q", reconciled, "llamacpp/gemma-26b")
}
}
func TestReconcileArms_NoModelsForForcedProvider(t *testing.T) {
r := New(Config{})
r.RegisterArm(dummyArm("llamacpp/default", true))
r.ForceArm("llamacpp/default")
// Discovery returns nothing (server down)
discovered := []DiscoveredModel{}
reconcileArms(r, discovered, noopFactory, slog.Default(), nil)
// Forced arm must NOT be removed
if got := r.ForcedArm(); got != "llamacpp/default" {
t.Errorf("ForcedArm() = %q, want %q (forced arm should survive empty discovery)", got, "llamacpp/default")
}
if _, ok := r.LookupArm("llamacpp/default"); !ok {
t.Error("forced arm should not be removed when discovery returns no models")
}
}
// stubProvider satisfies provider.Provider for tests that need a non-nil provider.
type stubProvider struct {
name string
model string
}
func (s *stubProvider) Name() string { return s.name }
func (s *stubProvider) DefaultModel() string { return s.model }
func (s *stubProvider) Models(_ context.Context) ([]provider.ModelInfo, error) {
return nil, nil
}
func (s *stubProvider) Stream(_ context.Context, _ provider.Request) (stream.Stream, error) {
return nil, nil
}