fix: [ENG-2941] tolerate missing body in auth:getState#709
Conversation
TUI sends no body for auth:getState, so the {projectPath} destructure
added in #653 threw and the existing catch-all swallowed it into
{isAuthorized: false} — forcing the TUI into the provider chooser and
re-prompting byterover login on every startup. The valid token already
proves the user is logged in; treat projectPath as optional and pull it
via optional chaining so a missing body returns the correct auth state.
|
Claude finished @bao-byterover's task in 3m 14s —— View job Review SummaryScore: 4.5 / 5 — clean, minimal, well-tested, properly scoped urgent fix. Two non-blocking observations below for follow-up.
What works well
Non-blocking observations (follow-ups, not for this PR)
Verification I performed
RecommendationShip it. The two notes above are genuine follow-ups, not gates on this PR — ENG-2941 is Urgent and this fix is the right minimal change. |
| } | ||
|
|
||
| const {projectPath} = data | ||
| const projectPath = data?.projectPath |
There was a problem hiding this comment.
note (non-blocking): The fix here is exactly right for ENG-2941. Flagging for a follow-up (already acknowledged in the PR description): the broad } catch { return {isAuthorized: false} } on lines 249–251 still converts any unexpected throw from userService.getCurrentUser (e.g. transient network error) into {isAuthorized: false}, which re-creates the exact "stuck on provider chooser" symptom this PR fixes — just on flaky networks instead of every startup. Worth narrowing the catch in a follow-up so token validity (the actual auth signal) is decoupled from user-info fetch failures. The new broadcastAuthStateChanged (lines 117–122) already does this — broadcasts {isAuthorized: true} without user details on getCurrentUser failure — so the pattern is already in this file.
| export interface AuthGetStateRequest { | ||
| projectPath: string | ||
| projectPath?: string | ||
| } |
There was a problem hiding this comment.
suggestion (non-blocking, if-minor): Widening projectPath: string → projectPath?: string is the right call, but the field now has subtle semantics that aren't obvious from the type alone: omitting it returns a valid AuthGetStateResponse without brvConfig. Consider a one-line JSDoc so future callers don't get caught the way the TUI did in #653:
| } | |
| export interface AuthGetStateRequest { | |
| /** Project-scoped lookup for `brvConfig` (space/team). Omit to skip the lookup and return auth-only state. */ | |
| projectPath?: string | |
| } |
| expect(result.authToken).to.have.property('accessToken', 'test-access-token') | ||
| expect(projectConfigStore.read.called, 'projectConfigStore.read should not be called without projectPath').to.be | ||
| .false | ||
| }) |
There was a problem hiding this comment.
praise: Good ENG-2941 regression guard. The expect(projectConfigStore.read.called, '...').to.be.false assertion (line 573–574) is the right thing to pin — it locks in both the new contract (projectPath is optional) and the runtime behavior (skip the project read when absent). The empty-object case below (line 588) and the projectPath-present case (line 577) cover the other branches cleanly. The five tests together are a solid spec for the handler.
cuongdo-byterover
left a comment
There was a problem hiding this comment.
good on my side
TUI sends no body for auth:getState, so the
{projectPath}destructure added in #653 threw and the existing catch-all swallowed it into{isAuthorized: false}, forcing the TUI into the provider chooser and re-prompting byterover login on every startup. The valid token already proves the user is logged in; treatprojectPathas optional and pull it via optional chaining so a missing body returns the correct auth state.Summary
brv status(CLI) and the local WebUI dashboard both reported the user as logged in with the right provider.auth:getStatehandler now readsprojectPathvia optional chaining (data?.projectPath) instead of destructuring, and the shared request type marksprojectPathas optional. Five regression tests added under a newsetupGetStatedescribe block (the path had zero coverage, which is why the original change slipped through).catchthat swallowsuserService.getCurrentUserfailures into{isAuthorized: false}is also untouched (separate latent issue, predates Proj/rework llm billing #653, not part of ENG-2941's "always stuck" symptom).Type of change
Scope (select all touched areas)
Linked issues
1c54787fe)Root cause (bug fixes only, otherwise write
N/A)auth:getStatehandler fromconst projectPath = this.resolveProjectPath(clientId)toconst {projectPath} = dataand updated the WebUI client to send{projectPath}. The TUI client (src/tui/features/auth/api/get-auth-state.ts:17) was missed and kept sendingundefinedas the body. On the daemon, destructuringundefinedthrew aTypeError, which the long-standing broadcatchswallowed and converted into{isAuthorized: false}. The TUI'suseAppViewModethen routed toConfigProviderPage, andprovider-flow.tsx's byterover branch re-prompted login.setupGetStatehad zero test coverage. The existingauth-handler.test.tsexercised broadcast, login, logout, and the external-auth-sync paths, but never the request/response path. TypeScript could not catch the gap either because the contract was changed in the type def (required) without updating the TUI caller.Test plan
test/unit/infra/transport/handlers/auth-handler.test.ts(newdescribe('setupGetState')block with five cases).undefinedreturns{isAuthorized: true}with user, noprojectConfigStore.readcall. This is the ENG-2941 regression guard.{projectPath: '/foo'}returns full payload includingbrvConfig. This locks the WebUI happy path.{}returns authorized withoutbrvConfig. Confirms the optional contract.{isAuthorized: false}. Confirms the not-logged-in check still wins.{isAuthorized: false}. Confirms token expiry still wins.User-visible changes
brv(no args) now drops the user straight into the REPL when the user is logged in and byterover is the active provider, instead of forcing the provider-chooser screen.brv status,brv providers list,brv login,brv logout): unchanged.Evidence
Before the fix, the new regression-guard test fails:
After the fix:
Checklist
npm test)npm run lint) - pre-existing submodule config error onmain(packages/byterover-packages/ui/...cannot resolve@workspace/typescript-config/react-library.json); reproduced on a cleanmaincheckout, not introduced by this PR. The pre-commitlint:fixhook scoped to the three changed files passes.npm run typecheck)npm run build)projectPath: stringtoprojectPath?: stringis strictly more permissive; all existing callers continue to type-checkmainRisks and mitigations
auth:getStatethat omits the body will now receive{isAuthorized: true}withbrvConfig: undefinedinstead of an error, so a caller that silently depends onbrvConfigbeing populated could readundefinedwhere it previously got a hard failure.src/andtest/confirms only two callers exist today: WebUI (always sends{projectPath}, behavior unchanged) and TUI (never readbrvConfigfrom this response). Five new tests pin the contract for both shapes.} catch { return {isAuthorized: false} }insidesetupGetStatestill silently downgradesuserService.getCurrentUserfailures to "not logged in", so a flaky network can still flip the TUI to onboarding intermittently.