feat(FN-4856): merge fusion/fn-4856

This commit is contained in:
gsxdsm
2026-05-17 01:08:01 -07:00
parent 248c7706ca
commit 7a6a3d0d22
4 changed files with 161 additions and 97 deletions

View File

@@ -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}

View File

@@ -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,

View File

@@ -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,