feat(FN-4723): complete Step 3 — gate SettingsModal worktrunk enable

Fusion-Task-Id: FN-4723
Fusion-Task-Lineage: b79b42a0-c65d-4e02-938f-d1ffabc272ee
This commit is contained in:
Fusion (runfusion.ai)
2026-05-16 07:01:56 -07:00
committed by gsxdsm
parent 26915af9a3
commit 15fdbd2cef
2 changed files with 114 additions and 1 deletions

View File

@@ -396,6 +396,7 @@ export function SettingsModal({
}: SettingsModalProps) {
const { confirm } = useConfirm();
const worktrunkInstall = useWorktrunkInstallStatus(projectId);
const worktrunkInstallVerified = worktrunkInstall.status === "installed";
const viewportMode = useViewportMode();
useMobileScrollLock(true);
const { keyboardOverlap, viewportHeight, viewportOffsetTop, keyboardOpen } = useMobileKeyboard({
@@ -1754,7 +1755,7 @@ export function SettingsModal({
worktreeInitCommand: form.worktreeInitCommand?.trim() || undefined,
worktreesDir: form.worktreesDir?.trim() || undefined,
worktrunk: {
enabled: form.worktrunk?.enabled === true,
enabled: worktrunkInstallVerified && form.worktrunk?.enabled === true,
binaryPath: form.worktrunk?.binaryPath?.trim() || undefined,
onFailure: form.worktrunk?.onFailure ?? "fail",
},
@@ -4108,6 +4109,7 @@ export function SettingsModal({
id="worktrunkEnabled"
type="checkbox"
checked={form.worktrunk?.enabled === true}
disabled={!worktrunkInstallVerified && form.worktrunk?.enabled !== true}
onChange={(e) =>
setForm((f) => ({
...f,
@@ -4124,6 +4126,9 @@ export function SettingsModal({
<small>
Disabled by default (opt-in). When enabled, Fusion shells out to <code>worktrunk</code> for worktree create, sync, prune, and remove operations and follows worktrunk&apos;s directory layout.
</small>
{!worktrunkInstallVerified && form.worktrunk?.enabled !== true && (
<small className="settings-muted">Install the worktrunk binary below to enable this integration.</small>
)}
</div>
<div className="form-group">
<label htmlFor="worktrunkBinaryPath">Worktrunk binary path</label>

View File

@@ -2189,7 +2189,115 @@ describe("SettingsModal", () => {
}
});
it.each(["missing", "installing", "pending-approval", "denied", "failed", "installed"])(
"gates worktrunk toggle enablement by install status (%s)",
async (status) => {
mockUseWorktrunkInstallStatus.mockReturnValue({
status,
requestInstall: vi.fn(),
requesting: false,
version: undefined,
installPath: undefined,
pendingApprovalId: undefined,
error: status === "denied" || status === "failed" ? "Denied" : undefined,
});
renderModal({ initialSection: "worktrees" });
await waitForSettingsModalReady();
const enabledToggle = screen.getByLabelText("Enable worktrunk integration") as HTMLInputElement;
expect(enabledToggle.disabled).toBe(status !== "installed");
if (status !== "installed") {
expect(screen.getByText("Install the worktrunk binary below to enable this integration.")).toBeInTheDocument();
} else {
expect(screen.queryByText("Install the worktrunk binary below to enable this integration.")).not.toBeInTheDocument();
}
},
);
it("keeps toggle enabled for recovery when worktrunk is already enabled but install is missing", async () => {
mockFetchSettings.mockResolvedValue({
...defaultSettings,
worktrunk: {
enabled: true,
onFailure: "fail",
},
});
mockFetchSettingsByScope.mockResolvedValue({
global: {},
project: {
worktrunk: {
enabled: true,
onFailure: "fail",
},
},
});
mockUseWorktrunkInstallStatus.mockReturnValue({
status: "missing",
requestInstall: vi.fn(),
requesting: false,
version: undefined,
installPath: undefined,
pendingApprovalId: undefined,
error: undefined,
});
renderModal({ initialSection: "worktrees" });
await waitForSettingsModalReady();
const enabledToggle = screen.getByLabelText("Enable worktrunk integration") as HTMLInputElement;
expect(enabledToggle.disabled).toBe(false);
});
it("clamps worktrunk enabled to false on save when install is not verified", async () => {
mockFetchSettings.mockResolvedValue({
...defaultSettings,
worktrunk: {
enabled: true,
onFailure: "fail",
},
});
mockFetchSettingsByScope.mockResolvedValue({
global: {},
project: {
worktrunk: {
enabled: true,
onFailure: "fail",
},
},
});
mockUseWorktrunkInstallStatus.mockReturnValue({
status: "missing",
requestInstall: vi.fn(),
requesting: false,
version: undefined,
installPath: undefined,
pendingApprovalId: undefined,
error: undefined,
});
renderModal({ initialSection: "worktrees" });
await waitForSettingsModalReady();
await userEvent.click(screen.getByRole("button", { name: "Save" }));
await waitFor(() => expect(mockUpdateSettings).toHaveBeenCalled());
const payload = mockUpdateSettings.mock.calls[0][0] as {
worktrunk?: { enabled?: boolean };
};
expect(payload.worktrunk?.enabled).toBe(false);
});
it.each(["fail", "fallback-native"])("saves worktrunk payload and defaults onFailure on first enable (%s)", async (onFailure) => {
mockUseWorktrunkInstallStatus.mockReturnValue({
status: "installed",
requestInstall: vi.fn(),
requesting: false,
version: "v1.2.3",
installPath: "~/.fusion/bin/worktrunk",
pendingApprovalId: undefined,
error: undefined,
});
renderModal();
await waitForSettingsModalReady();
await userEvent.click(screen.getByText("Worktrees"));