mirror of
https://github.com/ollama/ollama.git
synced 2026-07-16 05:05:37 -05:00
248 lines
7.3 KiB
Go
248 lines
7.3 KiB
Go
package chat
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestCloudAuthTickDoesNotPoll(t *testing.T) {
|
|
polls := 0
|
|
m := chatModel{
|
|
cloudAuthPrompt: &cloudAuthPrompt{polling: true},
|
|
opts: Options{
|
|
PollCloudAuth: func(context.Context) (string, bool, error) {
|
|
polls++
|
|
return "", false, nil
|
|
},
|
|
},
|
|
}
|
|
|
|
updated, cmd := m.updateCloudAuthPrompt(cloudAuthTickMsg{})
|
|
m = updated.(chatModel)
|
|
|
|
if m.cloudAuthPrompt.spinner != 1 {
|
|
t.Fatalf("spinner = %d, want 1", m.cloudAuthPrompt.spinner)
|
|
}
|
|
if polls != 0 {
|
|
t.Fatalf("polls = %d, want 0 before running returned tick command", polls)
|
|
}
|
|
if cmd == nil {
|
|
t.Fatal("tick should schedule the next tick")
|
|
}
|
|
if _, ok := cmd().(cloudAuthTickMsg); !ok {
|
|
t.Fatal("tick should schedule another tick, not a poll")
|
|
}
|
|
if polls != 0 {
|
|
t.Fatalf("polls = %d, want 0 after running returned tick command", polls)
|
|
}
|
|
}
|
|
|
|
func TestCloudAuthPollSchedulesNextPoll(t *testing.T) {
|
|
polls := 0
|
|
m := chatModel{
|
|
cloudAuthPrompt: &cloudAuthPrompt{polling: true},
|
|
opts: Options{
|
|
PollCloudAuth: func(context.Context) (string, bool, error) {
|
|
polls++
|
|
return "", false, nil
|
|
},
|
|
},
|
|
}
|
|
|
|
_, cmd := m.updateCloudAuthPrompt(cloudAuthPollMsg{})
|
|
if cmd == nil {
|
|
t.Fatal("poll should schedule the next poll")
|
|
}
|
|
msg, ok := cmd().(cloudAuthPollMsg)
|
|
if !ok {
|
|
t.Fatal("poll should schedule another poll, not a tick")
|
|
}
|
|
if msg.done {
|
|
t.Fatal("poll should report not done")
|
|
}
|
|
if polls != 1 {
|
|
t.Fatalf("polls = %d, want 1", polls)
|
|
}
|
|
}
|
|
|
|
func TestCloudModelPreflightFailureShowsPlanVerificationNotice(t *testing.T) {
|
|
m := chatModel{
|
|
opts: Options{
|
|
Model: "glm-5.2:cloud",
|
|
},
|
|
}
|
|
|
|
updated, cmd := m.updateCloudModelPreflight(cloudModelPreflightMsg{
|
|
model: "glm-5.2:cloud",
|
|
err: errors.New("temporary network failure"),
|
|
})
|
|
if cmd != nil {
|
|
t.Fatal("transient preflight failure should not start an auth modal")
|
|
}
|
|
m = updated.(chatModel)
|
|
|
|
if got := m.status; got != cloudPlanVerificationUnavailable {
|
|
t.Fatalf("status = %q", got)
|
|
}
|
|
if m.cloudAuthPrompt != nil {
|
|
t.Fatalf("cloud auth prompt = %#v, want nil", m.cloudAuthPrompt)
|
|
}
|
|
}
|
|
|
|
func TestCloudModelPreflightIgnoresStaleModel(t *testing.T) {
|
|
m := chatModel{
|
|
opts: Options{
|
|
Model: "glm-5.2:cloud",
|
|
},
|
|
status: "ready",
|
|
}
|
|
|
|
updated, _ := m.updateCloudModelPreflight(cloudModelPreflightMsg{
|
|
model: "kimi-k2.7-code:cloud",
|
|
err: errors.New("temporary network failure"),
|
|
})
|
|
m = updated.(chatModel)
|
|
|
|
if got := m.status; got != "ready" {
|
|
t.Fatalf("status = %q, want unchanged", got)
|
|
}
|
|
}
|
|
|
|
func TestCloudModelPreflightCommandChecksCloudModel(t *testing.T) {
|
|
var checkedModel, checkedPlan string
|
|
cmd := cloudModelPreflightCmd(context.Background(), Options{
|
|
CheckCloudModel: func(_ context.Context, model, requiredPlan string) error {
|
|
checkedModel = model
|
|
checkedPlan = requiredPlan
|
|
return errors.New("temporary network failure")
|
|
},
|
|
ModelOptions: func(context.Context) ([]ModelOption, error) {
|
|
return []ModelOption{{Name: "glm-5.2:cloud", RequiredPlan: "pro", Cloud: true}}, nil
|
|
},
|
|
}, "glm-5.2:cloud", "")
|
|
if cmd == nil {
|
|
t.Fatal("cloud preflight command should be scheduled")
|
|
}
|
|
raw := cmd()
|
|
msg, ok := raw.(cloudModelPreflightMsg)
|
|
if !ok {
|
|
t.Fatalf("message = %T, want cloudModelPreflightMsg", raw)
|
|
}
|
|
if checkedModel != "glm-5.2:cloud" || checkedPlan != "pro" {
|
|
t.Fatalf("checked model/plan = %q/%q", checkedModel, checkedPlan)
|
|
}
|
|
if msg.model != "glm-5.2:cloud" || msg.err == nil || !strings.Contains(msg.err.Error(), "temporary") {
|
|
t.Fatalf("message = %#v", msg)
|
|
}
|
|
}
|
|
|
|
func TestCloudAuthPollGivesUpAfterConsecutiveFailures(t *testing.T) {
|
|
pollErr := errors.New("whoami: connection refused")
|
|
m := chatModel{
|
|
cloudAuthPrompt: &cloudAuthPrompt{polling: true, kind: cloudAuthSignIn},
|
|
opts: Options{
|
|
PollCloudAuth: func(context.Context) (string, bool, error) {
|
|
return "", false, pollErr
|
|
},
|
|
},
|
|
}
|
|
|
|
// The first maxPollFailures-1 failures should keep retrying.
|
|
for i := 1; i < maxPollFailures; i++ {
|
|
updated, _ := m.updateCloudAuthPrompt(cloudAuthPollMsg{done: false, err: pollErr})
|
|
m = updated.(chatModel)
|
|
if m.cloudAuthPrompt == nil {
|
|
t.Fatalf("failure %d: prompt cleared early", i)
|
|
}
|
|
if got := m.cloudAuthPrompt.pollFailures; got != i {
|
|
t.Fatalf("failure %d: pollFailures = %d, want %d", i, got, i)
|
|
}
|
|
if m.cloudAuthPrompt.pollErr != pollErr.Error() {
|
|
t.Fatalf("failure %d: pollErr = %q, want %q", i, m.cloudAuthPrompt.pollErr, pollErr.Error())
|
|
}
|
|
}
|
|
|
|
// The threshold failure gives up: prompt cleared, back to ready, error entry.
|
|
updated, cmd := m.updateCloudAuthPrompt(cloudAuthPollMsg{done: false, err: pollErr})
|
|
m = updated.(chatModel)
|
|
if cmd != nil {
|
|
t.Fatalf("threshold failure should not reschedule, got cmd %T", cmd)
|
|
}
|
|
if m.cloudAuthPrompt != nil {
|
|
t.Fatalf("prompt = %#v, want nil after give-up", m.cloudAuthPrompt)
|
|
}
|
|
if m.status != "ready" {
|
|
t.Fatalf("status = %q, want ready", m.status)
|
|
}
|
|
if len(m.entries) == 0 {
|
|
t.Fatal("expected an error entry after give-up")
|
|
}
|
|
last := m.entries[len(m.entries)-1]
|
|
if last.role != "error" || !strings.Contains(last.content, "couldn't verify sign-in") {
|
|
t.Fatalf("last entry = %+v, want error containing sign-in failure", last)
|
|
}
|
|
}
|
|
|
|
func TestCloudAuthPollResetsFailuresOnHealthyResponse(t *testing.T) {
|
|
pollErr := errors.New("whoami: timeout")
|
|
m := chatModel{
|
|
cloudAuthPrompt: &cloudAuthPrompt{polling: true, kind: cloudAuthSignIn},
|
|
opts: Options{
|
|
PollCloudAuth: func(context.Context) (string, bool, error) {
|
|
return "", false, pollErr
|
|
},
|
|
},
|
|
}
|
|
|
|
// Accumulate some failures without hitting the threshold.
|
|
for range maxPollFailures - 2 {
|
|
updated, _ := m.updateCloudAuthPrompt(cloudAuthPollMsg{done: false, err: pollErr})
|
|
m = updated.(chatModel)
|
|
}
|
|
if got := m.cloudAuthPrompt.pollFailures; got != maxPollFailures-2 {
|
|
t.Fatalf("pollFailures = %d, want %d", got, maxPollFailures-2)
|
|
}
|
|
|
|
// A healthy (no-error, not-done) response resets the streak so a later
|
|
// transient blip isn't counted against a recovered connection.
|
|
updated, _ := m.updateCloudAuthPrompt(cloudAuthPollMsg{done: false, err: nil})
|
|
m = updated.(chatModel)
|
|
if m.cloudAuthPrompt == nil {
|
|
t.Fatal("healthy response should keep the prompt open")
|
|
}
|
|
if got := m.cloudAuthPrompt.pollFailures; got != 0 {
|
|
t.Fatalf("pollFailures = %d, want 0 after healthy response", got)
|
|
}
|
|
if m.cloudAuthPrompt.pollErr != "" {
|
|
t.Fatalf("pollErr = %q, want empty after healthy response", m.cloudAuthPrompt.pollErr)
|
|
}
|
|
}
|
|
|
|
func TestCloudAuthPollCompletesAfterFailures(t *testing.T) {
|
|
pollErr := errors.New("whoami: timeout")
|
|
m := chatModel{
|
|
cloudAuthPrompt: &cloudAuthPrompt{
|
|
modelName: "glm-5.2:cloud",
|
|
polling: true,
|
|
kind: cloudAuthSignIn,
|
|
pollFailures: maxPollFailures - 1,
|
|
},
|
|
opts: Options{
|
|
CheckCloudModel: func(context.Context, string, string) error { return nil },
|
|
PollCloudAuth: func(context.Context) (string, bool, error) { return "", false, pollErr },
|
|
},
|
|
}
|
|
|
|
// A successful sign-in mid-retry should clear the failure state and re-check.
|
|
updated, _ := m.updateCloudAuthPrompt(cloudAuthPollMsg{done: true})
|
|
m = updated.(chatModel)
|
|
if m.cloudAuthPrompt.polling {
|
|
t.Fatal("done should stop polling")
|
|
}
|
|
if m.cloudAuthPrompt.pollFailures != 0 || m.cloudAuthPrompt.pollErr != "" {
|
|
t.Fatalf("failure state not reset: failures=%d err=%q", m.cloudAuthPrompt.pollFailures, m.cloudAuthPrompt.pollErr)
|
|
}
|
|
}
|