Skip to content

Say when the team list could not be loaded during sign-in - #477

Draft
beardthelion wants to merge 1 commit into
vercel-labs:mainfrom
beardthelion:fix/login-team-fetch-warning
Draft

Say when the team list could not be loaded during sign-in#477
beardthelion wants to merge 1 commit into
vercel-labs:mainfrom
beardthelion:fix/login-team-fetch-warning

Conversation

@beardthelion

@beardthelion beardthelion commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Fixes #476.

Needs type: bug. I cannot apply labels, so this stays in draft until a maintainer adds it. Full CI is green on this commit across all four native runners.

A failed team lookup during sign-in was indistinguishable from an account that has no teams, so fx login reported success and quietly left you in personal scope. Both sign-in paths did:

var teams = fetchTeams(alloc, token.access_token, issuer_url) catch std.ArrayList(Team).empty;

Every failure became an empty list, which is exactly what a zero-team account produces. No picker, no message, auth.json written without team_slug or team_id, and stdout byte-identical to a healthy login.

The sign-in itself is fine, so this keeps it. The token pair is valid and the team fetch is auxiliary; failing the login over it would be worse, and re-running the device flow is expensive. What was missing was any signal, so that is what this adds.

What changed

Both call sites now track whether the list actually arrived. On a caught error the CLI prints one line after the existing gateway-access caveat, and the in-app flow emits a warning notice beside the existing sign-in notice. An account with genuinely no teams stays silent, which is the case that decides whether this is a useful signal or a false alarm.

parseTeams now counts entries it cannot read. A non-empty array that yields nothing usable is a response we could not read rather than an account with no teams, so it returns InvalidTeamsResponse; a partly readable list keeps what it got and still reports the shortfall, since the team you want may be the entry that dropped. Without this a 200 carrying {"teams":[{"id":123,"slug":"acme"}]} reproduced the original bug with no error anywhere.

That last part also reaches fx teams, which now fails loudly on a list it cannot read instead of reporting no teams. It already used try on the same call, so this is the direction it was already going, but it is a behavior change in a second command and worth your attention.

The two surfaces share a prefix constant and append their own remedy, following remote_revocation_warning, so the in-app notice does not tell you to run a shell command.

On the test harness

Two changes there were load-bearing rather than incidental.

TestAuth.openTeamPicker was a no-op that did not take the selection the way the real picker does. The consumer reads the flag before the picker branch precisely because openTeamPicker takes ownership, and with the old stub a unit test covering that could pass whether or not the read was in the right place. Making the stub faithful is what lets the test actually pin it: removing the hoist now turns it red.

TestApp discarded notice.tone, so nothing could distinguish a warning notice from a neutral one. It records it now, and the test asserts the tone.

Verification

Nine new end-to-end cases and six new unit tests. The three failure cases were each observed failing before the implementation, and both silence controls were proven load-bearing by forcing the flag true and watching them go red.

The full tui-auth-source-selection.test.ts run was compared against the same file on main: identical failing-test sets, 14 either way, none of them these. The Zig suite was compared the same way, by failing test name rather than count. zig fmt --check src/ passes.

Full CI on my fork is green on this commit. It took a few runs to get there, and the reason is worth recording rather than hiding: E2E (ReleaseSafe, macos-aarch64, shard 3/4) failed twice before passing, on subagent-manager tests.

That shard is flaky on main independently of this change. I pushed 261a7c3f unmodified to my fork and ran the same workflow: it passed once and then failed the same shard, on configure rejects a concurrent winner then retries the preserved draft once, persistent child pointer drag replaces the selected composer range, and persistent child preserves its reading position across both reopen paths. Across runs the failing set keeps changing, and the pointer-drag test failed on both main and this branch.

Tally on macos-aarch64 shard 3/4: unmodified main 1 pass / 1 fail, this branch 1 pass / 2 fail.

I mention it because my first read was that the shard could not be related since its tests never touch the login path, and that reasoning was wrong: every e2e test drives the same binary this change is compiled into. The A/B against unmodified main is what actually settles it, not the argument from test names.

Not covered

A socket-level transport failure is not directly exercised. It reaches the same catch as the HTTP cases, so the flag is covered, but the socket path itself is not.

The wasm fetchTeamsFromJsHost branch has no test here.

Re-authenticating over an existing session still clears a previously selected team when the fetch fails. That predates this change and fixing it means deciding a team should survive re-login, which felt like a separate call to make.

Both sign-in paths turned every team-fetch failure into an empty team
list, which is the same value an account with no teams produces. A 500,
a rejected credential, a malformed body, or a response whose entries do
not parse therefore left the user with a session scoped to their
personal account, no team picker, and output byte-identical to a healthy
login. The session persists that way, so gateway requests that need a
team are rejected until the next login.

Keep signing in, since the token pair is valid and the team fetch is
auxiliary, but track whether the list actually arrived and say so when
it did not. An account that genuinely has no teams stays silent.

parseTeams now counts the entries it cannot read. A non-empty array that
yields nothing usable is a response we could not read rather than an
account with no teams, so it returns InvalidTeamsResponse; a partly
readable list keeps the teams it got and still reports the shortfall,
because the team the user wants may be one of the entries that dropped.
This also reaches the fx teams command, which fails loudly on a list it
cannot read instead of reporting no teams.

The CLI prints one line after the existing gateway-access caveat. The
TUI emits a warning notice beside the existing sign-in notice, reading
the flag before the picker branch because openTeamPicker takes the
selection. The two surfaces share a prefix and append their own remedy,
so the TUI does not tell you to run a shell command.

TestAuth.openTeamPicker was a no-op that did not take the selection the
way the real picker does, and TestApp discarded the notice tone. Making
both faithful is what lets the unit tests cover a team-carrying
selection whose fetch failed, and assert the warning tone, neither of
which the end-to-end tests can reach.
@beardthelion

Copy link
Copy Markdown
Contributor Author

This needs a type: bug label and I can't add one, so per AGENTS.md I'm leaving it in draft. Happy to flip it to ready once a maintainer labels it.

On CI, so it isn't a surprise: upstream checks are green. On my fork's Full CI, everything passes except E2E (ReleaseSafe, macos-aarch64, shard 3/4), which fails on two subagent-manager tests (configure duration derives its stop boundary and survives restart, persistent child pointer drag replaces the selected composer range). A macos-aarch64 shard 1/4 failure in the first pass cleared on re-run; shard 3 did not.

I don't think that shard is related to this change, and the check that convinced me is that shard 3's file list is identical on main and on this branch, and tui-auth-source-selection.test.ts is not in it. tui-subagent-manager.test.ts never touches the login path. Happy to be told otherwise if that shard is known-good on your runners.

@beardthelion

Copy link
Copy Markdown
Contributor Author

Correcting my earlier note on that macOS shard. I said it looked unrelated because its tests never touch the login path, which was the wrong reason: every e2e test runs the same binary this change is compiled into, so the test file's contents prove nothing either way.

I settled it properly by pushing 261a7c3f unmodified to my fork and running the same workflow. That shard fails there too, on configure rejects a concurrent winner then retries the preserved draft once, persistent child pointer drag replaces the selected composer range, and persistent child preserves its reading position across both reopen paths. The failing set shifts between runs, and the pointer-drag test failed on both main and this branch. Tally on macos-aarch64 shard 3/4: unmodified main 1 pass / 1 fail, this branch 1 pass / 2 fail.

Full CI is green on this commit now. The PR body has the details.

@beardthelion
beardthelion marked this pull request as ready for review September 1, 2026 02:30
@beardthelion
beardthelion marked this pull request as draft September 1, 2026 02:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Failed team lookup during login is reported as a successful sign-in

1 participant