feat(FN-2611): merge fusion/fn-2611 (auto-resolved)

- feat(FN-2611): complete Step 4 — add changeset and docs
- test(FN-2611): complete Step 2 — cover legacy alias cleanup payload
- feat(FN-2611): complete Step 1 — normalize legacy experimental aliases
This commit is contained in:
Fusion
2026-04-26 14:19:18 -07:00
committed by gsxdsm
parent 6a54450288
commit 112ad671f8
3 changed files with 38 additions and 4 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Fix experimental feature save normalization so disabling Dev Server clears the legacy `devServer` alias (`null` delete) alongside canonical `devServerView`, preventing stale nav visibility after save.

View File

@@ -126,16 +126,22 @@ function isExperimentalFeatureEnabled(features: Record<string, boolean>, key: st
); );
} }
function normalizeExperimentalFeaturesForSave(features?: Record<string, boolean>): Record<string, boolean> { function normalizeExperimentalFeaturesForSave(features?: Record<string, boolean>): Record<string, boolean | null> {
if (!features) { if (!features) {
return {}; return {};
} }
const normalized: Record<string, boolean> = {}; const normalized: Record<string, boolean | null> = {};
for (const [key, enabled] of Object.entries(features)) { for (const [key, enabled] of Object.entries(features)) {
normalized[getCanonicalExperimentalFeatureKey(key)] = enabled; normalized[getCanonicalExperimentalFeatureKey(key)] = enabled;
} }
for (const [legacyKey, canonicalKey] of Object.entries(EXPERIMENTAL_FEATURE_LEGACY_ALIASES)) {
if (normalized[canonicalKey] !== undefined && !(legacyKey in normalized)) {
normalized[legacyKey] = null;
}
}
return normalized; return normalized;
} }

View File

@@ -1191,7 +1191,7 @@ describe("SettingsModal", () => {
}); });
}); });
it("normalizes legacy devServer flag to canonical devServerView on save", async () => { it("sends canonical devServerView=false and devServer=null when disabling legacy dev server flag", async () => {
mockFetchSettings.mockResolvedValue({ mockFetchSettings.mockResolvedValue({
...defaultSettings, ...defaultSettings,
experimentalFeatures: { devServer: true }, experimentalFeatures: { devServer: true },
@@ -1204,6 +1204,9 @@ describe("SettingsModal", () => {
const devServerToggle = screen.getByLabelText("Dev Server") as HTMLInputElement; const devServerToggle = screen.getByLabelText("Dev Server") as HTMLInputElement;
expect(devServerToggle).toBeChecked(); expect(devServerToggle).toBeChecked();
await userEvent.click(devServerToggle);
expect(devServerToggle).not.toBeChecked();
await userEvent.click(screen.getByText("Save")); await userEvent.click(screen.getByText("Save"));
await waitFor(() => { await waitFor(() => {
@@ -1211,7 +1214,27 @@ describe("SettingsModal", () => {
}); });
const payload = mockUpdateSettings.mock.calls[0][0]; const payload = mockUpdateSettings.mock.calls[0][0];
expect(payload.experimentalFeatures).toEqual({ devServerView: true }); expect(payload.experimentalFeatures).toEqual({ devServerView: false, devServer: null });
});
it("does not emit legacy alias null deletes when canonical key is absent", async () => {
mockFetchSettings.mockResolvedValue({
...defaultSettings,
experimentalFeatures: { insights: true },
});
renderModal();
await openExperimentalFeaturesSection();
await userEvent.click(screen.getByText("Save"));
await waitFor(() => {
expect(mockUpdateSettings).toHaveBeenCalledTimes(1);
});
const payload = mockUpdateSettings.mock.calls[0][0];
expect(payload.experimentalFeatures).toEqual({ insights: true });
expect(payload.experimentalFeatures.devServer).toBeUndefined(); expect(payload.experimentalFeatures.devServer).toBeUndefined();
}); });