feat(FN-2317): merge fusion/fn-2317
This commit is contained in:
@@ -439,6 +439,11 @@ export type ProviderConnectionStatus = "connected" | "not-connected" | "skipped"
|
||||
/** GitHub-specific status variants for richer connection feedback */
|
||||
type GitHubConnectionStatus = "connected" | "failed" | "pending" | "skipped" | "not-connected";
|
||||
|
||||
interface GhCliStatus {
|
||||
available: boolean;
|
||||
authenticated: boolean;
|
||||
}
|
||||
|
||||
/** Maximum number of poll cycles before timing out (150 × 2s = 5 minutes) */
|
||||
const MAX_POLL_CYCLES = 150;
|
||||
|
||||
@@ -477,6 +482,7 @@ export function ModelOnboardingModal({
|
||||
const [taskCreationError, setTaskCreationError] = useState<string | null>(null);
|
||||
const [inlineCreatedTask, setInlineCreatedTask] = useState<Task | null>(null);
|
||||
const [authProviders, setAuthProviders] = useState<AuthProvider[]>([]);
|
||||
const [ghCliStatus, setGhCliStatus] = useState<GhCliStatus | undefined>(undefined);
|
||||
const [authLoading, setAuthLoading] = useState(true);
|
||||
const [authActionInProgress, setAuthActionInProgress] = useState<string | null>(null);
|
||||
const [availableModels, setAvailableModels] = useState<ModelInfo[]>([]);
|
||||
@@ -583,8 +589,9 @@ export function ModelOnboardingModal({
|
||||
// Load auth providers
|
||||
const loadAuthStatus = useCallback(async () => {
|
||||
try {
|
||||
const { providers } = await fetchAuthStatus();
|
||||
const { providers, ghCli } = await fetchAuthStatus();
|
||||
setAuthProviders(providers);
|
||||
setGhCliStatus(ghCli);
|
||||
// Remove from skippedProviders when a provider becomes authenticated
|
||||
setSkippedProviders((prev) => {
|
||||
const updated = { ...prev };
|
||||
@@ -609,10 +616,14 @@ export function ModelOnboardingModal({
|
||||
aiSetupReturnRef.current = step !== "ai-setup";
|
||||
}, [step, loadAuthStatus]);
|
||||
|
||||
// Check if GitHub provider is configured and currently authenticated
|
||||
// OAuth status for the GitHub provider (used for OAuth-specific controls like Connect/Disconnect).
|
||||
const githubProvider = authProviders.find((p) => p.id === "github");
|
||||
const hasGithubProvider = !!githubProvider;
|
||||
const isGithubAuthenticated = githubProvider?.authenticated ?? false;
|
||||
const isGithubCliAuthenticated = ghCliStatus?.authenticated ?? false;
|
||||
// Effective GitHub readiness (matches useSetupReadiness): OAuth OR authenticated gh CLI session.
|
||||
const isGitHubReady = isGithubAuthenticated || isGithubCliAuthenticated;
|
||||
const isGitHubReadyViaCli = !isGithubAuthenticated && isGithubCliAuthenticated;
|
||||
|
||||
// Get provider connection status for UI display
|
||||
const getProviderStatus = useCallback((provider: AuthProvider): ProviderConnectionStatus => {
|
||||
@@ -651,7 +662,7 @@ export function ModelOnboardingModal({
|
||||
}
|
||||
|
||||
const getGitHubStatus = useCallback((): GitHubConnectionStatus => {
|
||||
if (isGithubAuthenticated) {
|
||||
if (isGitHubReady) {
|
||||
return "connected";
|
||||
}
|
||||
|
||||
@@ -667,7 +678,7 @@ export function ModelOnboardingModal({
|
||||
}
|
||||
|
||||
return "not-connected";
|
||||
}, [isGithubAuthenticated, loginOutcomes, isGithubSkipped]);
|
||||
}, [isGitHubReady, loginOutcomes, isGithubSkipped]);
|
||||
|
||||
function GitHubStatusBadge({ status }: { status: GitHubConnectionStatus }) {
|
||||
const config: Record<GitHubConnectionStatus, { text: string; className: string }> = {
|
||||
@@ -883,8 +894,9 @@ export function ModelOnboardingModal({
|
||||
}
|
||||
|
||||
try {
|
||||
const { providers } = await fetchAuthStatus();
|
||||
const { providers, ghCli } = await fetchAuthStatus();
|
||||
setAuthProviders(providers);
|
||||
setGhCliStatus(ghCli);
|
||||
const provider = providers.find((p) => p.id === providerId);
|
||||
if (provider?.authenticated) {
|
||||
if (pollIntervalRef.current) {
|
||||
@@ -1328,11 +1340,13 @@ export function ModelOnboardingModal({
|
||||
});
|
||||
}
|
||||
|
||||
if (isGithubAuthenticated) {
|
||||
if (isGitHubReady) {
|
||||
readinessItems.push({
|
||||
label: "GitHub",
|
||||
status: "connected",
|
||||
detail: "Issues and PRs can be imported",
|
||||
detail: isGitHubReadyViaCli
|
||||
? "Connected via GitHub CLI — imports and PR tracking are available"
|
||||
: "Issues and PRs can be imported",
|
||||
});
|
||||
} else if (!hasGithubProvider || isGithubSkipped) {
|
||||
readinessItems.push({
|
||||
@@ -1758,10 +1772,18 @@ export function ModelOnboardingModal({
|
||||
{!hasGithubProvider ? (
|
||||
<div className="model-onboarding-github-optional">
|
||||
<GitPullRequest size={48} className="optional-icon" />
|
||||
<p>
|
||||
GitHub integration isn't set up yet. You can enable it later
|
||||
in Settings → Authentication.
|
||||
</p>
|
||||
{isGitHubReadyViaCli ? (
|
||||
<p>
|
||||
GitHub CLI is already authenticated, so imports and PR tracking work now.
|
||||
OAuth integration in Settings → Authentication is optional and only controls
|
||||
dashboard-managed connect/disconnect.
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
GitHub OAuth isn't connected yet. You can set it up in Settings → Authentication,
|
||||
or continue now and connect later.
|
||||
</p>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-primary btn-sm"
|
||||
onClick={() => setStep("first-task")}
|
||||
@@ -1825,7 +1847,9 @@ export function ModelOnboardingModal({
|
||||
|
||||
{githubStatus === "connected" && (
|
||||
<div className="onboarding-github-feedback onboarding-github-feedback--success">
|
||||
GitHub is connected. You can import issues and track pull requests.
|
||||
{isGitHubReadyViaCli
|
||||
? "GitHub CLI is authenticated. Imports and pull request tracking are available. Connect OAuth in Settings → Authentication if you want dashboard-managed sign-in controls."
|
||||
: "GitHub OAuth is connected. You can import issues and track pull requests."}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1984,7 +2008,7 @@ export function ModelOnboardingModal({
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`onboarding-cta-card${!isGithubAuthenticated ? " onboarding-cta-card--disabled" : ""}`}
|
||||
className={`onboarding-cta-card${!isGitHubReady ? " onboarding-cta-card--disabled" : ""}`}
|
||||
data-testid="cta-github-import"
|
||||
onClick={handleOpenGitHubImport}
|
||||
disabled={saving}
|
||||
@@ -1995,7 +2019,7 @@ export function ModelOnboardingModal({
|
||||
<div className="cta-content">
|
||||
<strong>Import from GitHub</strong>
|
||||
<span>Turn GitHub issues into tasks you can track here</span>
|
||||
{!isGithubAuthenticated && (
|
||||
{!isGitHubReady && (
|
||||
<small className="onboarding-cta-note">Requires GitHub connection</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -942,12 +942,12 @@ describe("ModelOnboardingModal", () => {
|
||||
});
|
||||
|
||||
describe("GitHub step", () => {
|
||||
it("GitHub step shows fallback when no GitHub provider", async () => {
|
||||
it("GitHub step shows OAuth setup fallback when neither OAuth nor gh CLI auth is available", async () => {
|
||||
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} />);
|
||||
|
||||
await navigateToGitHubStep();
|
||||
|
||||
expect(screen.getByText(/GitHub integration isn't set up yet/)).toBeTruthy();
|
||||
expect(screen.getByText(/GitHub OAuth isn't connected yet/)).toBeTruthy();
|
||||
expect(screen.getByText(/Settings → Authentication/)).toBeTruthy();
|
||||
expect(screen.getByRole("button", { name: "Continue without GitHub →" })).toBeTruthy();
|
||||
});
|
||||
@@ -1002,6 +1002,21 @@ describe("ModelOnboardingModal", () => {
|
||||
expect(screen.queryByTestId("onboarding-github-connect-cta")).toBeNull();
|
||||
});
|
||||
|
||||
it("GitHub step explains gh CLI auth when OAuth provider is absent", async () => {
|
||||
mockFetchAuthStatus.mockResolvedValueOnce({
|
||||
providers: [],
|
||||
ghCli: { available: true, authenticated: true },
|
||||
});
|
||||
|
||||
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} />);
|
||||
|
||||
await navigateToGitHubStep();
|
||||
|
||||
expect(screen.getByText(/GitHub CLI is already authenticated/)).toBeTruthy();
|
||||
expect(screen.getByText(/OAuth integration in Settings → Authentication is optional/)).toBeTruthy();
|
||||
expect(screen.queryByTestId("onboarding-github-connect-cta")).toBeNull();
|
||||
});
|
||||
|
||||
describe("GitHub connection status feedback", () => {
|
||||
it("shows connected status with success feedback", async () => {
|
||||
mockFetchAuthStatus.mockResolvedValueOnce({
|
||||
@@ -1017,7 +1032,25 @@ describe("ModelOnboardingModal", () => {
|
||||
const badge = screen.getByTestId("github-status-badge");
|
||||
expect(badge).toHaveTextContent("✓ Connected");
|
||||
expect(badge).toHaveClass("connected");
|
||||
expect(screen.getByText("GitHub is connected. You can import issues and track pull requests.")).toBeTruthy();
|
||||
expect(screen.getByText("GitHub OAuth is connected. You can import issues and track pull requests.")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shows connected feedback when gh CLI is authenticated without GitHub OAuth", async () => {
|
||||
mockFetchAuthStatus.mockResolvedValueOnce({
|
||||
providers: [
|
||||
{ id: "github", name: "GitHub", authenticated: false, type: "oauth" },
|
||||
],
|
||||
ghCli: { available: true, authenticated: true },
|
||||
});
|
||||
|
||||
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} />);
|
||||
|
||||
await navigateToGitHubStep();
|
||||
|
||||
const badge = screen.getByTestId("github-status-badge");
|
||||
expect(badge).toHaveTextContent("✓ Connected");
|
||||
expect(badge).toHaveClass("connected");
|
||||
expect(screen.getByText(/GitHub CLI is authenticated/)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shows not-connected status and keeps default helper text", async () => {
|
||||
|
||||
@@ -727,6 +727,29 @@ describe("onboarding flow integration", () => {
|
||||
|
||||
expect(screen.getByText("Requires GitHub connection")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("skip warnings: gh CLI auth marks GitHub as ready in summary and hides connection-required note", async () => {
|
||||
mockFetchAuthStatus.mockResolvedValue({
|
||||
providers: [
|
||||
{ id: "anthropic", name: "Anthropic", authenticated: false, type: "oauth" },
|
||||
],
|
||||
ghCli: { available: true, authenticated: true },
|
||||
});
|
||||
|
||||
const renderResult = renderModal();
|
||||
|
||||
await advanceThroughSteps(renderResult, ["next", "next"]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Create Your First Task")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const readinessSummary = screen.getByTestId("readiness-summary");
|
||||
const githubItem = within(readinessSummary).getByText("GitHub").closest(".onboarding-readiness-item");
|
||||
expect(githubItem).toHaveAttribute("data-status", "connected");
|
||||
expect(screen.getByText("Connected via GitHub CLI — imports and PR tracking are available")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Requires GitHub connection")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("task creation flow", () => {
|
||||
|
||||
Reference in New Issue
Block a user