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:
2026-09-05 20:26:35 +00:00
parent c03fbda7a9
commit 9032c9245a
8 changed files with 317 additions and 86 deletions
@@ -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 ? (