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".
215 lines
5.9 KiB
Go
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
|
|
}
|