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:
committed by
gsxdsm
parent
26915af9a3
commit
15fdbd2cef
@@ -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'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>
|
||||
|
||||
@@ -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"));
|
||||
|
||||
Reference in New Issue
Block a user