fix: forge CLI pivot bugs against real tea/glab binaries
Fix critical bugs in the CLI transport layer that were masked by idealized unit test mocks. All fixes verified against live binaries. Server (gitea/client.js): - C1: Remove --paginate flag (tea rejects it with exit 1) - C2: Pass auth via -H 'Authorization: token' header instead of GITEA_SERVER_TOKEN env (tea ignores that env var) - C3: Parse --include output from stderr (tea writes headers to stderr, not stdout) - H2: Parse Link header for hasMore/pagination Server (gitlab/client.js): - C4: Remove /api/v4 prefix (glab adds it automatically; double prefix caused every call to 404) - H2: Parse Link header for hasMore/pagination Tests (both client.test.js): - H1: Rewrite mocks to match real CLI behavior: headers on stderr for tea, no --paginate, auth via -H header, Link header parsing - Add C3, C1, C4 specific regression tests UI (GiteaSettings, GitLabSettings): - U4: Replace return null loading state with animated skeleton (prevents blank flash) - U2: Surface actual error message in connect failure toast - U5: Add CLI transport hint below connect form - U6 (GitLab): Add transport hint for unconnected state UI (GiteaIssuePickerDialog, GitLabIssuePickerDialog): - U3: Add retry button when error state is displayed Live smoke tests passed: - tea api -H 'Authorization: token WRONG' /user → 401 - tea api /repos/Vibing/openchamber/issues?state=open&limit=2 → 200 - glab api user with GITLAB_TOKEN=dummy → 401 (not 404)
This commit is contained in:
@@ -84,7 +84,8 @@ export const GitLabSettings: React.FC<{ embedded?: boolean }> = ({ embedded = fa
|
||||
toast.success(t('settings.gitlab.page.toast.connected'));
|
||||
} catch (error) {
|
||||
console.error('Failed to connect GitLab:', error);
|
||||
toast.error(t('settings.gitlab.page.errors.failed'));
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
toast.error(t('settings.gitlab.page.errors.failed'), { description: message });
|
||||
} finally {
|
||||
setIsBusy(false);
|
||||
}
|
||||
@@ -144,7 +145,24 @@ export const GitLabSettings: React.FC<{ embedded?: boolean }> = ({ embedded = fa
|
||||
}, [runtimeGitLab, setStatus, t]);
|
||||
|
||||
if (isLoading) {
|
||||
return null;
|
||||
return (
|
||||
<SettingsSection
|
||||
title={t('settings.gitlab.page.title')}
|
||||
description={t('settings.gitlab.page.description')}
|
||||
info={t('settings.gitlab.page.tooltip.connectAccount')}
|
||||
settingsItem="git.gitlab-account"
|
||||
>
|
||||
<div className="rounded-lg bg-[var(--surface-elevated)]/70 overflow-hidden flex flex-col">
|
||||
<div className="px-4 py-3 flex items-center gap-4 animate-pulse">
|
||||
<div className="h-10 w-10 shrink-0 rounded-full bg-[var(--surface-muted)]" />
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="h-4 w-32 bg-[var(--surface-muted)] rounded" />
|
||||
<div className="h-3 w-48 bg-[var(--surface-muted)] rounded" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
||||
const connected = Boolean(status?.connected);
|
||||
@@ -232,6 +250,9 @@ export const GitLabSettings: React.FC<{ embedded?: boolean }> = ({ embedded = fa
|
||||
{t('settings.gitlab.page.actions.connect')}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="typography-micro text-muted-foreground">
|
||||
Authenticates via the glab CLI binary and your access token.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -89,7 +89,8 @@ export const GiteaSettings: React.FC<{ embedded?: boolean }> = ({ embedded = fal
|
||||
toast.success(t('settings.gitea.page.toast.connected'));
|
||||
} catch (error) {
|
||||
console.error('Failed to connect Gitea:', error);
|
||||
toast.error(t('settings.gitea.page.errors.failed'));
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
toast.error(t('settings.gitea.page.errors.failed'), { description: message });
|
||||
} finally {
|
||||
setIsBusy(false);
|
||||
}
|
||||
@@ -149,7 +150,24 @@ export const GiteaSettings: React.FC<{ embedded?: boolean }> = ({ embedded = fal
|
||||
}, [runtimeGitea, setStatus, t]);
|
||||
|
||||
if (isLoading) {
|
||||
return null;
|
||||
return (
|
||||
<SettingsSection
|
||||
title={t('settings.gitea.page.title')}
|
||||
description={t('settings.gitea.page.description')}
|
||||
info={t('settings.gitea.page.tooltip.connectAccount')}
|
||||
settingsItem="git.gitea-account"
|
||||
>
|
||||
<div className="rounded-lg bg-[var(--surface-elevated)]/70 overflow-hidden flex flex-col">
|
||||
<div className="px-4 py-3 flex items-center gap-4 animate-pulse">
|
||||
<div className="h-10 w-10 shrink-0 rounded-full bg-[var(--surface-muted)]" />
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="h-4 w-32 bg-[var(--surface-muted)] rounded" />
|
||||
<div className="h-3 w-48 bg-[var(--surface-muted)] rounded" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
||||
const connected = Boolean(status?.connected);
|
||||
@@ -242,6 +260,9 @@ export const GiteaSettings: React.FC<{ embedded?: boolean }> = ({ embedded = fal
|
||||
{t('settings.gitea.page.actions.connect')}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="typography-micro text-muted-foreground">
|
||||
Authenticates via the tea CLI binary and your stored token.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -545,7 +545,12 @@ export function GitLabIssuePickerDialog({
|
||||
) : null}
|
||||
|
||||
{error ? (
|
||||
<div className="text-center text-muted-foreground py-8 break-words">{error}</div>
|
||||
<div className="text-center text-muted-foreground py-8 space-y-2">
|
||||
<div className="break-words">{error}</div>
|
||||
<Button variant="outline" size="sm" onClick={refresh} disabled={isLoading}>
|
||||
{t('session.gitlabIssuePicker.actions.refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{directNumber && projectDirectory && gitlab && connected ? (
|
||||
|
||||
@@ -543,7 +543,12 @@ export function GiteaIssuePickerDialog({
|
||||
) : null}
|
||||
|
||||
{error ? (
|
||||
<div className="text-center text-muted-foreground py-8 break-words">{error}</div>
|
||||
<div className="text-center text-muted-foreground py-8 space-y-2">
|
||||
<div className="break-words">{error}</div>
|
||||
<Button variant="outline" size="sm" onClick={refresh} disabled={isLoading}>
|
||||
{t('session.giteaIssuePicker.actions.refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{directNumber && projectDirectory && gitea && connected ? (
|
||||
|
||||
Reference in New Issue
Block a user