refactor(desktop): unify validate+test into single ConnectionCheck#38
Merged
refactor(desktop): unify validate+test into single ConnectionCheck#38
Conversation
…/ segment When a user provides a custom baseUrl ending with /v1 (e.g. DuckCoding relay), the validate endpoint function was constructing https://proxy/v1/v1/models instead of https://proxy/v1/models. The connection:v1:test IPC used normalizeBaseUrl() and hit the correct URL, so validate showed a network error while Test showed Connected. Add normalizeValidateBaseUrl() that strips trailing slash and /v1 suffix before appending /v1/models, matching the behaviour of connection-ipc.ts. Also: update onboarding-ipc.test.ts to capture handler closures so we can assert that the IPC handler forwards baseUrl to pingProvider. Signed-off-by: hqhq1025 <[email protected]>
| * - anthropic: strip trailing /v1 — we append /v1/models below | ||
| */ | ||
| function normalizeValidateBaseUrl(baseUrl: string): string { | ||
| return baseUrl.replace(/\/+$/, '').replace(/\/v1$/, ''); |
Contributor
There was a problem hiding this comment.
Findings
- None.
Summary
- Review mode: initial
- No diff-attributable issues found in added/modified lines.
- Constraint checks in touched files: no direct provider SDK imports, no new dependencies/licenses, no UI token violations in scope, no newly introduced silent fallback behavior detected.
- Not found in repo/docs:
docs/VISION.md,docs/PRINCIPLES.md.
Testing
- Not run (automation)
open-codesign Bot
Remove the dual-state anti-pattern where `ValidationState` (from `onboarding.validateKey` / `pingProvider`) and `ConnectionState` (from `connection.test`) ran in parallel with inconsistent URL-building logic. Before: key paste triggered an auto-validate useEffect that called `pingProvider` (packages/providers/validate.ts) which built URLs by naively appending `/v1/models` to the raw baseUrl. Separately, the Test button called `connection.test` which uses `normalizeBaseUrl` + provider- aware path construction. The two paths could disagree on endpoints (e.g. double /v1 for OpenAI-compat proxies) and expose conflicting UX. After: - Single `ConnectionCheck` type: idle | testing | ok | failed - Provider detection (lightweight IPC, no network) runs on key change - Test button is the ONLY authority for key + endpoint verification, always using `connection:v1:test` (normalizeBaseUrl path) - When no custom baseUrl is set the renderer falls back to the same default URLs as `buildDefaultBaseUrl` in connection-ipc.ts - Continue button disabled until ConnectionCheck = ok - `ValidationState`, `onboarding.validateKey` call, and the 500ms auto-validate useEffect are fully removed from PasteKey - Test file rewritten to cover the unified ConnectionCheck logic including default-baseUrl fallback behaviour Signed-off-by: hqhq1025 <[email protected]>
Contributor
There was a problem hiding this comment.
Findings
- [Major] Detection IPC/network errors are silently downgraded to an "unknown key prefix" state, which hides operational failures and violates the no-silent-fallback constraint. In
detectProvider, exceptions are caught and mapped tounknownwithout surfacing error context; the UI then shows an unsupported-prefix message, which is incorrect for IPC/network failures. Evidence:apps/desktop/src/renderer/src/onboarding/PasteKey.tsx:93,apps/desktop/src/renderer/src/onboarding/PasteKey.tsx:95,apps/desktop/src/renderer/src/onboarding/PasteKey.tsx:434.
Suggested fix:type DetectState = | { kind: 'idle' } | { kind: 'detecting' } | { kind: 'detected'; provider: SupportedOnboardingProvider } | { kind: 'unknown' } | { kind: 'error'; message: string }; // in detectProvider catch } catch (err) { if (reqId !== reqIdRef.current) return; setDetectState({ kind: 'error', message: err instanceof Error ? err.message : 'Provider detection failed.', }); setConnCheck({ status: 'failed', code: 'NETWORK', hint: 'Provider detection failed.' }); return; }
Summary
- Review mode: follow-up after new commits
- 1 Major issue found in added/modified lines.
- Not found in repo/docs:
docs/VISION.md,docs/PRINCIPLES.md.
Testing
- Not run (automation)
open-codesign Bot
| detected = await window.codesign.detectProvider(trimmed); | ||
| } catch { | ||
| if (reqId !== reqIdRef.current) return; | ||
| setDetectState({ kind: 'unknown' }); |
Contributor
There was a problem hiding this comment.
This catch block suppresses detection failures by converting them to unknown and later rendering an "Unrecognized key prefix" message. That is a silent fallback and a misleading UX for IPC/network failures.
Suggested minimal change:
} catch (err) {
if (reqId !== reqIdRef.current) return;
setDetectState({
kind: 'error',
message: err instanceof Error ? err.message : 'Provider detection failed.',
});
return;
}Then render DetectLine for kind: 'error' with the message.
… detectProvider
Previously detectProvider's catch block collapsed every failure into
{ kind: 'unknown' } and the UI rendered an 'unrecognized key prefix'
message — silently downgrading IPC crashes and network outages to a
user-input problem. This violated the no-silent-fallback principle.
- Introduce DetectResult discriminated union: ok | unknown_prefix |
ipc_error | network_error.
- classifyDetectError inspects the thrown error (TypeError or
fetch/ECONN/ENOTFOUND/ETIMEDOUT/EAI_AGAIN tokens) to choose between
ipc_error and network_error; defaults to ipc_error since the
detect-provider IPC is local-only.
- DetectLine renders three distinct error messages so users see the
actual failure mode.
- Failed IPC/network detection also surfaces in ConnectionCheck so the
status row reflects the real fault rather than appearing idle.
- Adds 11 vitest cases covering each branch of detectProvider and
classifyDetectError.
Contributor
There was a problem hiding this comment.
Findings
- No issues found in added/modified lines.
Summary
- Review mode: follow-up after new commits
- No new issues found in the latest diff.
- Not found in repo/docs:
docs/VISION.md,docs/PRINCIPLES.md. - Residual risk: no Playwright coverage changes were included for the onboarding flow transitions (detect error -> test -> continue gating).
Testing
- Not run (automation)
open-codesign Bot
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The onboarding
PasteKeycomponent had two parallel, inconsistent validation flows:onboarding.validateKey→pingProviderinpackages/providers/validate.ts): built URLs by naively appending/v1/modelsto whateverbaseUrlthe user typed — no normalization.connection:v1:testinconnection-ipc.ts): usednormalizeBaseUrl+ provider-aware path construction before hitting the endpoint.Result: with an OpenAI-compat proxy at
https://host/v1, auto-validate would hithttps://host/v1/v1/models(404) while the Test button correctly hithttps://host/v1/models(200). The two flows disagreed on the same inputs, producing confusing UX and silent failures.Solution
Delete the split-state architecture. Replace
ValidationState+ConnectionStatewith a single unified type:```ts
type ConnectionCheck =
| { status: 'idle' }
| { status: 'testing' }
| { status: 'ok' }
| { status: 'failed'; code: ErrorCode; hint: string };
```
What changed
ValidationStatetype and the auto-validateuseEffectthat calledonboarding.validateKey/pingProvider.connection:v1:testwhich usesnormalizeBaseUrl+ correct per-provider path logic.baseUrlis entered, the renderer falls back to the same defaults asbuildDefaultBaseUrlinconnection-ipc.ts(https://api.anthropic.com,https://api.openai.com/v1,https://openrouter.ai/api/v1). The Test button works for the official endpoint without requiring the advanced section.connectionCheck.status === 'ok'with tooltip hint "Run Test to verify your connection first".onboarding.validateKey/providers.validateIPC no longer called fromPasteKey(the IPC handler still exists forSettingsusage).Deleted paths
ValidationState(idle/detecting/validating/ok/error)ConnectionCheck(idle/testing/ok/failed)connState(ConnectionState)ConnectionCheckuseEffect→onboarding.validateKeycallconnection.testPrinciples check
onboarding:validate-keystill registeredTest plan
pnpm -r typecheckpassespnpm lint— no new errors in changed filespnpm test— 104/104 tests pass including newPasteKey.test.tscases covering default-baseUrl fallback, 401/ECONNREFUSED errors, and Continue gatehttps://host/v1→ Test hits correct endpoint → no double/v1