diff --git a/internal/provider/forget_last_login_test.go b/internal/provider/forget_last_login_test.go index cabcd508..d7de3089 100644 --- a/internal/provider/forget_last_login_test.go +++ b/internal/provider/forget_last_login_test.go @@ -2,7 +2,7 @@ package provider import ( "encoding/json" - "strings" + "path/filepath" "testing" ) @@ -34,15 +34,23 @@ func TestForgetOnlyFirstLogin(t *testing.T) { } } - // two accounts: Remove on the first still asks for the other first, - // and signing the subscription out takes both + // two accounts: Remove on the first puts the other first, even one + // that was off, and signing the subscription out takes both + saveLogins(t, + savedLogin{Agent: "antigravity", User: "a@x.com", Auth: auth("ra"), On: true, First: true}, + savedLogin{Agent: "antigravity", User: "b@x.com", Auth: auth("rb")}) + ForgetAccounts() + if err := ForgetLogin("antigravity", "a@x.com"); err != nil { + t.Fatalf("removing the first of two: %v", err) + } + ForgetAccounts() + if ls := Logins("antigravity"); len(ls) != 1 || ls[0].User != "b@x.com" || !ls[0].Active || !ls[0].On { + t.Fatalf("after removing the first: %+v", ls) + } saveLogins(t, savedLogin{Agent: "antigravity", User: "a@x.com", Auth: auth("ra"), On: true, First: true}, savedLogin{Agent: "antigravity", User: "b@x.com", Auth: auth("rb"), On: true}) ForgetAccounts() - if err := ForgetLogin("antigravity", "a@x.com"); err == nil || !strings.Contains(err.Error(), "put another account first") { - t.Fatalf("removing the first of two: %v", err) - } if err := ForgetAccount("antigravity"); err != nil { t.Fatalf("signing out both: %v", err) } @@ -51,3 +59,41 @@ func TestForgetOnlyFirstLogin(t *testing.T) { t.Fatalf("still listed: %+v", ls) } } + +// 歧路亡羊 on Discord: Copilot signed in to the editors' own account and +// two of magpie's, one of them first. Removing the first puts the next +// first, and Sign out… takes them all, where it said "magpie uses wwjxhy +// first; put another account first". +func TestForgetFirstCopilotLogin(t *testing.T) { + signIn(t) + writeFile(t, filepath.Join(copilotConfigDir(), "github-copilot", "apps.json"), map[string]any{"github.com": map[string]any{"user": "octo", "oauth_token": "gho_own"}}) + tok := func(s string) json.RawMessage { return json.RawMessage(`{"oauth_token":"` + s + `"}`) } + saved := []savedLogin{ + {Agent: "copilot", User: "octo"}, + {Agent: "copilot", User: "wwjxhy", Auth: tok("gho_w"), On: true, First: true}, + {Agent: "copilot", User: "other", Auth: tok("gho_o"), On: true}, + } + saveLogins(t, saved...) + ForgetAccounts() + if ls := Logins("copilot"); len(ls) != 3 || ls[0].User != "wwjxhy" || !ls[0].Active { + t.Fatalf("logins before: %+v", ls) + } + if err := ForgetLogin("copilot", "wwjxhy"); err != nil { + t.Fatalf("removing the first: %v", err) + } + ForgetAccounts() + ls := Logins("copilot") + if len(ls) != 2 || !ls[0].Active || ls[0].User == "wwjxhy" || ls[1].User == "wwjxhy" { + t.Fatalf("after removing the first: %+v", ls) + } + + saveLogins(t, saved...) + ForgetAccounts() + if err := forgetLogins("copilot"); err != nil { + t.Fatalf("signing out every account: %v", err) + } + ForgetAccounts() + if ls := Logins("copilot"); len(ls) != 0 { + t.Fatalf("still listed: %+v", ls) + } +} diff --git a/internal/provider/grok_accounts_test.go b/internal/provider/grok_accounts_test.go index 3fd53691..8d7ab8e8 100644 --- a/internal/provider/grok_accounts_test.go +++ b/internal/provider/grok_accounts_test.go @@ -120,9 +120,6 @@ func TestGrokAccounts(t *testing.T) { if b, _ := os.ReadFile(filepath.Join(own, "auth.json")); len(b) == 0 { t.Fatal("the CLI's own sign-in was touched") } - if err := ForgetLogin("grok", "two@x.ai"); err == nil { - t.Fatal("forgot the account in use first") - } if err := SetLoginOn("grok", "me@x.ai", false); err != nil { t.Fatal(err) } diff --git a/internal/provider/kiro_signin_test.go b/internal/provider/kiro_signin_test.go index da7232c3..ea837eab 100644 --- a/internal/provider/kiro_signin_test.go +++ b/internal/provider/kiro_signin_test.go @@ -173,13 +173,17 @@ func TestKiroSeveralAccounts(t *testing.T) { home = l.Home } } - if err := ForgetLogin("kiro", "two@example.com"); err == nil { - t.Fatal("forgot the account in use first") + // the first removed, the next is put first + if err := ForgetLogin("kiro", "two@example.com"); err != nil { + t.Fatal(err) + } + if ls := Logins("kiro"); len(ls) != 1 || ls[0].User != "me@example.com" || !ls[0].Active { + t.Fatalf("after removing the first: %+v", ls) } if err := ForgetLogin("kiro", "me@example.com"); err != nil { t.Fatal(err) } - if _, err := os.Stat(home); !os.IsNotExist(err) || len(Logins("kiro")) != 1 { + if _, err := os.Stat(home); !os.IsNotExist(err) || len(Logins("kiro")) != 0 { t.Fatalf("home %s left: %v, %+v", home, err, Logins("kiro")) } } diff --git a/internal/provider/pluginsignin_test.go b/internal/provider/pluginsignin_test.go index 16117b7a..dd0d253b 100644 --- a/internal/provider/pluginsignin_test.go +++ b/internal/provider/pluginsignin_test.go @@ -192,9 +192,6 @@ func TestPluginAccounts(t *testing.T) { if a := also(); len(a) != 1 || a[0] != "blue@fake@fakeco" { t.Fatalf("also on %v", a) } - if err := ForgetLogin("fakeco", "red@fake"); err == nil { - t.Fatal("the first account was removed") - } if err := ForgetLogin("fakeco", "blue@fake"); err != nil { t.Fatal(err) } diff --git a/internal/provider/side_logins.go b/internal/provider/side_logins.go index fef12bd8..7ad68a61 100644 --- a/internal/provider/side_logins.go +++ b/internal/provider/side_logins.go @@ -168,12 +168,12 @@ func setSideLoginOn(agent, user string, on bool, ls []sideLogin) error { // forgetSideLogin drops an account magpie signed in; gone is told what it // kept. The agent's own sign-in is only hidden: its files stay as they -// are, and it shows again once the agent signs in anew (ownMark). Hidden -// while first, the next account is put first. One of magpie's that is -// first waits for another to be put first, unless it is the only one: then -// there is none to put first, and it goes, leaving the subscription with no -// account (#874: a removed subscription's Sign out… could never take its -// one account). +// are, and it shows again once the agent signs in anew (ownMark). Either +// one, removed while first, puts the next account first; the only one +// leaves the subscription with no account (#874: a removed subscription's +// Sign out… could never take its one account; 歧路亡羊 on Discord: one of +// magpie's that was first was refused, "put another account first", even +// by the subscription's Sign out…, which takes every account). func forgetSideLogin(agent, user string, ls []sideLogin, gone func(savedLogin)) error { own := slices.ContainsFunc(ls, func(l sideLogin) bool { return l.Own && strings.EqualFold(l.User, user) }) first := strings.EqualFold(activeOf(ls), user) @@ -186,22 +186,20 @@ func forgetSideLogin(agent, user string, ls []sideLogin, gone func(savedLogin)) } } } - if first && !own && next != "" { - return fmt.Errorf("magpie uses %s first; put another account first", user) - } mark := hiddenNoMark if own { mark = firstNonEmpty(ownMark(agent), hiddenNoMark) } var old savedLogin err := editSideLogin(agent, user, func(saved []savedLogin, i int) ([]savedLogin, error) { + for j := range saved { + if next != "" && j != i && saved[j].Agent == agent { + saved[j].First = strings.EqualFold(saved[j].User, next) + saved[j].On = saved[j].On || saved[j].First + } + } if saved[i].own() { saved[i].Hidden, saved[i].First = mark, false - for j := range saved { - if next != "" && saved[j].Agent == agent { - saved[j].First = strings.EqualFold(saved[j].User, next) - } - } return saved, nil } old = saved[i]