test(FN-4639): complete Step 5 — add sandbox exports and tests

Fusion-Task-Id: FN-4639
Fusion-Task-Lineage: 40745d49-dd20-4c51-87e3-42062417f788
This commit is contained in:
Fusion
2026-05-15 10:30:58 -07:00
committed by gsxdsm
parent c56ca5d5dd
commit 56a88de4a0
4 changed files with 175 additions and 2 deletions

View File

@@ -0,0 +1,55 @@
import { describe, expect, it } from "vitest";
import { SANDBOX_BACKEND_NAMES } from "../settings-validation.js";
import { parseSandboxPromptOverride, resolveSandboxBackend } from "../sandbox-prompt-override.js";
describe("sandbox prompt override", () => {
describe("parseSandboxPromptOverride", () => {
it.each(SANDBOX_BACKEND_NAMES)("parses %s", (backend) => {
expect(parseSandboxPromptOverride(`**Sandbox:** ${backend}`)).toBe(backend);
});
it("parses when prefix casing varies", () => {
expect(parseSandboxPromptOverride("**sAnDbOx:** native")).toBe("native");
});
it("does not accept mixed-case backend values", () => {
expect(parseSandboxPromptOverride("**Sandbox:** Docker")).toBeUndefined();
});
it("parses in multi-line prompt content", () => {
expect(parseSandboxPromptOverride("# Task\nSome content\n**Sandbox:** podman\nMore text")).toBe("podman");
});
it("returns undefined for missing or malformed inputs", () => {
expect(parseSandboxPromptOverride(undefined)).toBeUndefined();
expect(parseSandboxPromptOverride("")).toBeUndefined();
expect(parseSandboxPromptOverride("Sandbox: docker")).toBeUndefined();
expect(parseSandboxPromptOverride("**Sandbox:** firejail")).toBeUndefined();
});
});
describe("resolveSandboxBackend", () => {
it("prefers prompt override", () => {
expect(
resolveSandboxBackend(
{ sandbox: { backend: "docker" } },
"**Sandbox:** bubblewrap",
),
).toEqual({ backend: "bubblewrap", source: "prompt" });
});
it("falls back to project setting", () => {
expect(resolveSandboxBackend({ sandbox: { backend: "podman" } }, undefined)).toEqual({
backend: "podman",
source: "project",
});
});
it("falls back to default", () => {
expect(resolveSandboxBackend(undefined, undefined)).toEqual({
backend: "native",
source: "default",
});
});
});
});

View File

@@ -0,0 +1,105 @@
import { describe, expect, it } from "vitest";
import {
DEFAULT_PROJECT_SETTINGS,
GLOBAL_SETTINGS_KEYS,
PROJECT_SETTINGS_KEYS,
SANDBOX_BACKEND_NAMES,
SANDBOX_FAILURE_MODES,
validateSandboxBackendName,
validateSandboxFailureMode,
validateSandboxPolicy,
validateSandboxProjectSettings,
} from "../index.js";
describe("sandbox settings", () => {
it("defines sandbox defaults and scope keys", () => {
expect(DEFAULT_PROJECT_SETTINGS.sandbox).toEqual({
backend: "native",
policy: { allowNetwork: true, allowedPaths: [] },
failureMode: "fail-hard",
});
expect(PROJECT_SETTINGS_KEYS).toContain("sandbox");
expect(GLOBAL_SETTINGS_KEYS).not.toContain("sandbox");
});
describe("validateSandboxBackendName", () => {
it.each(SANDBOX_BACKEND_NAMES)("accepts %s", (backend) => {
expect(validateSandboxBackendName(backend)).toBe(backend);
});
it("rejects invalid values", () => {
expect(validateSandboxBackendName("firejail")).toBeUndefined();
expect(validateSandboxBackendName(123)).toBeUndefined();
expect(validateSandboxBackendName(null)).toBeUndefined();
expect(validateSandboxBackendName(undefined)).toBeUndefined();
expect(validateSandboxBackendName("")).toBeUndefined();
});
});
describe("validateSandboxFailureMode", () => {
it.each(SANDBOX_FAILURE_MODES)("accepts %s", (mode) => {
expect(validateSandboxFailureMode(mode)).toBe(mode);
});
it("rejects invalid values", () => {
expect(validateSandboxFailureMode("fallback")).toBeUndefined();
expect(validateSandboxFailureMode({})).toBeUndefined();
expect(validateSandboxFailureMode(null)).toBeUndefined();
expect(validateSandboxFailureMode(undefined)).toBeUndefined();
expect(validateSandboxFailureMode("")).toBeUndefined();
});
});
describe("validateSandboxPolicy", () => {
it("accepts individual valid keys", () => {
expect(validateSandboxPolicy({ allowNetwork: true })).toEqual({ allowNetwork: true });
expect(validateSandboxPolicy({ allowedPaths: ["foo", "bar/baz"] })).toEqual({
allowedPaths: ["foo", "bar/baz"],
});
});
it("rejects invalid policy payloads", () => {
expect(validateSandboxPolicy({ allowedPaths: [""] })).toBeUndefined();
expect(validateSandboxPolicy({ allowedPaths: "not-an-array" })).toBeUndefined();
expect(validateSandboxPolicy(null)).toBeUndefined();
expect(validateSandboxPolicy([])).toBeUndefined();
expect(validateSandboxPolicy(42)).toBeUndefined();
});
it("drops invalid sub-fields when a valid field remains", () => {
expect(validateSandboxPolicy({ allowNetwork: true, allowedPaths: ["", "ok"] })).toEqual({
allowNetwork: true,
});
expect(validateSandboxPolicy({ allowNetwork: "yes", allowedPaths: ["ok"] })).toEqual({
allowedPaths: ["ok"],
});
});
it("rejects traversal and tilde paths", () => {
expect(validateSandboxPolicy({ allowedPaths: ["../etc"] })).toBeUndefined();
expect(validateSandboxPolicy({ allowedPaths: ["~/secret"] })).toBeUndefined();
});
});
describe("validateSandboxProjectSettings", () => {
it("composes valid nested settings", () => {
expect(
validateSandboxProjectSettings({
backend: "docker",
failureMode: "fallback-native",
policy: { allowNetwork: false, allowedPaths: ["src/**"] },
}),
).toEqual({
backend: "docker",
failureMode: "fallback-native",
policy: { allowNetwork: false, allowedPaths: ["src/**"] },
});
});
it("returns undefined for empty and invalid inputs", () => {
expect(validateSandboxProjectSettings({})).toBeUndefined();
expect(validateSandboxProjectSettings({ backend: "firejail", policy: { allowedPaths: [""] } })).toBeUndefined();
expect(validateSandboxProjectSettings("nope")).toBeUndefined();
});
});
});

View File

@@ -1,5 +1,5 @@
export { COLUMNS, DEFAULT_COLUMN, isColumn, normalizeColumn, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, DEFAULT_SETTINGS, DEFAULT_GLOBAL_SETTINGS, DEFAULT_PROJECT_SETTINGS, GLOBAL_SETTINGS_KEYS, PROJECT_SETTINGS_KEYS, isGlobalSettingsKey, isProjectSettingsKey, resolvePersistAgentThinkingLog, THINKING_LEVELS, THEME_MODES, COLOR_THEMES, WORKFLOW_STEP_TEMPLATES, AGENT_PERMISSIONS, PERMANENT_AGENT_ACTION_CATEGORIES, AGENT_PERMISSION_POLICY_ACTION_CATEGORIES, AGENT_PROVISIONING_APPROVAL_MODES, AGENT_PERMISSION_POLICY_PRESET_IDS, LEGACY_AGENT_PERMISSION_POLICY_ACTION_CATEGORY_ALIASES, APPROVAL_REQUEST_STATUSES, APPROVAL_REQUEST_AUDIT_EVENT_TYPES, normalizeApprovalRequestActionCategory, isValidApprovalRequestTransition, agentToConfigSnapshot, diffConfigSnapshots, isEphemeralAgent, hasAgentIdentity, CheckoutConflictError, DEFAULT_HEARTBEAT_PROCEDURE_PATH, getDefaultHeartbeatProcedurePath, EXECUTION_MODES, DEFAULT_EXECUTION_MODE, TASK_PRIORITIES, DEFAULT_TASK_PRIORITY, HIGH_FANOUT_BLOCKER_TODO_THRESHOLD, STALE_HIGH_FANOUT_BLOCKER_AGE_THRESHOLD_MS, DASHBOARD_USER_ID, normalizeMessageParticipant, validateMessageMetadata, validateDockerNodeConfig, sanitizeDockerNodeConfigForResponse, normalizeMergeConflictStrategy, normalizeMergeStrategyOverlapBehavior, normalizePostMergeAuditMode, POST_MERGE_AUDIT_MODES, normalizeMergeAuditAutoRecovery, MERGE_AUDIT_AUTO_RECOVERY_MODES, normalizeAutoRecovery, AUTO_RECOVERY_MODES, buildResearchDocumentKey, REPO_OVERRIDE_RE, SHARED_STATE_SNAPSHOT_VERSION } from "./types.js";
export type { Column, IssueInfo, IssueState, TaskSourceIssue, PrInfo, PrStatus, Task, TaskTokenUsage, TaskAttachment, TaskComment, TaskCommentInput, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, TaskDocumentWithTask, TaskCreateInput, MeshReplicatedTaskCreatePayload, MeshReplicatedTaskApplyResult, TaskSource, SourceType, TaskDetail, RetrySummary, InboxTask, TodoList, TodoItem, TodoListCreateInput, TodoListUpdateInput, TodoItemCreateInput, TodoItemUpdateInput, TodoListWithItems, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, DistributedTaskIdReserveInput, DistributedTaskIdReserveResult, DistributedTaskIdCommitInput, DistributedTaskIdCommitResult, DistributedTaskIdAbortInput, DistributedTaskIdAbortResult, DistributedTaskIdStateInput, DistributedTaskIdStateResult, AutostashOrphanRecord, AutostashOutcome, MergeDetails, MergeResult, MergeConflictStrategy, CanonicalMergeConflictStrategy, MergeStrategyOverlapBehavior, PostMergeAuditMode, MergeAuditAutoRecoveryMode, AutoRecoveryMode, AutoRecoveryFailureClass, AutoRecoverySettings, DirectMergeCommitStrategy, Settings, GlobalSettings, ProjectSettings, WebSearchBackend, ResearchEnabledSources, ResearchGlobalDefaults, ResearchProjectLimits, ResearchProjectSettings, EvalFollowUpPolicy, EvalProjectSettings, ResolvedEvalSettings, SettingsScope, DaemonTokenSettings, TaskStep, StepStatus, TaskLogEntry, RunMutationContext, ActivityLogEntry, ActivityEventType, ThinkingLevel, ThemeMode, ColorTheme, ExecutionMode, TaskPriority, UnavailableNodePolicy, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, GithubIssueAction, ModelPreset, WorkflowStep, WorkflowStepMode, WorkflowStepGateMode, WorkflowStepPhase, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, OrgTreeNode, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentApiKey, AgentApiKeyCreateResult, AgentCapability, AgentPromptTemplate, AgentPromptsConfig, AgentPermission, PermanentAgentActionCategory, PermanentAgentSensitiveActionCategory, PermanentAgentGatingContext, AgentPermissionPolicy, AgentPermissionPolicyRules, AgentPermissionPolicyActionCategory, AgentProvisioningApprovalMode, LegacyAgentPermissionPolicyActionCategory, ApprovalRequestActionCategoryInput, ApprovalRequestActionCategory, AgentPermissionPolicyDisposition, AgentPermissionPolicyPresetId, ApprovalRequestStatus, ApprovalRequestAuditEventType, ApprovalRequestActorSnapshot, ApprovalRequestTargetAction, ApprovalRequestAuditEvent, ApprovalRequest, ApprovalRequestCreateInput, ApprovalRequestDecisionInput, ApprovalRequestCompletionInput, ApprovalRequestListInput, TaskAssignSource, AgentAccessState, AgentHeartbeatConfig, AgentBudgetConfig, AgentBudgetStatus, InstructionsBundleConfig, MessageResponseMode, AgentHeartbeatEvent, AgentHeartbeatRun, BlockedStateSnapshot, HeartbeatInvocationSource, AgentTaskSession, AgentRating, AgentRatingSummary, AgentRatingInput, AgentConfigSnapshot, RevisionFieldDiff, AgentConfigRevision, AgentStats, ReflectionTrigger, ReflectionMetrics, AgentReflection, AgentPerformanceSummary, NtfyNotificationEvent, NotificationEvent, NotificationPayload, NotificationProviderConfig, CustomProvider, SteeringComment, ParticipantType, MessageType, Message, MessageCreateInput, MessageFilter, MessageMetadata, MessageReplyReference, Mailbox, CheckoutLease, RunAuditDomain, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter, AgentMemoryInclusionMode, HeartbeatPromptTemplate, HeartbeatScopeDisciplineMode } from "./types.js";
export type { Column, IssueInfo, IssueState, TaskSourceIssue, PrInfo, PrStatus, Task, TaskTokenUsage, TaskAttachment, TaskComment, TaskCommentInput, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, TaskDocumentWithTask, TaskCreateInput, MeshReplicatedTaskCreatePayload, MeshReplicatedTaskApplyResult, TaskSource, SourceType, TaskDetail, RetrySummary, InboxTask, TodoList, TodoItem, TodoListCreateInput, TodoListUpdateInput, TodoItemCreateInput, TodoItemUpdateInput, TodoListWithItems, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, DistributedTaskIdReserveInput, DistributedTaskIdReserveResult, DistributedTaskIdCommitInput, DistributedTaskIdCommitResult, DistributedTaskIdAbortInput, DistributedTaskIdAbortResult, DistributedTaskIdStateInput, DistributedTaskIdStateResult, AutostashOrphanRecord, AutostashOutcome, MergeDetails, MergeResult, MergeConflictStrategy, CanonicalMergeConflictStrategy, MergeStrategyOverlapBehavior, PostMergeAuditMode, MergeAuditAutoRecoveryMode, AutoRecoveryMode, AutoRecoveryFailureClass, AutoRecoverySettings, DirectMergeCommitStrategy, Settings, GlobalSettings, ProjectSettings, WebSearchBackend, ResearchEnabledSources, ResearchGlobalDefaults, ResearchProjectLimits, ResearchProjectSettings, SandboxBackendName, SandboxFailureMode, SandboxPolicy, SandboxProjectSettings, EvalFollowUpPolicy, EvalProjectSettings, ResolvedEvalSettings, SettingsScope, DaemonTokenSettings, TaskStep, StepStatus, TaskLogEntry, RunMutationContext, ActivityLogEntry, ActivityEventType, ThinkingLevel, ThemeMode, ColorTheme, ExecutionMode, TaskPriority, UnavailableNodePolicy, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, GithubIssueAction, ModelPreset, WorkflowStep, WorkflowStepMode, WorkflowStepGateMode, WorkflowStepPhase, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, OrgTreeNode, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentApiKey, AgentApiKeyCreateResult, AgentCapability, AgentPromptTemplate, AgentPromptsConfig, AgentPermission, PermanentAgentActionCategory, PermanentAgentSensitiveActionCategory, PermanentAgentGatingContext, AgentPermissionPolicy, AgentPermissionPolicyRules, AgentPermissionPolicyActionCategory, AgentProvisioningApprovalMode, LegacyAgentPermissionPolicyActionCategory, ApprovalRequestActionCategoryInput, ApprovalRequestActionCategory, AgentPermissionPolicyDisposition, AgentPermissionPolicyPresetId, ApprovalRequestStatus, ApprovalRequestAuditEventType, ApprovalRequestActorSnapshot, ApprovalRequestTargetAction, ApprovalRequestAuditEvent, ApprovalRequest, ApprovalRequestCreateInput, ApprovalRequestDecisionInput, ApprovalRequestCompletionInput, ApprovalRequestListInput, TaskAssignSource, AgentAccessState, AgentHeartbeatConfig, AgentBudgetConfig, AgentBudgetStatus, InstructionsBundleConfig, MessageResponseMode, AgentHeartbeatEvent, AgentHeartbeatRun, BlockedStateSnapshot, HeartbeatInvocationSource, AgentTaskSession, AgentRating, AgentRatingSummary, AgentRatingInput, AgentConfigSnapshot, RevisionFieldDiff, AgentConfigRevision, AgentStats, ReflectionTrigger, ReflectionMetrics, AgentReflection, AgentPerformanceSummary, NtfyNotificationEvent, NotificationEvent, NotificationPayload, NotificationProviderConfig, CustomProvider, SteeringComment, ParticipantType, MessageType, Message, MessageCreateInput, MessageFilter, MessageMetadata, MessageReplyReference, Mailbox, CheckoutLease, RunAuditDomain, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter, AgentMemoryInclusionMode, HeartbeatPromptTemplate, HeartbeatScopeDisciplineMode } from "./types.js";
export { AGENT_VALID_TRANSITIONS } from "./types.js";
export {
resolveAgentMemoryInclusionMode,
@@ -247,12 +247,20 @@ export {
type NodeOverrideBlockReason,
} from "./node-override-guard.js";
export {
SANDBOX_BACKEND_NAMES,
SANDBOX_FAILURE_MODES,
validateDirectMergeCommitStrategy,
validateGithubAuthMode,
validateGithubRepoSlug,
validateSandboxBackendName,
validateSandboxFailureMode,
validateSandboxPolicy,
validateSandboxProjectSettings,
validateUnavailableNodePolicy,
} from "./settings-validation.js";
export { parseSandboxPromptOverride, resolveSandboxBackend } from "./sandbox-prompt-override.js";
// ── Routine System ───────────────────────────────────────────────────
export {
MAX_ROUTINE_RUN_HISTORY,

View File

@@ -13,7 +13,12 @@ export function parseSandboxPromptOverride(prompt: string | undefined): SandboxB
return undefined;
}
const match = prompt.match(SANDBOX_OVERRIDE_RE);
return match ? (match[1] as SandboxBackendName) : undefined;
if (!match) {
return undefined;
}
const backend = match[1];
return backend === backend.toLowerCase() ? (backend as SandboxBackendName) : undefined;
}
export function resolveSandboxBackend(