/** * apps/worker — automation executor registry (TASK-0112 §1/§3). * * `executorFor(type)` returns the executor for an action type. `validateAction` * is the single runtime re-validation entry: it runs the SHARED * `validateActionDto` (the same validator the API used at save) so a corrupt * or drifted stored row fails FAST with a fatal INVALID_CONFIG error (no * retry) instead of a cryptic executor crash. The processor (TASK-0113) calls * `validateAction` then `executorFor(...).execute(...)`. */ import { validateActionDto, AUTOMATION_ACTION_TYPES, type AutomationActionType } from "@factory/shared"; import { FatalError } from "./classify.js"; import type { ActionExecutor, ActionResult, AutomationRunContext, ExecutorDeps } from "./types.js"; import updateRecord from "./update-record.js"; import createPage from "./create-page.js"; import sendEmail from "./send-email.js"; import sendWebhook from "./send-webhook.js"; import createNotification from "./create-notification.js"; import assignUser from "./assign-user.js"; import changeStatus from "./change-status.js"; import callIntegration from "./call-integration.js"; import runAiAction from "./run-ai-action.js"; const registry = new Map( [ updateRecord, createPage, sendEmail, sendWebhook, createNotification, assignUser, changeStatus, callIntegration, runAiAction ].map((e) => [e.type, e]) ); /** * Test-injection seam (TASK-0113 integration): an override consulted BEFORE * the registry by the pipeline's per-action entry point. Empty in production * (behavior is byte-identical); the pipeline integration spec registers a * failing executor here to drive the retry → dead-letter path hermetically * (a real SEND_WEBHOOK failure requires DNS + network, which is not * deterministic in a test container). Register for a real type — the action * row's config still passes the shared schema validation first. */ const testOverrides = new Map(); export function registerTestExecutor(type: string, executor: ActionExecutor): void { testOverrides.set(type, executor); } export function clearTestExecutors(): void { testOverrides.clear(); } /** The executor for an action type (undefined for unknown types). */ export function executorFor(type: string): ActionExecutor | undefined { return testOverrides.get(type) ?? registry.get(type); } export interface ValidatedAction { type: string; config: Record; retries: number; timeoutMs: number; } /** * Runtime re-validation of a stored action. Throws FatalError INVALID_CONFIG * when the type is unknown or the config fails the shared schema. Returns the * normalized {type, config, retries, timeoutMs}. */ export function validateAction(action: { type: unknown; config: unknown; retries?: number; timeoutMs?: number }): ValidatedAction { const type = action.type as string; if (!type || !(AUTOMATION_ACTION_TYPES as readonly string[]).includes(type)) { throw new FatalError("INVALID_CONFIG", `unknown action type: ${String(type)}`); } // exactOptionalPropertyTypes: only set retries/timeoutMs when present. const dto: { type: AutomationActionType; config: Record; retries?: number; timeoutMs?: number } = { type: type as AutomationActionType, config: (action.config ?? {}) as Record }; if (action.retries !== undefined) dto.retries = action.retries; if (action.timeoutMs !== undefined) dto.timeoutMs = action.timeoutMs; const res = validateActionDto(dto); if (!res.ok) { const first = res.errors[0]; throw new FatalError("INVALID_CONFIG", `invalid action config (${first?.path ?? "?"}): ${first?.message ?? "unknown"}`); } return { type: res.normalized.type, config: res.normalized.config, retries: res.normalized.retries, timeoutMs: res.normalized.timeoutMs }; } /** * Validate + execute one action (the processor's per-action entry point). * Re-validates the stored row, dispatches to the executor. The processor * wraps the returned result (or thrown error) per the retry/fatal contract. */ export async function executeAction( action: { type: unknown; config: unknown; retries?: number; timeoutMs?: number }, ctx: AutomationRunContext, deps: ExecutorDeps, index: number ): Promise { const validated = validateAction(action); const executor = registry.get(validated.type); if (!executor) throw new FatalError("INVALID_CONFIG", `no executor for ${validated.type}`); return executor.execute(validated.config, ctx, deps, index); }