feat(FN-4856): merge fusion/fn-4856
This commit is contained in:
@@ -179,56 +179,65 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
// If onCreate is not provided, the component is disabled
|
||||
const isDisabled = !onCreate;
|
||||
|
||||
// Fetch models and settings if not provided by parent
|
||||
// Fetch models if not provided by parent
|
||||
useEffect(() => {
|
||||
if (availableModels) {
|
||||
setLoadedModels(availableModels);
|
||||
setModelsLoading(false);
|
||||
setModelsError(null);
|
||||
} else {
|
||||
let cancelled = false;
|
||||
setModelsLoading(true);
|
||||
setModelsError(null);
|
||||
fetchModels()
|
||||
.then((response) => {
|
||||
if (!cancelled) {
|
||||
setLoadedModels(response.models);
|
||||
// Only set internal favorites when parent doesn't manage them
|
||||
if (!parentFavoriteProviders) {
|
||||
setFavoriteProviders(response.favoriteProviders);
|
||||
}
|
||||
if (!parentFavoriteModels) {
|
||||
setFavoriteModels(response.favoriteModels);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (!cancelled) {
|
||||
setModelsError(getErrorMessage(err) || "Failed to load models");
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) {
|
||||
setModelsLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Also fetch settings for presets
|
||||
fetchSettings(projectId)
|
||||
.then((nextSettings) => {
|
||||
if (!cancelled) {
|
||||
setSettings(nextSettings);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Silently ignore settings fetch failure
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
return;
|
||||
}
|
||||
}, [availableModels, parentFavoriteProviders, parentFavoriteModels, projectId]);
|
||||
|
||||
let cancelled = false;
|
||||
setModelsLoading(true);
|
||||
setModelsError(null);
|
||||
fetchModels()
|
||||
.then((response) => {
|
||||
if (!cancelled) {
|
||||
setLoadedModels(response.models);
|
||||
// Only set internal favorites when parent doesn't manage them
|
||||
if (!parentFavoriteProviders) {
|
||||
setFavoriteProviders(response.favoriteProviders);
|
||||
}
|
||||
if (!parentFavoriteModels) {
|
||||
setFavoriteModels(response.favoriteModels);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (!cancelled) {
|
||||
setModelsError(getErrorMessage(err) || "Failed to load models");
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) {
|
||||
setModelsLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [availableModels, parentFavoriteProviders, parentFavoriteModels]);
|
||||
|
||||
// Settings always drive toggle/preset behavior, regardless of model source.
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
fetchSettings(projectId)
|
||||
.then((nextSettings) => {
|
||||
if (!cancelled) {
|
||||
setSettings(nextSettings);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Silently ignore settings fetch failure
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [projectId]);
|
||||
|
||||
const executorSelectionValue = getModelSelectionValue(executorProvider, executorModelId);
|
||||
const validatorSelectionValue = getModelSelectionValue(validatorProvider, validatorModelId);
|
||||
@@ -1898,8 +1907,13 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-sm ${effectiveGithubTracking ? "btn-primary" : ""}`}
|
||||
onClick={() => setGithubTrackingOverride(!effectiveGithubTracking)}
|
||||
className={`btn btn-sm ${githubTrackingProjectEnabled && effectiveGithubTracking ? "btn-primary" : ""}`}
|
||||
onClick={() => {
|
||||
if (!githubTrackingProjectEnabled) {
|
||||
return;
|
||||
}
|
||||
setGithubTrackingOverride((prev) => (prev ?? true) ? false : true);
|
||||
}}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
disabled={!githubTrackingProjectEnabled}
|
||||
aria-pressed={effectiveGithubTracking}
|
||||
|
||||
@@ -1659,7 +1659,7 @@ describe("App auto-open Settings on unauthenticated", () => {
|
||||
await waitFor(() => expect(fetchAuthStatus).toHaveBeenCalled());
|
||||
|
||||
// Settings modal should NOT be open
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalledTimes(1));
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
expect(screen.queryByText("Settings")).toBeNull();
|
||||
|
||||
// Onboarding modal should NOT be open
|
||||
@@ -1681,7 +1681,7 @@ describe("App auto-open Settings on unauthenticated", () => {
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => expect(fetchAuthStatus).toHaveBeenCalled());
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalledTimes(1));
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
|
||||
expect(screen.queryByText("Settings")).toBeNull();
|
||||
expect(screen.queryByText("Set Up AI")).toBeNull();
|
||||
@@ -1715,7 +1715,7 @@ describe("App auto-open Settings on unauthenticated", () => {
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => expect(fetchAuthStatus).toHaveBeenCalled());
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalledTimes(1));
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
|
||||
// Settings modal should NOT be open
|
||||
expect(screen.queryByText("Settings")).toBeNull();
|
||||
@@ -1746,7 +1746,7 @@ describe("App auto-open Settings on unauthenticated", () => {
|
||||
fireEvent.click(settingsButton);
|
||||
|
||||
// Settings should open with Authentication section (first/default)
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalledTimes(2));
|
||||
await waitFor(() => expect(fetchSettings.mock.calls.length).toBeGreaterThanOrEqual(2));
|
||||
await waitFor(() => expect(fetchAuthStatus).toHaveBeenCalled());
|
||||
|
||||
// Authentication section content should be visible (providers listed)
|
||||
@@ -1955,7 +1955,7 @@ describe("App engine pause (soft pause)", () => {
|
||||
describe("App view switching", () => {
|
||||
it("opens research view from overflow and persists view selection", async () => {
|
||||
localStorage.setItem("kb-dashboard-view-mode", "project");
|
||||
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
...defaultSettings,
|
||||
experimentalFeatures: {
|
||||
...defaultSettings.experimentalFeatures,
|
||||
@@ -2003,7 +2003,7 @@ describe("App view switching", () => {
|
||||
|
||||
it("does not expose research navigation when research feature is disabled", async () => {
|
||||
localStorage.setItem("kb-dashboard-view-mode", "project");
|
||||
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
...defaultSettings,
|
||||
experimentalFeatures: {
|
||||
...defaultSettings.experimentalFeatures,
|
||||
@@ -2026,7 +2026,7 @@ describe("App view switching", () => {
|
||||
it("initializes research view from persisted task-view when feature-enabled", async () => {
|
||||
localStorage.setItem("kb-dashboard-view-mode", "project");
|
||||
localStorage.setItem(taskViewStorageKey(), "research");
|
||||
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
...defaultSettings,
|
||||
experimentalFeatures: {
|
||||
...defaultSettings.experimentalFeatures,
|
||||
@@ -2047,7 +2047,7 @@ describe("App view switching", () => {
|
||||
it("falls back to board when research view is feature-disabled", async () => {
|
||||
localStorage.setItem("kb-dashboard-view-mode", "project");
|
||||
localStorage.setItem(taskViewStorageKey(), "research");
|
||||
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
...defaultSettings,
|
||||
experimentalFeatures: {
|
||||
...defaultSettings.experimentalFeatures,
|
||||
@@ -2069,7 +2069,7 @@ describe("App view switching", () => {
|
||||
it("falls back to board when evals view is feature-disabled", async () => {
|
||||
localStorage.setItem("kb-dashboard-view-mode", "project");
|
||||
localStorage.setItem(taskViewStorageKey(), "evals");
|
||||
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
...defaultSettings,
|
||||
experimentalFeatures: {
|
||||
...defaultSettings.experimentalFeatures,
|
||||
@@ -2308,7 +2308,7 @@ describe("App view switching", () => {
|
||||
|
||||
it("opens planning mode when TodoView triggers planning from todo item", async () => {
|
||||
localStorage.setItem("kb-dashboard-view-mode", "project");
|
||||
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
...defaultSettings,
|
||||
experimentalFeatures: {
|
||||
...defaultSettings.experimentalFeatures,
|
||||
|
||||
@@ -759,58 +759,69 @@ describe("QuickEntryBox", () => {
|
||||
expect(fastToggle.getAttribute("aria-pressed")).toBe("false");
|
||||
});
|
||||
|
||||
it("renders GitHub toggle off by default when project setting is disabled", async () => {
|
||||
vi.mocked(fetchSettings).mockResolvedValueOnce({
|
||||
githubTrackingEnabledByDefault: false,
|
||||
} as any);
|
||||
renderQuickEntryBox({ availableModels: undefined });
|
||||
it("keeps GitHub toggle disabled while project settings are still loading", async () => {
|
||||
vi.mocked(fetchSettings).mockReturnValueOnce(new Promise(() => undefined));
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
|
||||
const githubToggle = await screen.findByTestId("quick-entry-github-toggle");
|
||||
expect(githubToggle.getAttribute("aria-pressed")).toBe("false");
|
||||
expect(githubToggle).toBeDisabled();
|
||||
expect(githubToggle.getAttribute("aria-disabled")).toBe("true");
|
||||
expect(githubToggle.className.includes("btn-primary")).toBe(false);
|
||||
expect(githubToggle).toHaveAttribute("aria-disabled", "true");
|
||||
expect(githubToggle).toHaveAttribute("aria-pressed", "false");
|
||||
expect(githubToggle.classList.contains("btn-primary")).toBe(false);
|
||||
});
|
||||
|
||||
it("renders GitHub toggle on by default when project setting is enabled", async () => {
|
||||
it("renders GitHub toggle disabled when project setting is disabled", async () => {
|
||||
vi.mocked(fetchSettings).mockResolvedValueOnce({
|
||||
githubTrackingEnabledByDefault: false,
|
||||
} as any);
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
|
||||
const githubToggle = await screen.findByTestId("quick-entry-github-toggle");
|
||||
expect(githubToggle).toBeDisabled();
|
||||
expect(githubToggle).toHaveAttribute("aria-disabled", "true");
|
||||
expect(githubToggle).toHaveAttribute("aria-pressed", "false");
|
||||
expect(githubToggle.classList.contains("btn-primary")).toBe(false);
|
||||
});
|
||||
|
||||
it("renders GitHub toggle enabled and active when project setting is enabled", async () => {
|
||||
vi.mocked(fetchSettings).mockResolvedValueOnce({
|
||||
githubTrackingEnabledByDefault: true,
|
||||
} as any);
|
||||
renderQuickEntryBox({ availableModels: undefined });
|
||||
expandQuickEntry();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("quick-entry-github-toggle").getAttribute("aria-pressed")).toBe("true");
|
||||
});
|
||||
expect(screen.getByTestId("quick-entry-github-toggle").className.includes("btn-primary")).toBe(true);
|
||||
});
|
||||
|
||||
it("flips GitHub toggle from both project-default directions", async () => {
|
||||
vi.mocked(fetchSettings).mockResolvedValueOnce({
|
||||
githubTrackingEnabledByDefault: false,
|
||||
} as any);
|
||||
const { unmount } = renderQuickEntryBox({ availableModels: undefined });
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
|
||||
const githubToggle = await screen.findByTestId("quick-entry-github-toggle");
|
||||
expect(githubToggle.getAttribute("aria-pressed")).toBe("false");
|
||||
expect(githubToggle).toBeDisabled();
|
||||
await waitFor(() => {
|
||||
expect(githubToggle).not.toBeDisabled();
|
||||
});
|
||||
expect(githubToggle).toHaveAttribute("aria-pressed", "true");
|
||||
expect(githubToggle.classList.contains("btn-primary")).toBe(true);
|
||||
});
|
||||
|
||||
it("flips GitHub toggle pressed state deterministically when enabled", async () => {
|
||||
vi.mocked(fetchSettings).mockResolvedValueOnce({
|
||||
githubTrackingEnabledByDefault: true,
|
||||
} as any);
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
|
||||
const githubToggle = await screen.findByTestId("quick-entry-github-toggle");
|
||||
await waitFor(() => {
|
||||
expect(githubToggle).not.toBeDisabled();
|
||||
});
|
||||
|
||||
expect(githubToggle).toHaveAttribute("aria-pressed", "true");
|
||||
expect(githubToggle.classList.contains("btn-primary")).toBe(true);
|
||||
|
||||
fireEvent.click(githubToggle);
|
||||
expect(githubToggle.getAttribute("aria-pressed")).toBe("false");
|
||||
unmount();
|
||||
expect(githubToggle).toHaveAttribute("aria-pressed", "false");
|
||||
expect(githubToggle.classList.contains("btn-primary")).toBe(false);
|
||||
|
||||
vi.mocked(fetchSettings).mockResolvedValueOnce({
|
||||
githubTrackingEnabledByDefault: true,
|
||||
} as any);
|
||||
renderQuickEntryBox({ availableModels: undefined });
|
||||
expandQuickEntry();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("quick-entry-github-toggle").getAttribute("aria-pressed")).toBe("true");
|
||||
});
|
||||
fireEvent.click(screen.getByTestId("quick-entry-github-toggle"));
|
||||
expect(screen.getByTestId("quick-entry-github-toggle").getAttribute("aria-pressed")).toBe("false");
|
||||
fireEvent.click(githubToggle);
|
||||
expect(githubToggle).toHaveAttribute("aria-pressed", "true");
|
||||
expect(githubToggle.classList.contains("btn-primary")).toBe(true);
|
||||
});
|
||||
|
||||
it.each(["Enter", "Save"] as const)("submits executionMode=fast when Fast is active via %s", async (submitPath) => {
|
||||
@@ -854,10 +865,17 @@ describe("QuickEntryBox", () => {
|
||||
});
|
||||
|
||||
it("omits githubTracking when toggle is untouched", async () => {
|
||||
vi.mocked(fetchSettings).mockResolvedValueOnce({
|
||||
githubTrackingEnabledByDefault: true,
|
||||
} as any);
|
||||
const { props } = renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("quick-entry-github-toggle")).not.toBeDisabled();
|
||||
});
|
||||
|
||||
fireEvent.change(textarea, { target: { value: "No github override" } });
|
||||
fireEvent.keyDown(textarea, { key: "Enter" });
|
||||
|
||||
@@ -895,15 +913,16 @@ describe("QuickEntryBox", () => {
|
||||
vi.mocked(fetchSettings).mockResolvedValueOnce({
|
||||
githubTrackingEnabledByDefault: true,
|
||||
} as any);
|
||||
const { props } = renderQuickEntryBox({ availableModels: undefined });
|
||||
const { props } = renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
const githubToggle = await screen.findByTestId("quick-entry-github-toggle");
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("quick-entry-github-toggle").getAttribute("aria-pressed")).toBe("true");
|
||||
expect(githubToggle).not.toBeDisabled();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByTestId("quick-entry-github-toggle"));
|
||||
fireEvent.click(githubToggle);
|
||||
fireEvent.change(textarea, { target: { value: "Disable github tracking override" } });
|
||||
fireEvent.keyDown(textarea, { key: "Enter" });
|
||||
|
||||
@@ -915,6 +934,32 @@ describe("QuickEntryBox", () => {
|
||||
expect(payload.githubTracking).toEqual({ enabled: false });
|
||||
});
|
||||
|
||||
it("submits githubTracking enabled=true after opt-out then opt-in", async () => {
|
||||
vi.mocked(fetchSettings).mockResolvedValueOnce({
|
||||
githubTrackingEnabledByDefault: true,
|
||||
} as any);
|
||||
const { props } = renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
const githubToggle = await screen.findByTestId("quick-entry-github-toggle");
|
||||
await waitFor(() => {
|
||||
expect(githubToggle).not.toBeDisabled();
|
||||
});
|
||||
|
||||
fireEvent.click(githubToggle);
|
||||
fireEvent.click(githubToggle);
|
||||
fireEvent.change(textarea, { target: { value: "Re-enable github tracking override" } });
|
||||
fireEvent.keyDown(textarea, { key: "Enter" });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(props.onCreate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const payload = props.onCreate.mock.calls[0]?.[0];
|
||||
expect(payload.githubTracking).toEqual({ enabled: true });
|
||||
});
|
||||
|
||||
it("shows disabled GitHub tracking guidance label when project setting is disabled", async () => {
|
||||
vi.mocked(fetchSettings).mockResolvedValueOnce({
|
||||
githubTrackingEnabledByDefault: false,
|
||||
|
||||
Reference in New Issue
Block a user