feat(FN-4723): complete Step 4 — gate CLI 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:07:56 -07:00
committed by gsxdsm
parent 15fdbd2cef
commit 1b93e1a8b1
2 changed files with 78 additions and 1 deletions

View File

@@ -25,6 +25,11 @@ vi.mock("@fusion/core", () => {
return {
GlobalSettingsStore: vi.fn(),
DEFAULT_SETTINGS,
resolveWorktrunkSettings: (globalValue: any, projectValue: any) => ({
enabled: projectValue?.enabled ?? globalValue?.enabled ?? false,
...(projectValue?.binaryPath ?? globalValue?.binaryPath ? { binaryPath: projectValue?.binaryPath ?? globalValue?.binaryPath } : {}),
onFailure: projectValue?.onFailure ?? globalValue?.onFailure ?? "fail",
}),
};
});
@@ -32,6 +37,16 @@ vi.mock("../../project-context.js", () => ({
resolveProject: vi.fn(),
}));
const { resolveWorktrunkBinaryMock, probeWorktrunkMock } = vi.hoisted(() => ({
resolveWorktrunkBinaryMock: vi.fn(),
probeWorktrunkMock: vi.fn(),
}));
vi.mock("@fusion/engine", () => ({
resolveWorktrunkBinary: resolveWorktrunkBinaryMock,
probeWorktrunk: probeWorktrunkMock,
}));
import { GlobalSettingsStore, DEFAULT_SETTINGS } from "@fusion/core";
import { resolveProject } from "../../project-context.js";
import { runSettingsShow, runSettingsSet, parseValue, VALID_SETTINGS } from "../settings.js";
@@ -47,6 +62,8 @@ describe("settings commands", () => {
beforeEach(() => {
vi.clearAllMocks();
resolveWorktrunkBinaryMock.mockResolvedValue({ binaryPath: "/usr/local/bin/worktrunk" });
probeWorktrunkMock.mockResolvedValue({ ok: true, version: "1.0.0" });
logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
exitSpy = vi.spyOn(process, "exit").mockImplementation((code?: string | number | null) => {
@@ -265,6 +282,38 @@ describe("settings commands", () => {
expect(output).toContain("Max Parallel Steps");
});
it("rejects enabling worktrunk when binary is not verified", async () => {
const updateSettings = vi.fn();
const getSettings = vi.fn().mockResolvedValue(makeSettings({ worktrunk: { enabled: false, onFailure: "fail" } }));
(GlobalSettingsStore as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => ({
init: vi.fn().mockResolvedValue(undefined),
updateSettings,
getSettings,
}));
resolveWorktrunkBinaryMock.mockRejectedValueOnce(new Error("missing"));
await expect(runSettingsSet("worktrunk.enabled", "true")).rejects.toThrow("process.exit:1");
expect(updateSettings).not.toHaveBeenCalled();
expect(errorSpy).toHaveBeenCalledWith(
expect.stringContaining("worktrunk.enabled cannot be set to true until the binary is installed and verified"),
);
});
it("allows disabling worktrunk without binary verification", async () => {
const updateSettings = vi.fn().mockResolvedValue(makeSettings({ worktrunk: { enabled: false, onFailure: "fail" } }));
const getSettings = vi.fn().mockResolvedValue(makeSettings({ worktrunk: { enabled: true, onFailure: "fail" } }));
(GlobalSettingsStore as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => ({
init: vi.fn().mockResolvedValue(undefined),
updateSettings,
getSettings,
}));
await runSettingsSet("worktrunk.enabled", "false");
expect(updateSettings).toHaveBeenCalledWith({ worktrunk: { enabled: false, onFailure: "fail" } });
expect(resolveWorktrunkBinaryMock).not.toHaveBeenCalled();
expect(probeWorktrunkMock).not.toHaveBeenCalled();
});
it("runSettingsSet supports worktrunk dotted keys in global scope", async () => {
const updateSettings = vi.fn().mockResolvedValue(makeSettings({
worktrunk: { enabled: true, binaryPath: "/usr/local/bin/worktrunk", onFailure: "fail" },

View File

@@ -1,4 +1,11 @@
import { GlobalSettingsStore, type Settings, type GlobalSettings, DEFAULT_SETTINGS } from "@fusion/core";
import {
GlobalSettingsStore,
type Settings,
type GlobalSettings,
DEFAULT_SETTINGS,
resolveWorktrunkSettings,
} from "@fusion/core";
import { probeWorktrunk, resolveWorktrunkBinary } from "@fusion/engine";
import { resolveProject } from "../project-context.js";
// Settings that can be updated via CLI
@@ -350,6 +357,27 @@ export async function runSettingsSet(key: string, value: string, projectName?: s
return;
}
if (key === "worktrunk.enabled" && parsedValue === true) {
const currentWorktrunk = store
? await store.getSettingsByScope().then((scoped) =>
resolveWorktrunkSettings(scoped.global?.worktrunk, scoped.project?.worktrunk),
)
: resolveWorktrunkSettings((await globalStore!.getSettings()).worktrunk, undefined);
const nextWorktrunk = { ...currentWorktrunk, enabled: true };
try {
const resolved = await resolveWorktrunkBinary({ settings: nextWorktrunk });
const probe = await probeWorktrunk(resolved.binaryPath);
if (!probe.ok) {
throw new Error("worktrunk probe failed");
}
} catch {
throw new Error(
"worktrunk.enabled cannot be set to true until the binary is installed and verified. Install it from Settings → Worktrunk integration (or /api/worktrunk/install-request) first.",
);
}
}
if (store) {
if (key.includes(".")) {
const currentSettings = await store.getSettingsByScope();