What's wrong
ApplyOwnerToken (ProjectDirector/ProjectDirector.cs ~L338–341) returns TokenStorage.DrainUnavailableReport() when WriteOwnerToken refuses. Its own doc comment says a refusal "has to reach the user: the popup closes either way, so silence would look like success". But TokenStorage.ReportUnavailable (ProjectDirector/TokenStorage.cs ~L238) fills the report only on the first failure in the process, guarded by _unavailableReported.
At startup the constructor spends that report:
PrepareTokens calls ResolveGitHubCredentials(..., options.GitHubToken).
- The getter reads the store and the read fails, which sets the report.
DrainSecretStoreReport() logs it and clears it.
From then on, every "Set GitHub Owner Token" against the unavailable store gets false from WriteOwnerToken, and DrainUnavailableReport() returns "". Nothing is logged, the popup closes, and the token is discarded. A transient store failure, such as a locked keychain or libsecret throwing CredentialStoreException, goes silent the same way after its first occurrence.
The existing ApplyOwnerTokenReportsARefusal test passes only because UseCache resets the flag immediately before the call. It never exercises the startup → drain → apply sequence.
Failure scenario
- Run on Linux without a Secret Service provider.
- At launch the log shows the "No usable OS secret store..." line once.
- File → Set GitHub Owner Token →
ktsu-dev → paste a PAT → OK.
- Nothing is logged, and the next scan still runs unauthenticated for that owner.
Verified with a scratch MSTest whose store throws DllNotFoundException:
PrepareTokens(new options) returns a non-empty startup report.
DrainUnavailableReport() is called, as the constructor does.
ApplyOwnerToken("ktsu-dev", "ghp_typed") returns "".
Suggested fix / acceptance criteria
- When
WriteOwnerToken returns false, have ApplyOwnerToken always produce a message, not only when the once-per-process report is pending. For example: $"Could not save the token for {owner}: " + (TokenStorage.DrainUnavailableReport() is { Length: > 0 } r ? r : "the OS secret store is unavailable").
- Alternatively, keep the last failure reason separately from the "already reported" flag.
- Acceptance: a test that runs startup → drain →
ApplyOwnerToken against an unavailable store gets a non-empty message, and that message is logged.
What's wrong
ApplyOwnerToken(ProjectDirector/ProjectDirector.cs~L338–341) returnsTokenStorage.DrainUnavailableReport()whenWriteOwnerTokenrefuses. Its own doc comment says a refusal "has to reach the user: the popup closes either way, so silence would look like success". ButTokenStorage.ReportUnavailable(ProjectDirector/TokenStorage.cs~L238) fills the report only on the first failure in the process, guarded by_unavailableReported.At startup the constructor spends that report:
PrepareTokenscallsResolveGitHubCredentials(..., options.GitHubToken).DrainSecretStoreReport()logs it and clears it.From then on, every "Set GitHub Owner Token" against the unavailable store gets
falsefromWriteOwnerToken, andDrainUnavailableReport()returns"". Nothing is logged, the popup closes, and the token is discarded. A transient store failure, such as a locked keychain or libsecret throwingCredentialStoreException, goes silent the same way after its first occurrence.The existing
ApplyOwnerTokenReportsARefusaltest passes only becauseUseCacheresets the flag immediately before the call. It never exercises the startup → drain → apply sequence.Failure scenario
ktsu-dev→ paste a PAT → OK.Verified with a scratch MSTest whose store throws
DllNotFoundException:PrepareTokens(new options)returns a non-empty startup report.DrainUnavailableReport()is called, as the constructor does.ApplyOwnerToken("ktsu-dev", "ghp_typed")returns"".Suggested fix / acceptance criteria
WriteOwnerTokenreturns false, haveApplyOwnerTokenalways produce a message, not only when the once-per-process report is pending. For example:$"Could not save the token for {owner}: " + (TokenStorage.DrainUnavailableReport() is { Length: > 0 } r ? r : "the OS secret store is unavailable").ApplyOwnerTokenagainst an unavailable store gets a non-empty message, and that message is logged.