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:
56
packages/core/src/__tests__/worktrunk-settings.test.ts
Normal file
56
packages/core/src/__tests__/worktrunk-settings.test.ts
Normal 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 });
|
||||
});
|
||||
});
|
||||
74
packages/core/src/worktrunk-settings.ts
Normal file
74
packages/core/src/worktrunk-settings.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user