fix: say a sign-in error once, then in Details for that account (#4)

A failing account's error ("sign-in required … press L to run az login --tenant …") stayed in the footer for the whole session.

- **Footer:** it's now an ordinary message, shown once when an account's error first appears (at start, or when a load fails), and gone on the next key press. It's said again only when the errors change, or for another account.
- **Details:** with that account's group header or one of its sources selected, the Details pane shows the error and the sign-in hint, in place of "Select a vault or scope…".
- **`L`** signs in to the selected account when it needs it, else to the last one that asked, as before. With two accounts, `L` used to run whichever sign-in had failed most recently.

Test: two accounts, one failing to list its subscriptions. The message is in the footer at start and gone after a key press, in Details on that account's group, and `L` targets it. It isn't shown on the other account's rows.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Karl-Einar Bergstrøm <karl-einarb@outlook.com>
Reviewed-on: #4
This commit was merged in pull request #4.
This commit is contained in:
keb committed 2026-10-11 10:06:28 +02:00
1 parent 87d98d038d
commit a82f6bff27
3 files changed
+123 -15

No files matched your search

+47
View File
@@ -1022,3 +1022,50 @@ func TestMultiLineErrorsShownWhole(t *testing.T) {
t.Errorf("footer:\n%s", d.screen())
}
}
// signedOut is a second Azure account whose sign-in has expired.
type signedOut struct{ provider.Provider }
func (signedOut) ID() string { return "Azure:admin@contoso.com/t1" }
func (signedOut) Label() string { return "Azure · admin" }
func (signedOut) Sources(context.Context) ([]provider.Source, error) {
return nil, &provider.AuthError{Kind: provider.KindAzure, Account: "Azure · admin",
Login: []string{"az", "login", "--tenant", "t1"}, Err: errors.New("AADSTS70043")}
}
func TestSignInMessageOnceThenInDetails(t *testing.T) {
t.Parallel()
d := newDriverOpts(t, Options{CollapseGroups: false}, fake.Azure(0), signedOut{fake.Azure(0)})
hint := "Azure · admin: sign-in required"
if !strings.Contains(lastLines(d.screen(), 2), hint) {
t.Fatalf("not said at start:\n%s", d.screen())
}
d.keys("j")
if strings.Contains(lastLines(d.screen(), 2), hint) {
t.Errorf("still in the footer after moving:\n%s", d.screen())
}
// On the account's group, Details says it, and L signs in to it.
d.keys("G")
if r, _ := d.m.sources.Selected(); r.provider != "Azure:admin@contoso.com/t1" {
t.Fatalf("selected %+v", r)
}
if !strings.Contains(d.screen(), hint) || !strings.Contains(d.screen(), "press L to run az login --tenant t1") {
t.Errorf("not in Details:\n%s", d.screen())
}
if a := d.m.loginFor(); a == nil || a.Account != "Azure · admin" {
t.Errorf("L signs in to %+v", a)
}
// Elsewhere Details is as usual.
d.keys("g")
if strings.Contains(d.screen(), hint) {
t.Errorf("shown for another account:\n%s", d.screen())
}
}
// lastLines is the bottom n lines of a screen.
func lastLines(scr string, n int) string {
lines := strings.Split(strings.TrimRight(scr, "\n"), "\n")
return strings.Join(lines[max(0, len(lines)-n):], "\n")
}