diff --git a/.changeset/conditional-step-execution.md b/.changeset/conditional-step-execution.md index c94909304..57590b9aa 100644 --- a/.changeset/conditional-step-execution.md +++ b/.changeset/conditional-step-execution.md @@ -19,7 +19,7 @@ Add conditional step execution with skip infrastructure **Schema Changes:** -- New columns: required_input_pattern, forbidden_input_pattern, when_unmet, when_failed, skip_reason, skipped_at +- New columns: required_input_pattern, forbidden_input_pattern, when_unmet, when_exhausted, skip_reason, skipped_at - New step status: 'skipped' - New function: cascade_skip_steps() for skip propagation - FlowShape condition fields for auto-compilation drift detection diff --git a/.ignore b/.ignore index 538a29df5..fcd129cf9 100644 --- a/.ignore +++ b/.ignore @@ -1,2 +1,3 @@ !.claude +!.claude/* !.notes diff --git a/pkgs/core/schemas/0050_tables_definitions.sql b/pkgs/core/schemas/0050_tables_definitions.sql index 1ea94b079..68a6d2756 100644 --- a/pkgs/core/schemas/0050_tables_definitions.sql +++ b/pkgs/core/schemas/0050_tables_definitions.sql @@ -27,7 +27,7 @@ create table pgflow.steps ( required_input_pattern jsonb, -- JSON pattern for @> containment check (if) forbidden_input_pattern jsonb, -- JSON pattern for NOT @> containment check (ifNot) when_unmet text not null default 'skip', -- What to do when condition not met (skip is natural default) - when_failed text not null default 'fail', -- What to do when handler fails after retries + when_exhausted text not null default 'fail', -- What to do when handler fails after retries created_at timestamptz not null default now(), primary key (flow_slug, step_slug), unique (flow_slug, step_index), -- Ensure step_index is unique within a flow @@ -38,7 +38,7 @@ create table pgflow.steps ( constraint opt_timeout_is_positive check (opt_timeout is null or opt_timeout > 0), constraint opt_start_delay_is_nonnegative check (opt_start_delay is null or opt_start_delay >= 0), constraint when_unmet_is_valid check (when_unmet in ('fail', 'skip', 'skip-cascade')), - constraint when_failed_is_valid check (when_failed in ('fail', 'skip', 'skip-cascade')) + constraint when_exhausted_is_valid check (when_exhausted in ('fail', 'skip', 'skip-cascade')) ); -- Dependencies table - stores relationships between steps diff --git a/pkgs/core/schemas/0100_function__cascade_force_skip_steps.sql b/pkgs/core/schemas/0100_function__cascade_force_skip_steps.sql index 18d481d4e..b9f31e65a 100644 --- a/pkgs/core/schemas/0100_function__cascade_force_skip_steps.sql +++ b/pkgs/core/schemas/0100_function__cascade_force_skip_steps.sql @@ -1,5 +1,5 @@ -- _cascade_force_skip_steps: Skip a step and cascade to all downstream dependents --- Used when a condition is unmet (whenUnmet: skip-cascade) or handler fails (whenFailed: skip-cascade) +-- Used when a condition is unmet (whenUnmet: skip-cascade) or handler fails (whenExhausted: skip-cascade) create or replace function pgflow._cascade_force_skip_steps( run_id uuid, step_slug text, diff --git a/pkgs/core/schemas/0100_function_add_step.sql b/pkgs/core/schemas/0100_function_add_step.sql index 0ed71bf69..bb3475979 100644 --- a/pkgs/core/schemas/0100_function_add_step.sql +++ b/pkgs/core/schemas/0100_function_add_step.sql @@ -10,7 +10,7 @@ create or replace function pgflow.add_step( required_input_pattern jsonb default null, forbidden_input_pattern jsonb default null, when_unmet text default 'skip', - when_failed text default 'fail' + when_exhausted text default 'fail' ) returns pgflow.steps language plpgsql @@ -41,7 +41,7 @@ BEGIN INSERT INTO pgflow.steps ( flow_slug, step_slug, step_type, step_index, deps_count, opt_max_attempts, opt_base_delay, opt_timeout, opt_start_delay, - required_input_pattern, forbidden_input_pattern, when_unmet, when_failed + required_input_pattern, forbidden_input_pattern, when_unmet, when_exhausted ) VALUES ( add_step.flow_slug, @@ -56,7 +56,7 @@ BEGIN add_step.required_input_pattern, add_step.forbidden_input_pattern, add_step.when_unmet, - add_step.when_failed + add_step.when_exhausted ) ON CONFLICT ON CONSTRAINT steps_pkey DO UPDATE SET step_slug = EXCLUDED.step_slug diff --git a/pkgs/core/schemas/0100_function_compare_flow_shapes.sql b/pkgs/core/schemas/0100_function_compare_flow_shapes.sql index b91839faf..0c9388d7e 100644 --- a/pkgs/core/schemas/0100_function_compare_flow_shapes.sql +++ b/pkgs/core/schemas/0100_function_compare_flow_shapes.sql @@ -121,15 +121,15 @@ BEGIN ); END IF; - -- Compare whenFailed (structural - affects DAG execution semantics) - IF v_local_step->>'whenFailed' != v_db_step->>'whenFailed' THEN + -- Compare whenExhausted (structural - affects DAG execution semantics) + IF v_local_step->>'whenExhausted' != v_db_step->>'whenExhausted' THEN v_differences := array_append( v_differences, format( - $$Step at index %s: whenFailed differs '%s' vs '%s'$$, + $$Step at index %s: whenExhausted differs '%s' vs '%s'$$, v_idx, - v_local_step->>'whenFailed', - v_db_step->>'whenFailed' + v_local_step->>'whenExhausted', + v_db_step->>'whenExhausted' ) ); END IF; diff --git a/pkgs/core/schemas/0100_function_create_flow_from_shape.sql b/pkgs/core/schemas/0100_function_create_flow_from_shape.sql index 51b055482..c9501921f 100644 --- a/pkgs/core/schemas/0100_function_create_flow_from_shape.sql +++ b/pkgs/core/schemas/0100_function_create_flow_from_shape.sql @@ -49,7 +49,7 @@ BEGIN start_delay => (v_step_options->>'startDelay')::int, step_type => v_step->>'stepType', when_unmet => v_step->>'whenUnmet', - when_failed => v_step->>'whenFailed', + when_exhausted => v_step->>'whenExhausted', required_input_pattern => CASE WHEN (v_step->'requiredInputPattern'->>'defined')::boolean THEN v_step->'requiredInputPattern'->'value' diff --git a/pkgs/core/schemas/0100_function_fail_task.sql b/pkgs/core/schemas/0100_function_fail_task.sql index b8840d28b..e926dbd18 100644 --- a/pkgs/core/schemas/0100_function_fail_task.sql +++ b/pkgs/core/schemas/0100_function_fail_task.sql @@ -13,7 +13,7 @@ DECLARE v_run_failed boolean; v_step_failed boolean; v_step_skipped boolean; - v_when_failed text; + v_when_exhausted text; v_task_exhausted boolean; -- True if task has exhausted retries v_flow_slug_for_deps text; -- Used for decrementing remaining_deps on plain skip begin @@ -63,11 +63,11 @@ flow_info AS ( FROM pgflow.runs r WHERE r.run_id = fail_task.run_id ), -config AS ( + config AS ( SELECT COALESCE(s.opt_max_attempts, f.opt_max_attempts) AS opt_max_attempts, COALESCE(s.opt_base_delay, f.opt_base_delay) AS opt_base_delay, - s.when_failed + s.when_exhausted FROM pgflow.steps s JOIN pgflow.flows f ON f.flow_slug = s.flow_slug JOIN flow_info fi ON fi.flow_slug = s.flow_slug @@ -95,45 +95,45 @@ fail_or_retry_task as ( AND task.status = 'started' RETURNING * ), --- Determine if task exhausted retries and get when_failed mode -task_status AS ( - SELECT - (select status from fail_or_retry_task) AS new_task_status, - (select when_failed from config) AS when_failed_mode, + -- Determine if task exhausted retries and get when_exhausted mode + task_status AS ( + SELECT + (select status from fail_or_retry_task) AS new_task_status, + (select when_exhausted from config) AS when_exhausted_mode, -- Task is exhausted when it's failed (no more retries) ((select status from fail_or_retry_task) = 'failed') AS is_exhausted ), maybe_fail_step AS ( UPDATE pgflow.step_states SET - -- Status logic: - -- - If task not exhausted (retrying): keep current status - -- - If exhausted AND when_failed='fail': set to 'failed' - -- - If exhausted AND when_failed IN ('skip', 'skip-cascade'): set to 'skipped' - status = CASE - WHEN NOT (select is_exhausted from task_status) THEN pgflow.step_states.status - WHEN (select when_failed_mode from task_status) = 'fail' THEN 'failed' - ELSE 'skipped' -- skip or skip-cascade - END, + -- Status logic: + -- - If task not exhausted (retrying): keep current status + -- - If exhausted AND when_exhausted='fail': set to 'failed' + -- - If exhausted AND when_exhausted IN ('skip', 'skip-cascade'): set to 'skipped' + status = CASE + WHEN NOT (select is_exhausted from task_status) THEN pgflow.step_states.status + WHEN (select when_exhausted_mode from task_status) = 'fail' THEN 'failed' + ELSE 'skipped' -- skip or skip-cascade + END, failed_at = CASE - WHEN (select is_exhausted from task_status) AND (select when_failed_mode from task_status) = 'fail' THEN now() - ELSE NULL - END, + WHEN (select is_exhausted from task_status) AND (select when_exhausted_mode from task_status) = 'fail' THEN now() + ELSE NULL + END, error_message = CASE WHEN (select is_exhausted from task_status) THEN fail_task.error_message ELSE NULL END, skip_reason = CASE - WHEN (select is_exhausted from task_status) AND (select when_failed_mode from task_status) IN ('skip', 'skip-cascade') THEN 'handler_failed' + WHEN (select is_exhausted from task_status) AND (select when_exhausted_mode from task_status) IN ('skip', 'skip-cascade') THEN 'handler_failed' ELSE pgflow.step_states.skip_reason END, skipped_at = CASE - WHEN (select is_exhausted from task_status) AND (select when_failed_mode from task_status) IN ('skip', 'skip-cascade') THEN now() + WHEN (select is_exhausted from task_status) AND (select when_exhausted_mode from task_status) IN ('skip', 'skip-cascade') THEN now() ELSE pgflow.step_states.skipped_at END, -- Clear remaining_tasks when skipping (required by remaining_tasks_state_consistency constraint) remaining_tasks = CASE - WHEN (select is_exhausted from task_status) AND (select when_failed_mode from task_status) IN ('skip', 'skip-cascade') THEN NULL + WHEN (select is_exhausted from task_status) AND (select when_exhausted_mode from task_status) IN ('skip', 'skip-cascade') THEN NULL ELSE pgflow.step_states.remaining_tasks END FROM fail_or_retry_task @@ -141,7 +141,7 @@ maybe_fail_step AS ( AND pgflow.step_states.step_slug = fail_task.step_slug RETURNING pgflow.step_states.* ) --- Update run status: only fail when when_failed='fail' and step was failed + -- Update run status: only fail when when_exhausted='fail' and step was failed UPDATE pgflow.runs SET status = CASE WHEN (select status from maybe_fail_step) = 'failed' THEN 'failed' @@ -159,9 +159,9 @@ SET status = CASE WHERE pgflow.runs.run_id = fail_task.run_id RETURNING (status = 'failed') INTO v_run_failed; --- Capture when_failed mode and check if step was skipped for later processing -SELECT s.when_failed INTO v_when_failed -FROM pgflow.steps s + -- Capture when_exhausted mode and check if step was skipped for later processing + SELECT s.when_exhausted INTO v_when_exhausted + FROM pgflow.steps s JOIN pgflow.runs r ON r.flow_slug = s.flow_slug WHERE r.run_id = fail_task.run_id AND s.step_slug = fail_task.step_slug; @@ -194,8 +194,8 @@ IF v_step_failed THEN ); END IF; --- Handle step skipping (when_failed = 'skip' or 'skip-cascade') -IF v_step_skipped THEN + -- Handle step skipping (when_exhausted = 'skip' or 'skip-cascade') + IF v_step_skipped THEN -- Send broadcast event for step skipped PERFORM realtime.send( jsonb_build_object( @@ -212,8 +212,8 @@ IF v_step_skipped THEN false ); - -- For skip-cascade: cascade skip to all downstream dependents - IF v_when_failed = 'skip-cascade' THEN + -- For skip-cascade: cascade skip to all downstream dependents + IF v_when_exhausted = 'skip-cascade' THEN PERFORM pgflow._cascade_force_skip_steps(fail_task.run_id, fail_task.step_slug, 'handler_failed'); ELSE -- For plain 'skip': decrement remaining_deps on dependent steps diff --git a/pkgs/core/schemas/0100_function_get_flow_shape.sql b/pkgs/core/schemas/0100_function_get_flow_shape.sql index 985f0d4d1..2668648b0 100644 --- a/pkgs/core/schemas/0100_function_get_flow_shape.sql +++ b/pkgs/core/schemas/0100_function_get_flow_shape.sql @@ -24,7 +24,7 @@ as $$ '[]'::jsonb ), 'whenUnmet', step.when_unmet, - 'whenFailed', step.when_failed, + 'whenExhausted', step.when_exhausted, 'requiredInputPattern', CASE WHEN step.required_input_pattern IS NULL THEN '{"defined": false}'::jsonb diff --git a/pkgs/core/src/database-types.ts b/pkgs/core/src/database-types.ts index 690700c01..8252adb2a 100644 --- a/pkgs/core/src/database-types.ts +++ b/pkgs/core/src/database-types.ts @@ -299,7 +299,7 @@ export type Database = { step_index: number step_slug: string step_type: string - when_failed: string + when_exhausted: string when_unmet: string } Insert: { @@ -315,7 +315,7 @@ export type Database = { step_index?: number step_slug: string step_type?: string - when_failed?: string + when_exhausted?: string when_unmet?: string } Update: { @@ -331,7 +331,7 @@ export type Database = { step_index?: number step_slug?: string step_type?: string - when_failed?: string + when_exhausted?: string when_unmet?: string } Relationships: [ @@ -431,7 +431,7 @@ export type Database = { step_slug: string step_type?: string timeout?: number - when_failed?: string + when_exhausted?: string when_unmet?: string } Returns: { @@ -447,7 +447,7 @@ export type Database = { step_index: number step_slug: string step_type: string - when_failed: string + when_exhausted: string when_unmet: string } SetofOptions: { diff --git a/pkgs/core/supabase/migrations/20260121095914_pgflow_step_conditions.sql b/pkgs/core/supabase/migrations/20260123080735_pgflow_step_conditions.sql similarity index 96% rename from pkgs/core/supabase/migrations/20260121095914_pgflow_step_conditions.sql rename to pkgs/core/supabase/migrations/20260123080735_pgflow_step_conditions.sql index ae7702559..1b20a7e9d 100644 --- a/pkgs/core/supabase/migrations/20260121095914_pgflow_step_conditions.sql +++ b/pkgs/core/supabase/migrations/20260123080735_pgflow_step_conditions.sql @@ -15,7 +15,7 @@ END) <= 1), ADD CONSTRAINT "skip_reason_matches_status" CHECK (((status = 'skipp -- Create index "idx_step_states_skipped" to table: "step_states" CREATE INDEX "idx_step_states_skipped" ON "pgflow"."step_states" ("run_id", "step_slug") WHERE (status = 'skipped'::text); -- Modify "steps" table -ALTER TABLE "pgflow"."steps" ADD CONSTRAINT "when_failed_is_valid" CHECK (when_failed = ANY (ARRAY['fail'::text, 'skip'::text, 'skip-cascade'::text])), ADD CONSTRAINT "when_unmet_is_valid" CHECK (when_unmet = ANY (ARRAY['fail'::text, 'skip'::text, 'skip-cascade'::text])), ADD COLUMN "required_input_pattern" jsonb NULL, ADD COLUMN "forbidden_input_pattern" jsonb NULL, ADD COLUMN "when_unmet" text NOT NULL DEFAULT 'skip', ADD COLUMN "when_failed" text NOT NULL DEFAULT 'fail'; +ALTER TABLE "pgflow"."steps" ADD CONSTRAINT "when_exhausted_is_valid" CHECK (when_exhausted = ANY (ARRAY['fail'::text, 'skip'::text, 'skip-cascade'::text])), ADD CONSTRAINT "when_unmet_is_valid" CHECK (when_unmet = ANY (ARRAY['fail'::text, 'skip'::text, 'skip-cascade'::text])), ADD COLUMN "required_input_pattern" jsonb NULL, ADD COLUMN "forbidden_input_pattern" jsonb NULL, ADD COLUMN "when_unmet" text NOT NULL DEFAULT 'skip', ADD COLUMN "when_exhausted" text NOT NULL DEFAULT 'fail'; -- Modify "_compare_flow_shapes" function CREATE OR REPLACE FUNCTION "pgflow"."_compare_flow_shapes" ("p_local" jsonb, "p_db" jsonb) RETURNS text[] LANGUAGE plpgsql STABLE SET "search_path" = '' AS $BODY$ DECLARE @@ -130,15 +130,15 @@ BEGIN ); END IF; - -- Compare whenFailed (structural - affects DAG execution semantics) - IF v_local_step->>'whenFailed' != v_db_step->>'whenFailed' THEN + -- Compare whenExhausted (structural - affects DAG execution semantics) + IF v_local_step->>'whenExhausted' != v_db_step->>'whenExhausted' THEN v_differences := array_append( v_differences, format( - $$Step at index %s: whenFailed differs '%s' vs '%s'$$, + $$Step at index %s: whenExhausted differs '%s' vs '%s'$$, v_idx, - v_local_step->>'whenFailed', - v_db_step->>'whenFailed' + v_local_step->>'whenExhausted', + v_db_step->>'whenExhausted' ) ); END IF; @@ -177,7 +177,7 @@ BEGIN END; $BODY$; -- Create "add_step" function -CREATE FUNCTION "pgflow"."add_step" ("flow_slug" text, "step_slug" text, "deps_slugs" text[] DEFAULT '{}', "max_attempts" integer DEFAULT NULL::integer, "base_delay" integer DEFAULT NULL::integer, "timeout" integer DEFAULT NULL::integer, "start_delay" integer DEFAULT NULL::integer, "step_type" text DEFAULT 'single', "required_input_pattern" jsonb DEFAULT NULL::jsonb, "forbidden_input_pattern" jsonb DEFAULT NULL::jsonb, "when_unmet" text DEFAULT 'skip', "when_failed" text DEFAULT 'fail') RETURNS "pgflow"."steps" LANGUAGE plpgsql SET "search_path" = '' AS $$ +CREATE FUNCTION "pgflow"."add_step" ("flow_slug" text, "step_slug" text, "deps_slugs" text[] DEFAULT '{}', "max_attempts" integer DEFAULT NULL::integer, "base_delay" integer DEFAULT NULL::integer, "timeout" integer DEFAULT NULL::integer, "start_delay" integer DEFAULT NULL::integer, "step_type" text DEFAULT 'single', "required_input_pattern" jsonb DEFAULT NULL::jsonb, "forbidden_input_pattern" jsonb DEFAULT NULL::jsonb, "when_unmet" text DEFAULT 'skip', "when_exhausted" text DEFAULT 'fail') RETURNS "pgflow"."steps" LANGUAGE plpgsql SET "search_path" = '' AS $$ DECLARE result_step pgflow.steps; next_idx int; @@ -202,7 +202,7 @@ BEGIN INSERT INTO pgflow.steps ( flow_slug, step_slug, step_type, step_index, deps_count, opt_max_attempts, opt_base_delay, opt_timeout, opt_start_delay, - required_input_pattern, forbidden_input_pattern, when_unmet, when_failed + required_input_pattern, forbidden_input_pattern, when_unmet, when_exhausted ) VALUES ( add_step.flow_slug, @@ -217,7 +217,7 @@ BEGIN add_step.required_input_pattern, add_step.forbidden_input_pattern, add_step.when_unmet, - add_step.when_failed + add_step.when_exhausted ) ON CONFLICT ON CONSTRAINT steps_pkey DO UPDATE SET step_slug = EXCLUDED.step_slug @@ -274,7 +274,7 @@ BEGIN start_delay => (v_step_options->>'startDelay')::int, step_type => v_step->>'stepType', when_unmet => v_step->>'whenUnmet', - when_failed => v_step->>'whenFailed', + when_exhausted => v_step->>'whenExhausted', required_input_pattern => CASE WHEN (v_step->'requiredInputPattern'->>'defined')::boolean THEN v_step->'requiredInputPattern'->'value' @@ -308,7 +308,7 @@ SELECT jsonb_build_object( '[]'::jsonb ), 'whenUnmet', step.when_unmet, - 'whenFailed', step.when_failed, + 'whenExhausted', step.when_exhausted, 'requiredInputPattern', CASE WHEN step.required_input_pattern IS NULL THEN '{"defined": false}'::jsonb @@ -1179,7 +1179,7 @@ DECLARE v_run_failed boolean; v_step_failed boolean; v_step_skipped boolean; - v_when_failed text; + v_when_exhausted text; v_task_exhausted boolean; -- True if task has exhausted retries v_flow_slug_for_deps text; -- Used for decrementing remaining_deps on plain skip begin @@ -1229,11 +1229,11 @@ flow_info AS ( FROM pgflow.runs r WHERE r.run_id = fail_task.run_id ), -config AS ( + config AS ( SELECT COALESCE(s.opt_max_attempts, f.opt_max_attempts) AS opt_max_attempts, COALESCE(s.opt_base_delay, f.opt_base_delay) AS opt_base_delay, - s.when_failed + s.when_exhausted FROM pgflow.steps s JOIN pgflow.flows f ON f.flow_slug = s.flow_slug JOIN flow_info fi ON fi.flow_slug = s.flow_slug @@ -1261,45 +1261,45 @@ fail_or_retry_task as ( AND task.status = 'started' RETURNING * ), --- Determine if task exhausted retries and get when_failed mode -task_status AS ( - SELECT - (select status from fail_or_retry_task) AS new_task_status, - (select when_failed from config) AS when_failed_mode, + -- Determine if task exhausted retries and get when_exhausted mode + task_status AS ( + SELECT + (select status from fail_or_retry_task) AS new_task_status, + (select when_exhausted from config) AS when_exhausted_mode, -- Task is exhausted when it's failed (no more retries) ((select status from fail_or_retry_task) = 'failed') AS is_exhausted ), maybe_fail_step AS ( UPDATE pgflow.step_states SET - -- Status logic: - -- - If task not exhausted (retrying): keep current status - -- - If exhausted AND when_failed='fail': set to 'failed' - -- - If exhausted AND when_failed IN ('skip', 'skip-cascade'): set to 'skipped' - status = CASE - WHEN NOT (select is_exhausted from task_status) THEN pgflow.step_states.status - WHEN (select when_failed_mode from task_status) = 'fail' THEN 'failed' - ELSE 'skipped' -- skip or skip-cascade - END, + -- Status logic: + -- - If task not exhausted (retrying): keep current status + -- - If exhausted AND when_exhausted='fail': set to 'failed' + -- - If exhausted AND when_exhausted IN ('skip', 'skip-cascade'): set to 'skipped' + status = CASE + WHEN NOT (select is_exhausted from task_status) THEN pgflow.step_states.status + WHEN (select when_exhausted_mode from task_status) = 'fail' THEN 'failed' + ELSE 'skipped' -- skip or skip-cascade + END, failed_at = CASE - WHEN (select is_exhausted from task_status) AND (select when_failed_mode from task_status) = 'fail' THEN now() - ELSE NULL - END, + WHEN (select is_exhausted from task_status) AND (select when_exhausted_mode from task_status) = 'fail' THEN now() + ELSE NULL + END, error_message = CASE WHEN (select is_exhausted from task_status) THEN fail_task.error_message ELSE NULL END, skip_reason = CASE - WHEN (select is_exhausted from task_status) AND (select when_failed_mode from task_status) IN ('skip', 'skip-cascade') THEN 'handler_failed' + WHEN (select is_exhausted from task_status) AND (select when_exhausted_mode from task_status) IN ('skip', 'skip-cascade') THEN 'handler_failed' ELSE pgflow.step_states.skip_reason END, skipped_at = CASE - WHEN (select is_exhausted from task_status) AND (select when_failed_mode from task_status) IN ('skip', 'skip-cascade') THEN now() + WHEN (select is_exhausted from task_status) AND (select when_exhausted_mode from task_status) IN ('skip', 'skip-cascade') THEN now() ELSE pgflow.step_states.skipped_at END, -- Clear remaining_tasks when skipping (required by remaining_tasks_state_consistency constraint) remaining_tasks = CASE - WHEN (select is_exhausted from task_status) AND (select when_failed_mode from task_status) IN ('skip', 'skip-cascade') THEN NULL + WHEN (select is_exhausted from task_status) AND (select when_exhausted_mode from task_status) IN ('skip', 'skip-cascade') THEN NULL ELSE pgflow.step_states.remaining_tasks END FROM fail_or_retry_task @@ -1307,7 +1307,7 @@ maybe_fail_step AS ( AND pgflow.step_states.step_slug = fail_task.step_slug RETURNING pgflow.step_states.* ) --- Update run status: only fail when when_failed='fail' and step was failed + -- Update run status: only fail when when_exhausted='fail' and step was failed UPDATE pgflow.runs SET status = CASE WHEN (select status from maybe_fail_step) = 'failed' THEN 'failed' @@ -1325,9 +1325,9 @@ SET status = CASE WHERE pgflow.runs.run_id = fail_task.run_id RETURNING (status = 'failed') INTO v_run_failed; --- Capture when_failed mode and check if step was skipped for later processing -SELECT s.when_failed INTO v_when_failed -FROM pgflow.steps s + -- Capture when_exhausted mode and check if step was skipped for later processing + SELECT s.when_exhausted INTO v_when_exhausted + FROM pgflow.steps s JOIN pgflow.runs r ON r.flow_slug = s.flow_slug WHERE r.run_id = fail_task.run_id AND s.step_slug = fail_task.step_slug; @@ -1360,8 +1360,8 @@ IF v_step_failed THEN ); END IF; --- Handle step skipping (when_failed = 'skip' or 'skip-cascade') -IF v_step_skipped THEN + -- Handle step skipping (when_exhausted = 'skip' or 'skip-cascade') + IF v_step_skipped THEN -- Send broadcast event for step skipped PERFORM realtime.send( jsonb_build_object( @@ -1378,8 +1378,8 @@ IF v_step_skipped THEN false ); - -- For skip-cascade: cascade skip to all downstream dependents - IF v_when_failed = 'skip-cascade' THEN + -- For skip-cascade: cascade skip to all downstream dependents + IF v_when_exhausted = 'skip-cascade' THEN PERFORM pgflow._cascade_force_skip_steps(fail_task.run_id, fail_task.step_slug, 'handler_failed'); ELSE -- For plain 'skip': decrement remaining_deps on dependent steps diff --git a/pkgs/core/supabase/migrations/atlas.sum b/pkgs/core/supabase/migrations/atlas.sum index 3ba07e792..5ea2d1d2f 100644 --- a/pkgs/core/supabase/migrations/atlas.sum +++ b/pkgs/core/supabase/migrations/atlas.sum @@ -1,4 +1,4 @@ -h1:DjkOzhpajvyHsiYEjEsthnLUW8q+BFDiffT3B06DFYw= +h1:qo2KoP5JLydE6dWgRhc/JUzwE/c46mL2xIl+K0wgYjE= 20250429164909_pgflow_initial.sql h1:I3n/tQIg5Q5nLg7RDoU3BzqHvFVjmumQxVNbXTPG15s= 20250517072017_pgflow_fix_poll_for_tasks_to_use_separate_statement_for_polling.sql h1:wTuXuwMxVniCr3ONCpodpVWJcHktoQZIbqMZ3sUHKMY= 20250609105135_pgflow_add_start_tasks_and_started_status.sql h1:ggGanW4Wyt8Kv6TWjnZ00/qVb3sm+/eFVDjGfT8qyPg= @@ -17,4 +17,4 @@ h1:DjkOzhpajvyHsiYEjEsthnLUW8q+BFDiffT3B06DFYw= 20251225163110_pgflow_add_flow_input_column.sql h1:734uCbTgKmPhTK3TY56uNYZ31T8u59yll9ea7nwtEoc= 20260103145141_pgflow_step_output_storage.sql h1:mgVHSFDLdtYy//SZ6C03j9Str1iS9xCM8Rz/wyFwn3o= 20260120205547_pgflow_requeue_stalled_tasks.sql h1:4wCBBvjtETCgJf1eXmlH5wCTKDUhiLi0uzsFG1V528E= -20260121095914_pgflow_step_conditions.sql h1:sM3dEKD2L1OIurVI/Bu8qM7orMNPty+0ID3AvpqfPqI= +20260123080735_pgflow_step_conditions.sql h1:JoGuKg+thenZEELFsp8BUwuML51d47JFlbA4BIWXn5E= diff --git a/pkgs/core/supabase/tests/add_step/condition_invalid_values.test.sql b/pkgs/core/supabase/tests/add_step/condition_invalid_values.test.sql index 17458192e..c4a1999c6 100644 --- a/pkgs/core/supabase/tests/add_step/condition_invalid_values.test.sql +++ b/pkgs/core/supabase/tests/add_step/condition_invalid_values.test.sql @@ -1,5 +1,5 @@ -- Test: add_step - Invalid condition parameter values --- Verifies CHECK constraints reject invalid when_unmet and when_failed values +-- Verifies CHECK constraints reject invalid when_unmet and when_exhausted values begin; select plan(2); @@ -13,11 +13,11 @@ select throws_ok( 'Invalid when_unmet value should be rejected' ); --- Test 2: Invalid when_failed value should fail +-- Test 2: Invalid when_exhausted value should fail select throws_ok( - $$ SELECT pgflow.add_step('invalid_test', 'bad_step2', when_failed => 'invalid_value') $$, - 'new row for relation "steps" violates check constraint "when_failed_is_valid"', - 'Invalid when_failed value should be rejected' + $$ SELECT pgflow.add_step('invalid_test', 'bad_step2', when_exhausted => 'invalid_value') $$, + 'new row for relation "steps" violates check constraint "when_exhausted_is_valid"', + 'Invalid when_exhausted value should be rejected' ); select finish(); diff --git a/pkgs/core/supabase/tests/add_step/condition_parameters.test.sql b/pkgs/core/supabase/tests/add_step/condition_parameters.test.sql index 04344ca0a..3bd4bf2da 100644 --- a/pkgs/core/supabase/tests/add_step/condition_parameters.test.sql +++ b/pkgs/core/supabase/tests/add_step/condition_parameters.test.sql @@ -1,5 +1,5 @@ -- Test: add_step - New condition parameters --- Verifies required_input_pattern, when_unmet, when_failed parameters work correctly +-- Verifies required_input_pattern, when_unmet, when_exhausted parameters work correctly begin; select plan(9); @@ -48,32 +48,32 @@ select is( 'when_unmet should be skip-cascade' ); --- Test 4: Add step with when_failed = skip +-- Test 4: Add step with when_exhausted = skip select pgflow.add_step( 'condition_test', 'step_skip_failed', - when_failed => 'skip' + when_exhausted => 'skip' ); select is( - (select when_failed from pgflow.steps + (select when_exhausted from pgflow.steps where flow_slug = 'condition_test' and step_slug = 'step_skip_failed'), 'skip', - 'when_failed should be skip' + 'when_exhausted should be skip' ); --- Test 5: Add step with when_failed = skip-cascade +-- Test 5: Add step with when_exhausted = skip-cascade select pgflow.add_step( 'condition_test', 'step_skip_cascade_failed', - when_failed => 'skip-cascade' + when_exhausted => 'skip-cascade' ); select is( - (select when_failed from pgflow.steps + (select when_exhausted from pgflow.steps where flow_slug = 'condition_test' and step_slug = 'step_skip_cascade_failed'), 'skip-cascade', - 'when_failed should be skip-cascade' + 'when_exhausted should be skip-cascade' ); -- Test 6: Default when_unmet should be skip (natural default for conditions) @@ -86,12 +86,12 @@ select is( 'Default when_unmet should be skip' ); --- Test 7: Default when_failed should be fail +-- Test 7: Default when_exhausted should be fail select is( - (select when_failed from pgflow.steps + (select when_exhausted from pgflow.steps where flow_slug = 'condition_test' and step_slug = 'step_default_unmet'), 'fail', - 'Default when_failed should be fail' + 'Default when_exhausted should be fail' ); -- Test 8: Default required_input_pattern should be NULL @@ -108,14 +108,14 @@ select pgflow.add_step( 'step_all_params', required_input_pattern => '{"status": "active"}'::jsonb, when_unmet => 'skip', - when_failed => 'skip-cascade' + when_exhausted => 'skip-cascade' ); select ok( (select required_input_pattern = '{"status": "active"}'::jsonb AND when_unmet = 'skip' - AND when_failed = 'skip-cascade' + AND when_exhausted = 'skip-cascade' from pgflow.steps where flow_slug = 'condition_test' and step_slug = 'step_all_params'), 'All condition parameters should be stored correctly together' diff --git a/pkgs/core/supabase/tests/compare_flow_shapes/condition_mode_drift.test.sql b/pkgs/core/supabase/tests/compare_flow_shapes/condition_mode_drift.test.sql index 328c29cce..bc246baa1 100644 --- a/pkgs/core/supabase/tests/compare_flow_shapes/condition_mode_drift.test.sql +++ b/pkgs/core/supabase/tests/compare_flow_shapes/condition_mode_drift.test.sql @@ -8,7 +8,7 @@ select pgflow.add_step( flow_slug => 'drift_test', step_slug => 'step1', when_unmet => 'skip', - when_failed => 'fail' + when_exhausted => 'fail' ); -- Test: Detect whenUnmet drift @@ -16,7 +16,7 @@ select is( pgflow._compare_flow_shapes( '{ "steps": [ - {"slug": "step1", "stepType": "single", "dependencies": [], "whenUnmet": "skip-cascade", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} + {"slug": "step1", "stepType": "single", "dependencies": [], "whenUnmet": "skip-cascade", "whenExhausted": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} ] }'::jsonb, pgflow._get_flow_shape('drift_test') @@ -25,33 +25,33 @@ select is( 'Should detect whenUnmet mismatch' ); --- Test: Detect whenFailed drift +-- Test: Detect whenExhausted drift select is( pgflow._compare_flow_shapes( '{ "steps": [ - {"slug": "step1", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "skip-cascade", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} + {"slug": "step1", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenExhausted": "skip-cascade", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} ] }'::jsonb, pgflow._get_flow_shape('drift_test') ), - ARRAY[$$Step at index 0: whenFailed differs 'skip-cascade' vs 'fail'$$], - 'Should detect whenFailed mismatch' + ARRAY[$$Step at index 0: whenExhausted differs 'skip-cascade' vs 'fail'$$], + 'Should detect whenExhausted mismatch' ); --- Test: Detect both whenUnmet and whenFailed drift simultaneously +-- Test: Detect both whenUnmet and whenExhausted drift simultaneously select is( pgflow._compare_flow_shapes( '{ "steps": [ - {"slug": "step1", "stepType": "single", "dependencies": [], "whenUnmet": "fail", "whenFailed": "skip", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} + {"slug": "step1", "stepType": "single", "dependencies": [], "whenUnmet": "fail", "whenExhausted": "skip", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} ] }'::jsonb, pgflow._get_flow_shape('drift_test') ), ARRAY[ $$Step at index 0: whenUnmet differs 'fail' vs 'skip'$$, - $$Step at index 0: whenFailed differs 'skip' vs 'fail'$$ + $$Step at index 0: whenExhausted differs 'skip' vs 'fail'$$ ], 'Should detect both condition mode mismatches' ); diff --git a/pkgs/core/supabase/tests/compare_flow_shapes/pattern_differences.test.sql b/pkgs/core/supabase/tests/compare_flow_shapes/pattern_differences.test.sql index fea92b812..3d17e9115 100644 --- a/pkgs/core/supabase/tests/compare_flow_shapes/pattern_differences.test.sql +++ b/pkgs/core/supabase/tests/compare_flow_shapes/pattern_differences.test.sql @@ -7,12 +7,12 @@ select is( pgflow._compare_flow_shapes( '{ "steps": [ - {"slug": "step1", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": true, "value": {"status": "active"}}, "forbiddenInputPattern": {"defined": false}} + {"slug": "step1", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenExhausted": "fail", "requiredInputPattern": {"defined": true, "value": {"status": "active"}}, "forbiddenInputPattern": {"defined": false}} ] }'::jsonb, '{ "steps": [ - {"slug": "step1", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": true, "value": {"status": "active"}}, "forbiddenInputPattern": {"defined": false}} + {"slug": "step1", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenExhausted": "fail", "requiredInputPattern": {"defined": true, "value": {"status": "active"}}, "forbiddenInputPattern": {"defined": false}} ] }'::jsonb ), @@ -25,12 +25,12 @@ select is( pgflow._compare_flow_shapes( '{ "steps": [ - {"slug": "step1", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": true, "value": {"status": "active"}}, "forbiddenInputPattern": {"defined": false}} + {"slug": "step1", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenExhausted": "fail", "requiredInputPattern": {"defined": true, "value": {"status": "active"}}, "forbiddenInputPattern": {"defined": false}} ] }'::jsonb, '{ "steps": [ - {"slug": "step1", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": true, "value": {"status": "pending"}}, "forbiddenInputPattern": {"defined": false}} + {"slug": "step1", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenExhausted": "fail", "requiredInputPattern": {"defined": true, "value": {"status": "pending"}}, "forbiddenInputPattern": {"defined": false}} ] }'::jsonb ), diff --git a/pkgs/core/supabase/tests/create_flow_from_shape/basic_compile.test.sql b/pkgs/core/supabase/tests/create_flow_from_shape/basic_compile.test.sql index e74c19675..c4deb7176 100644 --- a/pkgs/core/supabase/tests/create_flow_from_shape/basic_compile.test.sql +++ b/pkgs/core/supabase/tests/create_flow_from_shape/basic_compile.test.sql @@ -7,9 +7,9 @@ select pgflow._create_flow_from_shape( 'test_flow', '{ "steps": [ - {"slug": "first", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, - {"slug": "second", "stepType": "single", "dependencies": ["first"], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, - {"slug": "third", "stepType": "single", "dependencies": ["second"], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} + {"slug": "first", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenExhausted": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "second", "stepType": "single", "dependencies": ["first"], "whenUnmet": "skip", "whenExhausted": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "third", "stepType": "single", "dependencies": ["second"], "whenUnmet": "skip", "whenExhausted": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} ] }'::jsonb ); @@ -47,9 +47,9 @@ select is( pgflow._get_flow_shape('test_flow'), '{ "steps": [ - {"slug": "first", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, - {"slug": "second", "stepType": "single", "dependencies": ["first"], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, - {"slug": "third", "stepType": "single", "dependencies": ["second"], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} + {"slug": "first", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenExhausted": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "second", "stepType": "single", "dependencies": ["first"], "whenUnmet": "skip", "whenExhausted": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "third", "stepType": "single", "dependencies": ["second"], "whenUnmet": "skip", "whenExhausted": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} ] }'::jsonb, 'Shape should round-trip correctly' diff --git a/pkgs/core/supabase/tests/create_flow_from_shape/condition_modes_compile.test.sql b/pkgs/core/supabase/tests/create_flow_from_shape/condition_modes_compile.test.sql index 99ff0942f..87ccf654d 100644 --- a/pkgs/core/supabase/tests/create_flow_from_shape/condition_modes_compile.test.sql +++ b/pkgs/core/supabase/tests/create_flow_from_shape/condition_modes_compile.test.sql @@ -2,14 +2,14 @@ begin; select plan(4); select pgflow_tests.reset_db(); --- Test: Compile flow with non-default whenUnmet/whenFailed values +-- Test: Compile flow with non-default whenUnmet/whenExhausted values select pgflow._create_flow_from_shape( 'condition_flow', '{ "steps": [ - {"slug": "always_run", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, - {"slug": "cascade_skip", "stepType": "single", "dependencies": ["always_run"], "whenUnmet": "skip-cascade", "whenFailed": "skip", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, - {"slug": "fail_on_unmet", "stepType": "single", "dependencies": ["always_run"], "whenUnmet": "fail", "whenFailed": "skip-cascade", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} + {"slug": "always_run", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenExhausted": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "cascade_skip", "stepType": "single", "dependencies": ["always_run"], "whenUnmet": "skip-cascade", "whenExhausted": "skip", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "fail_on_unmet", "stepType": "single", "dependencies": ["always_run"], "whenUnmet": "fail", "whenExhausted": "skip-cascade", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} ] }'::jsonb ); @@ -21,11 +21,11 @@ select results_eq( 'when_unmet values should be stored correctly' ); --- Verify when_failed values were stored correctly +-- Verify when_exhausted values were stored correctly select results_eq( - $$ SELECT step_slug, when_failed FROM pgflow.steps WHERE flow_slug = 'condition_flow' ORDER BY step_index $$, + $$ SELECT step_slug, when_exhausted FROM pgflow.steps WHERE flow_slug = 'condition_flow' ORDER BY step_index $$, $$ VALUES ('always_run', 'fail'), ('cascade_skip', 'skip'), ('fail_on_unmet', 'skip-cascade') $$, - 'when_failed values should be stored correctly' + 'when_exhausted values should be stored correctly' ); -- Verify shape round-trips correctly with all condition mode variants @@ -33,9 +33,9 @@ select is( pgflow._get_flow_shape('condition_flow'), '{ "steps": [ - {"slug": "always_run", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, - {"slug": "cascade_skip", "stepType": "single", "dependencies": ["always_run"], "whenUnmet": "skip-cascade", "whenFailed": "skip", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, - {"slug": "fail_on_unmet", "stepType": "single", "dependencies": ["always_run"], "whenUnmet": "fail", "whenFailed": "skip-cascade", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} + {"slug": "always_run", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenExhausted": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "cascade_skip", "stepType": "single", "dependencies": ["always_run"], "whenUnmet": "skip-cascade", "whenExhausted": "skip", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "fail_on_unmet", "stepType": "single", "dependencies": ["always_run"], "whenUnmet": "fail", "whenExhausted": "skip-cascade", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} ] }'::jsonb, 'Shape with condition modes should round-trip correctly' @@ -47,9 +47,9 @@ select is( pgflow._get_flow_shape('condition_flow'), '{ "steps": [ - {"slug": "always_run", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, - {"slug": "cascade_skip", "stepType": "single", "dependencies": ["always_run"], "whenUnmet": "skip-cascade", "whenFailed": "skip", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, - {"slug": "fail_on_unmet", "stepType": "single", "dependencies": ["always_run"], "whenUnmet": "fail", "whenFailed": "skip-cascade", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} + {"slug": "always_run", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenExhausted": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "cascade_skip", "stepType": "single", "dependencies": ["always_run"], "whenUnmet": "skip-cascade", "whenExhausted": "skip", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "fail_on_unmet", "stepType": "single", "dependencies": ["always_run"], "whenUnmet": "fail", "whenExhausted": "skip-cascade", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} ] }'::jsonb ), diff --git a/pkgs/core/supabase/tests/create_flow_from_shape/map_step_compile.test.sql b/pkgs/core/supabase/tests/create_flow_from_shape/map_step_compile.test.sql index 0c71d3a6d..37433656a 100644 --- a/pkgs/core/supabase/tests/create_flow_from_shape/map_step_compile.test.sql +++ b/pkgs/core/supabase/tests/create_flow_from_shape/map_step_compile.test.sql @@ -7,8 +7,8 @@ select pgflow._create_flow_from_shape( 'map_flow', '{ "steps": [ - {"slug": "root_map", "stepType": "map", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, - {"slug": "process", "stepType": "single", "dependencies": ["root_map"], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} + {"slug": "root_map", "stepType": "map", "dependencies": [], "whenUnmet": "skip", "whenExhausted": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "process", "stepType": "single", "dependencies": ["root_map"], "whenUnmet": "skip", "whenExhausted": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} ] }'::jsonb ); @@ -25,8 +25,8 @@ select is( pgflow._get_flow_shape('map_flow'), '{ "steps": [ - {"slug": "root_map", "stepType": "map", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, - {"slug": "process", "stepType": "single", "dependencies": ["root_map"], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} + {"slug": "root_map", "stepType": "map", "dependencies": [], "whenUnmet": "skip", "whenExhausted": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "process", "stepType": "single", "dependencies": ["root_map"], "whenUnmet": "skip", "whenExhausted": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} ] }'::jsonb, 'Shape should round-trips correctly' diff --git a/pkgs/core/supabase/tests/create_flow_from_shape/options_compile.test.sql b/pkgs/core/supabase/tests/create_flow_from_shape/options_compile.test.sql index 72a1542b6..4e1972669 100644 --- a/pkgs/core/supabase/tests/create_flow_from_shape/options_compile.test.sql +++ b/pkgs/core/supabase/tests/create_flow_from_shape/options_compile.test.sql @@ -7,7 +7,7 @@ select pgflow._create_flow_from_shape( 'flow_with_options', '{ "steps": [ - {"slug": "step1", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail"} + {"slug": "step1", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenExhausted": "fail"} ], "options": { "maxAttempts": 5, @@ -34,7 +34,7 @@ select pgflow._create_flow_from_shape( "stepType": "single", "dependencies": [], "whenUnmet": "skip", - "whenFailed": "fail", + "whenExhausted": "fail", "options": { "maxAttempts": 7, "baseDelay": 15, @@ -58,7 +58,7 @@ select pgflow._create_flow_from_shape( 'flow_no_options', '{ "steps": [ - {"slug": "step1", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail"} + {"slug": "step1", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenExhausted": "fail"} ] }'::jsonb ); @@ -87,7 +87,7 @@ select pgflow._create_flow_from_shape( "stepType": "single", "dependencies": [], "whenUnmet": "skip", - "whenFailed": "fail", + "whenExhausted": "fail", "options": { "timeout": 30 } diff --git a/pkgs/core/supabase/tests/ensure_flow_compiled/allow_data_loss_recompiles.test.sql b/pkgs/core/supabase/tests/ensure_flow_compiled/allow_data_loss_recompiles.test.sql index 41cd12f81..c6c54c400 100644 --- a/pkgs/core/supabase/tests/ensure_flow_compiled/allow_data_loss_recompiles.test.sql +++ b/pkgs/core/supabase/tests/ensure_flow_compiled/allow_data_loss_recompiles.test.sql @@ -17,7 +17,7 @@ select is( 'allow_loss_flow', '{ "steps": [ - {"slug": "new_step", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail"} + {"slug": "new_step", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenExhausted": "fail"} ] }'::jsonb, true -- allow_data_loss = true diff --git a/pkgs/core/supabase/tests/ensure_flow_compiled/auto_recompiles_when_local.test.sql b/pkgs/core/supabase/tests/ensure_flow_compiled/auto_recompiles_when_local.test.sql index 895dc70b6..3e394615b 100644 --- a/pkgs/core/supabase/tests/ensure_flow_compiled/auto_recompiles_when_local.test.sql +++ b/pkgs/core/supabase/tests/ensure_flow_compiled/auto_recompiles_when_local.test.sql @@ -17,7 +17,7 @@ select is( 'local_flow', '{ "steps": [ - {"slug": "new_step", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail"} + {"slug": "new_step", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenExhausted": "fail"} ] }'::jsonb ) as result diff --git a/pkgs/core/supabase/tests/ensure_flow_compiled/compiles_missing_flow.test.sql b/pkgs/core/supabase/tests/ensure_flow_compiled/compiles_missing_flow.test.sql index 1d083066c..203e2e891 100644 --- a/pkgs/core/supabase/tests/ensure_flow_compiled/compiles_missing_flow.test.sql +++ b/pkgs/core/supabase/tests/ensure_flow_compiled/compiles_missing_flow.test.sql @@ -10,7 +10,7 @@ select is( 'new_flow', '{ "steps": [ - {"slug": "first", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail"} + {"slug": "first", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenExhausted": "fail"} ] }'::jsonb ) as result diff --git a/pkgs/core/supabase/tests/ensure_flow_compiled/verifies_matching_shape.test.sql b/pkgs/core/supabase/tests/ensure_flow_compiled/verifies_matching_shape.test.sql index 34738ac94..206bbc314 100644 --- a/pkgs/core/supabase/tests/ensure_flow_compiled/verifies_matching_shape.test.sql +++ b/pkgs/core/supabase/tests/ensure_flow_compiled/verifies_matching_shape.test.sql @@ -15,8 +15,8 @@ select is( 'existing_flow', '{ "steps": [ - {"slug": "first", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, - {"slug": "second", "stepType": "single", "dependencies": ["first"], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} + {"slug": "first", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenExhausted": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "second", "stepType": "single", "dependencies": ["first"], "whenUnmet": "skip", "whenExhausted": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} ] }'::jsonb ) as result @@ -33,8 +33,8 @@ select is( 'existing_flow', '{ "steps": [ - {"slug": "first", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, - {"slug": "second", "stepType": "single", "dependencies": ["first"], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} + {"slug": "first", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenExhausted": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "second", "stepType": "single", "dependencies": ["first"], "whenUnmet": "skip", "whenExhausted": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} ] }'::jsonb ) as result diff --git a/pkgs/core/supabase/tests/fail_task_when_failed/skip_decrements_remaining_deps.test.sql b/pkgs/core/supabase/tests/fail_task_when_exhausted/skip_decrements_remaining_deps.test.sql similarity index 85% rename from pkgs/core/supabase/tests/fail_task_when_failed/skip_decrements_remaining_deps.test.sql rename to pkgs/core/supabase/tests/fail_task_when_exhausted/skip_decrements_remaining_deps.test.sql index c0d221d08..72c781acf 100644 --- a/pkgs/core/supabase/tests/fail_task_when_failed/skip_decrements_remaining_deps.test.sql +++ b/pkgs/core/supabase/tests/fail_task_when_exhausted/skip_decrements_remaining_deps.test.sql @@ -1,11 +1,11 @@ --- Test: when_failed='skip' (non-cascade) should decrement remaining_deps on dependent steps +-- Test: when_exhausted='skip' (non-cascade) should decrement remaining_deps on dependent steps -- This mirrors the behavior in cascade_resolve_conditions.sql for when_unmet='skip' -- -- Flow structure: --- step_a (when_failed='skip', max_attempts=0) → step_b +-- step_a (when_exhausted='skip', max_attempts=0) → step_b -- -- Expected behavior: --- 1. step_a fails, gets skipped (when_failed='skip') +-- 1. step_a fails, gets skipped (when_exhausted='skip') -- 2. step_b.remaining_deps decremented from 1 to 0 -- 3. step_b becomes ready and starts -- 4. Run continues (status != 'failed') @@ -14,9 +14,9 @@ begin; select plan(5); select pgflow_tests.reset_db(); --- Create flow with step_a → step_b where step_a has when_failed='skip' and max_attempts=0 +-- Create flow with step_a → step_b where step_a has when_exhausted='skip' and max_attempts=0 select pgflow.create_flow('skip_test'); -select pgflow.add_step('skip_test', 'step_a', max_attempts => 0, when_failed => 'skip'); +select pgflow.add_step('skip_test', 'step_a', max_attempts => 0, when_exhausted => 'skip'); select pgflow.add_step('skip_test', 'step_b', array['step_a']); -- Start the flow diff --git a/pkgs/core/supabase/tests/fail_task_when_failed/skip_diamond_multiple_dependents.test.sql b/pkgs/core/supabase/tests/fail_task_when_exhausted/skip_diamond_multiple_dependents.test.sql similarity index 92% rename from pkgs/core/supabase/tests/fail_task_when_failed/skip_diamond_multiple_dependents.test.sql rename to pkgs/core/supabase/tests/fail_task_when_exhausted/skip_diamond_multiple_dependents.test.sql index 0703908e9..33effbd82 100644 --- a/pkgs/core/supabase/tests/fail_task_when_failed/skip_diamond_multiple_dependents.test.sql +++ b/pkgs/core/supabase/tests/fail_task_when_exhausted/skip_diamond_multiple_dependents.test.sql @@ -1,7 +1,7 @@ --- Test: when_failed='skip' decrements remaining_deps on MULTIPLE dependent steps +-- Test: when_exhausted='skip' decrements remaining_deps on MULTIPLE dependent steps -- -- Flow structure (diamond pattern): --- step_a (when_failed='skip', max_attempts=0) +-- step_a (when_exhausted='skip', max_attempts=0) -- ├── step_b (depends on step_a) -- └── step_c (depends on step_a) -- @@ -16,7 +16,7 @@ select pgflow_tests.reset_db(); -- Create diamond flow: step_a -> step_b, step_a -> step_c select pgflow.create_flow('diamond_skip'); -select pgflow.add_step('diamond_skip', 'step_a', max_attempts => 0, when_failed => 'skip'); +select pgflow.add_step('diamond_skip', 'step_a', max_attempts => 0, when_exhausted => 'skip'); select pgflow.add_step('diamond_skip', 'step_b', array['step_a']); select pgflow.add_step('diamond_skip', 'step_c', array['step_a']); diff --git a/pkgs/core/supabase/tests/fail_task_when_failed/skip_only_step_completes_run.test.sql b/pkgs/core/supabase/tests/fail_task_when_exhausted/skip_only_step_completes_run.test.sql similarity index 88% rename from pkgs/core/supabase/tests/fail_task_when_failed/skip_only_step_completes_run.test.sql rename to pkgs/core/supabase/tests/fail_task_when_exhausted/skip_only_step_completes_run.test.sql index 9f82d53eb..d4e3d0ffe 100644 --- a/pkgs/core/supabase/tests/fail_task_when_failed/skip_only_step_completes_run.test.sql +++ b/pkgs/core/supabase/tests/fail_task_when_exhausted/skip_only_step_completes_run.test.sql @@ -1,7 +1,7 @@ --- Test: when_failed='skip' on the only step should complete the run +-- Test: when_exhausted='skip' on the only step should complete the run -- -- Flow structure: --- step_a (when_failed='skip', max_attempts=0) - only step in flow +-- step_a (when_exhausted='skip', max_attempts=0) - only step in flow -- -- Expected behavior: -- 1. step_a fails and gets skipped @@ -14,7 +14,7 @@ select pgflow_tests.reset_db(); -- Create flow with single step select pgflow.create_flow('single_skip'); -select pgflow.add_step('single_skip', 'step_a', max_attempts => 0, when_failed => 'skip'); +select pgflow.add_step('single_skip', 'step_a', max_attempts => 0, when_exhausted => 'skip'); -- Start the flow select pgflow.start_flow('single_skip', '"input"'::jsonb); diff --git a/pkgs/core/supabase/tests/fail_task_when_failed/skip_partial_deps_waits.test.sql b/pkgs/core/supabase/tests/fail_task_when_exhausted/skip_partial_deps_waits.test.sql similarity index 92% rename from pkgs/core/supabase/tests/fail_task_when_failed/skip_partial_deps_waits.test.sql rename to pkgs/core/supabase/tests/fail_task_when_exhausted/skip_partial_deps_waits.test.sql index f265b56da..e8fdb05b5 100644 --- a/pkgs/core/supabase/tests/fail_task_when_failed/skip_partial_deps_waits.test.sql +++ b/pkgs/core/supabase/tests/fail_task_when_exhausted/skip_partial_deps_waits.test.sql @@ -1,7 +1,7 @@ --- Test: when_failed='skip' decrements remaining_deps but step waits if other deps remain +-- Test: when_exhausted='skip' decrements remaining_deps but step waits if other deps remain -- -- Flow structure: --- step_a (when_failed='skip', max_attempts=0) ─┐ +-- step_a (when_exhausted='skip', max_attempts=0) ─┐ -- step_b ───────────────────────────────────────┼──> step_c (depends on both) -- -- Expected behavior: @@ -16,7 +16,7 @@ select pgflow_tests.reset_db(); -- Create flow: step_a + step_b -> step_c select pgflow.create_flow('partial_skip'); -select pgflow.add_step('partial_skip', 'step_a', max_attempts => 0, when_failed => 'skip'); +select pgflow.add_step('partial_skip', 'step_a', max_attempts => 0, when_exhausted => 'skip'); select pgflow.add_step('partial_skip', 'step_b'); select pgflow.add_step('partial_skip', 'step_c', array['step_a', 'step_b']); diff --git a/pkgs/core/supabase/tests/fail_task_when_failed/skip_propagates_to_map_step.test.sql b/pkgs/core/supabase/tests/fail_task_when_exhausted/skip_propagates_to_map_step.test.sql similarity index 93% rename from pkgs/core/supabase/tests/fail_task_when_failed/skip_propagates_to_map_step.test.sql rename to pkgs/core/supabase/tests/fail_task_when_exhausted/skip_propagates_to_map_step.test.sql index eb6d4bc58..633a7d68f 100644 --- a/pkgs/core/supabase/tests/fail_task_when_failed/skip_propagates_to_map_step.test.sql +++ b/pkgs/core/supabase/tests/fail_task_when_exhausted/skip_propagates_to_map_step.test.sql @@ -1,11 +1,11 @@ --- Test: when_failed='skip' propagates correctly to map step dependent +-- Test: when_exhausted='skip' propagates correctly to map step dependent -- -- This mirrors the behavior of when_unmet='skip' for conditions: -- - Map step with skipped dependency gets initial_tasks=0 -- - Map step auto-completes with output=[] -- -- Flow structure: --- producer (when_failed='skip', max_attempts=0) → map_consumer (map step) +-- producer (when_exhausted='skip', max_attempts=0) → map_consumer (map step) -- -- Expected behavior: -- 1. producer fails and gets skipped @@ -20,7 +20,7 @@ select pgflow_tests.reset_db(); -- Create flow with producer -> map_consumer select pgflow.create_flow('map_skip_test'); -select pgflow.add_step('map_skip_test', 'producer', max_attempts => 0, when_failed => 'skip'); +select pgflow.add_step('map_skip_test', 'producer', max_attempts => 0, when_exhausted => 'skip'); -- Map consumer: step_type='map' handles empty array from skipped producer select pgflow.add_step('map_skip_test', 'map_consumer', array['producer'], step_type => 'map'); diff --git a/pkgs/core/supabase/tests/fail_task_when_failed/skip_verifies_handler_failed_reason.test.sql b/pkgs/core/supabase/tests/fail_task_when_exhausted/skip_verifies_handler_failed_reason.test.sql similarity index 91% rename from pkgs/core/supabase/tests/fail_task_when_failed/skip_verifies_handler_failed_reason.test.sql rename to pkgs/core/supabase/tests/fail_task_when_exhausted/skip_verifies_handler_failed_reason.test.sql index 3a1d7c21c..739136e27 100644 --- a/pkgs/core/supabase/tests/fail_task_when_failed/skip_verifies_handler_failed_reason.test.sql +++ b/pkgs/core/supabase/tests/fail_task_when_exhausted/skip_verifies_handler_failed_reason.test.sql @@ -1,9 +1,9 @@ --- Test: when_failed='skip' sets skip_reason to 'handler_failed' +-- Test: when_exhausted='skip' sets skip_reason to 'handler_failed' -- -- This distinguishes handler-failure skips from condition-unmet skips. -- -- Flow structure: --- step_a (when_failed='skip', max_attempts=0) → step_b +-- step_a (when_exhausted='skip', max_attempts=0) → step_b -- -- Expected behavior: -- 1. step_a fails and gets skipped @@ -17,7 +17,7 @@ select pgflow_tests.reset_db(); -- Create flow select pgflow.create_flow('skip_reason_test'); -select pgflow.add_step('skip_reason_test', 'step_a', max_attempts => 0, when_failed => 'skip'); +select pgflow.add_step('skip_reason_test', 'step_a', max_attempts => 0, when_exhausted => 'skip'); select pgflow.add_step('skip_reason_test', 'step_b', array['step_a']); -- Start the flow diff --git a/pkgs/core/supabase/tests/fail_task_when_failed/type_violation_always_hard_fails.test.sql b/pkgs/core/supabase/tests/fail_task_when_exhausted/type_violation_always_hard_fails.test.sql similarity index 81% rename from pkgs/core/supabase/tests/fail_task_when_failed/type_violation_always_hard_fails.test.sql rename to pkgs/core/supabase/tests/fail_task_when_exhausted/type_violation_always_hard_fails.test.sql index 6d5e642a4..b7d2029b1 100644 --- a/pkgs/core/supabase/tests/fail_task_when_failed/type_violation_always_hard_fails.test.sql +++ b/pkgs/core/supabase/tests/fail_task_when_exhausted/type_violation_always_hard_fails.test.sql @@ -1,14 +1,14 @@ --- Test: TYPE_VIOLATION in complete_task always hard fails regardless of when_failed +-- Test: TYPE_VIOLATION in complete_task always hard fails regardless of when_exhausted -- TYPE_VIOLATION is a programming error (wrong return type), not a runtime condition --- It should always cause the run to fail, even with when_failed='skip' or 'skip-cascade' +-- It should always cause the run to fail, even with when_exhausted='skip' or 'skip-cascade' begin; select plan(4); select pgflow_tests.reset_db(); -- SETUP: Create a flow where step_a feeds into a map step --- step_a has when_failed='skip-cascade' but TYPE_VIOLATION should override this +-- step_a has when_exhausted='skip-cascade' but TYPE_VIOLATION should override this select pgflow.create_flow('test_flow'); -select pgflow.add_step('test_flow', 'step_a', when_failed => 'skip-cascade'); +select pgflow.add_step('test_flow', 'step_a', when_exhausted => 'skip-cascade'); select pgflow.add_step('test_flow', 'step_b', ARRAY['step_a'], step_type => 'map'); -- Start flow @@ -29,7 +29,7 @@ select pgflow.complete_task( select is( (select status from pgflow.step_states where flow_slug = 'test_flow' and step_slug = 'step_a'), 'failed', - 'step_a should be failed on TYPE_VIOLATION (not skipped despite when_failed=skip-cascade)' + 'step_a should be failed on TYPE_VIOLATION (not skipped despite when_exhausted=skip-cascade)' ); -- TEST 2: error_message should contain TYPE_VIOLATION @@ -43,7 +43,7 @@ select ok( select is( (select status from pgflow.runs where flow_slug = 'test_flow'), 'failed', - 'Run should be failed on TYPE_VIOLATION regardless of when_failed setting' + 'Run should be failed on TYPE_VIOLATION regardless of when_exhausted setting' ); -- TEST 4: step_b should NOT be skipped (run failed before cascade could happen) diff --git a/pkgs/core/supabase/tests/fail_task_when_failed/when_failed_fail_marks_run_failed.test.sql b/pkgs/core/supabase/tests/fail_task_when_exhausted/when_exhausted_fail_marks_run_failed.test.sql similarity index 78% rename from pkgs/core/supabase/tests/fail_task_when_failed/when_failed_fail_marks_run_failed.test.sql rename to pkgs/core/supabase/tests/fail_task_when_exhausted/when_exhausted_fail_marks_run_failed.test.sql index 057581b3b..f4ad7da44 100644 --- a/pkgs/core/supabase/tests/fail_task_when_failed/when_failed_fail_marks_run_failed.test.sql +++ b/pkgs/core/supabase/tests/fail_task_when_exhausted/when_exhausted_fail_marks_run_failed.test.sql @@ -1,10 +1,10 @@ --- Test: fail_task with when_failed='fail' (default) marks run as failed +-- Test: fail_task with when_exhausted='fail' (default) marks run as failed -- This is the current behavior and should remain unchanged begin; select plan(3); select pgflow_tests.reset_db(); --- SETUP: Create a flow with default when_failed='fail' (0 retries so it fails immediately) +-- SETUP: Create a flow with default when_exhausted='fail' (0 retries so it fails immediately) select pgflow.create_flow('test_flow'); select pgflow.add_step('test_flow', 'step_a', max_attempts => 0); @@ -30,7 +30,7 @@ select is( select is( (select status from pgflow.runs where flow_slug = 'test_flow'), 'failed', - 'Run should be marked as failed when when_failed=fail (default)' + 'Run should be marked as failed when when_exhausted=fail (default)' ); select finish(); diff --git a/pkgs/core/supabase/tests/fail_task_when_failed/when_failed_skip_cascade_skips_dependents.test.sql b/pkgs/core/supabase/tests/fail_task_when_exhausted/when_exhausted_skip_cascade_skips_dependents.test.sql similarity index 86% rename from pkgs/core/supabase/tests/fail_task_when_failed/when_failed_skip_cascade_skips_dependents.test.sql rename to pkgs/core/supabase/tests/fail_task_when_exhausted/when_exhausted_skip_cascade_skips_dependents.test.sql index ea0ecef9a..8e6ecadbe 100644 --- a/pkgs/core/supabase/tests/fail_task_when_failed/when_failed_skip_cascade_skips_dependents.test.sql +++ b/pkgs/core/supabase/tests/fail_task_when_exhausted/when_exhausted_skip_cascade_skips_dependents.test.sql @@ -1,12 +1,12 @@ --- Test: fail_task with when_failed='skip-cascade' skips step and cascades to dependents +-- Test: fail_task with when_exhausted='skip-cascade' skips step and cascades to dependents begin; select plan(7); select pgflow_tests.reset_db(); --- SETUP: Create a flow with when_failed='skip-cascade' +-- SETUP: Create a flow with when_exhausted='skip-cascade' -- step_a (will fail) -> step_b (depends on a) -> step_c (depends on b) select pgflow.create_flow('test_flow'); -select pgflow.add_step('test_flow', 'step_a', max_attempts => 0, when_failed => 'skip-cascade'); +select pgflow.add_step('test_flow', 'step_a', max_attempts => 0, when_exhausted => 'skip-cascade'); select pgflow.add_step('test_flow', 'step_b', ARRAY['step_a']); select pgflow.add_step('test_flow', 'step_c', ARRAY['step_b']); select pgflow.add_step('test_flow', 'step_d'); -- Independent step to verify run continues @@ -19,7 +19,7 @@ select pgflow_tests.poll_and_fail('test_flow'); select is( (select status from pgflow.step_states where flow_slug = 'test_flow' and step_slug = 'step_a'), 'skipped', - 'step_a should be marked as skipped when when_failed=skip-cascade' + 'step_a should be marked as skipped when when_exhausted=skip-cascade' ); -- TEST 2: step_a skip reason should be handler_failed @@ -61,7 +61,7 @@ select is( select isnt( (select status from pgflow.runs where flow_slug = 'test_flow'), 'failed', - 'Run should NOT be marked as failed when when_failed=skip-cascade' + 'Run should NOT be marked as failed when when_exhausted=skip-cascade' ); select finish(); diff --git a/pkgs/core/supabase/tests/fail_task_when_failed/when_failed_skip_skips_step.test.sql b/pkgs/core/supabase/tests/fail_task_when_exhausted/when_exhausted_skip_skips_step.test.sql similarity index 82% rename from pkgs/core/supabase/tests/fail_task_when_failed/when_failed_skip_skips_step.test.sql rename to pkgs/core/supabase/tests/fail_task_when_exhausted/when_exhausted_skip_skips_step.test.sql index 08d029cf1..d4f686c74 100644 --- a/pkgs/core/supabase/tests/fail_task_when_failed/when_failed_skip_skips_step.test.sql +++ b/pkgs/core/supabase/tests/fail_task_when_exhausted/when_exhausted_skip_skips_step.test.sql @@ -1,11 +1,11 @@ --- Test: fail_task with when_failed='skip' skips the step and continues run +-- Test: fail_task with when_exhausted='skip' skips the step and continues run begin; select plan(5); select pgflow_tests.reset_db(); --- SETUP: Create a flow with when_failed='skip' (0 retries so it fails immediately) +-- SETUP: Create a flow with when_exhausted='skip' (0 retries so it fails immediately) select pgflow.create_flow('test_flow'); -select pgflow.add_step('test_flow', 'step_a', max_attempts => 0, when_failed => 'skip'); +select pgflow.add_step('test_flow', 'step_a', max_attempts => 0, when_exhausted => 'skip'); select pgflow.add_step('test_flow', 'step_b'); -- Independent step to verify run continues -- Start flow and fail step_a's task @@ -23,7 +23,7 @@ select is( select is( (select status from pgflow.step_states where flow_slug = 'test_flow' and step_slug = 'step_a'), 'skipped', - 'Step should be marked as skipped when when_failed=skip' + 'Step should be marked as skipped when when_exhausted=skip' ); -- TEST 3: Skip reason should indicate handler failure @@ -37,7 +37,7 @@ select is( select isnt( (select status from pgflow.runs where flow_slug = 'test_flow'), 'failed', - 'Run should NOT be marked as failed when when_failed=skip' + 'Run should NOT be marked as failed when when_exhausted=skip' ); -- TEST 5: Error message should be preserved in step_states diff --git a/pkgs/core/supabase/tests/get_flow_shape/basic_shape.test.sql b/pkgs/core/supabase/tests/get_flow_shape/basic_shape.test.sql index 840af07c0..6dbc169a4 100644 --- a/pkgs/core/supabase/tests/get_flow_shape/basic_shape.test.sql +++ b/pkgs/core/supabase/tests/get_flow_shape/basic_shape.test.sql @@ -13,9 +13,9 @@ select is( pgflow._get_flow_shape('test_flow'), '{ "steps": [ - {"slug": "first", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, - {"slug": "second", "stepType": "single", "dependencies": ["first"], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, - {"slug": "third", "stepType": "single", "dependencies": ["second"], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} + {"slug": "first", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenExhausted": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "second", "stepType": "single", "dependencies": ["first"], "whenUnmet": "skip", "whenExhausted": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "third", "stepType": "single", "dependencies": ["second"], "whenUnmet": "skip", "whenExhausted": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} ] }'::jsonb, 'Should return correct shape for simple sequential flow' diff --git a/pkgs/core/supabase/tests/get_flow_shape/map_steps.test.sql b/pkgs/core/supabase/tests/get_flow_shape/map_steps.test.sql index 67c26e206..03f39f0cf 100644 --- a/pkgs/core/supabase/tests/get_flow_shape/map_steps.test.sql +++ b/pkgs/core/supabase/tests/get_flow_shape/map_steps.test.sql @@ -20,8 +20,8 @@ select is( pgflow._get_flow_shape('map_flow'), '{ "steps": [ - {"slug": "root_map", "stepType": "map", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, - {"slug": "process", "stepType": "single", "dependencies": ["root_map"], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} + {"slug": "root_map", "stepType": "map", "dependencies": [], "whenUnmet": "skip", "whenExhausted": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "process", "stepType": "single", "dependencies": ["root_map"], "whenUnmet": "skip", "whenExhausted": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} ] }'::jsonb, 'Should correctly identify map step type' diff --git a/pkgs/core/supabase/tests/get_flow_shape/multiple_deps_sorted.test.sql b/pkgs/core/supabase/tests/get_flow_shape/multiple_deps_sorted.test.sql index 099275036..adcf73f1a 100644 --- a/pkgs/core/supabase/tests/get_flow_shape/multiple_deps_sorted.test.sql +++ b/pkgs/core/supabase/tests/get_flow_shape/multiple_deps_sorted.test.sql @@ -16,10 +16,10 @@ select is( pgflow._get_flow_shape('multi_deps'), '{ "steps": [ - {"slug": "alpha", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, - {"slug": "beta", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, - {"slug": "gamma", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, - {"slug": "final", "stepType": "single", "dependencies": ["alpha", "beta", "gamma"], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} + {"slug": "alpha", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenExhausted": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "beta", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenExhausted": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "gamma", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenExhausted": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "final", "stepType": "single", "dependencies": ["alpha", "beta", "gamma"], "whenUnmet": "skip", "whenExhausted": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": false}} ] }'::jsonb, 'Dependencies should be sorted alphabetically' diff --git a/pkgs/core/supabase/tests/get_flow_shape/pattern_shape.test.sql b/pkgs/core/supabase/tests/get_flow_shape/pattern_shape.test.sql index 481d15a1e..8b415eaa5 100644 --- a/pkgs/core/supabase/tests/get_flow_shape/pattern_shape.test.sql +++ b/pkgs/core/supabase/tests/get_flow_shape/pattern_shape.test.sql @@ -13,9 +13,9 @@ select is( pgflow._get_flow_shape('test_flow'), '{ "steps": [ - {"slug": "step_with_if", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": true, "value": {"status": "active"}}, "forbiddenInputPattern": {"defined": false}}, - {"slug": "step_with_ifnot", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": true, "value": {"type": "deleted"}}}, - {"slug": "step_with_both", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenFailed": "fail", "requiredInputPattern": {"defined": true, "value": {"status": "active"}}, "forbiddenInputPattern": {"defined": true, "value": {"type": "archived"}}} + {"slug": "step_with_if", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenExhausted": "fail", "requiredInputPattern": {"defined": true, "value": {"status": "active"}}, "forbiddenInputPattern": {"defined": false}}, + {"slug": "step_with_ifnot", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenExhausted": "fail", "requiredInputPattern": {"defined": false}, "forbiddenInputPattern": {"defined": true, "value": {"type": "deleted"}}}, + {"slug": "step_with_both", "stepType": "single", "dependencies": [], "whenUnmet": "skip", "whenExhausted": "fail", "requiredInputPattern": {"defined": true, "value": {"status": "active"}}, "forbiddenInputPattern": {"defined": true, "value": {"type": "archived"}}} ] }'::jsonb, 'Should return correct shape with pattern conditions' diff --git a/pkgs/dsl/__tests__/runtime/flow-shape.test.ts b/pkgs/dsl/__tests__/runtime/flow-shape.test.ts index 50691a4b2..d5af897c6 100644 --- a/pkgs/dsl/__tests__/runtime/flow-shape.test.ts +++ b/pkgs/dsl/__tests__/runtime/flow-shape.test.ts @@ -62,7 +62,7 @@ describe('extractFlowShape', () => { stepType: 'single', dependencies: [], whenUnmet: 'skip', - whenFailed: 'fail', + whenExhausted: 'fail', requiredInputPattern: { defined: false }, forbiddenInputPattern: { defined: false }, }); @@ -113,7 +113,7 @@ describe('extractFlowShape', () => { stepType: 'single', dependencies: [], whenUnmet: 'skip', - whenFailed: 'fail', + whenExhausted: 'fail', requiredInputPattern: { defined: false }, forbiddenInputPattern: { defined: false }, options: { @@ -138,7 +138,7 @@ describe('extractFlowShape', () => { stepType: 'single', dependencies: [], whenUnmet: 'skip', - whenFailed: 'fail', + whenExhausted: 'fail', requiredInputPattern: { defined: false }, forbiddenInputPattern: { defined: false }, }); @@ -173,7 +173,7 @@ describe('extractFlowShape', () => { stepType: 'map', dependencies: [], whenUnmet: 'skip', - whenFailed: 'fail', + whenExhausted: 'fail', requiredInputPattern: { defined: false }, forbiddenInputPattern: { defined: false }, }); @@ -192,7 +192,7 @@ describe('extractFlowShape', () => { stepType: 'map', dependencies: ['get_items'], whenUnmet: 'skip', - whenFailed: 'fail', + whenExhausted: 'fail', requiredInputPattern: { defined: false }, forbiddenInputPattern: { defined: false }, }); @@ -235,7 +235,7 @@ describe('extractFlowShape', () => { stepType: 'single', dependencies: [], whenUnmet: 'skip', - whenFailed: 'fail', + whenExhausted: 'fail', requiredInputPattern: { defined: false }, forbiddenInputPattern: { defined: false }, }, @@ -244,7 +244,7 @@ describe('extractFlowShape', () => { stepType: 'single', dependencies: ['website'], whenUnmet: 'skip', - whenFailed: 'fail', + whenExhausted: 'fail', requiredInputPattern: { defined: false }, forbiddenInputPattern: { defined: false }, options: { @@ -257,7 +257,7 @@ describe('extractFlowShape', () => { stepType: 'single', dependencies: ['website'], whenUnmet: 'skip', - whenFailed: 'fail', + whenExhausted: 'fail', requiredInputPattern: { defined: false }, forbiddenInputPattern: { defined: false }, }, @@ -266,7 +266,7 @@ describe('extractFlowShape', () => { stepType: 'single', dependencies: ['sentiment', 'summary'], // sorted alphabetically whenUnmet: 'skip', - whenFailed: 'fail', + whenExhausted: 'fail', requiredInputPattern: { defined: false }, forbiddenInputPattern: { defined: false }, }, @@ -306,7 +306,7 @@ describe('extractFlowShape', () => { stepType: 'single', dependencies: [], whenUnmet: 'skip', - whenFailed: 'fail', + whenExhausted: 'fail', requiredInputPattern: { defined: true, value: { status: 'active' } }, forbiddenInputPattern: { defined: false }, }); @@ -324,9 +324,12 @@ describe('extractFlowShape', () => { stepType: 'single', dependencies: [], whenUnmet: 'skip', - whenFailed: 'fail', + whenExhausted: 'fail', requiredInputPattern: { defined: false }, - forbiddenInputPattern: { defined: true, value: { status: 'deleted' } }, + forbiddenInputPattern: { + defined: true, + value: { status: 'deleted' }, + }, }); }); @@ -348,7 +351,7 @@ describe('extractFlowShape', () => { stepType: 'single', dependencies: [], whenUnmet: 'skip', - whenFailed: 'fail', + whenExhausted: 'fail', requiredInputPattern: { defined: true, value: { status: 'active' } }, forbiddenInputPattern: { defined: true, value: { type: 'archived' } }, }); @@ -366,7 +369,7 @@ describe('extractFlowShape', () => { stepType: 'single', dependencies: [], whenUnmet: 'skip', - whenFailed: 'fail', + whenExhausted: 'fail', requiredInputPattern: { defined: false }, forbiddenInputPattern: { defined: false }, }); @@ -388,7 +391,7 @@ describe('compareFlowShapes', () => { stepType: 'single', dependencies: [], whenUnmet: 'skip', - whenFailed: 'fail', + whenExhausted: 'fail', requiredInputPattern: { defined: false }, forbiddenInputPattern: { defined: false }, }, @@ -423,7 +426,7 @@ describe('compareFlowShapes', () => { stepType: 'single', dependencies: [], whenUnmet: 'skip', - whenFailed: 'fail', + whenExhausted: 'fail', requiredInputPattern: { defined: false }, forbiddenInputPattern: { defined: false }, }, @@ -446,7 +449,7 @@ describe('compareFlowShapes', () => { stepType: 'single', dependencies: [], whenUnmet: 'skip', - whenFailed: 'fail', + whenExhausted: 'fail', requiredInputPattern: { defined: false }, forbiddenInputPattern: { defined: false }, }, @@ -470,7 +473,7 @@ describe('compareFlowShapes', () => { stepType: 'single', dependencies: [], whenUnmet: 'skip', - whenFailed: 'fail', + whenExhausted: 'fail', requiredInputPattern: { defined: false }, forbiddenInputPattern: { defined: false }, }, @@ -479,7 +482,7 @@ describe('compareFlowShapes', () => { stepType: 'single', dependencies: [], whenUnmet: 'skip', - whenFailed: 'fail', + whenExhausted: 'fail', requiredInputPattern: { defined: false }, forbiddenInputPattern: { defined: false }, }, @@ -492,7 +495,7 @@ describe('compareFlowShapes', () => { stepType: 'single', dependencies: [], whenUnmet: 'skip', - whenFailed: 'fail', + whenExhausted: 'fail', requiredInputPattern: { defined: false }, forbiddenInputPattern: { defined: false }, }, @@ -501,7 +504,7 @@ describe('compareFlowShapes', () => { stepType: 'single', dependencies: [], whenUnmet: 'skip', - whenFailed: 'fail', + whenExhausted: 'fail', requiredInputPattern: { defined: false }, forbiddenInputPattern: { defined: false }, }, @@ -528,7 +531,7 @@ describe('compareFlowShapes', () => { stepType: 'single', dependencies: [], whenUnmet: 'skip', - whenFailed: 'fail', + whenExhausted: 'fail', requiredInputPattern: { defined: false }, forbiddenInputPattern: { defined: false }, }, @@ -537,7 +540,7 @@ describe('compareFlowShapes', () => { stepType: 'single', dependencies: [], whenUnmet: 'skip', - whenFailed: 'fail', + whenExhausted: 'fail', requiredInputPattern: { defined: false }, forbiddenInputPattern: { defined: false }, }, @@ -550,7 +553,7 @@ describe('compareFlowShapes', () => { stepType: 'single', dependencies: [], whenUnmet: 'skip', - whenFailed: 'fail', + whenExhausted: 'fail', requiredInputPattern: { defined: false }, forbiddenInputPattern: { defined: false }, }, @@ -559,7 +562,7 @@ describe('compareFlowShapes', () => { stepType: 'single', dependencies: [], whenUnmet: 'skip', - whenFailed: 'fail', + whenExhausted: 'fail', requiredInputPattern: { defined: false }, forbiddenInputPattern: { defined: false }, }, @@ -586,7 +589,7 @@ describe('compareFlowShapes', () => { stepType: 'single', dependencies: [], whenUnmet: 'skip', - whenFailed: 'fail', + whenExhausted: 'fail', requiredInputPattern: { defined: false }, forbiddenInputPattern: { defined: false }, }, @@ -599,7 +602,7 @@ describe('compareFlowShapes', () => { stepType: 'map', dependencies: [], whenUnmet: 'skip', - whenFailed: 'fail', + whenExhausted: 'fail', requiredInputPattern: { defined: false }, forbiddenInputPattern: { defined: false }, }, @@ -623,7 +626,7 @@ describe('compareFlowShapes', () => { stepType: 'single', dependencies: [], whenUnmet: 'skip', - whenFailed: 'fail', + whenExhausted: 'fail', requiredInputPattern: { defined: false }, forbiddenInputPattern: { defined: false }, }, @@ -636,7 +639,7 @@ describe('compareFlowShapes', () => { stepType: 'single', dependencies: ['step0'], whenUnmet: 'skip', - whenFailed: 'fail', + whenExhausted: 'fail', requiredInputPattern: { defined: false }, forbiddenInputPattern: { defined: false }, }, @@ -658,7 +661,7 @@ describe('compareFlowShapes', () => { stepType: 'single', dependencies: ['dep1', 'dep2'], whenUnmet: 'skip', - whenFailed: 'fail', + whenExhausted: 'fail', requiredInputPattern: { defined: false }, forbiddenInputPattern: { defined: false }, }, @@ -671,7 +674,7 @@ describe('compareFlowShapes', () => { stepType: 'single', dependencies: ['dep1'], whenUnmet: 'skip', - whenFailed: 'fail', + whenExhausted: 'fail', requiredInputPattern: { defined: false }, forbiddenInputPattern: { defined: false }, }, @@ -693,7 +696,7 @@ describe('compareFlowShapes', () => { stepType: 'single', dependencies: ['old_dep'], whenUnmet: 'skip', - whenFailed: 'fail', + whenExhausted: 'fail', requiredInputPattern: { defined: false }, forbiddenInputPattern: { defined: false }, }, @@ -706,7 +709,7 @@ describe('compareFlowShapes', () => { stepType: 'single', dependencies: ['new_dep'], whenUnmet: 'skip', - whenFailed: 'fail', + whenExhausted: 'fail', requiredInputPattern: { defined: false }, forbiddenInputPattern: { defined: false }, }, @@ -766,7 +769,7 @@ describe('compareFlowShapes', () => { stepType: 'single', dependencies: [], whenUnmet: 'skip', - whenFailed: 'fail', + whenExhausted: 'fail', requiredInputPattern: { defined: false }, forbiddenInputPattern: { defined: false }, }, @@ -779,7 +782,7 @@ describe('compareFlowShapes', () => { stepType: 'map', dependencies: ['dep1'], whenUnmet: 'skip', - whenFailed: 'fail', + whenExhausted: 'fail', requiredInputPattern: { defined: false }, forbiddenInputPattern: { defined: false }, }, @@ -788,7 +791,7 @@ describe('compareFlowShapes', () => { stepType: 'single', dependencies: [], whenUnmet: 'skip', - whenFailed: 'fail', + whenExhausted: 'fail', requiredInputPattern: { defined: false }, forbiddenInputPattern: { defined: false }, }, diff --git a/pkgs/dsl/__tests__/runtime/when-failed-options.test.ts b/pkgs/dsl/__tests__/runtime/when-exhausted-options.test.ts similarity index 57% rename from pkgs/dsl/__tests__/runtime/when-failed-options.test.ts rename to pkgs/dsl/__tests__/runtime/when-exhausted-options.test.ts index 527bfb7bb..a3a949ec9 100644 --- a/pkgs/dsl/__tests__/runtime/when-failed-options.test.ts +++ b/pkgs/dsl/__tests__/runtime/when-exhausted-options.test.ts @@ -2,61 +2,61 @@ import { describe, it, expect } from 'vitest'; import { Flow } from '../../src/dsl.js'; import { compileFlow } from '../../src/compile-flow.js'; -describe('retriesExhausted Options', () => { - describe('DSL accepts retriesExhausted option', () => { - it('should accept retriesExhausted option on a step', () => { +describe('whenExhausted Options', () => { + describe('DSL accepts whenExhausted option', () => { + it('should accept whenExhausted option on a step', () => { const flow = new Flow({ slug: 'test_flow' }).step( - { slug: 'step1', retriesExhausted: 'skip' }, + { slug: 'step1', whenExhausted: 'skip' }, () => 'result' ); const step = flow.getStepDefinition('step1'); - expect(step.options.retriesExhausted).toBe('skip'); + expect(step.options.whenExhausted).toBe('skip'); }); - it('should accept retriesExhausted: fail (default behavior)', () => { + it('should accept whenExhausted: fail (default behavior)', () => { const flow = new Flow({ slug: 'test_flow' }).step( - { slug: 'step1', retriesExhausted: 'fail' }, + { slug: 'step1', whenExhausted: 'fail' }, () => 'result' ); const step = flow.getStepDefinition('step1'); - expect(step.options.retriesExhausted).toBe('fail'); + expect(step.options.whenExhausted).toBe('fail'); }); - it('should accept retriesExhausted: skip-cascade', () => { + it('should accept whenExhausted: skip-cascade', () => { const flow = new Flow({ slug: 'test_flow' }).step( - { slug: 'step1', retriesExhausted: 'skip-cascade' }, + { slug: 'step1', whenExhausted: 'skip-cascade' }, () => 'result' ); const step = flow.getStepDefinition('step1'); - expect(step.options.retriesExhausted).toBe('skip-cascade'); + expect(step.options.whenExhausted).toBe('skip-cascade'); }); - it('should accept retriesExhausted on dependent steps', () => { + it('should accept whenExhausted on dependent steps', () => { const flow = new Flow({ slug: 'test_flow' }) .step({ slug: 'first' }, () => ({ data: 'test' })) .step( { slug: 'second', dependsOn: ['first'], - retriesExhausted: 'skip', + whenExhausted: 'skip', }, () => 'result' ); const step = flow.getStepDefinition('second'); - expect(step.options.retriesExhausted).toBe('skip'); + expect(step.options.whenExhausted).toBe('skip'); }); - it('should accept retriesExhausted together with other options', () => { + it('should accept whenExhausted together with other options', () => { const flow = new Flow({ slug: 'test_flow' }).step( { slug: 'step1', maxAttempts: 3, timeout: 60, - retriesExhausted: 'skip-cascade', + whenExhausted: 'skip-cascade', }, () => 'result' ); @@ -64,16 +64,16 @@ describe('retriesExhausted Options', () => { const step = flow.getStepDefinition('step1'); expect(step.options.maxAttempts).toBe(3); expect(step.options.timeout).toBe(60); - expect(step.options.retriesExhausted).toBe('skip-cascade'); + expect(step.options.whenExhausted).toBe('skip-cascade'); }); - it('should accept both whenUnmet and retriesExhausted together', () => { + it('should accept both whenUnmet and whenExhausted together', () => { const flow = new Flow({ slug: 'test_flow' }).step( { slug: 'step1', if: { enabled: true }, whenUnmet: 'skip', - retriesExhausted: 'skip-cascade', + whenExhausted: 'skip-cascade', }, () => 'result' ); @@ -81,48 +81,48 @@ describe('retriesExhausted Options', () => { const step = flow.getStepDefinition('step1'); expect(step.options.if).toEqual({ enabled: true }); expect(step.options.whenUnmet).toBe('skip'); - expect(step.options.retriesExhausted).toBe('skip-cascade'); + expect(step.options.whenExhausted).toBe('skip-cascade'); }); }); - describe('compileFlow includes when_failed parameter', () => { - it('should compile when_failed for step', () => { + describe('compileFlow includes when_exhausted parameter', () => { + it('should compile when_exhausted for step', () => { const flow = new Flow({ slug: 'test_flow' }).step( - { slug: 'step1', retriesExhausted: 'skip' }, + { slug: 'step1', whenExhausted: 'skip' }, () => 'result' ); const statements = compileFlow(flow); expect(statements).toHaveLength(2); - expect(statements[1]).toContain("when_failed => 'skip'"); + expect(statements[1]).toContain("when_exhausted => 'skip'"); }); - it('should compile when_failed: fail', () => { + it('should compile when_exhausted: fail', () => { const flow = new Flow({ slug: 'test_flow' }).step( - { slug: 'step1', retriesExhausted: 'fail' }, + { slug: 'step1', whenExhausted: 'fail' }, () => 'result' ); const statements = compileFlow(flow); expect(statements).toHaveLength(2); - expect(statements[1]).toContain("when_failed => 'fail'"); + expect(statements[1]).toContain("when_exhausted => 'fail'"); }); - it('should compile when_failed: skip-cascade', () => { + it('should compile when_exhausted: skip-cascade', () => { const flow = new Flow({ slug: 'test_flow' }).step( - { slug: 'step1', retriesExhausted: 'skip-cascade' }, + { slug: 'step1', whenExhausted: 'skip-cascade' }, () => 'result' ); const statements = compileFlow(flow); expect(statements).toHaveLength(2); - expect(statements[1]).toContain("when_failed => 'skip-cascade'"); + expect(statements[1]).toContain("when_exhausted => 'skip-cascade'"); }); - it('should compile step with all options including retriesExhausted', () => { + it('should compile step with all options including whenExhausted', () => { const flow = new Flow({ slug: 'test_flow' }).step( { slug: 'step1', @@ -130,7 +130,7 @@ describe('retriesExhausted Options', () => { timeout: 60, if: { enabled: true }, whenUnmet: 'skip', - retriesExhausted: 'skip-cascade', + whenExhausted: 'skip-cascade', }, () => 'result' ); @@ -144,10 +144,10 @@ describe('retriesExhausted Options', () => { 'required_input_pattern => \'{"enabled":true}\'' ); expect(statements[1]).toContain("when_unmet => 'skip'"); - expect(statements[1]).toContain("when_failed => 'skip-cascade'"); + expect(statements[1]).toContain("when_exhausted => 'skip-cascade'"); }); - it('should not include when_failed when not specified', () => { + it('should not include when_exhausted when not specified', () => { const flow = new Flow({ slug: 'test_flow' }).step( { slug: 'step1' }, () => 'result' @@ -156,31 +156,31 @@ describe('retriesExhausted Options', () => { const statements = compileFlow(flow); expect(statements).toHaveLength(2); - expect(statements[1]).not.toContain('when_failed'); + expect(statements[1]).not.toContain('when_exhausted'); }); }); - describe('retriesExhausted on map steps', () => { - it('should accept retriesExhausted on map step', () => { + describe('whenExhausted on map steps', () => { + it('should accept whenExhausted on map step', () => { const flow = new Flow({ slug: 'test_flow' }).map( - { slug: 'map_step', retriesExhausted: 'skip' }, + { slug: 'map_step', whenExhausted: 'skip' }, (item) => item.toUpperCase() ); const step = flow.getStepDefinition('map_step'); - expect(step.options.retriesExhausted).toBe('skip'); + expect(step.options.whenExhausted).toBe('skip'); }); - it('should compile when_failed for map step', () => { + it('should compile when_exhausted for map step', () => { const flow = new Flow({ slug: 'test_flow' }).map( - { slug: 'map_step', retriesExhausted: 'skip-cascade' }, + { slug: 'map_step', whenExhausted: 'skip-cascade' }, (item) => item.toUpperCase() ); const statements = compileFlow(flow); expect(statements).toHaveLength(2); - expect(statements[1]).toContain("when_failed => 'skip-cascade'"); + expect(statements[1]).toContain("when_exhausted => 'skip-cascade'"); }); }); }); diff --git a/pkgs/dsl/__tests__/types/skippable-deps.test-d.ts b/pkgs/dsl/__tests__/types/skippable-deps.test-d.ts index eba36fd6d..b5cd609c8 100644 --- a/pkgs/dsl/__tests__/types/skippable-deps.test-d.ts +++ b/pkgs/dsl/__tests__/types/skippable-deps.test-d.ts @@ -4,7 +4,7 @@ import { describe, it, expectTypeOf } from 'vitest'; /** * Type tests for skippable step dependencies * - * When a step has `whenUnmet: 'skip' | 'skip-cascade'` or `retriesExhausted: 'skip' | 'skip-cascade'`, + * When a step has `whenUnmet: 'skip' | 'skip-cascade'` or `whenExhausted: 'skip' | 'skip-cascade'`, * it may not execute. Dependent steps should receive that step's output as an optional key. */ @@ -84,10 +84,10 @@ describe('skippable deps type safety', () => { }); }); - describe('core skippability - retriesExhausted', () => { - it('step with retriesExhausted: skip makes output optional for dependents', () => { + describe('core skippability - whenExhausted', () => { + it('step with whenExhausted: skip makes output optional for dependents', () => { const flow = new Flow<{ value: number }>({ slug: 'test' }) - .step({ slug: 'risky', retriesExhausted: 'skip' }, (input) => ({ + .step({ slug: 'risky', whenExhausted: 'skip' }, (input) => ({ result: input.value * 2, })) .step({ slug: 'dependent', dependsOn: ['risky'] }, (deps) => { @@ -103,11 +103,11 @@ describe('skippable deps type safety', () => { }>(); }); - it('step with retriesExhausted: skip-cascade keeps output required (cascade skips dependents)', () => { + it('step with whenExhausted: skip-cascade keeps output required (cascade skips dependents)', () => { // skip-cascade means if the step is skipped, its dependents are ALSO skipped // So if the dependent handler runs at all, the parent must have succeeded const flow = new Flow<{ value: number }>({ slug: 'test' }) - .step({ slug: 'risky', retriesExhausted: 'skip-cascade' }, (input) => ({ + .step({ slug: 'risky', whenExhausted: 'skip-cascade' }, (input) => ({ result: input.value * 2, })) .step({ slug: 'dependent', dependsOn: ['risky'] }, (deps) => { @@ -122,9 +122,9 @@ describe('skippable deps type safety', () => { }>(); }); - it('step with retriesExhausted: fail keeps output required', () => { + it('step with whenExhausted: fail keeps output required', () => { const flow = new Flow<{ value: number }>({ slug: 'test' }) - .step({ slug: 'risky', retriesExhausted: 'fail' }, (input) => ({ + .step({ slug: 'risky', whenExhausted: 'fail' }, (input) => ({ result: input.value * 2, })) .step({ slug: 'dependent', dependsOn: ['risky'] }, (deps) => { @@ -169,8 +169,8 @@ describe('skippable deps type safety', () => { it('all deps skippable: all optional', () => { const flow = new Flow<{ value: number }>({ slug: 'test' }) - .step({ slug: 'skip1', retriesExhausted: 'skip' }, () => ({ a: 1 })) - .step({ slug: 'skip2', retriesExhausted: 'skip' }, () => ({ b: 2 })) + .step({ slug: 'skip1', whenExhausted: 'skip' }, () => ({ a: 1 })) + .step({ slug: 'skip2', whenExhausted: 'skip' }, () => ({ b: 2 })) .step({ slug: 'dependent', dependsOn: ['skip1', 'skip2'] }, (deps) => { expectTypeOf(deps.skip1).toEqualTypeOf<{ a: number } | undefined>(); expectTypeOf(deps.skip2).toEqualTypeOf<{ b: number } | undefined>(); @@ -205,7 +205,7 @@ describe('skippable deps type safety', () => { describe('chains and graphs', () => { it('chain A(skip) -> B -> C: A optional in B, B required in C', () => { const flow = new Flow<{ value: number }>({ slug: 'test' }) - .step({ slug: 'a', retriesExhausted: 'skip' }, () => ({ aVal: 1 })) + .step({ slug: 'a', whenExhausted: 'skip' }, () => ({ aVal: 1 })) .step({ slug: 'b', dependsOn: ['a'] }, (deps) => { expectTypeOf(deps.a).toEqualTypeOf<{ aVal: number } | undefined>(); return { bVal: 2 }; @@ -225,7 +225,7 @@ describe('skippable deps type safety', () => { it('diamond: A(skip) -> B, A -> C, B+C -> D: A optional in B and C', () => { const flow = new Flow<{ value: number }>({ slug: 'test' }) - .step({ slug: 'a', retriesExhausted: 'skip' }, () => ({ aVal: 1 })) + .step({ slug: 'a', whenExhausted: 'skip' }, () => ({ aVal: 1 })) .step({ slug: 'b', dependsOn: ['a'] }, (deps) => { expectTypeOf(deps.a).toEqualTypeOf<{ aVal: number } | undefined>(); return { bVal: 2 }; @@ -259,7 +259,7 @@ describe('skippable deps type safety', () => { // If A is skipped, B is also skipped (cascade), so B never runs with undefined A // Therefore B should see A as required, not optional const flow = new Flow<{ value: number }>({ slug: 'test' }) - .step({ slug: 'a', retriesExhausted: 'skip-cascade' }, () => ({ + .step({ slug: 'a', whenExhausted: 'skip-cascade' }, () => ({ aVal: 1, })) .step({ slug: 'b', dependsOn: ['a'] }, (deps) => { @@ -284,7 +284,7 @@ describe('skippable deps type safety', () => { describe('edge cases', () => { it('root step with skip: valid config, no dependents affected (no deps)', () => { const flow = new Flow<{ value: number }>({ slug: 'test' }).step( - { slug: 'root', retriesExhausted: 'skip' }, + { slug: 'root', whenExhausted: 'skip' }, (input) => ({ result: input.value }) ); @@ -295,7 +295,7 @@ describe('skippable deps type safety', () => { it('map step with skip: entire output array is optional type', () => { const flow = new Flow({ slug: 'test' }) - .map({ slug: 'process', retriesExhausted: 'skip' }, (item) => + .map({ slug: 'process', whenExhausted: 'skip' }, (item) => item.toUpperCase() ) .step({ slug: 'aggregate', dependsOn: ['process'] }, (deps) => { @@ -311,7 +311,7 @@ describe('skippable deps type safety', () => { it('array step with skip: entire output array is optional type', () => { const flow = new Flow<{ count: number }>({ slug: 'test' }) - .array({ slug: 'generate', retriesExhausted: 'skip' }, (input) => + .array({ slug: 'generate', whenExhausted: 'skip' }, (input) => Array(input.count) .fill(0) .map((_, i) => i) @@ -327,14 +327,14 @@ describe('skippable deps type safety', () => { }>(); }); - it('both whenUnmet and retriesExhausted set: still skippable', () => { + it('both whenUnmet and whenExhausted set: still skippable', () => { const flow = new Flow<{ value: number }>({ slug: 'test' }) .step( { slug: 'both', if: { value: 42 }, whenUnmet: 'skip', - retriesExhausted: 'skip', + whenExhausted: 'skip', }, () => ({ result: 1 }) ) @@ -355,7 +355,7 @@ describe('skippable deps type safety', () => { describe('type inference and narrowing', () => { it('cannot access property on optional dep without null check', () => { const flow = new Flow<{ value: number }>({ slug: 'test' }) - .step({ slug: 'skippable', retriesExhausted: 'skip' }, () => ({ + .step({ slug: 'skippable', whenExhausted: 'skip' }, () => ({ foo: 'bar', })) .step({ slug: 'dependent', dependsOn: ['skippable'] }, (deps) => { @@ -374,7 +374,7 @@ describe('skippable deps type safety', () => { it('type narrowing works after existence check', () => { new Flow<{ value: number }>({ slug: 'test' }) - .step({ slug: 'skippable', retriesExhausted: 'skip' }, () => ({ + .step({ slug: 'skippable', whenExhausted: 'skip' }, () => ({ foo: 'bar', })) .step({ slug: 'dependent', dependsOn: ['skippable'] }, (deps) => { @@ -388,7 +388,7 @@ describe('skippable deps type safety', () => { it('handler receives correctly typed deps object', () => { new Flow<{ value: number }>({ slug: 'test' }) - .step({ slug: 'skip1', retriesExhausted: 'skip' }, () => ({ a: 1 })) + .step({ slug: 'skip1', whenExhausted: 'skip' }, () => ({ a: 1 })) .step({ slug: 'req1' }, () => ({ b: 'str' })) .step({ slug: 'dependent', dependsOn: ['skip1', 'req1'] }, (deps) => { // Handler parameter should have correct mixed optionality @@ -405,7 +405,7 @@ describe('skippable deps type safety', () => { it('StepOutput returns output type (not metadata)', () => { const flow = new Flow<{ value: number }>({ slug: 'test' }) .step({ slug: 'normal' }, () => ({ result: 42 })) - .step({ slug: 'skippable', retriesExhausted: 'skip' }, () => ({ + .step({ slug: 'skippable', whenExhausted: 'skip' }, () => ({ other: 'str', })); @@ -420,7 +420,7 @@ describe('skippable deps type safety', () => { it('keyof ExtractFlowSteps still returns slug union', () => { const flow = new Flow<{ value: number }>({ slug: 'test' }) .step({ slug: 'a' }, () => 1) - .step({ slug: 'b', retriesExhausted: 'skip' }, () => 2) + .step({ slug: 'b', whenExhausted: 'skip' }, () => 2) .step({ slug: 'c', dependsOn: ['a', 'b'] }, () => 3); type StepSlugs = keyof import('../../src/index.js').ExtractFlowSteps< @@ -433,7 +433,7 @@ describe('skippable deps type safety', () => { describe('dependent map with skippable array source', () => { it('dependent map on skippable array: deps should be optional', () => { const flow = new Flow<{ value: number }>({ slug: 'test' }) - .array({ slug: 'items', retriesExhausted: 'skip' }, () => [1, 2, 3]) + .array({ slug: 'items', whenExhausted: 'skip' }, () => [1, 2, 3]) .map({ slug: 'double', array: 'items' }, (item) => item * 2) .step({ slug: 'sum', dependsOn: ['double'] }, (deps) => { // The map step itself doesn't have skip, but its source does @@ -459,7 +459,7 @@ describe('skippable deps compile-time errors', () => { describe('direct property access on optional deps', () => { it('should reject direct property access on skippable dep without null check', () => { new Flow<{ value: number }>({ slug: 'test' }) - .step({ slug: 'maybeSkipped', retriesExhausted: 'skip' }, () => ({ + .step({ slug: 'maybeSkipped', whenExhausted: 'skip' }, () => ({ data: 'result', })) .step({ slug: 'consumer', dependsOn: ['maybeSkipped'] }, (deps) => { @@ -497,10 +497,10 @@ describe('skippable deps compile-time errors', () => { }); }); - it('should ALLOW direct property access with retriesExhausted: skip-cascade (cascade skips dependents)', () => { + it('should ALLOW direct property access with whenExhausted: skip-cascade (cascade skips dependents)', () => { // With skip-cascade, if the dependent runs, the parent must have succeeded new Flow<{ value: number }>({ slug: 'test' }) - .step({ slug: 'risky', retriesExhausted: 'skip-cascade' }, () => ({ + .step({ slug: 'risky', whenExhausted: 'skip-cascade' }, () => ({ status: 'ok', })) .step({ slug: 'next', dependsOn: ['risky'] }, (deps) => { @@ -515,7 +515,7 @@ describe('skippable deps compile-time errors', () => { it('should allow direct access on required dep but reject on optional', () => { new Flow<{ value: number }>({ slug: 'test' }) .step({ slug: 'required' }, () => ({ reqData: 'always' })) - .step({ slug: 'optional', retriesExhausted: 'skip' }, () => ({ + .step({ slug: 'optional', whenExhausted: 'skip' }, () => ({ optData: 'maybe', })) .step( @@ -536,7 +536,7 @@ describe('skippable deps compile-time errors', () => { describe('array and map steps with skip modes', () => { it('should reject direct access on skippable array step output', () => { new Flow<{ items: string[] }>({ slug: 'test' }) - .array({ slug: 'processed', retriesExhausted: 'skip' }, (input) => + .array({ slug: 'processed', whenExhausted: 'skip' }, (input) => input.items.map((s) => s.toUpperCase()) ) .step({ slug: 'consumer', dependsOn: ['processed'] }, (deps) => { @@ -549,7 +549,7 @@ describe('skippable deps compile-time errors', () => { it('should reject direct access on skippable map step output', () => { new Flow({ slug: 'test' }) .map( - { slug: 'doubled', retriesExhausted: 'skip' }, + { slug: 'doubled', whenExhausted: 'skip' }, (item) => item + item ) .step({ slug: 'consumer', dependsOn: ['doubled'] }, (deps) => { diff --git a/pkgs/dsl/src/compile-flow.ts b/pkgs/dsl/src/compile-flow.ts index 1286e2203..f3fdc976f 100644 --- a/pkgs/dsl/src/compile-flow.ts +++ b/pkgs/dsl/src/compile-flow.ts @@ -80,8 +80,8 @@ function formatRuntimeOptions( parts.push(`when_unmet => '${options.whenUnmet}'`); } - if ('retriesExhausted' in options && options.retriesExhausted !== undefined) { - parts.push(`when_failed => '${options.retriesExhausted}'`); + if ('whenExhausted' in options && options.whenExhausted !== undefined) { + parts.push(`when_exhausted => '${options.whenExhausted}'`); } return parts.length > 0 ? `, ${parts.join(', ')}` : ''; diff --git a/pkgs/dsl/src/dsl.ts b/pkgs/dsl/src/dsl.ts index c4a44d501..a34e3c4de 100644 --- a/pkgs/dsl/src/dsl.ts +++ b/pkgs/dsl/src/dsl.ts @@ -334,8 +334,8 @@ type OptionalDeps = { * Asymmetric step input type: * - Root steps (no dependencies): receive flow input directly * - Dependent steps: receive only their dependencies (flow input available via context) - * - Skippable deps (whenUnmet/retriesExhausted: 'skip') are optional - * - Cascade deps (whenUnmet/retriesExhausted: 'skip-cascade') are required + * - Skippable deps (whenUnmet/whenExhausted: 'skip') are optional + * - Cascade deps (whenUnmet/whenExhausted: 'skip-cascade') are required * (because if handler runs, the dependency must have succeeded) * - All other deps are required * @@ -439,15 +439,15 @@ export type WhenUnmetMode = 'fail' | 'skip' | 'skip-cascade'; * * @example * // Fail the run after retries exhausted (default) - * { retriesExhausted: 'fail' } + * { whenExhausted: 'fail' } * * @example * // Skip this step after retries exhausted, continue run - * { retriesExhausted: 'skip' } + * { whenExhausted: 'skip' } * * @example * // Skip this step and all dependents after retries exhausted - * { retriesExhausted: 'skip-cascade' } + * { whenExhausted: 'skip-cascade' } * * @remarks * - `'fail'`: Step fails -> run fails (default behavior) @@ -456,10 +456,10 @@ export type WhenUnmetMode = 'fail' | 'skip' | 'skip-cascade'; * * @note * TYPE_VIOLATION errors (e.g., single step returns non-array for map dependent) - * are NOT subject to retriesExhausted - these always hard fail as they indicate + * are NOT subject to whenExhausted - these always hard fail as they indicate * programming errors, not runtime conditions. */ -export type RetriesExhaustedMode = 'fail' | 'skip' | 'skip-cascade'; +export type WhenExhaustedMode = 'fail' | 'skip' | 'skip-cascade'; /** * Helper type for dependent step handlers - creates deps object with correct optionality. @@ -550,23 +550,23 @@ export interface StepRuntimeOptions extends RuntimeOptions { * @default 'fail' * * @example - * { retriesExhausted: 'fail' } // Step fails -> run fails - * { retriesExhausted: 'skip' } // Skip step, continue run - * { retriesExhausted: 'skip-cascade' } // Skip step + all dependents + * { whenExhausted: 'fail' } // Step fails -> run fails + * { whenExhausted: 'skip' } // Skip step, continue run + * { whenExhausted: 'skip-cascade' } // Skip step + all dependents * * @remarks * Only applies after maxAttempts retries are exhausted. * TYPE_VIOLATION errors always fail regardless of this setting. * - * @see RetriesExhaustedMode for detailed documentation of each mode + * @see WhenExhaustedMode for detailed documentation of each mode */ - retriesExhausted?: RetriesExhaustedMode; + whenExhausted?: WhenExhaustedMode; } // Base runtime options without condition-related fields interface BaseStepRuntimeOptions extends RuntimeOptions { startDelay?: number; - retriesExhausted?: RetriesExhaustedMode; + whenExhausted?: WhenExhaustedMode; } /** @@ -720,19 +720,19 @@ export class Flow< Slug extends string, TOutput, TWhenUnmet extends WhenUnmetMode | undefined = undefined, - TRetries extends RetriesExhaustedMode | undefined = undefined + TRetries extends WhenExhaustedMode | undefined = undefined >( opts: Simplify< { slug: Slug extends keyof Steps ? never : Slug; dependsOn?: never; - retriesExhausted?: TRetries; + whenExhausted?: TRetries; } & ( | (WithIfCondition & { whenUnmet?: TWhenUnmet }) | (WithIfNotCondition & { whenUnmet?: TWhenUnmet }) | WithoutCondition ) & - Omit + Omit >, handler: ( flowInput: TFlowInput, @@ -765,13 +765,13 @@ export class Flow< Deps extends Extract, TOutput, TWhenUnmet extends WhenUnmetMode | undefined = undefined, - TRetries extends RetriesExhaustedMode | undefined = undefined + TRetries extends WhenExhaustedMode | undefined = undefined >( opts: Simplify< { slug: Slug extends keyof Steps ? never : Slug; dependsOn: [Deps, ...Deps[]]; - retriesExhausted?: TRetries; + whenExhausted?: TRetries; } & ( | (WithIfCondition>> & { whenUnmet?: TWhenUnmet; @@ -781,7 +781,7 @@ export class Flow< > & { whenUnmet?: TWhenUnmet }) | WithoutCondition ) & - Omit + Omit >, handler: ( deps: Simplify>, @@ -834,8 +834,8 @@ export class Flow< if (opts.if !== undefined) options.if = opts.if; if (opts.ifNot !== undefined) options.ifNot = opts.ifNot; if (opts.whenUnmet !== undefined) options.whenUnmet = opts.whenUnmet; - if (opts.retriesExhausted !== undefined) - options.retriesExhausted = opts.retriesExhausted; + if (opts.whenExhausted !== undefined) + options.whenExhausted = opts.whenExhausted; // Validate runtime options (optional for step level) validateRuntimeOptions(options, { optional: true }); @@ -885,19 +885,19 @@ export class Flow< Slug extends string, TOutput extends readonly any[], TWhenUnmet extends WhenUnmetMode | undefined = undefined, - TRetries extends RetriesExhaustedMode | undefined = undefined + TRetries extends WhenExhaustedMode | undefined = undefined >( opts: Simplify< { slug: Slug extends keyof Steps ? never : Slug; dependsOn?: never; - retriesExhausted?: TRetries; + whenExhausted?: TRetries; } & ( | (WithIfCondition & { whenUnmet?: TWhenUnmet }) | (WithIfNotCondition & { whenUnmet?: TWhenUnmet }) | WithoutCondition ) & - Omit + Omit >, handler: ( flowInput: TFlowInput, @@ -929,13 +929,13 @@ export class Flow< Deps extends Extract, TOutput extends readonly any[], TWhenUnmet extends WhenUnmetMode | undefined = undefined, - TRetries extends RetriesExhaustedMode | undefined = undefined + TRetries extends WhenExhaustedMode | undefined = undefined >( opts: Simplify< { slug: Slug extends keyof Steps ? never : Slug; dependsOn: [Deps, ...Deps[]]; - retriesExhausted?: TRetries; + whenExhausted?: TRetries; } & ( | (WithIfCondition>> & { whenUnmet?: TWhenUnmet; @@ -945,7 +945,7 @@ export class Flow< > & { whenUnmet?: TWhenUnmet }) | WithoutCondition ) & - Omit + Omit >, handler: ( deps: Simplify>, @@ -997,18 +997,18 @@ export class Flow< ) => Json | Promise : never, TWhenUnmet extends WhenUnmetMode | undefined = undefined, - TRetries extends RetriesExhaustedMode | undefined = undefined + TRetries extends WhenExhaustedMode | undefined = undefined >( opts: Simplify< { slug: Slug extends keyof Steps ? never : Slug; - retriesExhausted?: TRetries; + whenExhausted?: TRetries; } & ( | (WithIfCondition & { whenUnmet?: TWhenUnmet }) | (WithIfNotCondition & { whenUnmet?: TWhenUnmet }) | WithoutCondition ) & - Omit + Omit >, handler: THandler ): Flow< @@ -1041,13 +1041,13 @@ export class Flow< ) => Json | Promise : never, TWhenUnmet extends WhenUnmetMode | undefined = undefined, - TRetries extends RetriesExhaustedMode | undefined = undefined + TRetries extends WhenExhaustedMode | undefined = undefined >( opts: Simplify< { slug: Slug extends keyof Steps ? never : Slug; array: TArrayDep; - retriesExhausted?: TRetries; + whenExhausted?: TRetries; } & ( | (WithIfCondition<{ [K in TArrayDep]: Steps[K]['output'] }> & { whenUnmet?: TWhenUnmet; @@ -1057,7 +1057,7 @@ export class Flow< }) | WithoutCondition ) & - Omit + Omit >, handler: THandler ): Flow< @@ -1113,8 +1113,8 @@ export class Flow< if (opts.if !== undefined) options.if = opts.if; if (opts.ifNot !== undefined) options.ifNot = opts.ifNot; if (opts.whenUnmet !== undefined) options.whenUnmet = opts.whenUnmet; - if (opts.retriesExhausted !== undefined) - options.retriesExhausted = opts.retriesExhausted; + if (opts.whenExhausted !== undefined) + options.whenExhausted = opts.whenExhausted; // Validate runtime options validateRuntimeOptions(options, { optional: true }); diff --git a/pkgs/dsl/src/flow-shape.ts b/pkgs/dsl/src/flow-shape.ts index 108fc3ab6..c62594985 100644 --- a/pkgs/dsl/src/flow-shape.ts +++ b/pkgs/dsl/src/flow-shape.ts @@ -1,4 +1,4 @@ -import { AnyFlow, WhenUnmetMode, RetriesExhaustedMode, Json } from './dsl.js'; +import { AnyFlow, WhenUnmetMode, WhenExhaustedMode, Json } from './dsl.js'; // ======================== // SHAPE TYPE DEFINITIONS @@ -9,9 +9,7 @@ import { AnyFlow, WhenUnmetMode, RetriesExhaustedMode, Json } from './dsl.js'; * - { defined: false } means no pattern (don't check) * - { defined: true, value: Json } means pattern is set (check against value) */ -export type InputPattern = - | { defined: false } - | { defined: true; value: Json }; +export type InputPattern = { defined: false } | { defined: true; value: Json }; /** * Step-level options that can be included in the shape for creation, @@ -41,7 +39,7 @@ export interface FlowShapeOptions { * shape comparison. Options can be tuned at runtime via SQL without * requiring recompilation. See: /deploy/tune-flow-config/ * - * `whenUnmet`, `whenFailed`, and pattern fields ARE structural - they affect + * `whenUnmet`, `whenExhausted`, and pattern fields ARE structural - they affect * DAG execution semantics and must match between worker and database. */ export interface StepShape { @@ -49,7 +47,7 @@ export interface StepShape { stepType: 'single' | 'map'; dependencies: string[]; // sorted alphabetically for deterministic comparison whenUnmet: WhenUnmetMode; - whenFailed: RetriesExhaustedMode; + whenExhausted: WhenExhaustedMode; requiredInputPattern: InputPattern; forbiddenInputPattern: InputPattern; options?: StepShapeOptions; @@ -125,7 +123,7 @@ export function extractFlowShape(flow: AnyFlow): FlowShape { dependencies: [...stepDef.dependencies].sort(), // Condition modes are structural - they affect DAG execution semantics whenUnmet: stepDef.options.whenUnmet ?? 'skip', - whenFailed: stepDef.options.retriesExhausted ?? 'fail', + whenExhausted: stepDef.options.whenExhausted ?? 'fail', // Input patterns use explicit wrapper to avoid null vs JSON-null ambiguity requiredInputPattern: stepDef.options.if !== undefined @@ -262,9 +260,9 @@ function compareSteps( ); } - if (a.whenFailed !== b.whenFailed) { + if (a.whenExhausted !== b.whenExhausted) { differences.push( - `Step at index ${index}: whenFailed differs '${a.whenFailed}' vs '${b.whenFailed}'` + `Step at index ${index}: whenExhausted differs '${a.whenExhausted}' vs '${b.whenExhausted}'` ); } diff --git a/pkgs/edge-worker/tests/integration/flow/compilationAtStartup.test.ts b/pkgs/edge-worker/tests/integration/flow/compilationAtStartup.test.ts index 2a61052d3..94ed1e314 100644 --- a/pkgs/edge-worker/tests/integration/flow/compilationAtStartup.test.ts +++ b/pkgs/edge-worker/tests/integration/flow/compilationAtStartup.test.ts @@ -306,7 +306,7 @@ Deno.test( stepType: 'single', dependencies: [], whenUnmet: 'skip', - whenFailed: 'fail', + whenExhausted: 'fail', requiredInputPattern: { defined: false }, forbiddenInputPattern: { defined: false }, }, diff --git a/pkgs/edge-worker/tests/integration/flow/retriesExhausted.test.ts b/pkgs/edge-worker/tests/integration/flow/whenExhausted.test.ts similarity index 94% rename from pkgs/edge-worker/tests/integration/flow/retriesExhausted.test.ts rename to pkgs/edge-worker/tests/integration/flow/whenExhausted.test.ts index 573db8f61..59b22f45a 100644 --- a/pkgs/edge-worker/tests/integration/flow/retriesExhausted.test.ts +++ b/pkgs/edge-worker/tests/integration/flow/whenExhausted.test.ts @@ -19,10 +19,10 @@ const workerConfig = { } as const; // ============================================================================= -// Test 1: Handler fails with when_failed='fail' (default) - run fails +// Test 1: Handler fails with when_exhausted='fail' (default) - run fails // ============================================================================= Deno.test( - 'retries exhausted with when_failed=fail causes run failure', + 'retries exhausted with when_exhausted=fail causes run failure', withPgNoTransaction(async (sql) => { await sql`select pgflow_tests.reset_db();`; @@ -38,7 +38,7 @@ Deno.test( slug: 'failing_step', dependsOn: ['first'], maxAttempts: 1, - // default retriesExhausted: 'fail' + // default whenExhausted: 'fail' }, async () => { await delay(1); @@ -88,10 +88,10 @@ Deno.test( ); // ============================================================================= -// Test 2: Handler fails with when_failed='skip' - step skipped, run completes +// Test 2: Handler fails with when_exhausted='skip' - step skipped, run completes // ============================================================================= Deno.test( - 'retries exhausted with when_failed=skip skips step with handler_failed', + 'retries exhausted with when_exhausted=skip skips step with handler_failed', withPgNoTransaction(async (sql) => { await sql`select pgflow_tests.reset_db();`; @@ -107,7 +107,7 @@ Deno.test( slug: 'optional_step', dependsOn: ['first'], maxAttempts: 1, - retriesExhausted: 'skip', + whenExhausted: 'skip', }, async () => { await delay(1); @@ -160,10 +160,10 @@ Deno.test( ); // ============================================================================= -// Test 3: Handler fails with when_failed='skip-cascade' - cascades to dependents +// Test 3: Handler fails with when_exhausted='skip-cascade' - cascades to dependents // ============================================================================= Deno.test( - 'retries exhausted with when_failed=skip-cascade skips dependents', + 'retries exhausted with when_exhausted=skip-cascade skips dependents', withPgNoTransaction(async (sql) => { await sql`select pgflow_tests.reset_db();`; @@ -179,7 +179,7 @@ Deno.test( slug: 'risky_step', dependsOn: ['first'], maxAttempts: 1, - retriesExhausted: 'skip-cascade', + whenExhausted: 'skip-cascade', }, async () => { await delay(1); @@ -251,7 +251,7 @@ Deno.test( dependsOn: ['init'], maxAttempts: 3, baseDelay: 1, - retriesExhausted: 'skip', + whenExhausted: 'skip', }, async () => { attemptCount++; @@ -313,7 +313,7 @@ Deno.test( { slug: 'maybe_skip', dependsOn: ['first'], - retriesExhausted: 'skip', // configured to skip on failure + whenExhausted: 'skip', // configured to skip on failure }, async (deps) => { await delay(1); @@ -342,7 +342,6 @@ Deno.test( assertEquals(state.skipped_at, null); } - // Only leaf steps (steps with no dependents) that completed are included in output assertEquals(polledRun.output, { final: { result: { processed: 10 } }, }); @@ -374,9 +373,9 @@ Deno.test( { slug: 'conditional_risky', dependsOn: ['base'], - if: { base: { risky: true } }, maxAttempts: 1, - retriesExhausted: 'skip', + if: { base: { risky: true } }, + whenExhausted: 'skip', }, async () => { await delay(1); diff --git a/pkgs/website/src/content/docs/build/conditional-steps/examples.mdx b/pkgs/website/src/content/docs/build/conditional-steps/examples.mdx index 15a92d3d2..caa2db9ac 100644 --- a/pkgs/website/src/content/docs/build/conditional-steps/examples.mdx +++ b/pkgs/website/src/content/docs/build/conditional-steps/examples.mdx @@ -112,7 +112,7 @@ new Flow<{ query: string }>({ slug: 'rag_fallback' }) slug: 'web', dependsOn: ['retrieve'], if: { retrieve: { confidence: 'low' } }, - retriesExhausted: 'skip', // Continue if web search fails + whenExhausted: 'skip', // Continue if web search fails }, async (_, ctx) => searchWeb((await ctx.flowInput).query) ) @@ -137,7 +137,7 @@ new Flow<{ query: string }>({ slug: 'rag_fallback' }) - Retrieval always runs first to check knowledge base - Web search is conditional on low confidence scores -- `retriesExhausted: 'skip'` ensures graceful degradation if web search fails +- `whenExhausted: 'skip'` ensures graceful degradation if web search fails --- @@ -173,7 +173,7 @@ new Flow<{ query: string }>({ slug: 'multi_retrieval' }) { slug: 'vector', dependsOn: ['embed'], - retriesExhausted: 'skip', + whenExhausted: 'skip', }, (deps) => searchPinecone(deps.embed.vector) ) @@ -181,7 +181,7 @@ new Flow<{ query: string }>({ slug: 'multi_retrieval' }) { slug: 'keyword', dependsOn: ['embed'], - retriesExhausted: 'skip', + whenExhausted: 'skip', }, async (_, ctx) => searchElastic((await ctx.flowInput).query) ) @@ -189,7 +189,7 @@ new Flow<{ query: string }>({ slug: 'multi_retrieval' }) { slug: 'graph', dependsOn: ['embed'], - retriesExhausted: 'skip', + whenExhausted: 'skip', }, async (_, ctx) => searchNeo4j((await ctx.flowInput).query) ) @@ -212,7 +212,7 @@ new Flow<{ query: string }>({ slug: 'multi_retrieval' }) **Key points:** - Three retrieval sources run **in parallel** after embedding -- Each source has `retriesExhausted: 'skip'` for resilience +- Each source has `whenExhausted: 'skip'` for resilience - `rerank` combines available results - handles undefined sources gracefully --- diff --git a/pkgs/website/src/content/docs/build/conditional-steps/index.mdx b/pkgs/website/src/content/docs/build/conditional-steps/index.mdx index c9883acbe..de131c68c 100644 --- a/pkgs/website/src/content/docs/build/conditional-steps/index.mdx +++ b/pkgs/website/src/content/docs/build/conditional-steps/index.mdx @@ -22,7 +22,7 @@ pgflow provides two ways to skip steps: | Feature | When Evaluated | Purpose | | ------------------------- | ---------------- | -------------------------------- | | `if`/`ifNot` conditions | Before step runs | Route based on input data | -| `retriesExhausted` option | After step fails | Recover gracefully from failures | +| `whenExhausted` option | After step fails | Recover gracefully from failures | Both use the same three modes: `fail`, `skip`, and `skip-cascade`. @@ -38,7 +38,7 @@ When a condition is unmet or a step fails, you control what happens: | Mode | Behavior | | -------------- | ----------------------------------------------------------------------------------------------- | -| `fail` | Step fails, entire run fails (default for `retriesExhausted`) | +| `fail` | Step fails, entire run fails (default for `whenExhausted`) | | `skip` | Step marked as skipped, run continues, dependents receive `undefined` (default for `whenUnmet`) | | `skip-cascade` | Step AND all downstream dependents skipped, run continues | @@ -77,14 +77,14 @@ Continue the workflow even if an optional step fails: slug: 'sendWelcomeEmail', dependsOn: ['createAccount'], maxAttempts: 3, - retriesExhausted: 'skip', // If email fails, continue anyway + whenExhausted: 'skip', // If email fails, continue anyway }, async (input) => { return await sendEmail(input.createAccount.accountId); }) ``` @@ -124,7 +124,7 @@ pgflow's type system tracks which steps may be skipped: { return await sendEmail(input.run.email); }) @@ -38,19 +38,19 @@ The `retriesExhausted` option controls what happens when a step fails after exha | `skip-cascade` | Step AND all downstream dependents skipped | ## TYPE_VIOLATION Exception -Programming errors always fail the run, regardless of `retriesExhausted`: +Programming errors always fail the run, regardless of `whenExhausted`: ```typescript -// This ALWAYS fails the run, even with retriesExhausted: 'skip' +// This ALWAYS fails the run, even with whenExhausted: 'skip' .step({ slug: 'fetchItems', - retriesExhausted: 'skip', + whenExhausted: 'skip', }, () => "not an array") // Returns string instead of array! .map({ slug: 'processItems', @@ -65,7 +65,7 @@ Programming errors always fail the run, regardless of `retriesExhausted`: ## Skip Reason -When `retriesExhausted: 'skip'` triggers, the step gets `skip_reason: 'handler_failed'` with the original error preserved in `error_message`. +When `whenExhausted: 'skip'` triggers, the step gets `skip_reason: 'handler_failed'` with the original error preserved in `error_message`. ```sql SELECT step_slug, error_message, skip_reason @@ -76,14 +76,14 @@ WHERE run_id = 'your-run-id' ## Combining with Conditions -You can use `retriesExhausted` together with `if` conditions for maximum flexibility: +You can use `whenExhausted` together with `if` conditions for maximum flexibility: ```typescript .step({ slug: 'enrichFromAPI', if: { includeEnrichment: true }, // Only attempt if requested maxAttempts: 3, - retriesExhausted: 'skip', // If API fails, continue anyway + whenExhausted: 'skip', // If API fails, continue anyway }, async (input) => { return await externalAPI.enrich(input.run.id); }) @@ -97,7 +97,7 @@ This step: ## Best Practices -**Do use `retriesExhausted: 'skip'` for:** +**Do use `whenExhausted: 'skip'` for:** - Notification steps (email, SMS, push) - Analytics and tracking events diff --git a/pkgs/website/src/content/docs/build/retrying-steps.mdx b/pkgs/website/src/content/docs/build/retrying-steps.mdx index d58efd02f..98034129a 100644 --- a/pkgs/website/src/content/docs/build/retrying-steps.mdx +++ b/pkgs/website/src/content/docs/build/retrying-steps.mdx @@ -74,7 +74,7 @@ Configure without retries: For detailed guidance on validation patterns, see [Validation Steps](/build/validation-steps/). @@ -133,7 +133,7 @@ new Flow({ The migration adds new columns (`required_input_pattern`, - `forbidden_input_pattern`, `when_unmet`, `when_failed`, `skip_reason`, + `forbidden_input_pattern`, `when_unmet`, `when_exhausted`, `skip_reason`, `skipped_at`) and a new step status (`skipped`). Existing flows continue to work unchanged. @@ -150,5 +150,5 @@ See [Install pgflow](/get-started/installation/) for upgrade details. - [Conditional Steps Overview](/build/conditional-steps/) - Introduction to the feature - [Pattern Matching](/build/conditional-steps/pattern-matching/) - JSON containment patterns - [Skip Modes](/build/conditional-steps/skip-modes/) - Detailed mode comparison -- [Graceful Failure](/build/graceful-failure/) - Handling failures with `retriesExhausted` +- [Graceful Failure](/build/graceful-failure/) - Handling failures with `whenExhausted` - [Examples](/build/conditional-steps/examples/) - AI/LLM patterns like query routing and RAG fallback diff --git a/pkgs/website/src/content/docs/reference/configuration/step-execution.mdx b/pkgs/website/src/content/docs/reference/configuration/step-execution.mdx index 0b7745e22..cb88ffedf 100644 --- a/pkgs/website/src/content/docs/reference/configuration/step-execution.mdx +++ b/pkgs/website/src/content/docs/reference/configuration/step-execution.mdx @@ -226,7 +226,7 @@ Controls what happens when `if` or `ifNot` condition is not met. workflow rather than failing the entire run. -### `retriesExhausted` +### `whenExhausted` **Type:** `'fail' | 'skip' | 'skip-cascade'` **Default:** `'fail'` @@ -243,24 +243,24 @@ Controls what happens when a step fails after exhausting all `maxAttempts` retry .step({ slug: 'sendEmail', maxAttempts: 3, - retriesExhausted: 'skip', // Don't fail run if email service is down + whenExhausted: 'skip', // Don't fail run if email service is down }, handler) .step({ slug: 'criticalOperation', maxAttempts: 5, - retriesExhausted: 'fail', // Default - fail if operation fails + whenExhausted: 'fail', // Default - fail if operation fails }, handler) ``` diff --git a/prompt.md b/prompt.md deleted file mode 100644 index e27ca6a39..000000000 --- a/prompt.md +++ /dev/null @@ -1 +0,0 @@ -your job is to read the beads related to the pgf-3hs epic about conditionals, verify current implementation in possibilities in @pkgs/dsl/__tests__/types/condition-pattern.test-d.ts @pkgs/dsl/__tests__/types/skippable-deps.test-d.ts and also some parts of schema @pkgs/core/schemas/0100_function__cascade_force_skip_steps.sql @pkgs/core/schemas/0100_function_complete_task.sql and the tests for the @pkgs/dsl/__tests__/runtime/condition-options.test.ts and @pkgs/core/schemas/0100_function_cascade_resolve_conditions.sql and the @pkgs/core/supabase/tests/condition_evaluation/ and @pkgs/core/supabase/tests/_cascade_force_skip_steps/ and the @.changeset/add-when-failed-option.md @.changeset/skip-infrastructure-schema.md and @pkgs/dsl/__tests__/runtime/when-failed-options.test.ts @pkgs/core/supabase/tests/fail_task_when_failed/ and anything you think is related, then you should read the current docs, especially the onboarding in @pkgs/website/src/content/docs/get-started/ , the @pkgs/website/src/content/docs/concepts/, and @pkgs/website/src/content/docs/build/ and you should come up with a plan on how it would be best to explain the conditions, skipping, skip cascades, whenFailed, json containment matching and all that in a way that is easy to understand, visual and have examples. i think we would need a DAG d2 diagrams showing step by step what happens for each skip/fail mode, showing the differences of what happens whe stuff is skipped/failed and what happes to dependents/run based on what whenUnmet/retriesExhausted modes selected. probably need a new color for skipped status in @pkgs/website/src/assets/pgflow-theme.d2 and new explanation in the @.claude/skills/writing-d2-diagrams/SKILL.md and some updates into @.claude/skills/writing-pgflow-flows/SKILL.md . you should leverage the parallel task agents for as much work as possible in order to answer as many questions that you have created. you should do the research in multiple stages. first, think about what you should understand first, so then you can spawn multiple task agents that will read and summarize and explain stuff to you. then you will be able to plan additional stages of tasks for the parallel subagents. the results of your work should be a comphrehensive plan