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 */
|
/** GitHub-specific status variants for richer connection feedback */
|
||||||
type GitHubConnectionStatus = "connected" | "failed" | "pending" | "skipped" | "not-connected";
|
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) */
|
/** Maximum number of poll cycles before timing out (150 × 2s = 5 minutes) */
|
||||||
const MAX_POLL_CYCLES = 150;
|
const MAX_POLL_CYCLES = 150;
|
||||||
|
|
||||||
@@ -477,6 +482,7 @@ export function ModelOnboardingModal({
|
|||||||
const [taskCreationError, setTaskCreationError] = useState<string | null>(null);
|
const [taskCreationError, setTaskCreationError] = useState<string | null>(null);
|
||||||
const [inlineCreatedTask, setInlineCreatedTask] = useState<Task | null>(null);
|
const [inlineCreatedTask, setInlineCreatedTask] = useState<Task | null>(null);
|
||||||
const [authProviders, setAuthProviders] = useState<AuthProvider[]>([]);
|
const [authProviders, setAuthProviders] = useState<AuthProvider[]>([]);
|
||||||
|
const [ghCliStatus, setGhCliStatus] = useState<GhCliStatus | undefined>(undefined);
|
||||||
const [authLoading, setAuthLoading] = useState(true);
|
const [authLoading, setAuthLoading] = useState(true);
|
||||||
const [authActionInProgress, setAuthActionInProgress] = useState<string | null>(null);
|
const [authActionInProgress, setAuthActionInProgress] = useState<string | null>(null);
|
||||||
const [availableModels, setAvailableModels] = useState<ModelInfo[]>([]);
|
const [availableModels, setAvailableModels] = useState<ModelInfo[]>([]);
|
||||||
@@ -583,8 +589,9 @@ export function ModelOnboardingModal({
|
|||||||
// Load auth providers
|
// Load auth providers
|
||||||
const loadAuthStatus = useCallback(async () => {
|
const loadAuthStatus = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const { providers } = await fetchAuthStatus();
|
const { providers, ghCli } = await fetchAuthStatus();
|
||||||
setAuthProviders(providers);
|
setAuthProviders(providers);
|
||||||
|
setGhCliStatus(ghCli);
|
||||||
// Remove from skippedProviders when a provider becomes authenticated
|
// Remove from skippedProviders when a provider becomes authenticated
|
||||||
setSkippedProviders((prev) => {
|
setSkippedProviders((prev) => {
|
||||||
const updated = { ...prev };
|
const updated = { ...prev };
|
||||||
@@ -609,10 +616,14 @@ export function ModelOnboardingModal({
|
|||||||
aiSetupReturnRef.current = step !== "ai-setup";
|
aiSetupReturnRef.current = step !== "ai-setup";
|
||||||
}, [step, loadAuthStatus]);
|
}, [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 githubProvider = authProviders.find((p) => p.id === "github");
|
||||||
const hasGithubProvider = !!githubProvider;
|
const hasGithubProvider = !!githubProvider;
|
||||||
const isGithubAuthenticated = githubProvider?.authenticated ?? false;
|
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
|
// Get provider connection status for UI display
|
||||||
const getProviderStatus = useCallback((provider: AuthProvider): ProviderConnectionStatus => {
|
const getProviderStatus = useCallback((provider: AuthProvider): ProviderConnectionStatus => {
|
||||||
@@ -651,7 +662,7 @@ export function ModelOnboardingModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getGitHubStatus = useCallback((): GitHubConnectionStatus => {
|
const getGitHubStatus = useCallback((): GitHubConnectionStatus => {
|
||||||
if (isGithubAuthenticated) {
|
if (isGitHubReady) {
|
||||||
return "connected";
|
return "connected";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -667,7 +678,7 @@ export function ModelOnboardingModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return "not-connected";
|
return "not-connected";
|
||||||
}, [isGithubAuthenticated, loginOutcomes, isGithubSkipped]);
|
}, [isGitHubReady, loginOutcomes, isGithubSkipped]);
|
||||||
|
|
||||||
function GitHubStatusBadge({ status }: { status: GitHubConnectionStatus }) {
|
function GitHubStatusBadge({ status }: { status: GitHubConnectionStatus }) {
|
||||||
const config: Record<GitHubConnectionStatus, { text: string; className: string }> = {
|
const config: Record<GitHubConnectionStatus, { text: string; className: string }> = {
|
||||||
@@ -883,8 +894,9 @@ export function ModelOnboardingModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { providers } = await fetchAuthStatus();
|
const { providers, ghCli } = await fetchAuthStatus();
|
||||||
setAuthProviders(providers);
|
setAuthProviders(providers);
|
||||||
|
setGhCliStatus(ghCli);
|
||||||
const provider = providers.find((p) => p.id === providerId);
|
const provider = providers.find((p) => p.id === providerId);
|
||||||
if (provider?.authenticated) {
|
if (provider?.authenticated) {
|
||||||
if (pollIntervalRef.current) {
|
if (pollIntervalRef.current) {
|
||||||
@@ -1328,11 +1340,13 @@ export function ModelOnboardingModal({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isGithubAuthenticated) {
|
if (isGitHubReady) {
|
||||||
readinessItems.push({
|
readinessItems.push({
|
||||||
label: "GitHub",
|
label: "GitHub",
|
||||||
status: "connected",
|
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) {
|
} else if (!hasGithubProvider || isGithubSkipped) {
|
||||||
readinessItems.push({
|
readinessItems.push({
|
||||||
@@ -1758,10 +1772,18 @@ export function ModelOnboardingModal({
|
|||||||
{!hasGithubProvider ? (
|
{!hasGithubProvider ? (
|
||||||
<div className="model-onboarding-github-optional">
|
<div className="model-onboarding-github-optional">
|
||||||
<GitPullRequest size={48} className="optional-icon" />
|
<GitPullRequest size={48} className="optional-icon" />
|
||||||
<p>
|
{isGitHubReadyViaCli ? (
|
||||||
GitHub integration isn't set up yet. You can enable it later
|
<p>
|
||||||
in Settings → Authentication.
|
GitHub CLI is already authenticated, so imports and PR tracking work now.
|
||||||
</p>
|
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
|
<button
|
||||||
className="btn btn-primary btn-sm"
|
className="btn btn-primary btn-sm"
|
||||||
onClick={() => setStep("first-task")}
|
onClick={() => setStep("first-task")}
|
||||||
@@ -1825,7 +1847,9 @@ export function ModelOnboardingModal({
|
|||||||
|
|
||||||
{githubStatus === "connected" && (
|
{githubStatus === "connected" && (
|
||||||
<div className="onboarding-github-feedback onboarding-github-feedback--success">
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1984,7 +2008,7 @@ export function ModelOnboardingModal({
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<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"
|
data-testid="cta-github-import"
|
||||||
onClick={handleOpenGitHubImport}
|
onClick={handleOpenGitHubImport}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
@@ -1995,7 +2019,7 @@ export function ModelOnboardingModal({
|
|||||||
<div className="cta-content">
|
<div className="cta-content">
|
||||||
<strong>Import from GitHub</strong>
|
<strong>Import from GitHub</strong>
|
||||||
<span>Turn GitHub issues into tasks you can track here</span>
|
<span>Turn GitHub issues into tasks you can track here</span>
|
||||||
{!isGithubAuthenticated && (
|
{!isGitHubReady && (
|
||||||
<small className="onboarding-cta-note">Requires GitHub connection</small>
|
<small className="onboarding-cta-note">Requires GitHub connection</small>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -942,12 +942,12 @@ describe("ModelOnboardingModal", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("GitHub step", () => {
|
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()} />);
|
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} />);
|
||||||
|
|
||||||
await navigateToGitHubStep();
|
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.getByText(/Settings → Authentication/)).toBeTruthy();
|
||||||
expect(screen.getByRole("button", { name: "Continue without GitHub →" })).toBeTruthy();
|
expect(screen.getByRole("button", { name: "Continue without GitHub →" })).toBeTruthy();
|
||||||
});
|
});
|
||||||
@@ -1002,6 +1002,21 @@ describe("ModelOnboardingModal", () => {
|
|||||||
expect(screen.queryByTestId("onboarding-github-connect-cta")).toBeNull();
|
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", () => {
|
describe("GitHub connection status feedback", () => {
|
||||||
it("shows connected status with success feedback", async () => {
|
it("shows connected status with success feedback", async () => {
|
||||||
mockFetchAuthStatus.mockResolvedValueOnce({
|
mockFetchAuthStatus.mockResolvedValueOnce({
|
||||||
@@ -1017,7 +1032,25 @@ describe("ModelOnboardingModal", () => {
|
|||||||
const badge = screen.getByTestId("github-status-badge");
|
const badge = screen.getByTestId("github-status-badge");
|
||||||
expect(badge).toHaveTextContent("✓ Connected");
|
expect(badge).toHaveTextContent("✓ Connected");
|
||||||
expect(badge).toHaveClass("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 () => {
|
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();
|
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", () => {
|
describe("task creation flow", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user