feat(FN-1658): merge fusion/fn-1658
This commit is contained in:
@@ -394,4 +394,193 @@ describe("SettingsModal", () => {
|
||||
expect(editor.value).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Experimental Features section", () => {
|
||||
it("renders the Experimental Features section in the sidebar", async () => {
|
||||
renderModal();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetchSettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(screen.getByText("Experimental Features")).toBeDefined();
|
||||
});
|
||||
|
||||
it("shows empty state message when no experimental features are configured", async () => {
|
||||
renderModal();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetchSettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByText("Experimental Features"));
|
||||
|
||||
expect(screen.getByText(/No experimental features configured/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows feature flags when experimentalFeatures is set", async () => {
|
||||
mockFetchSettings.mockResolvedValue({
|
||||
...defaultSettings,
|
||||
experimentalFeatures: { "my-feature": true, "another-feature": false },
|
||||
});
|
||||
|
||||
renderModal();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetchSettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByText("Experimental Features"));
|
||||
|
||||
expect(screen.getByText("my-feature")).toBeInTheDocument();
|
||||
expect(screen.getByText("another-feature")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("feature flags are unchecked when value is false", async () => {
|
||||
mockFetchSettings.mockResolvedValue({
|
||||
...defaultSettings,
|
||||
experimentalFeatures: { "my-feature": false },
|
||||
});
|
||||
|
||||
renderModal();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetchSettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByText("Experimental Features"));
|
||||
|
||||
const checkbox = screen.getByLabelText("my-feature") as HTMLInputElement;
|
||||
expect(checkbox.checked).toBe(false);
|
||||
});
|
||||
|
||||
it("feature flags are checked when value is true", async () => {
|
||||
mockFetchSettings.mockResolvedValue({
|
||||
...defaultSettings,
|
||||
experimentalFeatures: { "my-feature": true },
|
||||
});
|
||||
|
||||
renderModal();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetchSettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByText("Experimental Features"));
|
||||
|
||||
const checkbox = screen.getByLabelText("my-feature") as HTMLInputElement;
|
||||
expect(checkbox.checked).toBe(true);
|
||||
});
|
||||
|
||||
it("toggling a feature flag updates the form state", async () => {
|
||||
mockFetchSettings.mockResolvedValue({
|
||||
...defaultSettings,
|
||||
experimentalFeatures: { "my-feature": false },
|
||||
});
|
||||
|
||||
renderModal();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetchSettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByText("Experimental Features"));
|
||||
|
||||
const checkbox = screen.getByLabelText("my-feature") as HTMLInputElement;
|
||||
expect(checkbox.checked).toBe(false);
|
||||
|
||||
// Toggle it
|
||||
await userEvent.click(checkbox);
|
||||
expect(checkbox.checked).toBe(true);
|
||||
});
|
||||
|
||||
it("saving with toggled feature flag includes experimentalFeatures in payload", async () => {
|
||||
mockFetchSettings.mockResolvedValue({
|
||||
...defaultSettings,
|
||||
experimentalFeatures: { "my-feature": false },
|
||||
});
|
||||
|
||||
renderModal();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetchSettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByText("Experimental Features"));
|
||||
|
||||
// Toggle the feature
|
||||
const checkbox = screen.getByLabelText("my-feature") as HTMLInputElement;
|
||||
await userEvent.click(checkbox);
|
||||
|
||||
// Save
|
||||
await userEvent.click(screen.getByText("Save"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateSettings).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const payload = mockUpdateSettings.mock.calls[0][0];
|
||||
expect(payload.experimentalFeatures).toEqual({ "my-feature": true });
|
||||
});
|
||||
|
||||
it("shows project scope banner in Experimental Features section", async () => {
|
||||
renderModal();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetchSettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByText("Experimental Features"));
|
||||
|
||||
// Should show project scope indicator
|
||||
expect(screen.getByText(/only affect this project/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("handles undefined experimentalFeatures (falls back to empty)", async () => {
|
||||
mockFetchSettings.mockResolvedValue({
|
||||
...defaultSettings,
|
||||
experimentalFeatures: undefined,
|
||||
});
|
||||
|
||||
renderModal();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetchSettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByText("Experimental Features"));
|
||||
|
||||
// Should show empty state since undefined falls back to {}
|
||||
expect(screen.getByText(/No experimental features configured/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("saves experimentalFeatures with multiple toggled flags", async () => {
|
||||
mockFetchSettings.mockResolvedValue({
|
||||
...defaultSettings,
|
||||
experimentalFeatures: { "feature-a": true, "feature-b": false },
|
||||
});
|
||||
|
||||
renderModal();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetchSettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByText("Experimental Features"));
|
||||
|
||||
// Toggle feature-b to true
|
||||
const checkboxB = screen.getByLabelText("feature-b") as HTMLInputElement;
|
||||
await userEvent.click(checkboxB);
|
||||
|
||||
// Save
|
||||
await userEvent.click(screen.getByText("Save"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateSettings).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const payload = mockUpdateSettings.mock.calls[0][0];
|
||||
expect(payload.experimentalFeatures).toEqual({ "feature-a": true, "feature-b": true });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -70,6 +70,7 @@ const SETTINGS_SECTIONS: SettingsSection[] = [
|
||||
{ id: "commands", label: "Commands", scope: "project" },
|
||||
{ id: "merge", label: "Merge", scope: "project" },
|
||||
{ id: "memory", label: "Memory", scope: "project" },
|
||||
{ id: "experimental", label: "Experimental Features", scope: "project" },
|
||||
{ id: "prompts", label: "Prompts", scope: "project" },
|
||||
{ id: "backups", label: "Backups", scope: "project" },
|
||||
{ id: "plugins", label: "Plugins", scope: "project" },
|
||||
@@ -1796,6 +1797,56 @@ export function SettingsModal({
|
||||
</>
|
||||
);
|
||||
}
|
||||
case "experimental": {
|
||||
const experimentalFeatures = form.experimentalFeatures ?? {};
|
||||
const featureFlags = Object.entries(experimentalFeatures).sort(([a], [b]) => a.localeCompare(b));
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderScopeBanner()}
|
||||
<h4 className="settings-section-heading">Experimental Features</h4>
|
||||
<div className="form-group">
|
||||
<small>
|
||||
Experimental features are early capabilities that are not yet fully stable.
|
||||
Enable them to test new functionality, but be aware they may change or be removed.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
{featureFlags.length === 0 ? (
|
||||
<div className="form-group">
|
||||
<small className="settings-muted">
|
||||
No experimental features configured. Features will appear here once added by the system.
|
||||
</small>
|
||||
</div>
|
||||
) : (
|
||||
<div className="form-group">
|
||||
<label>Feature Flags</label>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "var(--space-sm)" }}>
|
||||
{featureFlags.map(([key, enabled]) => (
|
||||
<label key={key} htmlFor={`experimental-${key}`} className="checkbox-label">
|
||||
<input
|
||||
id={`experimental-${key}`}
|
||||
type="checkbox"
|
||||
checked={enabled}
|
||||
onChange={(e) => {
|
||||
setForm((f) => ({
|
||||
...f,
|
||||
experimentalFeatures: {
|
||||
...(f.experimentalFeatures ?? {}),
|
||||
[key]: e.target.checked,
|
||||
},
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
<span>{key}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
case "backups":
|
||||
return (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user