FN-7964: fix Project Models workflow model lane persistence

Keep pending Project Models workflow lane overrides registered across section navigation so primary Settings Save still flushes them.

- Stop clearing the workflow-lane saver ref on Project Models unmount
- Add regression coverage for save-after-section-nav and reload override display
- Add patch changeset for the dashboard fix

Files changed:
 .changeset/fn-7964-project-models-workflow-lanes.md                            |  7 +++++++
 packages/dashboard/app/components/SettingsModal.tsx                            |  8 +++++++-
 packages/dashboard/app/components/__tests__/SettingsModal.models-auth.test.tsx | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 72 insertions(+), 1 deletion(-)

Fusion-Task-Id: FN-7964

Fusion-Task-Lineage: ed4e6268-d5b7-44ba-bbcc-2d44b301403e

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-15 12:33:06 -07:00
parent de25e32eac
commit a8b6387420
3 changed files with 72 additions and 1 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Fix Project Models workflow model lane saves.
category: fix
dev: Keeps pending default-workflow model lane overrides registered for Settings Save after section navigation.

View File

@@ -999,7 +999,13 @@ export function SettingsModal({
const settingsContentRef = useRef<HTMLDivElement>(null);
const workflowLaneSaverRef = useRef<SectionSaveHandler | null>(null);
const registerWorkflowLaneSaver = useCallback((saver: SectionSaveHandler | null) => {
workflowLaneSaverRef.current = saver;
/*
FNXC:ProjectModelsWorkflowLanes 2026-07-14-09:07:
Project Models workflow lane edits are workflow setting-values, not normal project settings. Keep the latest saver registered across section unmounts so the primary Settings Save still flushes project-scoped workflow overrides when operators navigate away before saving.
*/
if (saver) {
workflowLaneSaverRef.current = saver;
}
}, []);
// Modal-only: persist user-resized dialog dimensions. Embedded view fills its host and is not resizable.
useModalResizePersist(modalRef, resizePersistEnabled, "fusion:settings-modal-size");

View File

@@ -482,6 +482,34 @@ describe("SettingsModal", () => {
expect(onClose).toHaveBeenCalled();
});
it("renders saved workflow model lane values as project overrides after reload", async () => {
const expectedPatch = { planningProvider: "openai", planningModelId: "gpt-4o" };
mockUpdateWorkflowSettingValues.mockResolvedValue({
stored: expectedPatch,
effective: expectedPatch,
orphaned: [],
});
await setupWorkflowModelLaneTest();
await settingsModalUser.click(screen.getByLabelText("Plan/Triage Model"));
await settingsModalUser.click(await screen.findByText("GPT-4o"));
await settingsModalUser.click(screen.getByRole("button", { name: "Save" }));
await waitFor(() => {
expect(mockUpdateWorkflowSettingValues).toHaveBeenCalledWith("workflow-custom", expectedPatch, "proj-1");
});
cleanup();
mockFetchWorkflow.mockClear();
mockFetchWorkflowSettingValues.mockClear();
mockUpdateWorkflowSettingValues.mockClear();
await setupWorkflowModelLaneTest({ stored: expectedPatch, effective: expectedPatch });
const lane = screen.getByTestId("workflow-model-lane-planning");
expect(within(lane).getByText("Override (Project)")).toBeInTheDocument();
expect(within(lane).getByText("GPT-4o")).toBeInTheDocument();
expect(screen.getByTestId("workflow-model-lane-execution")).toHaveTextContent("Inherited (Workflow)");
});
it("renders fallback workflow model lanes only when the default workflow declares them", async () => {
await setupWorkflowModelLaneTest();
@@ -566,6 +594,36 @@ describe("SettingsModal", () => {
expect(mockUpdateWorkflowSettingValues).not.toHaveBeenCalled();
});
it("preserves pending workflow lane edits when Project Models unmounts before primary Save", async () => {
const expectedPatch = { planningProvider: "openai", planningModelId: "gpt-4o" };
mockUpdateWorkflowSettingValues.mockResolvedValue({
stored: expectedPatch,
effective: expectedPatch,
orphaned: [],
});
const onClose = vi.fn();
await setupWorkflowModelLaneTest({ renderProps: { onClose } });
await settingsModalUser.click(screen.getByLabelText("Plan/Triage Model"));
await settingsModalUser.click(await screen.findByText("GPT-4o"));
expect(within(screen.getByTestId("workflow-model-lane-planning")).getByText("GPT-4o")).toBeInTheDocument();
await settingsModalUser.click(screen.getByRole("button", { name: "General" }));
await waitFor(() => {
expect(screen.queryByTestId("workflow-model-lane-planning")).not.toBeInTheDocument();
});
await settingsModalUser.click(screen.getByRole("button", { name: "Save" }));
await waitFor(() => {
expect(mockUpdateWorkflowSettingValues).toHaveBeenCalledWith(
"workflow-custom",
expectedPatch,
"proj-1",
);
});
expect(onClose).toHaveBeenCalled();
});
it("resets workflow model lanes by sending null patches from the primary Settings Save", async () => {
await setupWorkflowModelLaneTest({
stored: { executionProvider: "anthropic", executionModelId: "claude-sonnet-4-5" },