fix: wire triage recovery into SelfHealingManager and fix duplicate imports
- Pass recoverApprovedTriageTask and getSpecifyingTaskIds callbacks from TriageProcessor to SelfHealingManager in InProcessRuntime. Without these, approved triage tasks stuck in 'specifying' status were never recovered. - Remove duplicate imports in ModelOnboardingModal.tsx that caused TypeScript errors (duplicate identifiers: useEffect, useRef, ToastType, fetchAuthStatus). - Add mock for model-onboarding-state in App.test.tsx.
This commit is contained in:
@@ -3,6 +3,7 @@ import { X, Loader2, CheckCircle, Key, Zap, GitPullRequest, Rocket, Plus } from
|
||||
import type { AuthProvider, ModelInfo } from "../api";
|
||||
import {
|
||||
fetchAuthStatus,
|
||||
fetchGlobalSettings,
|
||||
loginProvider,
|
||||
logoutProvider,
|
||||
saveApiKey,
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
clearOnboardingState,
|
||||
type OnboardingStepId,
|
||||
} from "./model-onboarding-state";
|
||||
import type { SectionId } from "./SettingsModal";
|
||||
|
||||
export interface ModelOnboardingModalProps {
|
||||
/** Called when onboarding is complete or dismissed */
|
||||
@@ -848,7 +850,7 @@ import type { ColorTheme, Column, MergeResult, Task, ThemeMode } from "@fusion/c
|
||||
import type { UseProjectActionsResult } from "../hooks/useProjectActions";
|
||||
import type { ModalManager } from "../hooks/useModalManager";
|
||||
import type { UseTaskHandlersResult } from "../hooks/useTaskHandlers";
|
||||
import type { Toast, ToastType } from "../hooks/useToast";
|
||||
import type { Toast } from "../hooks/useToast";
|
||||
import { ModalErrorBoundary } from "./ErrorBoundary";
|
||||
import { TaskDetailModal } from "./TaskDetailModal";
|
||||
import { SettingsModal } from "./SettingsModal";
|
||||
@@ -1120,10 +1122,6 @@ export function AppModals({
|
||||
);
|
||||
}
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { fetchAuthStatus, fetchGlobalSettings } from "../api";
|
||||
import type { SectionId } from "../components/SettingsModal";
|
||||
|
||||
export interface UseAuthOnboardingOptions {
|
||||
projectId?: string;
|
||||
openModelOnboarding: () => void;
|
||||
|
||||
@@ -97,6 +97,21 @@ vi.mock("../../context/NodeContext", () => ({
|
||||
useNodeContext: vi.fn(() => mockNodeContextValue),
|
||||
}));
|
||||
|
||||
// Mock model-onboarding-state
|
||||
const mockIsOnboardingResumable = vi.fn();
|
||||
const mockGetOnboardingResumeStep = vi.fn();
|
||||
const mockGetOnboardingState = vi.fn();
|
||||
const mockSaveOnboardingState = vi.fn();
|
||||
const mockClearOnboardingState = vi.fn();
|
||||
|
||||
vi.mock("../../components/model-onboarding-state", () => ({
|
||||
isOnboardingResumable: (...args: unknown[]) => mockIsOnboardingResumable(...args),
|
||||
getOnboardingResumeStep: (...args: unknown[]) => mockGetOnboardingResumeStep(...args),
|
||||
getOnboardingState: (...args: unknown[]) => mockGetOnboardingState(...args),
|
||||
saveOnboardingState: (...args: unknown[]) => mockSaveOnboardingState(...args),
|
||||
clearOnboardingState: (...args: unknown[]) => mockClearOnboardingState(...args),
|
||||
}));
|
||||
|
||||
// Mock state holders for dynamic mocking
|
||||
const mockProjectsState = {
|
||||
projects: [] as any[],
|
||||
@@ -193,6 +208,17 @@ beforeEach(() => {
|
||||
mockNodeContextValue.clearCurrentNode.mockClear();
|
||||
// Clear node selection from localStorage to avoid cross-test leakage
|
||||
localStorage.removeItem("fusion-dashboard-current-node");
|
||||
// Clear onboarding state from localStorage
|
||||
localStorage.removeItem("kb-onboarding-state");
|
||||
// Reset onboarding state mocks
|
||||
mockIsOnboardingResumable.mockReset();
|
||||
mockIsOnboardingResumable.mockReturnValue(false);
|
||||
mockGetOnboardingResumeStep.mockReset();
|
||||
mockGetOnboardingResumeStep.mockReturnValue(null);
|
||||
mockGetOnboardingState.mockReset();
|
||||
mockGetOnboardingState.mockReturnValue(null);
|
||||
mockSaveOnboardingState.mockReset();
|
||||
mockClearOnboardingState.mockReset();
|
||||
});
|
||||
|
||||
describe("App deep link handling", () => {
|
||||
@@ -737,6 +763,107 @@ describe("App auto-open Settings on unauthenticated", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("App OnboardingResumeCard", () => {
|
||||
it("shows resume card when onboarding is resumable and modal is closed", async () => {
|
||||
// Configure mock to indicate onboarding is resumable
|
||||
mockIsOnboardingResumable.mockReturnValue(true);
|
||||
mockGetOnboardingResumeStep.mockReturnValue({ currentStep: "github", label: "GitHub" });
|
||||
// Make sure onboarding is complete so the auto-trigger doesn't open the modal
|
||||
(fetchGlobalSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
modelOnboardingComplete: true,
|
||||
});
|
||||
|
||||
render(<App />);
|
||||
|
||||
// Wait for dashboard to finish loading
|
||||
await waitFor(() => expect(screen.queryByRole("status", { name: /loading fusion dashboard/i })).toBeNull(), { timeout: 5000 });
|
||||
|
||||
// Resume card should be visible
|
||||
expect(screen.getByText("Continue where you left off")).toBeTruthy();
|
||||
expect(screen.getByText(/Resume onboarding at/i)).toBeTruthy();
|
||||
expect(screen.getByText("GitHub")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("hides resume card while onboarding modal is open", async () => {
|
||||
// Configure mock to indicate onboarding is resumable
|
||||
mockIsOnboardingResumable.mockReturnValue(true);
|
||||
mockGetOnboardingResumeStep.mockReturnValue({ currentStep: "ai-setup", label: "AI Setup" });
|
||||
// fetchGlobalSettings returns {} by default (modelOnboardingComplete is undefined) - auto-trigger will open modal
|
||||
|
||||
render(<App />);
|
||||
|
||||
// Wait for the auth status check to trigger auto-open
|
||||
await waitFor(() => expect(fetchAuthStatus).toHaveBeenCalled());
|
||||
|
||||
// Wait for onboarding modal to potentially auto-open (since fetchGlobalSettings returns {})
|
||||
// Note: Due to async nature of the hook, we wait briefly for the modal to render if it opens
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
});
|
||||
|
||||
// Resume card should NOT be visible - either modal is open or auto-trigger hasn't fired yet
|
||||
// The key behavior we're testing is that the resume card doesn't show alongside an open modal
|
||||
const modalOpen = await waitFor(() => {
|
||||
try {
|
||||
return screen.queryByText("Set Up AI") !== null;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}).catch(() => false);
|
||||
|
||||
if (modalOpen) {
|
||||
// If modal is open, resume card should be hidden
|
||||
expect(screen.queryByText("Continue where you left off")).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it("hides resume card when persisted state is terminal/complete", async () => {
|
||||
// Configure mock to indicate onboarding is NOT resumable (completed)
|
||||
mockIsOnboardingResumable.mockReturnValue(false);
|
||||
// Make sure onboarding is complete
|
||||
(fetchGlobalSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
modelOnboardingComplete: true,
|
||||
});
|
||||
|
||||
render(<App />);
|
||||
|
||||
// Wait for dashboard to finish loading
|
||||
await waitFor(() => expect(screen.queryByRole("status", { name: /loading fusion dashboard/i })).toBeNull(), { timeout: 5000 });
|
||||
|
||||
// Resume card should NOT be visible
|
||||
expect(screen.queryByText("Continue where you left off")).toBeNull();
|
||||
});
|
||||
|
||||
it("resume button is clickable and triggers resume action", async () => {
|
||||
// Configure mock to indicate onboarding is resumable
|
||||
mockIsOnboardingResumable.mockReturnValue(true);
|
||||
mockGetOnboardingResumeStep.mockReturnValue({ currentStep: "github", label: "GitHub" });
|
||||
// Make sure onboarding is complete so the auto-trigger doesn't open the modal
|
||||
(fetchGlobalSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
modelOnboardingComplete: true,
|
||||
});
|
||||
|
||||
render(<App />);
|
||||
|
||||
// Wait for dashboard to finish loading
|
||||
await waitFor(() => expect(screen.queryByRole("status", { name: /loading fusion dashboard/i })).toBeNull(), { timeout: 5000 });
|
||||
|
||||
// Resume card should be visible with Continue onboarding button
|
||||
const continueButton = screen.getByRole("button", { name: "Continue onboarding" });
|
||||
expect(continueButton).toBeTruthy();
|
||||
|
||||
// Button should be enabled and clickable
|
||||
expect(continueButton).not.toBeDisabled();
|
||||
|
||||
// Click the resume button - the onContinue callback should be triggered
|
||||
fireEvent.click(continueButton);
|
||||
|
||||
// Verify the button was clicked (no error thrown)
|
||||
// The App component passes modalManager.openModelOnboarding as the callback
|
||||
// so clicking should trigger the modal open state
|
||||
});
|
||||
});
|
||||
|
||||
describe("App global pause (hard stop)", () => {
|
||||
it("initializes global pause state from fetchSettings", async () => {
|
||||
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
|
||||
@@ -37,6 +37,17 @@ vi.mock("../CustomModelDropdown", () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock model-onboarding-state
|
||||
const mockGetOnboardingState = vi.fn();
|
||||
const mockSaveOnboardingState = vi.fn();
|
||||
const mockClearOnboardingState = vi.fn();
|
||||
|
||||
vi.mock("../model-onboarding-state", () => ({
|
||||
getOnboardingState: (...args: unknown[]) => mockGetOnboardingState(...args),
|
||||
saveOnboardingState: (...args: unknown[]) => mockSaveOnboardingState(...args),
|
||||
clearOnboardingState: (...args: unknown[]) => mockClearOnboardingState(...args),
|
||||
}));
|
||||
|
||||
const defaultAuthProviders: AuthProvider[] = [
|
||||
{ id: "anthropic", name: "Anthropic", authenticated: false, type: "oauth" },
|
||||
{ id: "openai", name: "OpenAI", authenticated: false, type: "api_key" },
|
||||
@@ -75,6 +86,15 @@ beforeEach(() => {
|
||||
mockLogoutProvider.mockResolvedValue({ success: true });
|
||||
mockSaveApiKey.mockResolvedValue({ success: true });
|
||||
mockClearApiKey.mockResolvedValue({ success: true });
|
||||
// Default to no persisted state (start at ai-setup)
|
||||
mockGetOnboardingState.mockReturnValue(null);
|
||||
mockSaveOnboardingState.mockImplementation(() => {});
|
||||
mockClearOnboardingState.mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up localStorage
|
||||
localStorage.removeItem("kb-onboarding-state");
|
||||
});
|
||||
|
||||
describe("ModelOnboardingModal", () => {
|
||||
|
||||
Reference in New Issue
Block a user