package pi import ( "context" "encoding/json" "errors" "io" "strings" "testing" "github.com/aritmolab/thothii/tools/thothctl/internal/compose" ) func TestDoctorRequiresExternalEndpointAuthPiStateAndHealth(t *testing.T) { fake := newFakeRunner() if err := Doctor(context.Background(), fake); err != nil { t.Fatalf("Doctor() error = %v", err) } for _, command := range []string{"pi --version", "test -w /home/thoth/.pi", "test -r /home/thoth/.pi/agent/auth.json", "/health"} { assertCalled(t, fake.calls, command) } } func TestConfigureRestoresAndVerifiesOldSettingsAfterEveryPostSnapshotFailure(t *testing.T) { for _, failure := range []string{"helper", "readback", "digest"} { t.Run(failure, func(t *testing.T) { fake := &configureRunner{failure: failure, settings: Defaults{Provider: "old", Model: "old-model", Thinking: "low"}} err := Configure(context.Background(), fake, Defaults{Provider: "new", Model: "new-model", Thinking: "high"}) if err == nil { t.Fatal("Configure() error = nil, want injected failure") } if fake.settings != (Defaults{Provider: "old", Model: "old-model", Thinking: "low"}) { t.Fatalf("settings after failure = %#v, want old snapshot", fake.settings) } minimumReads := 3 if failure == "helper" { minimumReads = 2 } if fake.settingsReads < minimumReads { t.Fatalf("settings read count = %d, want capture/failure reads plus verified restore", fake.settingsReads) } }) } } type configureRunner struct { failure string settings Defaults settingsReads int configReads int } func (f *configureRunner) Run(_ context.Context, args []string, _ io.Reader) (compose.Result, error) { call := strings.Join(args, " ") switch { case strings.Contains(call, "config --format json"): f.configReads++ endpoint := "https://llm.example.invalid" if f.failure == "digest" && f.configReads > 1 { endpoint = "https://drift.example.invalid" } return compose.Result{Stdout: `{"services":{"core":{"image":"thothii-core:local","environment":{"THT_LLM_URL":"` + endpoint + `"}}}}`}, nil case strings.Contains(call, "/models"): return compose.Result{Stdout: `{"models":[{"provider":"old","id":"old-model"},{"provider":"new","id":"new-model"}]}`}, nil case strings.Contains(call, "settings-cli.js"): if strings.Contains(call, "--provider new") { f.settings = Defaults{Provider: "new", Model: "new-model", Thinking: "high"} if f.failure == "helper" { return compose.Result{ExitCode: 17}, errors.New("injected helper failure") } } else { f.settings = Defaults{Provider: "old", Model: "old-model", Thinking: "low"} } return compose.Result{}, nil case strings.Contains(call, "/settings"): f.settingsReads++ if f.failure == "readback" && f.settingsReads == 2 { return compose.Result{Stdout: `{}`}, nil } contents, _ := json.Marshal(f.settings) return compose.Result{Stdout: string(contents)}, nil default: return compose.Result{}, nil } } func TestConfigureValidatesBackendModelOptionsWritesRealCoreSettingsAndUsesUpstreamIdentity(t *testing.T) { fake := newFakeRunner() if err := Configure(context.Background(), fake, Defaults{Provider: "provider", Model: "model", Thinking: "medium"}); err != nil { t.Fatal(err) } assertCalled(t, fake.calls, "node /app/backend/dist/settings/settings-cli.js --provider provider --model model --thinking medium") assertCalled(t, fake.calls, "x-thoth-principal-subject: thothctl-maintenance") if got := strings.Join(fake.calls, "\n"); strings.Contains(got, "pi-defaults.json") || strings.Contains(got, "secret") { t.Fatalf("commands=%q", got) } if err := Configure(context.Background(), fake, Defaults{Provider: "provider", Model: "unknown", Thinking: "medium"}); err == nil { t.Fatal("expected unknown model rejection") } } func TestTestUsesOnlySanitizedPiAndCoreProbes(t *testing.T) { fake := newFakeRunner() if err := Test(context.Background(), fake); err != nil { t.Fatalf("Test() error = %v", err) } for _, command := range []string{"pi --version", "/health", "/models", "/settings"} { assertCalled(t, fake.calls, command) } if got := strings.Join(fake.calls, "\n"); strings.Contains(got, "secret") { t.Fatalf("probe commands expose secret: %s", got) } } func TestTestRequiresConfiguredProviderAndModelToMatchOneAvailableEntry(t *testing.T) { fake := newFakeRunner() fake.modelsWire = `{"models":[{"id":"different-model","provider":"provider"}]}` if err := Test(context.Background(), fake); err == nil || !strings.Contains(err.Error(), "configured provider/model") { t.Fatalf("Test() error = %v, want exact settings/model mismatch", err) } fake.modelsWire = `{"models":[{"id":"model","provider":"provider"}]}` if err := Test(context.Background(), fake); err != nil { t.Fatalf("Test() exact match error = %v", err) } assertCalled(t, fake.calls, "pi --version") }