feat(FN-4621): complete Step 3 — add worktrunk resolver and validator

Fusion-Task-Id: FN-4621
Fusion-Task-Lineage: fc1b0b13-9052-4378-86e3-3634d6c9db4e
This commit is contained in:
Fusion
2026-05-15 10:53:53 -07:00
committed by gsxdsm
parent d2066cfa58
commit c2260db363
2 changed files with 130 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
import { describe, expect, it } from "vitest";
import {
resolveWorktrunkSettings,
validateWorktrunkSettings,
} from "../worktrunk-settings.js";
describe("worktrunk-settings", () => {
it("returns defaults when settings are empty", () => {
expect(resolveWorktrunkSettings({}, {})).toEqual({
enabled: false,
onFailure: "fail",
});
});
it("lets project enabled override global enabled", () => {
expect(resolveWorktrunkSettings({ enabled: false }, { enabled: true })).toEqual({
enabled: true,
onFailure: "fail",
});
});
it("retains global binaryPath when project only sets enabled", () => {
expect(
resolveWorktrunkSettings(
{ enabled: false, binaryPath: "/x", onFailure: "fail" },
{ enabled: true },
),
).toEqual({
enabled: true,
binaryPath: "/x",
onFailure: "fail",
});
});
it("falls back to defaults when both scopes are undefined", () => {
expect(resolveWorktrunkSettings(undefined, undefined)).toEqual({
enabled: false,
onFailure: "fail",
});
});
it("validator rejects invalid values", () => {
expect(() => validateWorktrunkSettings({ onFailure: "ignore" })).toThrow(
"worktrunk.onFailure must be one of",
);
expect(() => validateWorktrunkSettings({ binaryPath: 123 })).toThrow(
"worktrunk.binaryPath must be a string when set",
);
expect(() => validateWorktrunkSettings("oops")).toThrow("worktrunk settings must be an object");
});
it("validator drops unknown keys", () => {
expect(validateWorktrunkSettings({ enabled: true, unknown: "x" })).toEqual({ enabled: true });
});
});

View File

@@ -0,0 +1,74 @@
import type { WorktrunkOnFailure, WorktrunkSettings } from "./types.js";
const WORKTRUNK_ON_FAILURE_VALUES: readonly WorktrunkOnFailure[] = [
"fail",
"fallback-native",
] as const;
export const DEFAULT_WORKTRUNK_SETTINGS: Required<Pick<WorktrunkSettings, "enabled" | "onFailure">> &
Pick<WorktrunkSettings, "binaryPath"> = {
enabled: false,
binaryPath: undefined,
onFailure: "fail",
};
/** Merge global ← project field-by-field so partial project overrides don't
* wipe global values. A project value of `undefined` is treated as "inherit
* from global". */
export function resolveWorktrunkSettings(
globalValue: WorktrunkSettings | undefined,
projectValue: WorktrunkSettings | undefined,
): WorktrunkSettings {
const enabled = projectValue?.enabled ?? globalValue?.enabled ?? DEFAULT_WORKTRUNK_SETTINGS.enabled;
const binaryPath =
projectValue?.binaryPath ?? globalValue?.binaryPath ?? DEFAULT_WORKTRUNK_SETTINGS.binaryPath;
const onFailure =
projectValue?.onFailure ?? globalValue?.onFailure ?? DEFAULT_WORKTRUNK_SETTINGS.onFailure;
return {
enabled,
...(binaryPath !== undefined ? { binaryPath } : {}),
onFailure,
};
}
/** Strict validator used by GlobalSettingsStore writes and CLI parsing. */
export function validateWorktrunkSettings(value: unknown): WorktrunkSettings {
if (value === undefined || value === null) {
return {};
}
if (typeof value !== "object" || Array.isArray(value)) {
throw new Error("worktrunk settings must be an object");
}
const input = value as Record<string, unknown>;
const validated: WorktrunkSettings = {};
if (input.enabled !== undefined) {
validated.enabled = Boolean(input.enabled);
}
if (input.binaryPath !== undefined) {
if (typeof input.binaryPath !== "string") {
throw new Error("worktrunk.binaryPath must be a string when set");
}
const trimmed = input.binaryPath.trim();
if (trimmed.length === 0) {
throw new Error("worktrunk.binaryPath cannot be empty");
}
validated.binaryPath = trimmed;
}
if (input.onFailure !== undefined) {
if (typeof input.onFailure !== "string") {
throw new Error("worktrunk.onFailure must be a string when set");
}
if (!(WORKTRUNK_ON_FAILURE_VALUES as readonly string[]).includes(input.onFailure)) {
throw new Error('worktrunk.onFailure must be one of: "fail", "fallback-native"');
}
validated.onFailure = input.onFailure as WorktrunkOnFailure;
}
return validated;
}