feat(FN-2718): add unavailable node policy setting and validation
- Define and export unavailable node policy types in core settings interfaces - Add project-level unavailable node policy default and runtime validation helper - Add core unit coverage for unavailable node policy parsing and acceptance cases - Guard Paperclip mint requests to include companyId only when available for type-safe payloads - Document unavailable node policy in the settings reference
This commit is contained in:
42
packages/core/src/__tests__/unavailable-node-policy.test.ts
Normal file
42
packages/core/src/__tests__/unavailable-node-policy.test.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { describe, expect, expectTypeOf, it } from "vitest";
|
||||
import {
|
||||
DEFAULT_PROJECT_SETTINGS,
|
||||
PROJECT_SETTINGS_KEYS,
|
||||
isGlobalSettingsKey,
|
||||
isProjectSettingsKey,
|
||||
type UnavailableNodePolicy,
|
||||
validateUnavailableNodePolicy,
|
||||
} from "../index.js";
|
||||
|
||||
describe("unavailableNodePolicy settings contract", () => {
|
||||
it("accepts both supported UnavailableNodePolicy values", () => {
|
||||
expectTypeOf<UnavailableNodePolicy>().toEqualTypeOf<"block" | "fallback-local">();
|
||||
|
||||
const block: UnavailableNodePolicy = "block";
|
||||
const fallbackLocal: UnavailableNodePolicy = "fallback-local";
|
||||
|
||||
expect(block).toBe("block");
|
||||
expect(fallbackLocal).toBe("fallback-local");
|
||||
});
|
||||
|
||||
it("defaults unavailableNodePolicy to block", () => {
|
||||
expect(DEFAULT_PROJECT_SETTINGS.unavailableNodePolicy).toBe("block");
|
||||
});
|
||||
|
||||
it("classifies unavailableNodePolicy as project-only scope", () => {
|
||||
expect(isProjectSettingsKey("unavailableNodePolicy")).toBe(true);
|
||||
expect(isGlobalSettingsKey("unavailableNodePolicy")).toBe(false);
|
||||
});
|
||||
|
||||
it("includes unavailableNodePolicy in PROJECT_SETTINGS_KEYS", () => {
|
||||
expect(PROJECT_SETTINGS_KEYS).toContain("unavailableNodePolicy");
|
||||
});
|
||||
|
||||
it("validates supported and unsupported policy values", () => {
|
||||
expect(validateUnavailableNodePolicy("block")).toBe("block");
|
||||
expect(validateUnavailableNodePolicy("fallback-local")).toBe("fallback-local");
|
||||
expect(validateUnavailableNodePolicy(undefined)).toBeUndefined();
|
||||
expect(validateUnavailableNodePolicy("fallback-remote")).toBeUndefined();
|
||||
expect(validateUnavailableNodePolicy(42)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
export { COLUMNS, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, DEFAULT_SETTINGS, DEFAULT_GLOBAL_SETTINGS, DEFAULT_PROJECT_SETTINGS, GLOBAL_SETTINGS_KEYS, PROJECT_SETTINGS_KEYS, isGlobalSettingsKey, isProjectSettingsKey, THINKING_LEVELS, THEME_MODES, COLOR_THEMES, WORKFLOW_STEP_TEMPLATES, AGENT_PERMISSIONS, agentToConfigSnapshot, diffConfigSnapshots, isEphemeralAgent, hasAgentIdentity, CheckoutConflictError, EXECUTION_MODES, DEFAULT_EXECUTION_MODE, TASK_PRIORITIES, DEFAULT_TASK_PRIORITY, validateMessageMetadata } from "./types.js";
|
||||
export type { Column, IssueInfo, IssueState, TaskSourceIssue, PrInfo, PrStatus, Task, TaskTokenUsage, TaskAttachment, TaskComment, TaskCommentInput, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, TaskDocumentWithTask, TaskCreateInput, TaskDetail, InboxTask, TodoList, TodoItem, TodoListCreateInput, TodoListUpdateInput, TodoItemCreateInput, TodoItemUpdateInput, TodoListWithItems, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, MergeDetails, MergeResult, Settings, GlobalSettings, ProjectSettings, SettingsScope, DaemonTokenSettings, TaskStep, StepStatus, TaskLogEntry, RunMutationContext, ActivityLogEntry, ActivityEventType, ThinkingLevel, ThemeMode, ColorTheme, ExecutionMode, TaskPriority, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, ModelPreset, WorkflowStep, WorkflowStepMode, WorkflowStepPhase, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, OrgTreeNode, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentApiKey, AgentApiKeyCreateResult, AgentCapability, AgentPromptTemplate, AgentPromptsConfig, AgentPermission, TaskAssignSource, AgentAccessState, AgentHeartbeatConfig, AgentBudgetConfig, AgentBudgetStatus, InstructionsBundleConfig, MessageResponseMode, AgentHeartbeatEvent, AgentHeartbeatRun, BlockedStateSnapshot, HeartbeatInvocationSource, AgentTaskSession, AgentRating, AgentRatingSummary, AgentRatingInput, AgentConfigSnapshot, RevisionFieldDiff, AgentConfigRevision, AgentStats, ReflectionTrigger, ReflectionMetrics, AgentReflection, AgentPerformanceSummary, NtfyNotificationEvent, SteeringComment, ParticipantType, MessageType, Message, MessageCreateInput, MessageFilter, MessageMetadata, MessageReplyReference, Mailbox, CheckoutLease, RunAuditDomain, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter } from "./types.js";
|
||||
export type { Column, IssueInfo, IssueState, TaskSourceIssue, PrInfo, PrStatus, Task, TaskTokenUsage, TaskAttachment, TaskComment, TaskCommentInput, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, TaskDocumentWithTask, TaskCreateInput, TaskDetail, InboxTask, TodoList, TodoItem, TodoListCreateInput, TodoListUpdateInput, TodoItemCreateInput, TodoItemUpdateInput, TodoListWithItems, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, MergeDetails, MergeResult, Settings, GlobalSettings, ProjectSettings, SettingsScope, DaemonTokenSettings, TaskStep, StepStatus, TaskLogEntry, RunMutationContext, ActivityLogEntry, ActivityEventType, ThinkingLevel, ThemeMode, ColorTheme, ExecutionMode, TaskPriority, UnavailableNodePolicy, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, ModelPreset, WorkflowStep, WorkflowStepMode, WorkflowStepPhase, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, OrgTreeNode, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentApiKey, AgentApiKeyCreateResult, AgentCapability, AgentPromptTemplate, AgentPromptsConfig, AgentPermission, TaskAssignSource, AgentAccessState, AgentHeartbeatConfig, AgentBudgetConfig, AgentBudgetStatus, InstructionsBundleConfig, MessageResponseMode, AgentHeartbeatEvent, AgentHeartbeatRun, BlockedStateSnapshot, HeartbeatInvocationSource, AgentTaskSession, AgentRating, AgentRatingSummary, AgentRatingInput, AgentConfigSnapshot, RevisionFieldDiff, AgentConfigRevision, AgentStats, ReflectionTrigger, ReflectionMetrics, AgentReflection, AgentPerformanceSummary, NtfyNotificationEvent, SteeringComment, ParticipantType, MessageType, Message, MessageCreateInput, MessageFilter, MessageMetadata, MessageReplyReference, Mailbox, CheckoutLease, RunAuditDomain, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter } from "./types.js";
|
||||
export { AGENT_VALID_TRANSITIONS } from "./types.js";
|
||||
export {
|
||||
BUILTIN_AGENT_PROMPTS,
|
||||
@@ -74,6 +74,7 @@ export { AutomationStore } from "./automation-store.js";
|
||||
export type { AutomationStoreEvents } from "./automation-store.js";
|
||||
export { runCommandAsync } from "./run-command.js";
|
||||
export type { RunCommandOptions, RunCommandResult } from "./run-command.js";
|
||||
export { validateUnavailableNodePolicy } from "./settings-validation.js";
|
||||
|
||||
// ── Routine System ───────────────────────────────────────────────────
|
||||
export {
|
||||
|
||||
@@ -77,6 +77,7 @@ export const DEFAULT_PROJECT_SETTINGS = {
|
||||
mergeStrategy: "direct",
|
||||
pushAfterMerge: false,
|
||||
pushRemote: "origin",
|
||||
unavailableNodePolicy: "block",
|
||||
worktreeInitCommand: undefined,
|
||||
testCommand: undefined,
|
||||
buildCommand: undefined,
|
||||
|
||||
20
packages/core/src/settings-validation.ts
Normal file
20
packages/core/src/settings-validation.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { UnavailableNodePolicy } from "./types.js";
|
||||
|
||||
const UNAVAILABLE_NODE_POLICIES: readonly UnavailableNodePolicy[] = ["block", "fallback-local"] as const;
|
||||
|
||||
/**
|
||||
* Validates a project unavailable-node routing policy value.
|
||||
*
|
||||
* Returns the normalized policy value when valid, otherwise undefined.
|
||||
*/
|
||||
export function validateUnavailableNodePolicy(value: unknown): UnavailableNodePolicy | undefined {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
return (UNAVAILABLE_NODE_POLICIES as readonly string[]).includes(value)
|
||||
? (value as UnavailableNodePolicy)
|
||||
: undefined;
|
||||
}
|
||||
@@ -92,6 +92,8 @@ export type ColorTheme = (typeof COLOR_THEMES)[number];
|
||||
|
||||
export type PrStatus = "open" | "closed" | "merged";
|
||||
export type MergeStrategy = "direct" | "pull-request";
|
||||
/** Policy for handling task execution when the selected node is unavailable/unhealthy. */
|
||||
export type UnavailableNodePolicy = "block" | "fallback-local";
|
||||
|
||||
export interface ModelPreset {
|
||||
id: string;
|
||||
@@ -1321,6 +1323,11 @@ export interface ProjectSettings {
|
||||
* When set to "remote branch" format, both the remote and branch are specified.
|
||||
* Only used when pushAfterMerge is true. Default: "origin". */
|
||||
pushRemote?: string;
|
||||
/** Policy for how to route execution when the selected node is unavailable/unhealthy.
|
||||
* Applies to both project default node selection and per-task node overrides.
|
||||
* - "block": prevent execution until the selected node is healthy/available (default)
|
||||
* - "fallback-local": run on the local node when the selected node is unavailable */
|
||||
unavailableNodePolicy?: UnavailableNodePolicy;
|
||||
/** Shell command to run inside each new worktree immediately after creation.
|
||||
* Useful for project-specific setup (e.g. `pnpm install --frozen-lockfile`, `cp .env.local .env`). */
|
||||
worktreeInitCommand?: string;
|
||||
|
||||
Reference in New Issue
Block a user