From e1138574a1aa028a866df2a30bc4f0941de3345a Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Tue, 5 May 2026 11:23:10 -0400 Subject: [PATCH 1/6] Drop pre-1.0 data-shape migrations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-1.0 Data Machine accumulated a chain of persisted-shape migrations (layered architecture move, agent_ping → system_task → agent_call, handler_slug singular → plural → cardinality-aware, queue payload split + queue_mode collapse, webhook auth v2, settings/agent model shape, ai-http-client provider keys, redundant post pipeline meta, update → upsert, etc). Carrying all of those forward forever is fat, not contract — every site that has ever booted current code already ran them. Remove the entire migration chain plus its smokes. Keep only the current deploy-time schema ensures (bundle artifacts, processed-item claim columns, pending-action table) so an in-place plugin update without a reactivation toggle still gets the current schema. Activation: drop the init-priority "run full activation on every DATAMACHINE_VERSION bump" hook and the legacy SiteContext/log-level option scrubs that only existed to clean up after older versions. uninstall.php: remove deletes for old per-provider auth options, chubes_ai_* user_meta, and datamachine_log_level_* options. FlowStepConfig::normalizeHandlerShape: stop cross-inferring between scalar and plural handler shapes. Canonical only — single-handler steps own handler_slug/handler_config, multi-handler steps own handler_slugs/handler_configs. PostTracking: drop comment referencing the deleted post-pipeline-meta migration. Tests: delete every migration-runtime smoke that pinned the old chain. Rewrite tests/migration-runtime-smoke.php to assert the current schema runtime exists and the deleted migrations are gone. --- data-machine.php | 23 +- inc/Core/Steps/FlowStepConfig.php | 53 +- inc/Core/WordPress/PostTracking.php | 4 +- inc/migrations/activation.php | 35 -- inc/migrations/agent-config-model-shape.php | 127 ---- inc/migrations/agent-ping.php | 332 ---------- inc/migrations/ai-enabled-tools.php | 122 ---- inc/migrations/ai-provider-keys.php | 51 -- inc/migrations/backfill.php | 222 ------- inc/migrations/handler-keys.php | 137 ---- inc/migrations/handler-slug-scalar.php | 110 ---- inc/migrations/layered-architecture.php | 309 --------- inc/migrations/load.php | 24 +- inc/migrations/network-scope.php | 402 ------------ inc/migrations/post-pipeline-meta.php | 69 --- inc/migrations/runtime.php | 139 +---- inc/migrations/settings-mode-models.php | 96 --- inc/migrations/site-md.php | 137 ---- inc/migrations/split-queue-payload.php | 189 ------ .../strip-pipeline-step-provider-model.php | 96 --- inc/migrations/update-to-upsert.php | 126 ---- inc/migrations/user-message-queue-mode.php | 249 -------- inc/migrations/webhook-auth-v2.php | 194 ------ .../PostPipelineMetaMigrationTest.php | 66 -- tests/agent-call-migration-smoke.php | 91 --- ...ent-config-model-shape-migration-smoke.php | 355 ----------- tests/ai-enabled-tools-smoke.php | 128 +--- tests/handler-slug-scalar-migration-smoke.php | 193 ------ tests/migration-runtime-smoke.php | 331 ++-------- tests/queue-mode-collapse-smoke.php | 585 ------------------ tests/queue-payload-split-smoke.php | 564 ----------------- .../settings-mode-models-migration-smoke.php | 201 ------ .../system-task-config-passthrough-smoke.php | 54 +- tests/webhook-auth-v2-migration-smoke.php | 209 ------- tests/wp-ai-client-provider-admin-smoke.php | 13 - uninstall.php | 21 - 36 files changed, 94 insertions(+), 5963 deletions(-) delete mode 100644 inc/migrations/activation.php delete mode 100644 inc/migrations/agent-config-model-shape.php delete mode 100644 inc/migrations/agent-ping.php delete mode 100644 inc/migrations/ai-enabled-tools.php delete mode 100644 inc/migrations/ai-provider-keys.php delete mode 100644 inc/migrations/backfill.php delete mode 100644 inc/migrations/handler-keys.php delete mode 100644 inc/migrations/handler-slug-scalar.php delete mode 100644 inc/migrations/layered-architecture.php delete mode 100644 inc/migrations/network-scope.php delete mode 100644 inc/migrations/post-pipeline-meta.php delete mode 100644 inc/migrations/settings-mode-models.php delete mode 100644 inc/migrations/split-queue-payload.php delete mode 100644 inc/migrations/strip-pipeline-step-provider-model.php delete mode 100644 inc/migrations/update-to-upsert.php delete mode 100644 inc/migrations/user-message-queue-mode.php delete mode 100644 inc/migrations/webhook-auth-v2.php delete mode 100644 tests/Unit/Migrations/PostPipelineMetaMigrationTest.php delete mode 100644 tests/agent-call-migration-smoke.php delete mode 100644 tests/agent-config-model-shape-migration-smoke.php delete mode 100644 tests/handler-slug-scalar-migration-smoke.php delete mode 100644 tests/queue-mode-collapse-smoke.php delete mode 100644 tests/queue-payload-split-smoke.php delete mode 100644 tests/settings-mode-models-migration-smoke.php delete mode 100644 tests/webhook-auth-v2-migration-smoke.php diff --git a/data-machine.php b/data-machine.php index e1d661868..d5ce0ebbe 100644 --- a/data-machine.php +++ b/data-machine.php @@ -204,10 +204,8 @@ function () { // when the registry fires lazily (always after init), so translations are // already loaded by execution time. // - // Previously this was wrapped in add_action('init', ...) at priority 10, but - // datamachine_maybe_run_migrations() at init priority 5 triggers the registry - // via ScaffoldAbilities::get_ability() → WP_Abilities_Registry::get_instance(), - // firing wp_abilities_api_init before the hooks were registered. + // Register eagerly so any later lazy WP_Abilities_Registry initialization sees + // every ability hook in this request. new \DataMachine\Abilities\AuthAbilities(); new \DataMachine\Abilities\AI\InspectRequestAbility(); new \DataMachine\Abilities\File\AgentFileAbilities(); @@ -691,25 +689,14 @@ function datamachine_activate_for_site() { set_transient( 'datamachine_needs_scaffold', 1, HOUR_IN_SECONDS ); } - // Run the shared migration chain. Each migration is idempotent and - // option-gated; this same function fires from - // `datamachine_maybe_run_deferred_migrations()` at plugins_loaded:5 - // when a deploy advances DATAMACHINE_VERSION past the persisted - // `datamachine_db_version` option (#1301). + // Ensure current deploy-time schema additions exist. datamachine_run_schema_migrations(); // Regenerate every composable memory file (SITE.md, NETWORK.md, AGENTS.md, …) - // from their registered sections, and clean up the legacy SiteContext transient. + // from their registered sections. // Activation-only — composable regeneration is heavy and shouldn't fire on - // every deploy (the version-gated runtime path is for schema-shape drift, - // not opportunistic content refresh). + // every deploy. \DataMachine\Engine\AI\ComposableFileGenerator::regenerate_all(); - delete_transient( 'datamachine_site_context_data' ); - - // Clean up legacy per-agent-type log level options (idempotent). - foreach ( array( 'pipeline', 'chat', 'system' ) as $legacy_agent_type ) { - delete_option( "datamachine_log_level_{$legacy_agent_type}" ); - } // Re-schedule any flows with non-manual scheduling datamachine_activate_scheduled_flows(); diff --git a/inc/Core/Steps/FlowStepConfig.php b/inc/Core/Steps/FlowStepConfig.php index 1c4d0f11b..ba0896d55 100644 --- a/inc/Core/Steps/FlowStepConfig.php +++ b/inc/Core/Steps/FlowStepConfig.php @@ -364,59 +364,35 @@ public static function normalizeHandlerShape( array $step_config ): array { $is_multi = self::isMultiHandler( $step_config ); $step_type = $step_config['step_type'] ?? ''; - $legacy_slugs = self::sanitizeSlugList( is_array( $step_config['handler_slugs'] ?? null ) ? $step_config['handler_slugs'] : array() ); - $legacy_configs = is_array( $step_config['handler_configs'] ?? null ) ? $step_config['handler_configs'] : array(); - $config_slugs = self::sanitizeSlugList( array_keys( $legacy_configs ) ); $scalar_slug = is_string( $step_config['handler_slug'] ?? null ) ? $step_config['handler_slug'] : ''; $scalar_config = is_array( $step_config['handler_config'] ?? null ) ? $step_config['handler_config'] : array(); + $handler_slugs = self::sanitizeSlugList( is_array( $step_config['handler_slugs'] ?? null ) ? $step_config['handler_slugs'] : array() ); + $handler_configs = is_array( $step_config['handler_configs'] ?? null ) ? $step_config['handler_configs'] : array(); unset( $step_config['handler'], $step_config['handler_slug'], $step_config['handler_slugs'], $step_config['handler_config'], $step_config['handler_configs'] ); if ( ! $uses_handler ) { - $config = $scalar_config; - if ( empty( $config ) && '' !== $step_type && isset( $legacy_configs[ $step_type ] ) && is_array( $legacy_configs[ $step_type ] ) ) { - $config = $legacy_configs[ $step_type ]; - } - if ( ! empty( $config ) ) { - $step_config['handler_config'] = $config; + if ( ! empty( $scalar_config ) ) { + $step_config['handler_config'] = $scalar_config; } return $step_config; } if ( $is_multi ) { - $slugs = $legacy_slugs; - if ( empty( $slugs ) && '' !== $scalar_slug ) { - $slugs = array( $scalar_slug ); - } - if ( empty( $slugs ) && ! empty( $config_slugs ) ) { - $slugs = $config_slugs; - } - - $configs = $legacy_configs; - if ( empty( $configs ) && ! empty( $scalar_config ) && ! empty( $slugs ) ) { - $configs = array( $slugs[0] => $scalar_config ); + if ( ! empty( $handler_slugs ) ) { + $step_config['handler_slugs'] = $handler_slugs; } - - if ( ! empty( $slugs ) ) { - $step_config['handler_slugs'] = $slugs; - } - if ( ! empty( $configs ) ) { - $step_config['handler_configs'] = $configs; + if ( ! empty( $handler_configs ) ) { + $step_config['handler_configs'] = $handler_configs; } return $step_config; } - $slug = '' !== $scalar_slug ? $scalar_slug : ( $legacy_slugs[0] ?? ( $config_slugs[0] ?? '' ) ); - if ( '' !== $slug ) { - $step_config['handler_slug'] = $slug; - } - - $config = $scalar_config; - if ( empty( $config ) && '' !== $slug && isset( $legacy_configs[ $slug ] ) && is_array( $legacy_configs[ $slug ] ) ) { - $config = $legacy_configs[ $slug ]; + if ( '' !== $scalar_slug ) { + $step_config['handler_slug'] = $scalar_slug; } - if ( ! empty( $config ) || '' !== $slug ) { - $step_config['handler_config'] = $config; + if ( ! empty( $scalar_config ) || '' !== $scalar_slug ) { + $step_config['handler_config'] = $scalar_config; } return $step_config; } @@ -424,10 +400,7 @@ public static function normalizeHandlerShape( array $step_config ): array { /** * Get the AI step's enabled tools. * - * Reads the dedicated `enabled_tools` field. The pre-Phase 2b shape - * (AI tools stored under `handler_slugs`) is migrated on activation - * by inc/migrations/ai-enabled-tools.php — there is no runtime - * fallback to legacy data. + * Reads the dedicated `enabled_tools` field. * * @since 0.81.0 * diff --git a/inc/Core/WordPress/PostTracking.php b/inc/Core/WordPress/PostTracking.php index 2b6bada76..97f68705d 100644 --- a/inc/Core/WordPress/PostTracking.php +++ b/inc/Core/WordPress/PostTracking.php @@ -23,9 +23,7 @@ * tools receive the same origin meta as handler tools * (#1084). * @since 0.69.1 Dropped redundant pipeline_id post meta — derivable from - * flow_id via the flows table (#1091). Existing legacy rows - * are cleared by the datamachine_drop_redundant_post_pipeline_meta - * migration. + * flow_id via the flows table (#1091). */ namespace DataMachine\Core\WordPress; diff --git a/inc/migrations/activation.php b/inc/migrations/activation.php deleted file mode 100644 index e25b6326e..000000000 --- a/inc/migrations/activation.php +++ /dev/null @@ -1,35 +0,0 @@ -=' ) ) { - return; - } - - datamachine_activate_for_site(); - update_option( 'datamachine_db_version', DATAMACHINE_VERSION, true ); -} -add_action( 'init', 'datamachine_maybe_run_migrations', 5 ); diff --git a/inc/migrations/agent-config-model-shape.php b/inc/migrations/agent-config-model-shape.php deleted file mode 100644 index 794688d46..000000000 --- a/inc/migrations/agent-config-model-shape.php +++ /dev/null @@ -1,127 +0,0 @@ -base_prefix . 'datamachine_agents'; - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL - $table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $agents_table ) ); - if ( ! $table_exists ) { - update_option( 'datamachine_agent_config_model_shape_migrated', true, true ); - return; - } - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL - $rows = $wpdb->get_results( "SELECT agent_id, agent_config FROM {$agents_table}", ARRAY_A ); - - $migrated = 0; - - if ( ! empty( $rows ) ) { - foreach ( $rows as $row ) { - $config = json_decode( $row['agent_config'] ?? '', true ); - if ( ! is_array( $config ) ) { - continue; - } - - if ( ! isset( $config['model'] ) || ! is_array( $config['model'] ) ) { - continue; - } - - $legacy = isset( $config['model']['default'] ) && is_array( $config['model']['default'] ) - ? $config['model']['default'] - : array(); - $provider = isset( $legacy['provider'] ) ? trim( (string) $legacy['provider'] ) : ''; - $model = isset( $legacy['model'] ) ? trim( (string) $legacy['model'] ) : ''; - - unset( $config['model'] ); - - if ( '' !== $provider && ! isset( $config['default_provider'] ) ) { - $config['default_provider'] = $provider; - } - if ( '' !== $model && ! isset( $config['default_model'] ) ) { - $config['default_model'] = $model; - } - - // Empty array → empty object on the JSON side, so the column - // stays `{}` rather than `[]` and matches every other code - // path that writes agent_config. - $encoded = empty( $config ) - ? '{}' - : wp_json_encode( $config ); - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching - $wpdb->update( - $agents_table, - array( 'agent_config' => $encoded ), - array( 'agent_id' => (int) $row['agent_id'] ), - array( '%s' ), - array( '%d' ) - ); - - ++$migrated; - } - } - - update_option( 'datamachine_agent_config_model_shape_migrated', true, true ); - - if ( $migrated > 0 ) { - do_action( - 'datamachine_log', - 'info', - 'Migrated stale agent_config.model.default.* shape to default_provider/default_model.', - array( - 'agents_updated' => $migrated, - ) - ); - } -} diff --git a/inc/migrations/agent-ping.php b/inc/migrations/agent-ping.php deleted file mode 100644 index bd932d0fd..000000000 --- a/inc/migrations/agent-ping.php +++ /dev/null @@ -1,332 +0,0 @@ - 'agent_call', - 'params' => array( - 'target' => array( - 'type' => 'webhook', - 'id' => $old_config['webhook_url'] ?? '', - 'auth' => array( - 'header_name' => $old_config['auth_header_name'] ?? '', - 'token' => $old_config['auth_token'] ?? '', - ), - ), - 'input' => array( - 'task' => $old_config['prompt'] ?? '', - 'messages' => array(), - 'context' => array(), - ), - 'delivery' => array( - 'mode' => 'fire_and_forget', - 'timeout' => 30, - 'reply_to' => $old_config['reply_to'] ?? '', - ), - ), - ); -} - -/** - * Migrate agent_ping step types to agent_call system_task steps in flow configs. - * - * Converts existing agent_ping steps to system_task steps with - * task: 'agent_call' in handler_config. Preserves webhook_url, prompt, - * auth settings, queue_enabled, and prompt_queue. - * - * Idempotent: guarded by datamachine_agent_ping_migrated option. - * - * @since 0.60.0 - */ -function datamachine_migrate_agent_ping_to_system_task(): void { - if ( get_option( 'datamachine_agent_ping_migrated', false ) ) { - return; - } - - global $wpdb; - $table = $wpdb->prefix . 'datamachine_flows'; - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery - // phpcs:disable WordPress.DB.PreparedSQL -- Table name from $wpdb->prefix. - $table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ); - // phpcs:enable WordPress.DB.PreparedSQL - if ( ! $table_exists ) { - update_option( 'datamachine_agent_ping_migrated', true, true ); - return; - } - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching - // phpcs:disable WordPress.DB.PreparedSQL -- Table name from $wpdb->prefix. - $rows = $wpdb->get_results( "SELECT flow_id, flow_config FROM {$table}", ARRAY_A ); - // phpcs:enable WordPress.DB.PreparedSQL - if ( empty( $rows ) ) { - update_option( 'datamachine_agent_ping_migrated', true, true ); - return; - } - - $migrated = 0; - - foreach ( $rows as $row ) { - $flow_config = json_decode( $row['flow_config'], true ); - if ( ! is_array( $flow_config ) ) { - continue; - } - - $changed = false; - foreach ( $flow_config as $step_id => &$step ) { - if ( ! is_array( $step ) ) { - continue; - } - - // Only convert steps with agent_ping step type. - if ( 'agent_ping' !== ( $step['step_type'] ?? '' ) ) { - continue; - } - - // Extract existing agent_ping handler config. - $old_config = $step['handler_configs']['agent_ping'] ?? array(); - - // Build new system_task handler_config. - $new_config = datamachine_agent_call_config_from_legacy_ping( $old_config ); - - // Convert step type and handler references. - $step['step_type'] = 'system_task'; - $step['handler_config'] = $new_config; - unset( $step['handler_slug'], $step['handler_slugs'], $step['handler_configs'] ); - - // queue_enabled and prompt_queue stay at their existing positions - // in the step config — no changes needed, they're already there. - - $changed = true; - } - unset( $step ); - - if ( $changed ) { - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching - $wpdb->update( - $table, - array( 'flow_config' => wp_json_encode( $flow_config ) ), - array( 'flow_id' => $row['flow_id'] ), - array( '%s' ), - array( '%d' ) - ); - ++$migrated; - } - } - - update_option( 'datamachine_agent_ping_migrated', true, true ); - - if ( $migrated > 0 ) { - do_action( - 'datamachine_log', - 'info', - 'Migrated agent_ping steps to agent_call system_task steps in flow configs', - array( 'flows_updated' => $migrated ) - ); - } -} - -/** - * Migrate agent_ping step types to agent_call system_task steps in pipeline configs. - * - * Follow-up to datamachine_migrate_agent_ping_to_system_task() which only - * migrated flow configs. Pipeline configs were missed, leaving orphaned - * agent_ping steps in the pipeline UI. - * - * Idempotent: guarded by datamachine_agent_ping_pipeline_migrated option. - * - * @since 0.73.0 - */ -function datamachine_migrate_agent_ping_pipeline_to_system_task(): void { - if ( get_option( 'datamachine_agent_ping_pipeline_migrated', false ) ) { - return; - } - - global $wpdb; - $table = $wpdb->prefix . 'datamachine_pipelines'; - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery - // phpcs:disable WordPress.DB.PreparedSQL -- Table name from $wpdb->prefix. - $table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ); - // phpcs:enable WordPress.DB.PreparedSQL - if ( ! $table_exists ) { - update_option( 'datamachine_agent_ping_pipeline_migrated', true, true ); - return; - } - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching - // phpcs:disable WordPress.DB.PreparedSQL -- Table name from $wpdb->prefix. - $rows = $wpdb->get_results( "SELECT pipeline_id, pipeline_config FROM {$table}", ARRAY_A ); - // phpcs:enable WordPress.DB.PreparedSQL - if ( empty( $rows ) ) { - update_option( 'datamachine_agent_ping_pipeline_migrated', true, true ); - return; - } - - $migrated = 0; - - foreach ( $rows as $row ) { - $pipeline_config = json_decode( $row['pipeline_config'], true ); - if ( ! is_array( $pipeline_config ) ) { - continue; - } - - $changed = false; - foreach ( $pipeline_config as $step_id => &$step ) { - if ( ! is_array( $step ) ) { - continue; - } - - // Only convert steps with agent_ping step type. - if ( 'agent_ping' !== ( $step['step_type'] ?? '' ) ) { - continue; - } - - // Extract existing agent_ping handler config (if present). - $old_config = $step['handler_configs']['agent_ping'] ?? array(); - - // Build new system_task handler_config. - $new_config = datamachine_agent_call_config_from_legacy_ping( $old_config ); - - // Convert step type and handler references. - $step['step_type'] = 'system_task'; - $step['handler_config'] = $new_config; - unset( $step['handler_slug'], $step['handler_slugs'], $step['handler_configs'] ); - - $changed = true; - } - unset( $step ); - - if ( $changed ) { - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching - $wpdb->update( - $table, - array( 'pipeline_config' => wp_json_encode( $pipeline_config ) ), - array( 'pipeline_id' => $row['pipeline_id'] ), - array( '%s' ), - array( '%d' ) - ); - ++$migrated; - } - } - - update_option( 'datamachine_agent_ping_pipeline_migrated', true, true ); - - if ( $migrated > 0 ) { - do_action( - 'datamachine_log', - 'info', - 'Migrated agent_ping steps to agent_call system_task steps in pipeline configs', - array( 'pipelines_updated' => $migrated ) - ); - } -} - -/** - * Migrate already-system-task agent_ping configs to agent_call configs. - * - * This catches installs that already ran the older agent_ping→system_task - * migration before agent_call became the canonical task vocabulary. - * - * Idempotent: guarded by datamachine_agent_ping_task_to_agent_call_migrated option. - * - * @since 0.87.0 - */ -function datamachine_migrate_agent_ping_task_to_agent_call(): void { - if ( get_option( 'datamachine_agent_ping_task_to_agent_call_migrated', false ) ) { - return; - } - - global $wpdb; - $tables = array( - array( $wpdb->prefix . 'datamachine_flows', 'flow_id', 'flow_config', 'flows_updated' ), - array( $wpdb->prefix . 'datamachine_pipelines', 'pipeline_id', 'pipeline_config', 'pipelines_updated' ), - ); - - $updated = array(); - foreach ( $tables as $table_def ) { - list( $table, $id_column, $config_column, $metric ) = $table_def; - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery - // phpcs:disable WordPress.DB.PreparedSQL -- Table name from $wpdb->prefix. - $table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ); - // phpcs:enable WordPress.DB.PreparedSQL - if ( ! $table_exists ) { - $updated[ $metric ] = 0; - continue; - } - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching - // phpcs:disable WordPress.DB.PreparedSQL -- Table/column names are constants from this migration. - $rows = $wpdb->get_results( "SELECT {$id_column}, {$config_column} FROM {$table}", ARRAY_A ); - // phpcs:enable WordPress.DB.PreparedSQL - - $count = 0; - foreach ( $rows as $row ) { - $config = json_decode( $row[ $config_column ], true ); - if ( ! is_array( $config ) ) { - continue; - } - - $changed = false; - foreach ( $config as &$step ) { - if ( ! is_array( $step ) || 'system_task' !== ( $step['step_type'] ?? '' ) ) { - continue; - } - - $handler_config = $step['handler_config'] ?? array(); - if ( ! is_array( $handler_config ) || 'agent_ping' !== ( $handler_config['task'] ?? '' ) ) { - continue; - } - - $old_params = is_array( $handler_config['params'] ?? null ) ? $handler_config['params'] : array(); - $step['handler_config'] = datamachine_agent_call_config_from_legacy_ping( $old_params ); - $changed = true; - } - unset( $step ); - - if ( ! $changed ) { - continue; - } - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching - $wpdb->update( - $table, - array( $config_column => wp_json_encode( $config ) ), - array( $id_column => $row[ $id_column ] ), - array( '%s' ), - array( '%d' ) - ); - ++$count; - } - - $updated[ $metric ] = $count; - } - - update_option( 'datamachine_agent_ping_task_to_agent_call_migrated', true, true ); - - if ( array_sum( $updated ) > 0 ) { - do_action( - 'datamachine_log', - 'info', - 'Migrated agent_ping system_task configs to agent_call configs', - $updated - ); - } -} diff --git a/inc/migrations/ai-enabled-tools.php b/inc/migrations/ai-enabled-tools.php deleted file mode 100644 index e226c3590..000000000 --- a/inc/migrations/ai-enabled-tools.php +++ /dev/null @@ -1,122 +0,0 @@ -prefix . 'datamachine_flows'; - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery - // phpcs:disable WordPress.DB.PreparedSQL -- Table name from $wpdb->prefix. - $table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ); - // phpcs:enable WordPress.DB.PreparedSQL - if ( ! $table_exists ) { - update_option( 'datamachine_ai_enabled_tools_migrated', true, true ); - return; - } - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery - // phpcs:disable WordPress.DB.PreparedSQL -- Table name from $wpdb->prefix. - $rows = $wpdb->get_results( "SELECT flow_id, flow_config FROM {$table}", ARRAY_A ); - // phpcs:enable WordPress.DB.PreparedSQL - - if ( empty( $rows ) ) { - update_option( 'datamachine_ai_enabled_tools_migrated', true, true ); - return; - } - - $migrated = 0; - foreach ( $rows as $row ) { - $flow_config = json_decode( $row['flow_config'], true ); - if ( ! is_array( $flow_config ) ) { - continue; - } - - $changed = false; - foreach ( $flow_config as $step_id => &$step ) { - if ( ! is_array( $step ) ) { - continue; - } - - if ( 'ai' !== ( $step['step_type'] ?? '' ) ) { - continue; - } - - // Already migrated. - if ( ! empty( $step['enabled_tools'] ) && is_array( $step['enabled_tools'] ) ) { - if ( ! empty( $step['handler_slugs'] ) ) { - $step['handler_slugs'] = array(); - $changed = true; - } - continue; - } - - $legacy = $step['handler_slugs'] ?? array(); - if ( empty( $legacy ) || ! is_array( $legacy ) ) { - // Nothing to migrate; ensure the field exists for shape consistency. - if ( ! isset( $step['enabled_tools'] ) ) { - $step['enabled_tools'] = array(); - $changed = true; - } - continue; - } - - $step['enabled_tools'] = array_values( $legacy ); - $step['handler_slugs'] = array(); - $changed = true; - } - unset( $step ); - - if ( $changed ) { - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching - $wpdb->update( - $table, - array( 'flow_config' => wp_json_encode( $flow_config ) ), - array( 'flow_id' => $row['flow_id'] ), - array( '%s' ), - array( '%d' ) - ); - ++$migrated; - } - } - - update_option( 'datamachine_ai_enabled_tools_migrated', true, true ); - - if ( $migrated > 0 ) { - do_action( - 'datamachine_log', - 'info', - 'Migrated AI step tools from handler_slugs to enabled_tools', - array( 'flows_updated' => $migrated ) - ); - } -} diff --git a/inc/migrations/ai-provider-keys.php b/inc/migrations/ai-provider-keys.php deleted file mode 100644 index 31a116d44..000000000 --- a/inc/migrations/ai-provider-keys.php +++ /dev/null @@ -1,51 +0,0 @@ - $api_key ) { - $provider = sanitize_key( (string) $provider ); - $api_key = is_scalar( $api_key ) ? (string) $api_key : ''; - - if ( '' === $provider || '' === $api_key ) { - continue; - } - - $setting_name = 'connectors_ai_' . str_replace( '-', '_', $provider ) . '_api_key'; - if ( '' === (string) get_option( $setting_name, '' ) ) { - update_option( $setting_name, $api_key, true ); - } - } - } - - update_option( 'datamachine_ai_provider_keys_migrated', true, true ); -} diff --git a/inc/migrations/backfill.php b/inc/migrations/backfill.php deleted file mode 100644 index 0b9ab1049..000000000 --- a/inc/migrations/backfill.php +++ /dev/null @@ -1,222 +0,0 @@ - 0 but no agent_id, looks up the agent - * via Agents::get_by_owner_id() and sets agent_id. Also bootstraps agent_access - * rows so owners have admin access to their agents. - * - * Idempotent: only processes rows where agent_id IS NULL and user_id > 0. - * Skipped entirely on fresh installs (no rows to backfill). - * - * @since 0.41.0 - */ -function datamachine_backfill_agent_ids(): void { - if ( get_option( 'datamachine_agent_ids_backfilled', false ) ) { - return; - } - - global $wpdb; - - $agents_repo = new \DataMachine\Core\Database\Agents\Agents(); - $access_repo = new \DataMachine\Core\Database\Agents\AgentAccess(); - - $tables = array( - $wpdb->prefix . 'datamachine_pipelines', - $wpdb->prefix . 'datamachine_flows', - $wpdb->prefix . 'datamachine_jobs', - ); - - // Cache of user_id → agent_id to avoid repeated lookups. - $agent_map = array(); - $backfilled = 0; - - foreach ( $tables as $table ) { - // Check table exists. - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery - $table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ); - if ( ! $table_exists ) { - continue; - } - - // Check agent_id column exists (migration may not have run yet). - if ( ! \DataMachine\Core\Database\BaseRepository::column_exists( $table, 'agent_id', $wpdb ) ) { - continue; - } - - // Get distinct user_ids that need backfill. - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching - // phpcs:disable WordPress.DB.PreparedSQL -- Table name from $wpdb->prefix, not user input. - $user_ids = $wpdb->get_col( - "SELECT DISTINCT user_id FROM {$table} WHERE user_id > 0 AND agent_id IS NULL" - ); - // phpcs:enable WordPress.DB.PreparedSQL - - if ( empty( $user_ids ) ) { - continue; - } - - foreach ( $user_ids as $user_id ) { - $user_id = (int) $user_id; - - if ( ! isset( $agent_map[ $user_id ] ) ) { - $agent = $agents_repo->get_by_owner_id( $user_id ); - if ( $agent ) { - $agent_map[ $user_id ] = (int) $agent['agent_id']; - - // Bootstrap agent_access for owner. - $access_repo->bootstrap_owner_access( (int) $agent['agent_id'], $user_id ); - } else { - // Try to create agent for this user. - $created_id = datamachine_resolve_or_create_agent_id( $user_id ); - $agent_map[ $user_id ] = $created_id; - - if ( $created_id > 0 ) { - $access_repo->bootstrap_owner_access( $created_id, $user_id ); - } - } - } - - $agent_id = $agent_map[ $user_id ]; - if ( $agent_id <= 0 ) { - continue; - } - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching - // phpcs:disable WordPress.DB.PreparedSQL -- Table name from $wpdb->prefix, not user input. - $updated = $wpdb->query( - $wpdb->prepare( - "UPDATE {$table} SET agent_id = %d WHERE user_id = %d AND agent_id IS NULL", - $agent_id, - $user_id - ) - ); - // phpcs:enable WordPress.DB.PreparedSQL - - if ( false !== $updated ) { - $backfilled += $updated; - } - } - } - - update_option( 'datamachine_agent_ids_backfilled', true, true ); - - if ( $backfilled > 0 ) { - do_action( - 'datamachine_log', - 'info', - 'Backfilled agent_id on existing pipelines, flows, and jobs', - array( - 'rows_updated' => $backfilled, - 'agent_map' => $agent_map, - ) - ); - } -} - -/** - * Assign orphaned resources to the sole agent on single-agent installs. - * - * Handles the case where pipelines, flows, and jobs were created before - * agent scoping existed (user_id=0, agent_id=NULL). If exactly one agent - * exists, assigns all unowned resources to it. - * - * Idempotent: runs once per install, skipped if multi-agent (>1 agent). - * - * @since 0.41.0 - */ -function datamachine_assign_orphaned_resources_to_sole_agent(): void { - if ( get_option( 'datamachine_orphaned_resources_assigned', false ) ) { - return; - } - - global $wpdb; - - $agents_repo = new \DataMachine\Core\Database\Agents\Agents(); - - // Only proceed for single-agent installs. - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching - $agent_count = (int) $wpdb->get_var( - $wpdb->prepare( 'SELECT COUNT(*) FROM %i', $wpdb->base_prefix . 'datamachine_agents' ) - ); - - if ( 1 !== $agent_count ) { - // 0 agents: nothing to assign to. >1 agents: ambiguous, skip. - update_option( 'datamachine_orphaned_resources_assigned', true, true ); - return; - } - - // Get the sole agent's ID. - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching - $agent_id = (int) $wpdb->get_var( - $wpdb->prepare( 'SELECT agent_id FROM %i LIMIT 1', $wpdb->base_prefix . 'datamachine_agents' ) - ); - - if ( $agent_id <= 0 ) { - update_option( 'datamachine_orphaned_resources_assigned', true, true ); - return; - } - - $tables = array( - $wpdb->prefix . 'datamachine_pipelines', - $wpdb->prefix . 'datamachine_flows', - $wpdb->prefix . 'datamachine_jobs', - ); - - $total_assigned = 0; - - foreach ( $tables as $table ) { - // Check table exists. - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery - $table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ); - if ( ! $table_exists ) { - continue; - } - - // Check agent_id column exists. - if ( ! \DataMachine\Core\Database\BaseRepository::column_exists( $table, 'agent_id', $wpdb ) ) { - continue; - } - - // Assign orphaned rows (agent_id IS NULL) to the sole agent. - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching - // phpcs:disable WordPress.DB.PreparedSQL -- Table name from $wpdb->prefix. - $updated = $wpdb->query( - $wpdb->prepare( - "UPDATE {$table} SET agent_id = %d WHERE agent_id IS NULL", - $agent_id - ) - ); - // phpcs:enable WordPress.DB.PreparedSQL - - if ( false !== $updated ) { - $total_assigned += $updated; - } - } - - update_option( 'datamachine_orphaned_resources_assigned', true, true ); - - if ( $total_assigned > 0 ) { - do_action( - 'datamachine_log', - 'info', - 'Assigned orphaned resources to sole agent', - array( - 'agent_id' => $agent_id, - 'rows_updated' => $total_assigned, - ) - ); - } -} diff --git a/inc/migrations/handler-keys.php b/inc/migrations/handler-keys.php deleted file mode 100644 index 0d8bea1f9..000000000 --- a/inc/migrations/handler-keys.php +++ /dev/null @@ -1,137 +0,0 @@ -prefix . 'datamachine_flows'; - - // Check table exists (fresh installs won't have legacy data). - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery - // phpcs:disable WordPress.DB.PreparedSQL -- Table name from $wpdb->prefix. - $table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ); - // phpcs:enable WordPress.DB.PreparedSQL - if ( ! $table_exists ) { - update_option( 'datamachine_handler_keys_migrated', true, true ); - return; - } - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery - // phpcs:disable WordPress.DB.PreparedSQL -- Table name from $wpdb->prefix. - $rows = $wpdb->get_results( "SELECT flow_id, flow_config FROM {$table}", ARRAY_A ); - // phpcs:enable WordPress.DB.PreparedSQL - - if ( empty( $rows ) ) { - update_option( 'datamachine_handler_keys_migrated', true, true ); - return; - } - - $migrated = 0; - foreach ( $rows as $row ) { - $flow_config = json_decode( $row['flow_config'], true ); - if ( ! is_array( $flow_config ) ) { - continue; - } - - $changed = false; - foreach ( $flow_config as $step_id => &$step ) { - if ( ! is_array( $step ) ) { - continue; - } - - // Skip flow-level metadata keys. - if ( 'memory_files' === $step_id ) { - continue; - } - - // Already has plural keys — check if singular leftovers need cleanup. - if ( isset( $step['handler_slugs'] ) && is_array( $step['handler_slugs'] ) ) { - // Ensure handler_configs exists when handler_slugs does. - if ( ! isset( $step['handler_configs'] ) || ! is_array( $step['handler_configs'] ) ) { - $primary = $step['handler_slugs'][0] ?? ''; - $config = $step['handler_config'] ?? array(); - $step['handler_configs'] = ! empty( $primary ) ? array( $primary => $config ) : array(); - $changed = true; - } - // Remove any leftover singular keys. - if ( isset( $step['handler_slug'] ) ) { - unset( $step['handler_slug'] ); - $changed = true; - } - if ( isset( $step['handler_config'] ) ) { - unset( $step['handler_config'] ); - $changed = true; - } - continue; - } - - // Convert singular to plural. - $slug = $step['handler_slug'] ?? ''; - $config = $step['handler_config'] ?? array(); - - if ( ! empty( $slug ) ) { - $step['handler_slugs'] = array( $slug ); - $step['handler_configs'] = array( $slug => $config ); - } else { - // Self-configuring steps (webhook_gate, system_task). - $step_type = $step['step_type'] ?? ''; - if ( ! empty( $step_type ) && ! empty( $config ) ) { - $step['handler_slugs'] = array( $step_type ); - $step['handler_configs'] = array( $step_type => $config ); - } else { - $step['handler_slugs'] = array(); - $step['handler_configs'] = array(); - } - } - - unset( $step['handler_slug'], $step['handler_config'] ); - $changed = true; - } - unset( $step ); - - if ( $changed ) { - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching - $wpdb->update( - $table, - array( 'flow_config' => wp_json_encode( $flow_config ) ), - array( 'flow_id' => $row['flow_id'] ), - array( '%s' ), - array( '%d' ) - ); - ++$migrated; - } - } - - update_option( 'datamachine_handler_keys_migrated', true, true ); - - if ( $migrated > 0 ) { - do_action( - 'datamachine_log', - 'info', - 'Migrated flow_config handler keys from singular to plural', - array( 'flows_updated' => $migrated ) - ); - } -} diff --git a/inc/migrations/handler-slug-scalar.php b/inc/migrations/handler-slug-scalar.php deleted file mode 100644 index 9a0d7bce6..000000000 --- a/inc/migrations/handler-slug-scalar.php +++ /dev/null @@ -1,110 +0,0 @@ -prefix . 'datamachine_flows', 'flow_id', 'flow_config' ); - $pipelines_updated = datamachine_migrate_handler_slug_scalar_table( $wpdb->prefix . 'datamachine_pipelines', 'pipeline_id', 'pipeline_config' ); - - update_option( 'datamachine_handler_slug_scalar_migrated', true, true ); - - if ( $flows_updated > 0 || $pipelines_updated > 0 ) { - do_action( - 'datamachine_log', - 'info', - 'Migrated handler slug storage to canonical scalar/list shape', - array( - 'flows_updated' => $flows_updated, - 'pipelines_updated' => $pipelines_updated, - ) - ); - } -} - -/** - * Migrate one config-bearing database table. - * - * @param string $table Table name. - * @param string $id_column Primary ID column. - * @param string $config_column JSON config column. - * @return int Rows updated. - */ -function datamachine_migrate_handler_slug_scalar_table( string $table, string $id_column, string $config_column ): int { - global $wpdb; - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery - // phpcs:disable WordPress.DB.PreparedSQL -- Table name from $wpdb->prefix. - $table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ); - // phpcs:enable WordPress.DB.PreparedSQL - if ( ! $table_exists ) { - return 0; - } - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching - // phpcs:disable WordPress.DB.PreparedSQL -- Table/column names are internal constants from this migration. - $rows = $wpdb->get_results( "SELECT {$id_column}, {$config_column} FROM {$table}", ARRAY_A ); - // phpcs:enable WordPress.DB.PreparedSQL - if ( empty( $rows ) ) { - return 0; - } - - $updated = 0; - foreach ( $rows as $row ) { - $config = json_decode( $row[ $config_column ], true ); - if ( ! is_array( $config ) ) { - continue; - } - - $changed = false; - foreach ( $config as $step_id => &$step ) { - if ( ! is_array( $step ) || 'memory_files' === $step_id ) { - continue; - } - - $normalized = \DataMachine\Core\Steps\FlowStepConfig::normalizeHandlerShape( $step ); - if ( $normalized !== $step ) { - $step = $normalized; - $changed = true; - } - } - unset( $step ); - - if ( ! $changed ) { - continue; - } - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching - $wpdb->update( - $table, - array( $config_column => wp_json_encode( $config ) ), - array( $id_column => $row[ $id_column ] ), - array( '%s' ), - array( '%d' ) - ); - ++$updated; - } - - return $updated; -} diff --git a/inc/migrations/layered-architecture.php b/inc/migrations/layered-architecture.php deleted file mode 100644 index 5c53e5214..000000000 --- a/inc/migrations/layered-architecture.php +++ /dev/null @@ -1,309 +0,0 @@ -get_agent_directory(); // .../datamachine-files/agent - $shared_dir = $directory_manager->get_shared_directory(); - - update_option( - 'datamachine_layered_arch_migration_backup', - array( - 'legacy_agent_base' => $legacy_agent_base, - 'migrated_at' => current_time( 'mysql', true ), - ), - false - ); - - if ( ! is_dir( $shared_dir ) ) { - wp_mkdir_p( $shared_dir ); - } - - $site_md = trailingslashit( $shared_dir ) . 'SITE.md'; - if ( ! file_exists( $site_md ) ) { - // Compose from registered sections. Caller already guarantees the - // shared dir exists; ComposableFileGenerator will set permissions. - \DataMachine\Engine\AI\ComposableFileGenerator::regenerate( 'SITE.md' ); - } - - $index_file = trailingslashit( $shared_dir ) . 'index.php'; - if ( ! file_exists( $index_file ) ) { - $fs->put_contents( $index_file, "user_login ) : 'user-' . $user_id; - $agent_name = $user ? $user->display_name : 'User ' . $user_id; - $agent_model = \DataMachine\Core\PluginSettings::getModelForMode( 'chat' ); - - $agent_id = $agents_repo->create_if_missing( - $agent_slug, - $agent_name, - $user_id, - array( - 'model' => array( - 'default' => $agent_model, - ), - ) - ); - - $agent_identity_dir = $directory_manager->get_agent_identity_directory( $agent_slug ); - $user_dir = $directory_manager->get_user_directory( $user_id ); - - if ( ! is_dir( $agent_identity_dir ) ) { - wp_mkdir_p( $agent_identity_dir ); - } - if ( ! is_dir( $user_dir ) ) { - wp_mkdir_p( $user_dir ); - } - - $agent_index = trailingslashit( $agent_identity_dir ) . 'index.php'; - if ( ! file_exists( $agent_index ) ) { - $fs->put_contents( $agent_index, "put_contents( $user_index, "copy( $legacy_soul, $new_soul, true, FS_CHMOD_FILE ); - \DataMachine\Core\FilesRepository\FilesystemHelper::make_group_writable( $new_soul ); - } - if ( file_exists( $legacy_memory ) && ! file_exists( $new_memory ) ) { - $fs->copy( $legacy_memory, $new_memory, true, FS_CHMOD_FILE ); - \DataMachine\Core\FilesRepository\FilesystemHelper::make_group_writable( $new_memory ); - } - if ( file_exists( $legacy_user ) && ! file_exists( $new_user ) ) { - $fs->copy( $legacy_user, $new_user, true, FS_CHMOD_FILE ); - \DataMachine\Core\FilesRepository\FilesystemHelper::make_group_writable( $new_user ); - } elseif ( ! file_exists( $new_user ) ) { - $user_profile_lines = array(); - $user_profile_lines[] = '# User Profile'; - $user_profile_lines[] = ''; - $user_profile_lines[] = '## About'; - $user_profile_lines[] = '- **Name:** ' . ( $user ? $user->display_name : 'User ' . $user_id ); - if ( $user && ! empty( $user->user_email ) ) { - $user_profile_lines[] = '- **Email:** ' . $user->user_email; - } - $user_profile_lines[] = '- **User ID:** ' . $user_id; - $user_profile_lines[] = ''; - $user_profile_lines[] = '## Preferences'; - $user_profile_lines[] = ''; - - $fs->put_contents( $new_user, implode( "\n", $user_profile_lines ) . "\n", FS_CHMOD_FILE ); - \DataMachine\Core\FilesRepository\FilesystemHelper::make_group_writable( $new_user ); - } - - if ( is_dir( $legacy_daily ) && ! is_dir( $new_daily ) ) { - datamachine_copy_directory_recursive( $legacy_daily, $new_daily ); - } - - // Backfill chat sessions for this user. - global $wpdb; - $chat_table = $chat_db->get_table_name(); - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.NotPrepared - $wpdb->query( - $wpdb->prepare( - 'UPDATE %i SET agent_id = %d WHERE user_id = %d AND (agent_id IS NULL OR agent_id = 0)', - $chat_table, - $agent_id, - $user_id - ) - ); - } - } - - // Single-agent case: .md files live directly in agent/ with no numeric subdirs. - // This is the most common layout for sites that never had multi-user partitioning. - $legacy_md_files = glob( trailingslashit( $legacy_agent_base ) . '*.md' ); - - if ( ! empty( $legacy_md_files ) ) { - $default_user_id = \DataMachine\Core\FilesRepository\DirectoryManager::get_default_agent_user_id(); - $default_user = get_user_by( 'id', $default_user_id ); - $default_slug = $default_user ? sanitize_title( $default_user->user_login ) : 'user-' . $default_user_id; - $default_name = $default_user ? $default_user->display_name : 'User ' . $default_user_id; - $default_model = \DataMachine\Core\PluginSettings::getModelForMode( 'chat' ); - - $agents_repo->create_if_missing( - $default_slug, - $default_name, - $default_user_id, - array( - 'model' => array( - 'default' => $default_model, - ), - ) - ); - - $default_identity_dir = $directory_manager->get_agent_identity_directory( $default_slug ); - $default_user_dir = $directory_manager->get_user_directory( $default_user_id ); - - if ( ! is_dir( $default_identity_dir ) ) { - wp_mkdir_p( $default_identity_dir ); - } - if ( ! is_dir( $default_user_dir ) ) { - wp_mkdir_p( $default_user_dir ); - } - - $default_agent_index = trailingslashit( $default_identity_dir ) . 'index.php'; - if ( ! file_exists( $default_agent_index ) ) { - $fs->put_contents( $default_agent_index, "put_contents( $default_user_index, "copy( $legacy_file, $dest, true, FS_CHMOD_FILE ); - \DataMachine\Core\FilesRepository\FilesystemHelper::make_group_writable( $dest ); - } - } - - // Migrate daily memory directory. - $legacy_daily = trailingslashit( $legacy_agent_base ) . 'daily'; - $new_daily = trailingslashit( $default_identity_dir ) . 'daily'; - - if ( is_dir( $legacy_daily ) && ! is_dir( $new_daily ) ) { - datamachine_copy_directory_recursive( $legacy_daily, $new_daily ); - } - } - - update_option( 'datamachine_layered_arch_migrated', 1, false ); -} - -/** - * Copy directory contents recursively without deleting source. - * - * Existing destination files are preserved. - * - * @since 0.36.1 - * @param string $source_dir Source directory path. - * @param string $target_dir Target directory path. - * @return void - */ -function datamachine_copy_directory_recursive( string $source_dir, string $target_dir ): void { - if ( ! is_dir( $source_dir ) ) { - return; - } - - $fs = \DataMachine\Core\FilesRepository\FilesystemHelper::get(); - if ( ! $fs ) { - return; - } - - if ( ! is_dir( $target_dir ) ) { - wp_mkdir_p( $target_dir ); - } - - $iterator = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator( $source_dir, RecursiveDirectoryIterator::SKIP_DOTS ), - RecursiveIteratorIterator::SELF_FIRST - ); - - foreach ( $iterator as $item ) { - $source_path = $item->getPathname(); - $relative = ltrim( str_replace( $source_dir, '', $source_path ), DIRECTORY_SEPARATOR ); - $target_path = trailingslashit( $target_dir ) . $relative; - - if ( $item->isDir() ) { - if ( ! is_dir( $target_path ) ) { - wp_mkdir_p( $target_path ); - } - continue; - } - - if ( file_exists( $target_path ) ) { - continue; - } - - $parent = dirname( $target_path ); - if ( ! is_dir( $parent ) ) { - wp_mkdir_p( $parent ); - } - - $fs->copy( $source_path, $target_path, true, FS_CHMOD_FILE ); - \DataMachine\Core\FilesRepository\FilesystemHelper::make_group_writable( $target_path ); - } -} diff --git a/inc/migrations/load.php b/inc/migrations/load.php index 3dffcf64e..ed56957d4 100644 --- a/inc/migrations/load.php +++ b/inc/migrations/load.php @@ -11,33 +11,13 @@ defined( 'ABSPATH' ) || exit; -require_once __DIR__ . '/activation.php'; -require_once __DIR__ . '/handler-keys.php'; -require_once __DIR__ . '/layered-architecture.php'; -require_once __DIR__ . '/agent-ping.php'; require_once __DIR__ . '/scaffolding.php'; require_once __DIR__ . '/site-md.php'; -require_once __DIR__ . '/backfill.php'; -require_once __DIR__ . '/network-scope.php'; require_once __DIR__ . '/flows.php'; -require_once __DIR__ . '/post-pipeline-meta.php'; -require_once __DIR__ . '/update-to-upsert.php'; -require_once __DIR__ . '/strip-pipeline-step-provider-model.php'; -require_once __DIR__ . '/ai-enabled-tools.php'; -require_once __DIR__ . '/handler-slug-scalar.php'; -require_once __DIR__ . '/split-queue-payload.php'; -require_once __DIR__ . '/user-message-queue-mode.php'; -require_once __DIR__ . '/webhook-auth-v2.php'; -require_once __DIR__ . '/agent-config-model-shape.php'; -require_once __DIR__ . '/settings-mode-models.php'; -require_once __DIR__ . '/ai-provider-keys.php'; require_once __DIR__ . '/bundle-artifacts.php'; require_once __DIR__ . '/processed-item-claims.php'; require_once __DIR__ . '/pending-actions.php'; -// Schema-migration runtime — defines `datamachine_run_schema_migrations()` -// and `datamachine_maybe_run_deferred_migrations()`. Hooked at -// plugins_loaded priority 5 so deploys that bump DATAMACHINE_VERSION past -// the persisted `datamachine_db_version` option auto-migrate without -// requiring an activation cycle (#1301). +// Current schema runtime — creates current deploy-time tables/columns without +// carrying pre-1.0 data-shape migrations forward indefinitely. require_once __DIR__ . '/runtime.php'; diff --git a/inc/migrations/network-scope.php b/inc/migrations/network-scope.php deleted file mode 100644 index 258da2e35..000000000 --- a/inc/migrations/network-scope.php +++ /dev/null @@ -1,402 +0,0 @@ - 100 ) ); - - // Get all WordPress users to check for USER.md across sites. - $users = get_users( array( - 'fields' => 'ID', - 'number' => 100, - ) ); - - $migrated_users = 0; - - foreach ( $users as $user_id ) { - $user_id = absint( $user_id ); - - // New network-scoped destination (from updated get_user_directory). - $network_user_dir = $directory_manager->get_user_directory( $user_id ); - $network_user_file = trailingslashit( $network_user_dir ) . 'USER.md'; - - // If the file already exists at the network location, skip. - if ( file_exists( $network_user_file ) ) { - continue; - } - - // Search all subsites for the richest USER.md for this user. - $best_content = ''; - $best_size = 0; - - foreach ( $sites as $site ) { - $blog_id = (int) $site->blog_id; - - switch_to_blog( $blog_id ); - $site_upload_dir = wp_upload_dir(); - restore_current_blog(); - - $site_user_file = trailingslashit( $site_upload_dir['basedir'] ) - . 'datamachine-files/users/' . $user_id . '/USER.md'; - - if ( file_exists( $site_user_file ) ) { - $size = filesize( $site_user_file ); - if ( $size > $best_size ) { - $best_size = $size; - $best_content = $fs->get_contents( $site_user_file ); - } - } - } - - if ( ! empty( $best_content ) ) { - if ( ! is_dir( $network_user_dir ) ) { - wp_mkdir_p( $network_user_dir ); - } - - $index_file = trailingslashit( $network_user_dir ) . 'index.php'; - if ( ! file_exists( $index_file ) ) { - $fs->put_contents( $index_file, "put_contents( $network_user_file, $best_content, FS_CHMOD_FILE ); - \DataMachine\Core\FilesRepository\FilesystemHelper::make_group_writable( $network_user_file ); - ++$migrated_users; - } - } - - // Create NETWORK.md if it doesn't exist. - $network_dir = $directory_manager->get_network_directory(); - if ( ! is_dir( $network_dir ) ) { - wp_mkdir_p( $network_dir ); - } - - $network_md = trailingslashit( $network_dir ) . 'NETWORK.md'; - if ( ! file_exists( $network_md ) ) { - // Compose from registered sections. Generator self-skips on single-site - // installs because every NETWORK.md section returns an empty string. - \DataMachine\Engine\AI\ComposableFileGenerator::regenerate( 'NETWORK.md' ); - } - - $network_index = trailingslashit( $network_dir ) . 'index.php'; - if ( ! file_exists( $network_index ) ) { - $fs->put_contents( $network_index, " 0 ) { - do_action( - 'datamachine_log', - 'info', - 'Migrated USER.md to network-scoped paths', - array( 'users_migrated' => $migrated_users ) - ); - } -} - -/** - * Migrate per-site agent rows to the network-scoped table. - * - * On multisite, agent tables previously used $wpdb->prefix (per-site). - * This migration consolidates per-site agent rows into the network table - * ($wpdb->base_prefix) and sets site_scope to the originating blog_id. - * - * Deduplication: if an agent_slug already exists in the network table, - * the per-site row is skipped (the network table wins). - * - * Idempotent — guarded by a network-level site option. - * - * @since 0.52.0 - */ -function datamachine_migrate_agents_to_network_scope() { - if ( ! is_multisite() ) { - return; - } - - if ( get_site_option( 'datamachine_agents_network_migrated' ) ) { - return; - } - - global $wpdb; - - $network_agents_table = $wpdb->base_prefix . 'datamachine_agents'; - $network_access_table = $wpdb->base_prefix . 'datamachine_agent_access'; - $network_tokens_table = $wpdb->base_prefix . 'datamachine_agent_tokens'; - $migrated_agents = 0; - $migrated_access = 0; - - $sites = get_sites( array( 'fields' => 'ids' ) ); - - foreach ( $sites as $blog_id ) { - $site_prefix = $wpdb->get_blog_prefix( $blog_id ); - - // Skip the main site — its prefix IS the base_prefix, so the table is already network-level. - if ( $site_prefix === $wpdb->base_prefix ) { - // Set site_scope on existing main-site agents that don't have one yet. - // Table name is built from $wpdb->base_prefix, not user input — safe to interpolate. - // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared - $wpdb->query( - $wpdb->prepare( - "UPDATE `{$network_agents_table}` SET site_scope = %d WHERE site_scope IS NULL", - (int) $blog_id - ) - ); - // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared - continue; - } - - $site_agents_table = $site_prefix . 'datamachine_agents'; - $site_access_table = $site_prefix . 'datamachine_agent_access'; - $site_tokens_table = $site_prefix . 'datamachine_agent_tokens'; - - // Check if per-site agents table exists. - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared - $table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $site_agents_table ) ); - if ( ! $table_exists ) { - continue; - } - - // Get all agents from the per-site table. - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared - $site_agents = $wpdb->get_results( "SELECT * FROM `{$site_agents_table}`", ARRAY_A ); - - if ( empty( $site_agents ) ) { - continue; - } - - foreach ( $site_agents as $agent ) { - $old_agent_id = (int) $agent['agent_id']; - - // Check if slug already exists in network table. - // Table name from $wpdb->base_prefix — safe to interpolate. - // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared - $existing = $wpdb->get_row( - $wpdb->prepare( - "SELECT agent_id FROM `{$network_agents_table}` WHERE agent_slug = %s", - $agent['agent_slug'] - ), - ARRAY_A - ); - // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared - - if ( $existing ) { - // Slug already exists in network table — skip this agent. - continue; - } - - // Insert into network table with site_scope. - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery - $wpdb->insert( - $network_agents_table, - array( - 'agent_slug' => $agent['agent_slug'], - 'agent_name' => $agent['agent_name'], - 'owner_id' => (int) $agent['owner_id'], - 'site_scope' => (int) $blog_id, - 'agent_config' => $agent['agent_config'], - 'created_at' => $agent['created_at'], - 'updated_at' => $agent['updated_at'], - ), - array( '%s', '%s', '%d', '%d', '%s', '%s', '%s' ) - ); - - $new_agent_id = (int) $wpdb->insert_id; - - if ( $new_agent_id <= 0 ) { - continue; - } - - ++$migrated_agents; - - // Migrate access grants for this agent. - // Table name from per-site $wpdb->prefix — safe to interpolate. - // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared - $site_access = $wpdb->get_results( - $wpdb->prepare( "SELECT * FROM `{$site_access_table}` WHERE agent_id = %d", $old_agent_id ), - ARRAY_A - ); - // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared - - foreach ( $site_access as $access ) { - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery - $wpdb->insert( - $network_access_table, - array( - 'agent_id' => $new_agent_id, - 'user_id' => (int) $access['user_id'], - 'role' => $access['role'], - 'granted_at' => $access['granted_at'], - ), - array( '%d', '%d', '%s', '%s' ) - ); - ++$migrated_access; - } - - // Migrate tokens for this agent (if any). - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared - $token_table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $site_tokens_table ) ); - if ( $token_table_exists ) { - // Table name from per-site $wpdb->prefix — safe to interpolate. - // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared - $site_tokens = $wpdb->get_results( - $wpdb->prepare( "SELECT * FROM `{$site_tokens_table}` WHERE agent_id = %d", $old_agent_id ), - ARRAY_A - ); - // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared - - foreach ( $site_tokens as $token ) { - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery - $wpdb->insert( - $network_tokens_table, - array( - 'agent_id' => $new_agent_id, - 'token_hash' => $token['token_hash'], - 'token_prefix' => $token['token_prefix'], - 'label' => $token['label'], - 'capabilities' => $token['capabilities'], - 'last_used_at' => $token['last_used_at'], - 'expires_at' => $token['expires_at'], - 'created_at' => $token['created_at'], - ), - array( '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s' ) - ); - } - } - } - } - - update_site_option( 'datamachine_agents_network_migrated', true ); - - if ( $migrated_agents > 0 || $migrated_access > 0 ) { - do_action( - 'datamachine_log', - 'info', - 'Migrated per-site agents to network-scoped tables', - array( - 'agents_migrated' => $migrated_agents, - 'access_migrated' => $migrated_access, - ) - ); - } -} - -/** - * Drop orphaned per-site agent tables after network migration. - * - * After datamachine_migrate_agents_to_network_scope() has consolidated - * all agent data into the network-scoped tables (base_prefix), the - * per-site copies (e.g. c8c_7_datamachine_agents) serve no purpose. - * They can't be queried (all repositories use base_prefix) and their - * presence is confusing. - * - * This function drops the orphaned per-site agent, access, and token - * tables for every subsite. Idempotent — safe to call multiple times. - * Only runs on multisite after the network migration flag is set. - * - * @since 0.43.0 - */ -function datamachine_drop_orphaned_agent_tables() { - if ( ! is_multisite() ) { - return; - } - - if ( ! get_site_option( 'datamachine_agents_network_migrated' ) ) { - return; - } - - if ( get_site_option( 'datamachine_orphaned_agent_tables_dropped' ) ) { - return; - } - - global $wpdb; - - $table_suffixes = array( - 'datamachine_agents', - 'datamachine_agent_access', - 'datamachine_agent_tokens', - ); - - $sites = get_sites( array( 'fields' => 'ids' ) ); - $dropped = 0; - - foreach ( $sites as $blog_id ) { - $site_prefix = $wpdb->get_blog_prefix( $blog_id ); - - // Skip the main site — its prefix IS the base_prefix, - // so these are the canonical network tables. - if ( $site_prefix === $wpdb->base_prefix ) { - continue; - } - - foreach ( $table_suffixes as $suffix ) { - $table_name = $site_prefix . $suffix; - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared - $exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ); - - if ( $exists ) { - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared - $wpdb->query( "DROP TABLE `{$table_name}`" ); - ++$dropped; - } - } - } - - update_site_option( 'datamachine_orphaned_agent_tables_dropped', true ); - - if ( $dropped > 0 ) { - do_action( - 'datamachine_log', - 'info', - 'Dropped orphaned per-site agent tables after network migration', - array( 'tables_dropped' => $dropped ) - ); - } -} diff --git a/inc/migrations/post-pipeline-meta.php b/inc/migrations/post-pipeline-meta.php deleted file mode 100644 index fb2f8d4b1..000000000 --- a/inc/migrations/post-pipeline-meta.php +++ /dev/null @@ -1,69 +0,0 @@ -delete( - $wpdb->postmeta, - array( 'meta_key' => '_datamachine_post_pipeline_id' ), - array( '%s' ) - ); - - if ( false === $deleted ) { - do_action( - 'datamachine_log', - 'error', - 'Failed to drop redundant _datamachine_post_pipeline_id rows', - array( - 'db_error' => $wpdb->last_error, - ) - ); - return; - } - - update_option( 'datamachine_post_pipeline_meta_dropped', true, true ); - - if ( $deleted > 0 ) { - do_action( - 'datamachine_log', - 'info', - 'Dropped redundant _datamachine_post_pipeline_id rows (#1091)', - array( - 'rows_deleted' => (int) $deleted, - ) - ); - } -} diff --git a/inc/migrations/runtime.php b/inc/migrations/runtime.php index 69f203fb0..ae4a8f68e 100644 --- a/inc/migrations/runtime.php +++ b/inc/migrations/runtime.php @@ -1,38 +1,11 @@ $config ) { - if ( ! is_array( $config ) ) { - continue; - } - - $mode = sanitize_key( (string) $mode ); - if ( '' === $mode ) { - continue; - } - - $sanitized[ $mode ] = array( - 'provider' => sanitize_text_field( $config['provider'] ?? '' ), - 'model' => sanitize_text_field( $config['model'] ?? '' ), - ); - } - - return $sanitized; -} diff --git a/inc/migrations/site-md.php b/inc/migrations/site-md.php index d833483b7..b6be2b705 100644 --- a/inc/migrations/site-md.php +++ b/inc/migrations/site-md.php @@ -8,15 +8,6 @@ * handled generically by ComposableFileGenerator + ComposableFileInvalidation; * this file owns the *content* of the core sections only. * - * Migration history: - * - 0.36.1 — initial SiteContext class injecting JSON into prompts. - * - 0.48.0 — replaced with monolithic SITE.md/NETWORK.md generators and a - * single whole-string filter per file. - * - x.y.z — files made composable; monolithic generators broken into - * per-section callbacks. Legacy whole-string filters preserved - * via `datamachine_composable_content` shim with a deprecation - * notice. - * * @package DataMachine * @since 0.60.0 */ @@ -98,59 +89,6 @@ function datamachine_register_core_invalidation_hooks( array $hooks ): array { } add_filter( 'datamachine_composable_invalidation_hooks', 'datamachine_register_core_invalidation_hooks' ); -/** - * Soft-deprecation shim for the legacy whole-string filters. - * - * `datamachine_site_scaffold_content` and `datamachine_network_scaffold_content` - * are superseded by `SectionRegistry::register()`. To minimize breakage, we - * still fire them on the assembled output of SITE.md / NETWORK.md so existing - * consumers keep working, but emit a `_doing_it_wrong` notice when a callback - * is attached so consumers know to migrate. - * - * Will be removed in a future major version. - * - * @since x.y.z - * - * @param string $content Assembled file content. - * @param string $filename Composable filename. - * @return string - */ -function datamachine_legacy_scaffold_filter_shim( string $content, string $filename ): string { - $legacy_filter = ''; - $replacement = ''; - - if ( 'SITE.md' === $filename ) { - $legacy_filter = 'datamachine_site_scaffold_content'; - $replacement = "SectionRegistry::register( 'SITE.md', '', , )"; - } elseif ( 'NETWORK.md' === $filename ) { - $legacy_filter = 'datamachine_network_scaffold_content'; - $replacement = "SectionRegistry::register( 'NETWORK.md', '', , )"; - } - - if ( '' === $legacy_filter ) { - return $content; - } - - if ( has_filter( $legacy_filter ) ) { - _doing_it_wrong( - esc_html( $legacy_filter ), - sprintf( - /* translators: 1: legacy filter name, 2: replacement code snippet */ - esc_html__( 'The %1$s filter is deprecated. Register a SectionRegistry section instead: %2$s', 'data-machine' ), - esc_html( $legacy_filter ), - esc_html( $replacement ) - ), - 'x.y.z' - ); - - /** This filter is documented in inc/migrations/site-md.php (legacy). */ - $content = (string) apply_filters( $legacy_filter, $content ); - } - - return $content; -} -add_filter( 'datamachine_composable_content', 'datamachine_legacy_scaffold_filter_shim', 10, 2 ); - // ----------------------------------------------------------------------------- // SITE.md sections. // ----------------------------------------------------------------------------- @@ -665,78 +603,3 @@ function datamachine_network_section_shared_resources(): string { return implode( "\n", $lines ); } - -// ----------------------------------------------------------------------------- -// Backwards-compat shims for callers that still build SITE.md / NETWORK.md -// content directly (activation, layered-architecture migration, network-scope -// migration). These delegate to ComposableFileGenerator and remain available -// so external callers don't break across the migration. -// ----------------------------------------------------------------------------- - -/** - * Build SITE.md content via the SectionRegistry. - * - * Preserved for backwards compatibility with any external callers that - * imported the legacy function. Internal callers should use - * `\DataMachine\Engine\AI\SectionRegistry::generate( 'SITE.md' )` directly. - * - * @since 0.36.1 - * @since x.y.z Delegates to SectionRegistry. The whole-string filter - * `datamachine_site_scaffold_content` is now applied via the - * `datamachine_composable_content` shim and emits a deprecation - * notice when used. - * @return string - */ -function datamachine_get_site_scaffold_content(): string { - $content = SectionRegistry::generate( 'SITE.md' ); - return '' === $content ? '' : $content . "\n"; -} - -/** - * Build NETWORK.md content via the SectionRegistry. - * - * Preserved for backwards compatibility. Returns an empty string on - * single-site installs (every NETWORK.md section short-circuits there). - * - * @since 0.48.0 - * @since x.y.z Delegates to SectionRegistry. - * @return string - */ -function datamachine_get_network_scaffold_content(): string { - if ( ! is_multisite() ) { - return ''; - } - $content = SectionRegistry::generate( 'NETWORK.md' ); - return '' === $content ? '' : $content . "\n"; -} - -/** - * Regenerate SITE.md on disk. - * - * @since 0.48.0 - * @since x.y.z Delegates to ComposableFileGenerator. - * @return void - */ -function datamachine_regenerate_site_md(): void { - if ( ! \DataMachine\Core\PluginSettings::get( 'site_context_enabled', true ) ) { - return; - } - \DataMachine\Engine\AI\ComposableFileGenerator::regenerate( 'SITE.md' ); -} - -/** - * Regenerate NETWORK.md on disk. - * - * @since 0.49.1 - * @since x.y.z Delegates to ComposableFileGenerator. - * @return void - */ -function datamachine_regenerate_network_md(): void { - if ( ! is_multisite() ) { - return; - } - if ( ! \DataMachine\Core\PluginSettings::get( 'site_context_enabled', true ) ) { - return; - } - \DataMachine\Engine\AI\ComposableFileGenerator::regenerate( 'NETWORK.md' ); -} diff --git a/inc/migrations/split-queue-payload.php b/inc/migrations/split-queue-payload.php deleted file mode 100644 index 11759dd42..000000000 --- a/inc/migrations/split-queue-payload.php +++ /dev/null @@ -1,189 +0,0 @@ - (AI only) - * - config_patch_queue — array<{patch:array, added_at}> (Fetch only) - * - * This migration walks every flow_config and, for each fetch step, - * decodes the JSON-encoded `prompt` field of every prompt_queue entry - * back into an object and moves it to a new `config_patch_queue` - * entry under the `patch` field. The fetch step's `prompt_queue` is - * then unset (not just emptied — fetch steps no longer have a - * prompt_queue at all under the post-split shape). - * - * Idempotent. Gated on the `datamachine_queue_payload_split_migrated` - * option. Misshaped entries (a `prompt` string that does not - * JSON-decode to an object) are logged at level=warning with the - * flow_id, step_id and a short preview, then skipped — they would - * have silently no-op'd at runtime under the pre-split code anyway, - * so dropping them on the floor is the same observable behaviour. - * - * AI-step prompt_queue entries are left untouched. Other step types - * (publish/upsert/system_task/etc.) are also untouched — they have no - * queueable consumer and any prompt_queue rows on them are stale. - * - * @package DataMachine - * @since 0.84.0 - */ - -defined( 'ABSPATH' ) || exit; - -/** - * Move fetch-step queue entries from `prompt_queue` (with JSON-encoded - * `prompt` strings) to a new `config_patch_queue` slot (with decoded - * `patch` arrays). - * - * Idempotent: gated on `datamachine_queue_payload_split_migrated`. - * - * @since 0.84.0 - */ -function datamachine_migrate_split_queue_payload(): void { - $already_done = get_option( 'datamachine_queue_payload_split_migrated', false ); - if ( $already_done ) { - return; - } - - global $wpdb; - $table = $wpdb->prefix . 'datamachine_flows'; - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery - // phpcs:disable WordPress.DB.PreparedSQL -- Table name from $wpdb->prefix. - $table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ); - // phpcs:enable WordPress.DB.PreparedSQL - if ( ! $table_exists ) { - update_option( 'datamachine_queue_payload_split_migrated', true, true ); - return; - } - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery - // phpcs:disable WordPress.DB.PreparedSQL -- Table name from $wpdb->prefix. - $rows = $wpdb->get_results( "SELECT flow_id, flow_config FROM {$table}", ARRAY_A ); - // phpcs:enable WordPress.DB.PreparedSQL - - if ( empty( $rows ) ) { - update_option( 'datamachine_queue_payload_split_migrated', true, true ); - return; - } - - $migrated_flows = 0; - $migrated_entries = 0; - $skipped_entries = 0; - - foreach ( $rows as $row ) { - $flow_config = json_decode( $row['flow_config'], true ); - if ( ! is_array( $flow_config ) ) { - continue; - } - - $changed = false; - foreach ( $flow_config as $step_id => &$step ) { - if ( ! is_array( $step ) ) { - continue; - } - - if ( 'fetch' !== ( $step['step_type'] ?? '' ) ) { - continue; - } - - $legacy_queue = $step['prompt_queue'] ?? null; - if ( ! is_array( $legacy_queue ) || empty( $legacy_queue ) ) { - // Nothing to migrate; ensure the prompt_queue field is - // gone so fetch steps have a uniform shape post-split. - if ( array_key_exists( 'prompt_queue', $step ) ) { - unset( $step['prompt_queue'] ); - $changed = true; - } - continue; - } - - $existing_patch_queue = $step['config_patch_queue'] ?? array(); - if ( ! is_array( $existing_patch_queue ) ) { - $existing_patch_queue = array(); - } - - $migrated_for_step = array(); - foreach ( $legacy_queue as $idx => $entry ) { - if ( ! is_array( $entry ) || ! isset( $entry['prompt'] ) ) { - ++$skipped_entries; - do_action( - 'datamachine_log', - 'warning', - 'Split queue payload migration: skipped malformed prompt_queue entry on fetch step', - array( - 'flow_id' => $row['flow_id'], - 'step_id' => $step_id, - 'index' => $idx, - 'preview' => is_string( $entry ) ? substr( $entry, 0, 80 ) : gettype( $entry ), - ) - ); - continue; - } - - $decoded = json_decode( (string) $entry['prompt'], true ); - if ( ! is_array( $decoded ) || empty( $decoded ) ) { - ++$skipped_entries; - do_action( - 'datamachine_log', - 'warning', - 'Split queue payload migration: prompt_queue entry on fetch step is not a JSON object — skipped', - array( - 'flow_id' => $row['flow_id'], - 'step_id' => $step_id, - 'index' => $idx, - 'preview' => substr( (string) $entry['prompt'], 0, 80 ), - ) - ); - continue; - } - - $migrated_for_step[] = array( - 'patch' => $decoded, - 'added_at' => $entry['added_at'] ?? gmdate( 'c' ), - ); - ++$migrated_entries; - } - - $step['config_patch_queue'] = array_merge( $existing_patch_queue, $migrated_for_step ); - unset( $step['prompt_queue'] ); - $changed = true; - } - unset( $step ); - - if ( $changed ) { - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching - $wpdb->update( - $table, - array( 'flow_config' => wp_json_encode( $flow_config ) ), - array( 'flow_id' => $row['flow_id'] ), - array( '%s' ), - array( '%d' ) - ); - ++$migrated_flows; - } - } - - update_option( 'datamachine_queue_payload_split_migrated', true, true ); - - if ( $migrated_flows > 0 || $skipped_entries > 0 ) { - do_action( - 'datamachine_log', - 'info', - 'Split queue payload migration complete', - array( - 'flows_updated' => $migrated_flows, - 'entries_migrated' => $migrated_entries, - 'entries_skipped' => $skipped_entries, - ) - ); - } -} diff --git a/inc/migrations/strip-pipeline-step-provider-model.php b/inc/migrations/strip-pipeline-step-provider-model.php deleted file mode 100644 index 6e32c01c1..000000000 --- a/inc/migrations/strip-pipeline-step-provider-model.php +++ /dev/null @@ -1,96 +0,0 @@ -prefix . 'datamachine_pipelines'; - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL - $table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $pipelines_table ) ); - if ( ! $table_exists ) { - update_option( 'datamachine_pipeline_step_provider_model_stripped', true, true ); - return; - } - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL - $rows = $wpdb->get_results( "SELECT pipeline_id, pipeline_config FROM {$pipelines_table}", ARRAY_A ); - - $migrated = 0; - - if ( ! empty( $rows ) ) { - foreach ( $rows as $row ) { - $config = json_decode( $row['pipeline_config'] ?? '', true ); - if ( ! is_array( $config ) ) { - continue; - } - - $changed = false; - - foreach ( $config as $step_id => &$step ) { - if ( ! is_array( $step ) ) { - continue; - } - if ( array_key_exists( 'provider', $step ) || array_key_exists( 'model', $step ) ) { - unset( $step['provider'], $step['model'] ); - $changed = true; - } - } - unset( $step ); - - if ( $changed ) { - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching - $wpdb->update( - $pipelines_table, - array( 'pipeline_config' => wp_json_encode( $config ) ), - array( 'pipeline_id' => $row['pipeline_id'] ), - array( '%s' ), - array( '%d' ) - ); - ++$migrated; - } - } - } - - update_option( 'datamachine_pipeline_step_provider_model_stripped', true, true ); - - if ( $migrated > 0 ) { - do_action( - 'datamachine_log', - 'info', - 'Stripped dead `provider`/`model` keys from pipeline_config (see data-machine#1180)', - array( - 'pipelines_updated' => $migrated, - ) - ); - } -} diff --git a/inc/migrations/update-to-upsert.php b/inc/migrations/update-to-upsert.php deleted file mode 100644 index fc43f15e9..000000000 --- a/inc/migrations/update-to-upsert.php +++ /dev/null @@ -1,126 +0,0 @@ -prefix . 'datamachine_pipelines'; - $flows_table = $wpdb->prefix . 'datamachine_flows'; - - $pipelines_migrated = datamachine_rewrite_update_step_type_in_table( - $pipelines_table, - 'pipeline_id', - 'pipeline_config' - ); - $flows_migrated = datamachine_rewrite_update_step_type_in_table( - $flows_table, - 'flow_id', - 'flow_config' - ); - - update_option( 'datamachine_update_to_upsert_migrated', true, true ); - - if ( $pipelines_migrated > 0 || $flows_migrated > 0 ) { - do_action( - 'datamachine_log', - 'info', - 'Migrated `update` step type to `upsert` in pipeline and flow configs', - array( - 'pipelines_updated' => $pipelines_migrated, - 'flows_updated' => $flows_migrated, - ) - ); - } -} - -/** - * Rewrite `step_type: 'update'` → `'upsert'` inside a table's JSON config column. - * - * Walks every row, decodes the JSON config (keyed by step_id), and rewrites - * any entry whose step_type is `update` to `upsert`. Also updates the - * `handler_configs` key if it was keyed by `'update'` (it shouldn't be — those - * are keyed by handler slug like `wordpress_update` — but we're defensive). - * - * @param string $table Fully-qualified table name. - * @param string $id_column Primary key column name. - * @param string $config_col JSON config column name. - * @return int Number of rows updated. - */ -function datamachine_rewrite_update_step_type_in_table( string $table, string $id_column, string $config_col ): int { - global $wpdb; - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL - $table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ); - if ( ! $table_exists ) { - return 0; - } - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL - $rows = $wpdb->get_results( "SELECT {$id_column}, {$config_col} FROM {$table}", ARRAY_A ); - if ( empty( $rows ) ) { - return 0; - } - - $migrated = 0; - - foreach ( $rows as $row ) { - $config = json_decode( $row[ $config_col ] ?? '', true ); - if ( ! is_array( $config ) ) { - continue; - } - - $changed = false; - - foreach ( $config as $step_id => &$step ) { - if ( ! is_array( $step ) ) { - continue; - } - - if ( 'update' === ( $step['step_type'] ?? '' ) ) { - $step['step_type'] = 'upsert'; - $changed = true; - } - } - unset( $step ); - - if ( $changed ) { - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching - $wpdb->update( - $table, - array( $config_col => wp_json_encode( $config ) ), - array( $id_column => $row[ $id_column ] ), - array( '%s' ), - array( '%d' ) - ); - ++$migrated; - } - } - - return $migrated; -} diff --git a/inc/migrations/user-message-queue-mode.php b/inc/migrations/user-message-queue-mode.php deleted file mode 100644 index 48bb96e60..000000000 --- a/inc/migrations/user-message-queue-mode.php +++ /dev/null @@ -1,249 +0,0 @@ - (AI consumer) - * - config_patch_queue — array<{patch, added_at}> (Fetch consumer, untouched here) - * - queue_mode — "drain" | "loop" | "static" (replaces queue_enabled) - * - * The `user_message` field is deleted from AI steps. The - * `queue_enabled` boolean is deleted from every queueable step. - * - * Migration shape mirrors split-queue-payload.php (#1294) and - * ai-enabled-tools.php (#1216): one-shot, idempotent, gated on a - * single option, no runtime fallback shim. Per the no-shim rule. - * - * Boolean → mode resolution: - * queue_enabled === true → queue_mode = "drain" (matches today's pop-per-tick) - * queue_enabled === false → queue_mode = "static" (matches today's peek-without-pop; - * the multi-entry-queue case is - * named explicitly as the manual - * stockpile / iterative-dev pattern) - * - * AI-step-only handling for `user_message`: - * 1. prompt_queue empty + user_message non-empty - * → seed queue as [{prompt: user_message, added_at: now()}], queue_mode=static - * 2. prompt_queue non-empty + user_message non-empty - * → keep queue as-is, queue_mode=static, drop user_message - * (matches existing AIStep precedence: queue head wins; the fallback was - * shadowed at runtime), log dropped value at info level for traceability - * 3. user_message empty - * → just unset the dead key; nothing to seed - * - * `loop` mode is net-new on both consumers — no flow gets it from - * migration; opt-in via `flow queue mode loop`. - * - * Fetch steps have no `user_message` to migrate (the - * `config_patch_queue` slot is the only fetch-side storage post-#1294). - * The migration just resolves `queue_enabled` → `queue_mode` and drops - * the dead key. - * - * @package DataMachine - * @since 0.85.0 - */ - -defined( 'ABSPATH' ) || exit; - -/** - * Replace `user_message` + `queue_enabled` with the unified `queue_mode` - * enum on every flow step. - * - * Idempotent: gated on `datamachine_user_message_collapsed`. - * - * @since 0.85.0 - */ -function datamachine_migrate_user_message_queue_mode(): void { - $already_done = get_option( 'datamachine_user_message_collapsed', false ); - if ( $already_done ) { - return; - } - - global $wpdb; - $table = $wpdb->prefix . 'datamachine_flows'; - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery - // phpcs:disable WordPress.DB.PreparedSQL -- Table name from $wpdb->prefix. - $table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ); - // phpcs:enable WordPress.DB.PreparedSQL - if ( ! $table_exists ) { - update_option( 'datamachine_user_message_collapsed', true, true ); - return; - } - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery - // phpcs:disable WordPress.DB.PreparedSQL -- Table name from $wpdb->prefix. - $rows = $wpdb->get_results( "SELECT flow_id, flow_config FROM {$table}", ARRAY_A ); - // phpcs:enable WordPress.DB.PreparedSQL - - if ( empty( $rows ) ) { - update_option( 'datamachine_user_message_collapsed', true, true ); - return; - } - - $migrated_flows = 0; - $user_messages_seeded = 0; - $user_messages_dropped = 0; - $queue_modes_resolved = 0; - - foreach ( $rows as $row ) { - $flow_config = json_decode( $row['flow_config'], true ); - if ( ! is_array( $flow_config ) ) { - continue; - } - - $changed = false; - foreach ( $flow_config as $step_id => &$step ) { - if ( ! is_array( $step ) ) { - continue; - } - - $step_type = $step['step_type'] ?? ''; - - $has_queue_enabled = array_key_exists( 'queue_enabled', $step ); - $has_user_message = array_key_exists( 'user_message', $step ); - - // Skip steps that touch neither key — nothing to migrate. - if ( ! $has_queue_enabled && ! $has_user_message ) { - continue; - } - - // Resolve queue_mode from queue_enabled (or default to static - // when only user_message was set without queue toggling). - $queue_enabled = $has_queue_enabled ? (bool) $step['queue_enabled'] : false; - $queue_mode = $queue_enabled ? 'drain' : 'static'; - - $step['queue_mode'] = $queue_mode; - ++$queue_modes_resolved; - - // AI-step-only: collapse user_message into prompt_queue. - // - // Three cases, distinguished by what was actually running each - // tick pre-#1291 (per AIStep::execute() lines 140-173 in the - // pre-collapse code): - // - // (a) queue empty + user_message="X" (queue_enabled=any) - // → pre: pop/peek returned '', fell through to "X" - // every tick. Migration: seed prompt_queue=[{X}], - // queue_mode=static. (Static peeks "X" forever, - // matching de-facto behaviour: queue_enabled=true - // with an empty queue never gained entries on its - // own, so user_message ran every tick regardless.) - // - // (b) non-empty queue + user_message + queue_enabled=true - // → pre: drain the queue, then fall through to - // user_message once the queue empties. Migration: - // keep queue, queue_mode=drain (matching the - // boolean), DROP user_message. The drain-then- - // fallback semantic does not have a clean - // post-#1291 equivalent — drain emptied → - // COMPLETED_NO_ITEMS skip. The dropped user_message - // is logged with `lossy_fallback: true` so operators - // can re-add it via `flow queue add` if they want - // the post-drain fallback behaviour to keep working. - // - // (c) non-empty queue + user_message + queue_enabled=false - // → pre: peek the queue head every tick; user_message - // was permanently shadowed. Migration: keep queue, - // queue_mode=static (matching the boolean), drop - // user_message. Behaviour-preserving — the queue - // head was the active prompt and continues to be. - // - // Critical: `queue_mode` for cases (b) and (c) follows the - // original `queue_enabled` boolean. Forcing static when - // queue_enabled=true was previously set would silently - // convert a draining flow into a static one, which is a - // real behaviour change. - if ( 'ai' === $step_type && $has_user_message ) { - $user_message = is_string( $step['user_message'] ) ? trim( $step['user_message'] ) : ''; - $queue = isset( $step['prompt_queue'] ) && is_array( $step['prompt_queue'] ) - ? $step['prompt_queue'] - : array(); - - if ( '' !== $user_message ) { - if ( empty( $queue ) ) { - // Case (a): seed 1-entry static queue with the - // legacy user_message. Pre-#1291 ran the - // user_message every tick when the queue was - // empty regardless of queue_enabled — static - // preserves that. - $step['prompt_queue'] = array( - array( - 'prompt' => $user_message, - 'added_at' => gmdate( 'c' ), - ), - ); - $step['queue_mode'] = 'static'; - ++$user_messages_seeded; - } else { - // Cases (b) and (c): drop user_message; keep the - // queue_mode resolved from the original - // queue_enabled boolean. Lossy for case (b) — - // log loudly so operators can recover. - do_action( - 'datamachine_log', - 'info', - 'user_message → prompt_queue migration: dropped user_message that was already shadowed by non-empty prompt_queue head', - array( - 'flow_id' => $row['flow_id'], - 'step_id' => $step_id, - 'queue_depth' => count( $queue ), - 'resolved_queue_mode' => $queue_mode, - 'lossy_fallback' => 'drain' === $queue_mode, - 'dropped_value' => mb_substr( $user_message, 0, 200 ), - ) - ); - // queue_mode stays at the boolean-resolved value - // (drain or static); do NOT force static here. - ++$user_messages_dropped; - } - } - } - - // Always strip the dead keys (no runtime fallback shim). - unset( $step['user_message'] ); - unset( $step['queue_enabled'] ); - - $changed = true; - } - unset( $step ); - - if ( $changed ) { - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching - $wpdb->update( - $table, - array( 'flow_config' => wp_json_encode( $flow_config ) ), - array( 'flow_id' => $row['flow_id'] ), - array( '%s' ), - array( '%d' ) - ); - ++$migrated_flows; - } - } - - update_option( 'datamachine_user_message_collapsed', true, true ); - - if ( $migrated_flows > 0 || $queue_modes_resolved > 0 ) { - do_action( - 'datamachine_log', - 'info', - 'user_message → queue_mode collapse migration complete', - array( - 'flows_updated' => $migrated_flows, - 'queue_modes_resolved' => $queue_modes_resolved, - 'user_messages_seeded' => $user_messages_seeded, - 'user_messages_dropped' => $user_messages_dropped, - ) - ); - } -} diff --git a/inc/migrations/webhook-auth-v2.php b/inc/migrations/webhook-auth-v2.php deleted file mode 100644 index b40cd6419..000000000 --- a/inc/migrations/webhook-auth-v2.php +++ /dev/null @@ -1,194 +0,0 @@ -prefix . 'datamachine_flows'; - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery - // phpcs:disable WordPress.DB.PreparedSQL -- Table name from $wpdb->prefix. - $table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ); - // phpcs:enable WordPress.DB.PreparedSQL - if ( ! $table_exists ) { - update_option( 'datamachine_webhook_auth_v2_migrated', true, true ); - return; - } - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching - // phpcs:disable WordPress.DB.PreparedSQL -- Table/column names are internal constants from this migration. - $rows = $wpdb->get_results( "SELECT flow_id, scheduling_config FROM {$table}", ARRAY_A ); - // phpcs:enable WordPress.DB.PreparedSQL - - $updated = 0; - foreach ( $rows as $row ) { - $scheduling_config = json_decode( $row['scheduling_config'] ?? '', true ); - if ( ! is_array( $scheduling_config ) ) { - continue; - } - - $normalized = datamachine_normalize_webhook_auth_v2_config( $scheduling_config ); - if ( ! $normalized['changed'] ) { - continue; - } - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching - $wpdb->update( - $table, - array( 'scheduling_config' => wp_json_encode( $normalized['config'] ) ), - array( 'flow_id' => $row['flow_id'] ), - array( '%s' ), - array( '%d' ) - ); - ++$updated; - } - - update_option( 'datamachine_webhook_auth_v2_migrated', true, true ); - - if ( $updated > 0 ) { - do_action( - 'datamachine_log', - 'info', - 'Migrated webhook auth scheduling configs to canonical v2 shape', - array( 'flows_updated' => $updated ) - ); - } -} - -/** - * Normalize one scheduling config. - * - * @param array $scheduling_config Scheduling config. - * @return array{config:array,changed:bool} - */ -function datamachine_normalize_webhook_auth_v2_config( array $scheduling_config ): array { - $legacy_mode = $scheduling_config['webhook_auth_mode'] ?? null; - $has_legacy_fields = isset( $scheduling_config['webhook_signature_header'] ) - || isset( $scheduling_config['webhook_signature_format'] ) - || isset( $scheduling_config['webhook_secret'] ); - - if ( 'hmac_sha256' !== $legacy_mode && ! $has_legacy_fields ) { - return array( - 'config' => $scheduling_config, - 'changed' => false, - ); - } - - if ( 'hmac_sha256' !== $legacy_mode ) { - $scheduling_config = datamachine_webhook_auth_remove_v1_fields( $scheduling_config ); - - return array( - 'config' => $scheduling_config, - 'changed' => true, - ); - } - - $scheduling_config['webhook_auth_mode'] = 'hmac'; - - if ( empty( $scheduling_config['webhook_auth'] ) ) { - $scheduling_config['webhook_auth'] = datamachine_webhook_auth_v1_template( - (string) ( $scheduling_config['webhook_signature_header'] ?? 'X-Hub-Signature-256' ), - (string) ( $scheduling_config['webhook_signature_format'] ?? 'sha256=hex' ) - ); - } - - if ( empty( $scheduling_config['webhook_secrets'] ) && ! empty( $scheduling_config['webhook_secret'] ) ) { - $scheduling_config['webhook_secrets'] = array( - array( - 'id' => 'current', - 'value' => (string) $scheduling_config['webhook_secret'], - ), - ); - } - - $scheduling_config = datamachine_webhook_auth_remove_v1_fields( $scheduling_config ); - - return array( - 'config' => $scheduling_config, - 'changed' => true, - ); -} - -/** - * Remove legacy v1 webhook auth fields from a scheduling config. - * - * @param array $scheduling_config Scheduling config. - * @return array - */ -function datamachine_webhook_auth_remove_v1_fields( array $scheduling_config ): array { - unset( - $scheduling_config['webhook_signature_header'], - $scheduling_config['webhook_signature_format'], - $scheduling_config['webhook_secret'] - ); - - return $scheduling_config; -} - -/** - * Build the canonical v2 verifier template equivalent for legacy v1 fields. - * - * @param string $header Signature header name. - * @param string $format Legacy signature format enum. - * @return array - */ -function datamachine_webhook_auth_v1_template( string $header, string $format ): array { - $signature_source = array( - 'header' => $header, - 'extract' => array( 'kind' => 'raw' ), - 'encoding' => 'hex', - ); - - switch ( $format ) { - case 'sha256=hex': - $signature_source['extract'] = array( - 'kind' => 'prefix', - 'key' => 'sha256=', - ); - $signature_source['encoding'] = 'hex'; - break; - case 'base64': - $signature_source['encoding'] = 'base64'; - break; - case 'hex': - default: - $signature_source['encoding'] = 'hex'; - break; - } - - return array( - 'mode' => 'hmac', - 'algo' => 'sha256', - 'signed_template' => '{body}', - 'signature_source' => $signature_source, - 'max_body_bytes' => \DataMachine\Api\WebhookVerifier::DEFAULT_MAX_BODY_BYTES, - ); -} diff --git a/tests/Unit/Migrations/PostPipelineMetaMigrationTest.php b/tests/Unit/Migrations/PostPipelineMetaMigrationTest.php deleted file mode 100644 index 8828423f2..000000000 --- a/tests/Unit/Migrations/PostPipelineMetaMigrationTest.php +++ /dev/null @@ -1,66 +0,0 @@ -post->create(); - $post_b = self::factory()->post->create(); - $post_c = self::factory()->post->create(); - - update_post_meta( $post_a, '_datamachine_post_pipeline_id', 100 ); - update_post_meta( $post_b, '_datamachine_post_pipeline_id', 200 ); - update_post_meta( $post_c, '_datamachine_post_handler', 'rss' ); - update_post_meta( $post_c, '_datamachine_post_flow_id', 5 ); - - datamachine_drop_redundant_post_pipeline_meta(); - - $this->assertSame( '', get_post_meta( $post_a, '_datamachine_post_pipeline_id', true ) ); - $this->assertSame( '', get_post_meta( $post_b, '_datamachine_post_pipeline_id', true ) ); - // Non-pipeline meta untouched on post C. - $this->assertSame( 'rss', get_post_meta( $post_c, '_datamachine_post_handler', true ) ); - $this->assertSame( '5', get_post_meta( $post_c, '_datamachine_post_flow_id', true ) ); - } - - public function test_migration_sets_completion_flag(): void { - $this->assertFalse( get_option( 'datamachine_post_pipeline_meta_dropped', false ) ); - - datamachine_drop_redundant_post_pipeline_meta(); - - $this->assertTrue( (bool) get_option( 'datamachine_post_pipeline_meta_dropped' ) ); - } - - public function test_migration_is_idempotent_after_completion(): void { - datamachine_drop_redundant_post_pipeline_meta(); - - // Subsequent rows should NOT be touched because the flag short-circuits - // the migration — protecting any intentional writes after completion. - $post_id = self::factory()->post->create(); - update_post_meta( $post_id, '_datamachine_post_pipeline_id', 42 ); - - datamachine_drop_redundant_post_pipeline_meta(); - - $this->assertSame( '42', get_post_meta( $post_id, '_datamachine_post_pipeline_id', true ) ); - } - - public function test_migration_no_op_when_no_legacy_rows_exist(): void { - // Clean site with no DM history — migration should still set the flag - // and do nothing else. - datamachine_drop_redundant_post_pipeline_meta(); - - $this->assertTrue( (bool) get_option( 'datamachine_post_pipeline_meta_dropped' ) ); - } -} diff --git a/tests/agent-call-migration-smoke.php b/tests/agent-call-migration-smoke.php deleted file mode 100644 index cc4915244..000000000 --- a/tests/agent-call-migration-smoke.php +++ /dev/null @@ -1,91 +0,0 @@ - "https://example.test/hook\nhttps://example.test/other", - 'prompt' => 'Review this packet', - 'auth_header_name' => 'X-Agent-Token', - 'auth_token' => 'secret-token', - 'reply_to' => 'thread-123', -) ); -assert_agent_call( 'task is agent_call', 'agent_call' === ( $canonical['task'] ?? null ) ); -assert_agent_call( 'target.type is webhook', 'webhook' === ( $canonical['params']['target']['type'] ?? null ) ); -assert_agent_call( 'target.id preserves webhook_url', "https://example.test/hook\nhttps://example.test/other" === ( $canonical['params']['target']['id'] ?? null ) ); -assert_agent_call( 'auth header name is nested under target.auth', 'X-Agent-Token' === ( $canonical['params']['target']['auth']['header_name'] ?? null ) ); -assert_agent_call( 'auth token is nested under target.auth', 'secret-token' === ( $canonical['params']['target']['auth']['token'] ?? null ) ); -assert_agent_call( 'input.task preserves prompt', 'Review this packet' === ( $canonical['params']['input']['task'] ?? null ) ); -assert_agent_call( 'input.messages is initialized', array() === ( $canonical['params']['input']['messages'] ?? null ) ); -assert_agent_call( 'input.context is initialized', array() === ( $canonical['params']['input']['context'] ?? null ) ); -assert_agent_call( 'delivery.mode is fire_and_forget', 'fire_and_forget' === ( $canonical['params']['delivery']['mode'] ?? null ) ); -assert_agent_call( 'delivery.reply_to preserves reply_to', 'thread-123' === ( $canonical['params']['delivery']['reply_to'] ?? null ) ); - -echo "\n[migration:2] Runtime chain includes the post-system-task migration\n"; -$runtime_src = (string) file_get_contents( $root_dir . '/inc/migrations/runtime.php' ); -$pos_flow = strpos( $runtime_src, 'datamachine_migrate_agent_ping_to_system_task();' ); -$pos_pipe = strpos( $runtime_src, 'datamachine_migrate_agent_ping_pipeline_to_system_task();' ); -$pos_call = strpos( $runtime_src, 'datamachine_migrate_agent_ping_task_to_agent_call();' ); -assert_agent_call( 'agent_call migration is wired into runtime chain', false !== $pos_call ); -assert_agent_call( 'agent_call migration runs after both legacy agent_ping step migrations', false !== $pos_flow && false !== $pos_pipe && false !== $pos_call && $pos_flow < $pos_call && $pos_pipe < $pos_call ); - -echo "\n[task:1] System task vocabulary is agent_call only\n"; -$provider_src = (string) file_get_contents( $root_dir . '/inc/Engine/AI/System/SystemAgentServiceProvider.php' ); -assert_agent_call( 'service provider registers agent_call task', false !== strpos( $provider_src, "\$tasks['agent_call']" ) ); -assert_agent_call( 'service provider no longer registers agent_ping task', false === strpos( $provider_src, "\$tasks['agent_ping']" ) ); - -$task_src = (string) file_get_contents( $root_dir . '/inc/Engine/AI/System/Tasks/AgentCallTask.php' ); -assert_agent_call( 'AgentCallTask returns agent_call type', false !== strpos( $task_src, "return 'agent_call';" ) ); -assert_agent_call( 'AgentCallTask executes datamachine/agent-call ability', false !== strpos( $task_src, "wp_get_ability( 'datamachine/agent-call' )" ) ); -assert_agent_call( 'AgentCallTask rejects missing ability by canonical name', false !== strpos( $task_src, 'Ability datamachine/agent-call not registered.' ) ); - -echo "\n[ability:1] Agent call ability owns the canonical runtime surface\n"; -$ability_src = (string) file_get_contents( $root_dir . '/inc/Abilities/AgentCall/AgentCallAbility.php' ); -assert_agent_call( 'ability registers datamachine/agent-call', false !== strpos( $ability_src, "wp_register_ability(\n\t\t\t\t'datamachine/agent-call'" ) ); -assert_agent_call( 'ability supports webhook target type', false !== strpos( $ability_src, "'webhook'" ) ); -assert_agent_call( 'ability supports fire_and_forget delivery mode', false !== strpos( $ability_src, "'fire_and_forget'" ) ); -assert_agent_call( 'ability returns proposed status envelope', false !== strpos( $ability_src, "'remote_run_id'" ) && false !== strpos( $ability_src, "'resume_token'" ) ); - -echo "\n[tool:1] Existing chat tool bridges to agent_call without old ability shim\n"; -$tool_src = (string) file_get_contents( $root_dir . '/inc/Api/Chat/Tools/SendPing.php' ); -assert_agent_call( 'send_ping tool now points at datamachine/agent-call', false !== strpos( $tool_src, "'ability' => 'datamachine/agent-call'" ) ); -assert_agent_call( 'send_ping tool transforms webhook_url into target.id', false !== strpos( $tool_src, "'id' => \$parameters['webhook_url'] ?? ''" ) ); -assert_agent_call( 'no datamachine/send-ping runtime shim remains', false === strpos( $tool_src, 'datamachine/send-ping' ) ); - -echo "\n"; -$failure_count = (int) $GLOBALS['failed']; -if ( 0 === $failure_count ) { - echo "=== agent-call-migration-smoke: ALL PASS ({$total}) ===\n"; - exit( 0 ); -} - -echo "=== agent-call-migration-smoke: {$failure_count} FAIL of {$total} ===\n"; -exit( 1 ); diff --git a/tests/agent-config-model-shape-migration-smoke.php b/tests/agent-config-model-shape-migration-smoke.php deleted file mode 100644 index 0fc4a27ec..000000000 --- a/tests/agent-config-model-shape-migration-smoke.php +++ /dev/null @@ -1,355 +0,0 @@ ->> */ - public array $rows = array(); - - /** Toggle to simulate "table does not exist". */ - public bool $table_present = true; - - public function prepare( string $query, ...$args ): string { - return vsprintf( str_replace( '%s', "'%s'", $query ), $args ); - } - - public function get_var( string $query ) { - if ( ! $this->table_present ) { - return null; - } - foreach ( array_keys( $this->rows ) as $table ) { - if ( str_contains( $query, $table ) ) { - return $table; - } - } - return null; - } - - public function get_results( string $query, $output ) { - foreach ( $this->rows as $table => $rows ) { - if ( str_contains( $query, $table ) ) { - return $rows; - } - } - return array(); - } - - public function update( string $table, array $data, array $where, array $formats, array $where_formats ): bool { - $id_column = array_key_first( $where ); - if ( null === $id_column ) { - return false; - } - $id_value = $where[ $id_column ]; - - foreach ( $this->rows[ $table ] as &$row ) { - if ( array_key_exists( $id_column, $row ) && (string) $row[ $id_column ] === (string) $id_value ) { - $row = array_merge( $row, $data ); - return true; - } - } - unset( $row ); - - return false; - } -} - -require_once __DIR__ . '/../inc/migrations/agent-config-model-shape.php'; - -$failures = array(); -$passes = 0; - -function assert_equals( $expected, $actual, string $name, array &$failures, int &$passes ): void { - if ( $expected === $actual ) { - ++$passes; - echo " PASS: {$name}\n"; - return; - } - - $failures[] = $name; - echo " FAIL: {$name}\n"; - echo ' expected: ' . var_export( $expected, true ) . "\n"; - echo ' actual: ' . var_export( $actual, true ) . "\n"; -} - -echo "agent-config-model-shape migration smoke\n"; -echo "----------------------------------------\n"; - -// --------------------------------------------------------------------- -// Section 1: end-to-end on a representative spread of agent_config rows. -// --------------------------------------------------------------------- - -echo "\n[shape:1] Flatten legacy and preserve siblings\n"; - -$wpdb = new AgentConfigModelShapeWpdb(); -$rows = array( - // Legacy with provider+model and tool_policy. - array( - 'agent_id' => 1, - 'agent_config' => wp_json_encode( - array( - 'model' => array( - 'default' => array( - 'provider' => 'openai', - 'model' => 'gpt-5-mini', - ), - ), - 'tool_policy' => array( - 'mode' => 'deny', - 'tools' => array( 'progress_story' ), - ), - 'extra_key' => 'preserve_me', - ) - ), - ), - // Legacy with empty provider/model — should leave both fields off - // so site/network defaults apply. - array( - 'agent_id' => 2, - 'agent_config' => wp_json_encode( - array( - 'model' => array( - 'default' => array( - 'provider' => '', - 'model' => '', - ), - ), - ) - ), - ), - // Already current shape — nothing to flatten, no `model` key. - array( - 'agent_id' => 3, - 'agent_config' => wp_json_encode( - array( - 'default_provider' => 'openai', - 'default_model' => 'gpt-5.4-nano', - ) - ), - ), - // No legacy and no current — should not be touched. - array( - 'agent_id' => 4, - 'agent_config' => wp_json_encode( - array( - 'allowed_redirect_uris' => array( 'example.com' ), - ) - ), - ), -); -$wpdb->rows['wp_datamachine_agents'] = $rows; - -datamachine_migrate_agent_config_model_shape(); - -$by_id = array(); -foreach ( $wpdb->rows['wp_datamachine_agents'] as $row ) { - $by_id[ $row['agent_id'] ] = json_decode( $row['agent_config'], true ); -} - -assert_equals( - array( - 'tool_policy' => array( - 'mode' => 'deny', - 'tools' => array( 'progress_story' ), - ), - 'extra_key' => 'preserve_me', - 'default_provider' => 'openai', - 'default_model' => 'gpt-5-mini', - ), - $by_id[1], - 'agent 1 — legacy flattened, tool_policy + extra keys preserved', - $failures, - $passes -); - -assert_equals( - false, - isset( $by_id[2]['default_provider'] ) || isset( $by_id[2]['default_model'] ) || isset( $by_id[2]['model'] ), - 'agent 2 — empty legacy values dropped, no pinned empty strings', - $failures, - $passes -); - -assert_equals( - array( - 'default_provider' => 'openai', - 'default_model' => 'gpt-5.4-nano', - ), - $by_id[3], - 'agent 3 — already-current shape preserved verbatim (gpt-5.4-nano stays)', - $failures, - $passes -); - -assert_equals( - array( - 'allowed_redirect_uris' => array( 'example.com' ), - ), - $by_id[4], - 'agent 4 — rows without legacy model key are left alone', - $failures, - $passes -); - -assert_equals( - true, - get_option( 'datamachine_agent_config_model_shape_migrated' ), - 'gate option set after first run', - $failures, - $passes -); - -// --------------------------------------------------------------------- -// Section 2: empty agent_config encodes as `{}`, not `[]`. -// --------------------------------------------------------------------- - -echo "\n[shape:2] Empty resulting config encodes as object, not array\n"; - -global $options; -$options = array(); -$wpdb_b = new AgentConfigModelShapeWpdb(); -$wpdb_b->rows['wp_datamachine_agents'] = array( - array( - 'agent_id' => 5, - 'agent_config' => wp_json_encode( - array( - 'model' => array( - 'default' => array( - 'provider' => '', - 'model' => '', - ), - ), - ) - ), - ), -); - -global $wpdb; -$wpdb = $wpdb_b; -datamachine_migrate_agent_config_model_shape(); - -assert_equals( - '{}', - $wpdb_b->rows['wp_datamachine_agents'][0]['agent_config'], - 'fully drained config writes `{}` not `[]`', - $failures, - $passes -); - -// --------------------------------------------------------------------- -// Section 3: idempotent — second call is a no-op. -// --------------------------------------------------------------------- - -echo "\n[shape:3] Second invocation short-circuits\n"; - -$wpdb_c = new AgentConfigModelShapeWpdb(); -$wpdb_c->rows['wp_datamachine_agents'] = array( - array( - 'agent_id' => 6, - 'agent_config' => wp_json_encode( - array( - 'model' => array( 'default' => array( 'provider' => 'openai', 'model' => 'gpt-5-mini' ) ), - ) - ), - ), -); -$wpdb = $wpdb_c; -// Gate from section 2 above is still set; this call must be a no-op. -datamachine_migrate_agent_config_model_shape(); - -$post = json_decode( $wpdb_c->rows['wp_datamachine_agents'][0]['agent_config'], true ); -assert_equals( - true, - isset( $post['model']['default'] ), - 'gated re-entry leaves stale shape untouched (idempotent)', - $failures, - $passes -); - -// --------------------------------------------------------------------- -// Section 4: missing table is a no-op but still sets the gate. -// --------------------------------------------------------------------- - -echo "\n[shape:4] Missing agents table sets gate without erroring\n"; - -$options = array(); -$wpdb_d = new AgentConfigModelShapeWpdb(); -$wpdb_d->table_present = false; -$wpdb_d->rows['wp_datamachine_agents'] = array(); -$wpdb = $wpdb_d; -datamachine_migrate_agent_config_model_shape(); - -assert_equals( - true, - get_option( 'datamachine_agent_config_model_shape_migrated' ), - 'gate set even when table is missing', - $failures, - $passes -); - -echo "\n----------------------------------------\n"; -$total = $passes + count( $failures ); -echo "{$passes} / {$total} passed\n"; - -if ( ! empty( $failures ) ) { - echo "\nFailures:\n"; - foreach ( $failures as $failure ) { - echo " - {$failure}\n"; - } - exit( 1 ); -} - -echo "\nAll assertions passed.\n"; diff --git a/tests/ai-enabled-tools-smoke.php b/tests/ai-enabled-tools-smoke.php index 2fe934795..08b4b76d5 100644 --- a/tests/ai-enabled-tools-smoke.php +++ b/tests/ai-enabled-tools-smoke.php @@ -11,12 +11,9 @@ * - handler_slugs is single-purpose: [handler_slug] or []. * - AI tools live in flow_step_config['enabled_tools']. * - FlowStepConfig::getEnabledTools() reads the new field. No runtime - * fallback to handler_slugs — legacy on-disk rows are migrated by - * inc/migrations/ai-enabled-tools.php on activation. + * fallback to handler_slugs. * - * This file covers both: - * 1. The accessor (no shim, no fallback). - * 2. The migration that flips legacy rows in place. + * This file covers the accessor contract: no shim, no fallback. * * @package DataMachine\Tests */ @@ -44,45 +41,6 @@ function get_enabled_tools_for_test( array $step_config ): array { return array_values( $enabled ); } -/** - * Inline reimplementation of the per-flow_config migration step in - * datamachine_migrate_ai_enabled_tools(). Mirrors - * inc/migrations/ai-enabled-tools.php so a regression there shows up - * here as a fixture diverging. - */ -function migrate_ai_enabled_tools_for_test( array $flow_config ): array { - foreach ( $flow_config as $step_id => &$step ) { - if ( ! is_array( $step ) ) { - continue; - } - - if ( 'ai' !== ( $step['step_type'] ?? '' ) ) { - continue; - } - - if ( ! empty( $step['enabled_tools'] ) && is_array( $step['enabled_tools'] ) ) { - if ( ! empty( $step['handler_slugs'] ) ) { - $step['handler_slugs'] = array(); - } - continue; - } - - $legacy = $step['handler_slugs'] ?? array(); - if ( empty( $legacy ) || ! is_array( $legacy ) ) { - if ( ! isset( $step['enabled_tools'] ) ) { - $step['enabled_tools'] = array(); - } - continue; - } - - $step['enabled_tools'] = array_values( $legacy ); - $step['handler_slugs'] = array(); - } - unset( $step ); - - return $flow_config; -} - $failures = array(); $passes = 0; @@ -157,88 +115,6 @@ function assert_equals( $expected, $actual, string $name, array &$failures, int ); assert_equals( array(), get_enabled_tools_for_test( $config ), 'non-array enabled_tools → empty (no fatal)', $failures, $passes ); -// ----- inc/migrations/ai-enabled-tools.php ----- - -echo "\n[7] Migration: legacy AI row flips handler_slugs → enabled_tools:\n"; -$flow_config = array( - 'step_a' => array( - 'step_type' => 'ai', - 'handler_slugs' => array( 'intelligence/search', 'intelligence/wiki-upsert' ), - ), -); -$migrated = migrate_ai_enabled_tools_for_test( $flow_config ); -assert_equals( array( 'intelligence/search', 'intelligence/wiki-upsert' ), $migrated['step_a']['enabled_tools'], 'enabled_tools populated from handler_slugs', $failures, $passes ); -assert_equals( array(), $migrated['step_a']['handler_slugs'], 'handler_slugs cleared', $failures, $passes ); - -echo "\n[8] Migration: AI row already on Phase 2b shape is left alone:\n"; -$flow_config = array( - 'step_a' => array( - 'step_type' => 'ai', - 'handler_slugs' => array(), - 'enabled_tools' => array( 'intelligence/search' ), - ), -); -$migrated = migrate_ai_enabled_tools_for_test( $flow_config ); -assert_equals( array( 'intelligence/search' ), $migrated['step_a']['enabled_tools'], 'enabled_tools preserved', $failures, $passes ); -assert_equals( array(), $migrated['step_a']['handler_slugs'], 'handler_slugs stays empty', $failures, $passes ); - -echo "\n[9] Migration: dual-shape row (both populated) clears handler_slugs without overwriting enabled_tools:\n"; -// Defensive against partial-state rows. enabled_tools wins; handler_slugs is wiped. -$flow_config = array( - 'step_a' => array( - 'step_type' => 'ai', - 'handler_slugs' => array( 'intelligence/legacy_search' ), - 'enabled_tools' => array( 'intelligence/wiki-upsert' ), - ), -); -$migrated = migrate_ai_enabled_tools_for_test( $flow_config ); -assert_equals( array( 'intelligence/wiki-upsert' ), $migrated['step_a']['enabled_tools'], 'enabled_tools wins on dual shape', $failures, $passes ); -assert_equals( array(), $migrated['step_a']['handler_slugs'], 'handler_slugs wiped on dual shape', $failures, $passes ); - -echo "\n[10] Migration: AI row with no tools at all gets enabled_tools=[]:\n"; -$flow_config = array( - 'step_a' => array( - 'step_type' => 'ai', - ), -); -$migrated = migrate_ai_enabled_tools_for_test( $flow_config ); -assert_equals( array(), $migrated['step_a']['enabled_tools'], 'enabled_tools field added empty', $failures, $passes ); - -echo "\n[11] Migration: non-AI step left alone:\n"; -$flow_config = array( - 'step_a' => array( - 'step_type' => 'publish', - 'handler_slugs' => array( 'wordpress_publish' ), - 'handler_configs' => array( 'wordpress_publish' => array( 'post_status' => 'draft' ) ), - ), -); -$migrated = migrate_ai_enabled_tools_for_test( $flow_config ); -assert_equals( array( 'wordpress_publish' ), $migrated['step_a']['handler_slugs'], 'publish handler_slugs untouched', $failures, $passes ); -assert_equals( false, isset( $migrated['step_a']['enabled_tools'] ), 'no enabled_tools added to non-AI step', $failures, $passes ); - -echo "\n[12] Migration: mixed flow with AI + publish + system_task:\n"; -$flow_config = array( - 'step_a' => array( - 'step_type' => 'ai', - 'handler_slugs' => array( 'intelligence/search' ), - ), - 'step_b' => array( - 'step_type' => 'publish', - 'handler_slugs' => array( 'wordpress_publish' ), - 'handler_configs' => array( 'wordpress_publish' => array() ), - ), - 'step_c' => array( - 'step_type' => 'system_task', - 'handler_slugs' => array( 'system_task' ), - 'handler_configs' => array( 'system_task' => array( 'task' => 'daily_memory_generation' ) ), - ), -); -$migrated = migrate_ai_enabled_tools_for_test( $flow_config ); -assert_equals( array( 'intelligence/search' ), $migrated['step_a']['enabled_tools'], 'AI step migrated', $failures, $passes ); -assert_equals( array(), $migrated['step_a']['handler_slugs'], 'AI handler_slugs cleared', $failures, $passes ); -assert_equals( array( 'wordpress_publish' ), $migrated['step_b']['handler_slugs'], 'publish step untouched', $failures, $passes ); -assert_equals( array( 'system_task' ), $migrated['step_c']['handler_slugs'], 'system_task synthetic slug untouched', $failures, $passes ); - echo "\n---------------------------------------\n"; $total = $passes + count( $failures ); echo "{$passes} / {$total} passed\n"; diff --git a/tests/handler-slug-scalar-migration-smoke.php b/tests/handler-slug-scalar-migration-smoke.php deleted file mode 100644 index ab9501a07..000000000 --- a/tests/handler-slug-scalar-migration-smoke.php +++ /dev/null @@ -1,193 +0,0 @@ - array( 'uses_handler' => false, 'multi_handler' => false ), - 'system_task' => array( 'uses_handler' => false, 'multi_handler' => false ), - 'webhook_gate' => array( 'uses_handler' => false, 'multi_handler' => false ), - 'fetch' => array( 'uses_handler' => true, 'multi_handler' => false ), - 'publish' => array( 'uses_handler' => true, 'multi_handler' => true ), - 'upsert' => array( 'uses_handler' => true, 'multi_handler' => true ), - ); -} - -class HandlerSlugScalarMigrationWpdb { - public string $prefix = 'wp_'; - - /** @var array>> */ - public array $rows = array(); - - public function prepare( string $query, ...$args ): string { - return vsprintf( str_replace( '%s', "'%s'", $query ), $args ); - } - - public function get_var( string $query ) { - foreach ( array_keys( $this->rows ) as $table ) { - if ( str_contains( $query, $table ) ) { - return $table; - } - } - return null; - } - - public function get_results( string $query, $output ) { - foreach ( $this->rows as $table => $rows ) { - if ( str_contains( $query, $table ) ) { - return $rows; - } - } - return array(); - } - - public function update( string $table, array $data, array $where, array $formats, array $where_formats ): bool { - $GLOBALS['__handler_slug_scalar_migration_update_formats'][] = array( $formats, $where_formats ); - $id_column = array_key_first( $where ); - if ( null === $id_column ) { - return false; - } - $id_value = $where[ $id_column ]; - - foreach ( $this->rows[ $table ] as &$row ) { - if ( array_key_exists( $id_column, $row ) && (string) $row[ $id_column ] === (string) $id_value ) { - $row = array_merge( $row, $data ); - return true; - } - } - unset( $row ); - - return false; - } -} - -require_once __DIR__ . '/../inc/Core/Steps/FlowStepConfig.php'; -require_once __DIR__ . '/../inc/migrations/handler-slug-scalar.php'; - -$failures = array(); -$passes = 0; - -function assert_equals( $expected, $actual, string $name, array &$failures, int &$passes ): void { - if ( $expected === $actual ) { - ++$passes; - echo " ✓ {$name}\n"; - return; - } - - $failures[] = $name; - echo " ✗ {$name}\n"; - echo ' expected: ' . var_export( $expected, true ) . "\n"; - echo ' actual: ' . var_export( $actual, true ) . "\n"; -} - -echo "handler slug scalar migration smoke\n"; -echo "-----------------------------------\n"; - -$wpdb = new HandlerSlugScalarMigrationWpdb(); -$wpdb->rows['wp_datamachine_flows'] = array( - array( - 'flow_id' => 10, - 'flow_config' => wp_json_encode( - array( - 'fetch_10' => array( - 'step_type' => 'fetch', - 'handler_slugs' => array( 'rss' ), - 'handler_configs' => array( 'rss' => array( 'url' => 'https://example.com/feed.xml' ) ), - ), - 'system_10' => array( - 'step_type' => 'system_task', - 'handler_slugs' => array( 'system_task' ), - 'handler_configs' => array( 'system_task' => array( 'task' => 'daily_memory_generation' ) ), - ), - 'publish_10' => array( - 'step_type' => 'publish', - 'handler_slugs' => array( 'wordpress_publish', 'email_publish' ), - 'handler_configs' => array( - 'wordpress_publish' => array( 'post_type' => 'post' ), - 'email_publish' => array( 'to' => 'ops@example.com' ), - ), - ), - ) - ), - ), -); -$wpdb->rows['wp_datamachine_pipelines'] = array( - array( - 'pipeline_id' => 20, - 'pipeline_config' => wp_json_encode( - array( - 'system_pipeline' => array( - 'step_type' => 'system_task', - 'handler_slugs' => array( 'system_task' ), - 'handler_configs' => array( 'system_task' => array( 'task' => 'agent_call' ) ), - ), - ) - ), - ), -); - -datamachine_migrate_handler_slug_scalar(); - -$flow_config = json_decode( $wpdb->rows['wp_datamachine_flows'][0]['flow_config'], true ); -$pipeline_config = json_decode( $wpdb->rows['wp_datamachine_pipelines'][0]['pipeline_config'], true ); - -assert_equals( true, get_option( 'datamachine_handler_slug_scalar_migrated' ), 'migration gate set', $failures, $passes ); -assert_equals( 'rss', $flow_config['fetch_10']['handler_slug'] ?? null, 'fetch slug collapsed to scalar', $failures, $passes ); -assert_equals( array( 'url' => 'https://example.com/feed.xml' ), $flow_config['fetch_10']['handler_config'] ?? null, 'fetch config collapsed to scalar', $failures, $passes ); -assert_equals( false, array_key_exists( 'handler_slugs', $flow_config['fetch_10'] ), 'fetch plural slugs removed', $failures, $passes ); -assert_equals( array( 'task' => 'daily_memory_generation' ), $flow_config['system_10']['handler_config'] ?? null, 'system_task config collapsed', $failures, $passes ); -assert_equals( false, array_key_exists( 'handler_slugs', $flow_config['system_10'] ), 'system_task synthetic slugs removed', $failures, $passes ); -assert_equals( array( 'wordpress_publish', 'email_publish' ), $flow_config['publish_10']['handler_slugs'] ?? null, 'publish multi slugs preserved', $failures, $passes ); -assert_equals( array( 'task' => 'agent_call' ), $pipeline_config['system_pipeline']['handler_config'] ?? null, 'pipeline system_task config collapsed', $failures, $passes ); - -echo "\n-----------------------------------\n"; -$total = $passes + count( $failures ); -echo "{$passes} / {$total} passed\n"; - -if ( ! empty( $failures ) ) { - echo "\nFailures:\n"; - foreach ( $failures as $failure ) { - echo " - {$failure}\n"; - } - exit( 1 ); -} - -echo "\nAll assertions passed.\n"; diff --git a/tests/migration-runtime-smoke.php b/tests/migration-runtime-smoke.php index e819d27ba..9e657ba50 100644 --- a/tests/migration-runtime-smoke.php +++ b/tests/migration-runtime-smoke.php @@ -1,37 +1,14 @@ $c > 1 ); -assert_runtime( - 'no migration called more than once', - 0 === count( $duplicates ) -); +assert_runtime( 'all current schema ensures were called', $schema_chain === $GLOBALS['__test_schema_calls'] ); -echo "\n[chain:3] Chain order is preserved (queue split runs before queue mode)\n"; -$queue_split_pos = array_search( - 'datamachine_migrate_split_queue_payload', - $GLOBALS['__test_migration_calls'], - true -); -$queue_mode_pos = array_search( - 'datamachine_migrate_user_message_queue_mode', - $GLOBALS['__test_migration_calls'], - true -); -assert_runtime( - 'split_queue_payload runs before user_message_queue_mode', - false !== $queue_split_pos - && false !== $queue_mode_pos - && $queue_split_pos < $queue_mode_pos -); -$handler_keys_pos = array_search( - 'datamachine_migrate_handler_keys_to_plural', - $GLOBALS['__test_migration_calls'], - true -); -$ai_tools_pos = array_search( - 'datamachine_migrate_ai_enabled_tools', - $GLOBALS['__test_migration_calls'], - true -); -assert_runtime( - 'handler_keys_to_plural runs before ai_enabled_tools (#1216 dependency)', - false !== $handler_keys_pos - && false !== $ai_tools_pos - && $handler_keys_pos < $ai_tools_pos -); -$handler_scalar_pos = array_search( - 'datamachine_migrate_handler_slug_scalar', - $GLOBALS['__test_migration_calls'], - true -); -assert_runtime( - 'handler_slug_scalar runs after ai_enabled_tools and before split_queue_payload (#1293 dependency)', - false !== $handler_scalar_pos - && false !== $ai_tools_pos - && false !== $queue_split_pos - && $ai_tools_pos < $handler_scalar_pos - && $handler_scalar_pos < $queue_split_pos -); - -// --------------------------------------------------------------- -// SECTION 2: deferred runtime hook gates on db_version. -// --------------------------------------------------------------- - -echo "\n[deferred:1] Cheap path: matching option short-circuits without running chain\n"; -$GLOBALS['__test_migration_calls'] = array(); -$GLOBALS['__test_options']['datamachine_db_version'] = DATAMACHINE_VERSION; -datamachine_maybe_run_deferred_migrations(); -assert_runtime( - 'no migrations called when option matches constant', - 0 === count( $GLOBALS['__test_migration_calls'] ) -); -assert_runtime( - 'option preserved at the matching value', - DATAMACHINE_VERSION === $GLOBALS['__test_options']['datamachine_db_version'] -); - -echo "\n[deferred:2] Stale option triggers chain + bumps option to current\n"; -$GLOBALS['__test_migration_calls'] = array(); -$GLOBALS['__test_options']['datamachine_db_version'] = '0.79.0-stale'; +echo "\n[deferred:1] Cheap path: matching option short-circuits\n"; +$GLOBALS['__test_schema_calls'] = array(); +$GLOBALS['__test_options']['datamachine_db_version'] = DATAMACHINE_VERSION; datamachine_maybe_run_deferred_migrations(); -assert_runtime( - 'all migrations called when option lags constant', - count( $GLOBALS['__test_migration_calls'] ) === count( $migration_chain ) -); -assert_runtime( - 'option bumped to current DATAMACHINE_VERSION after chain', - DATAMACHINE_VERSION === $GLOBALS['__test_options']['datamachine_db_version'] -); +assert_runtime( 'no schema ensures called when option matches constant', array() === $GLOBALS['__test_schema_calls'] ); -echo "\n[deferred:3] Missing option (fresh install pre-activation) triggers chain\n"; -$GLOBALS['__test_migration_calls'] = array(); -unset( $GLOBALS['__test_options']['datamachine_db_version'] ); +echo "\n[deferred:2] Stale option triggers current schema ensures + bumps option\n"; +$GLOBALS['__test_schema_calls'] = array(); +$GLOBALS['__test_options']['datamachine_db_version'] = '0.1.0-stale'; datamachine_maybe_run_deferred_migrations(); -assert_runtime( - 'missing option treated as mismatch — chain runs', - count( $GLOBALS['__test_migration_calls'] ) === count( $migration_chain ) -); -assert_runtime( - 'option created and set to current after chain', - DATAMACHINE_VERSION === $GLOBALS['__test_options']['datamachine_db_version'] -); - -echo "\n[deferred:4] Re-entry after bump is the cheap path again\n"; -$GLOBALS['__test_migration_calls'] = array(); -datamachine_maybe_run_deferred_migrations(); -assert_runtime( - 'second call is a no-op now that option matches', - 0 === count( $GLOBALS['__test_migration_calls'] ) -); - -echo "\n[deferred:5] Newer option than constant (downgrade path) is also cheap\n"; -// The contract is "match → no-op". Anything that mismatches re-enters, -// including downgrade. That's safe because each migration is gated on its -// own option; a downgraded constant doesn't undo persisted shape. -$GLOBALS['__test_options']['datamachine_db_version'] = '99.99.0-future'; -$GLOBALS['__test_migration_calls'] = array(); -datamachine_maybe_run_deferred_migrations(); -assert_runtime( - 'downgrade triggers chain (idempotent gates make it safe)', - count( $GLOBALS['__test_migration_calls'] ) === count( $migration_chain ) -); -assert_runtime( - 'option bumped down to current constant after chain', - DATAMACHINE_VERSION === $GLOBALS['__test_options']['datamachine_db_version'] -); - -// --------------------------------------------------------------- -// SECTION 3: hook registration shape. -// --------------------------------------------------------------- +assert_runtime( 'current schema ensures called when option lags', $schema_chain === $GLOBALS['__test_schema_calls'] ); +assert_runtime( 'option bumped to current DATAMACHINE_VERSION', DATAMACHINE_VERSION === $GLOBALS['__test_options']['datamachine_db_version'] ); -echo "\n[hook:1] Deferred runtime is hooked at plugins_loaded priority 5\n"; +echo "\n[hook:1] Runtime is hooked before main bootstrap\n"; $matching_hook = null; foreach ( $GLOBALS['__test_actions'] as $registered ) { - if ( - 'plugins_loaded' === $registered['hook'] - && 'datamachine_maybe_run_deferred_migrations' === $registered['callback'] - ) { + if ( 'plugins_loaded' === $registered['hook'] && 'datamachine_maybe_run_deferred_migrations' === $registered['callback'] ) { $matching_hook = $registered; break; } } -assert_runtime( - 'plugins_loaded hook registered for datamachine_maybe_run_deferred_migrations', - null !== $matching_hook -); -assert_runtime( - 'priority is 5 (before main bootstrap at 20)', - 5 === ( $matching_hook['priority'] ?? null ) -); - -// --------------------------------------------------------------- -// SECTION 4: activation path still calls the shared entry point. -// --------------------------------------------------------------- - -echo "\n[activation:1] data-machine.php delegates to the shared runtime\n"; -// We can't load data-machine.php in this harness (it pulls in real WP), -// but we can grep it. The contract: the activation function calls -// `datamachine_run_schema_migrations()` instead of inlining the chain. -$plugin_main = (string) file_get_contents( - dirname( __DIR__ ) . '/data-machine.php' -); -assert_runtime( - 'activation calls shared datamachine_run_schema_migrations()', - false !== strpos( - $plugin_main, - 'datamachine_run_schema_migrations();' - ) -); -assert_runtime( - 'activation no longer inlines datamachine_migrate_to_layered_architecture()', - 0 === substr_count( - $plugin_main, - 'datamachine_migrate_to_layered_architecture(' - ) -); -assert_runtime( - 'activation no longer inlines datamachine_migrate_user_message_queue_mode()', - 0 === substr_count( - $plugin_main, - 'datamachine_migrate_user_message_queue_mode(' - ) -); -assert_runtime( - 'activation still bumps datamachine_db_version after the chain', - false !== strpos( - $plugin_main, - "update_option( 'datamachine_db_version', DATAMACHINE_VERSION, true );" - ) -); - -// --------------------------------------------------------------- -// SECTION 5: runtime file declares the chain in the documented order. -// --------------------------------------------------------------- +assert_runtime( 'plugins_loaded hook registered', null !== $matching_hook ); +assert_runtime( 'priority is 5', 5 === ( $matching_hook['priority'] ?? null ) ); -echo "\n[runtime-file:1] runtime.php chain matches the test fixture\n"; -$runtime_src = (string) file_get_contents( - dirname( __DIR__ ) . '/inc/migrations/runtime.php' +echo "\n[runtime-file:1] Old data-shape migration calls are gone\n"; +$runtime_src = (string) file_get_contents( dirname( __DIR__ ) . '/inc/migrations/runtime.php' ); +$removed = array( + 'datamachine_migrate_to_layered_architecture', + 'datamachine_migrate_handler_keys_to_plural', + 'datamachine_migrate_agent_ping_to_system_task', + 'datamachine_migrate_update_to_upsert_step_type', + 'datamachine_migrate_ai_enabled_tools', + 'datamachine_migrate_split_queue_payload', + 'datamachine_migrate_user_message_queue_mode', + 'datamachine_migrate_webhook_auth_v2', + 'datamachine_migrate_settings_mode_models', + 'datamachine_migrate_ai_provider_keys_to_connectors', ); -foreach ( $migration_chain as $expected ) { - assert_runtime( - "runtime.php declares {$expected}();", - false !== strpos( $runtime_src, $expected . '();' ) - ); +foreach ( $removed as $symbol ) { + assert_runtime( "runtime.php does not call {$symbol}()", ! str_contains( $runtime_src, $symbol . '();' ) ); } echo "\n"; diff --git a/tests/queue-mode-collapse-smoke.php b/tests/queue-mode-collapse-smoke.php deleted file mode 100644 index b79013518..000000000 --- a/tests/queue-mode-collapse-smoke.php +++ /dev/null @@ -1,585 +0,0 @@ - &$step ) { - if ( ! is_array( $step ) ) { - continue; - } - - $step_type = $step['step_type'] ?? ''; - $has_queue_enabled = array_key_exists( 'queue_enabled', $step ); - $has_user_message = array_key_exists( 'user_message', $step ); - - if ( ! $has_queue_enabled && ! $has_user_message ) { - continue; - } - - $queue_enabled = $has_queue_enabled ? (bool) $step['queue_enabled'] : false; - $queue_mode = $queue_enabled ? 'drain' : 'static'; - - $step['queue_mode'] = $queue_mode; - - if ( 'ai' === $step_type && $has_user_message ) { - $user_message = is_string( $step['user_message'] ) ? trim( $step['user_message'] ) : ''; - $queue = isset( $step['prompt_queue'] ) && is_array( $step['prompt_queue'] ) - ? $step['prompt_queue'] - : array(); - - if ( '' !== $user_message ) { - if ( empty( $queue ) ) { - // Empty queue + user_message: seed 1-entry static - // queue. Pre-#1291 ran user_message every tick when - // the queue was empty, regardless of queue_enabled. - $step['prompt_queue'] = array( - array( - 'prompt' => $user_message, - 'added_at' => '2026-04-26T00:00:00+00:00', - ), - ); - $step['queue_mode'] = 'static'; - ++$seeded; - } else { - // Non-empty queue + user_message: drop user_message - // (queue head was already shadowing it). queue_mode - // stays at the boolean-resolved value (drain or - // static); do NOT force static — that would silently - // stop a draining flow from draining. - ++$dropped; - } - } - } - - unset( $step['user_message'] ); - unset( $step['queue_enabled'] ); - } - unset( $step ); - - return array( $flow_config, $dropped, $seeded ); -} - -/** - * Inline mirror of QueueableTrait::consumeFromQueueSlot logic for tests. - * Returns [updated_queue, consumed_entry_or_null, mutated_bool]. - */ -function consume_for_test( array $queue, string $queue_mode ): array { - if ( empty( $queue ) ) { - return array( $queue, null, 'static' !== $queue_mode ); - } - - if ( 'static' === $queue_mode ) { - return array( $queue, $queue[0], false ); - } - - $entry = array_shift( $queue ); - - if ( 'loop' === $queue_mode ) { - $queue[] = $entry; - } - - return array( $queue, $entry, true ); -} - -/** - * Inline mirror of FlowStepHelpers::updateUserMessage post-#1291. The - * helper is repurposed: the dedicated user_message slot is gone, the - * helper rewrites prompt_queue + queue_mode instead. - */ -function update_user_message_for_test( array $flow_step_config, string $user_message ): array { - $sanitized = trim( (string) $user_message ); - - if ( '' === $sanitized ) { - $flow_step_config['prompt_queue'] = array(); - } else { - $flow_step_config['prompt_queue'] = array( - array( - 'prompt' => $sanitized, - 'added_at' => '2026-04-26T00:00:00+00:00', - ), - ); - } - $flow_step_config['queue_mode'] = 'static'; - - // Critical: no user_message field stored anywhere. - unset( $flow_step_config['user_message'] ); - - return $flow_step_config; -} - -/** - * Inline mirror of QueueAbility::executeQueueMode validation. - */ -function queue_mode_validate_for_test( $mode ): array { - if ( ! is_string( $mode ) || ! in_array( $mode, array( 'drain', 'loop', 'static' ), true ) ) { - return array( - 'success' => false, - 'error' => 'mode must be one of: drain, loop, static', - ); - } - return array( - 'success' => true, - 'queue_mode' => $mode, - ); -} - -echo "=== queue-mode-collapse-smoke ===\n"; - -// ===================================================================== -// Migration tests -// ===================================================================== - -echo "\n[migration:1] queue_enabled=true → queue_mode=drain on AI step\n"; -$flow_config = array( - 'ai_step_42' => array( - 'step_type' => 'ai', - 'user_message' => '', - 'queue_enabled' => true, - 'prompt_queue' => array( - array( 'prompt' => 'hello', 'added_at' => 'x' ), - ), - ), -); -[ $migrated, $dropped, $seeded ] = migrate_collapse_for_test( $flow_config ); -assert_collapse( - 'queue_enabled=true resolved to drain', - 'drain' === ( $migrated['ai_step_42']['queue_mode'] ?? '' ), - 'mode=' . ( $migrated['ai_step_42']['queue_mode'] ?? 'NULL' ) -); -assert_collapse( - 'user_message stripped', - ! array_key_exists( 'user_message', $migrated['ai_step_42'] ) -); -assert_collapse( - 'queue_enabled stripped', - ! array_key_exists( 'queue_enabled', $migrated['ai_step_42'] ) -); -assert_collapse( - 'prompt_queue preserved as-is', - 1 === count( $migrated['ai_step_42']['prompt_queue'] ) - && 'hello' === $migrated['ai_step_42']['prompt_queue'][0]['prompt'] -); -assert_collapse( 'no user_message seeded for empty user_message', 0 === $seeded ); -assert_collapse( 'no user_message dropped (was empty)', 0 === $dropped ); - -echo "\n[migration:2] queue_enabled=false + non-empty queue → queue_mode=static (named stockpile)\n"; -$flow_config = array( - 'ai_step_42' => array( - 'step_type' => 'ai', - 'user_message' => '', - 'queue_enabled' => false, - 'prompt_queue' => array( - array( 'prompt' => 'first', 'added_at' => 'a' ), - array( 'prompt' => 'second', 'added_at' => 'b' ), - array( 'prompt' => 'third', 'added_at' => 'c' ), - ), - ), -); -[ $migrated ] = migrate_collapse_for_test( $flow_config ); -assert_collapse( - 'static mode preserves first-entry-wins-every-tick', - 'static' === ( $migrated['ai_step_42']['queue_mode'] ?? '' ) -); -assert_collapse( - 'all 3 stockpile entries preserved', - 3 === count( $migrated['ai_step_42']['prompt_queue'] ) -); - -echo "\n[migration:3] empty prompt_queue + non-empty user_message → 1-entry static queue\n"; -$flow_config = array( - 'ai_step_42' => array( - 'step_type' => 'ai', - 'user_message' => 'My per-flow framing prompt', - 'queue_enabled' => false, - 'prompt_queue' => array(), - ), -); -[ $migrated, $dropped, $seeded ] = migrate_collapse_for_test( $flow_config ); -assert_collapse( - 'user_message seeded into 1-entry queue', - 1 === count( $migrated['ai_step_42']['prompt_queue'] ) - && 'My per-flow framing prompt' === $migrated['ai_step_42']['prompt_queue'][0]['prompt'] -); -assert_collapse( - 'mode forced to static', - 'static' === $migrated['ai_step_42']['queue_mode'] -); -assert_collapse( 'seeded counter incremented', 1 === $seeded ); -assert_collapse( 'dropped counter not touched', 0 === $dropped ); -assert_collapse( - 'user_message field gone', - ! array_key_exists( 'user_message', $migrated['ai_step_42'] ) -); - -echo "\n[migration:4a] queue_enabled=true + non-empty queue + user_message → drain mode preserved\n"; -// Regression test for the original #1291 migration which forced -// queue_mode=static here. That silently converted a draining flow -// into a static one — a real behaviour change. Correct behaviour is -// to keep queue_mode=drain (matching the original boolean) and drop -// user_message. The drain-then-fallback semantic is lossy in the new -// model; the migration logs the dropped value so operators can -// recover it via `flow queue add` if needed. -$flow_config = array( - 'ai_step_42' => array( - 'step_type' => 'ai', - 'user_message' => 'shadowed legacy message', - 'queue_enabled' => true, - 'prompt_queue' => array( - array( 'prompt' => 'queued head wins', 'added_at' => 'x' ), - ), - ), -); -[ $migrated, $dropped, $seeded ] = migrate_collapse_for_test( $flow_config ); -assert_collapse( - 'queue preserved as-is', - 1 === count( $migrated['ai_step_42']['prompt_queue'] ) - && 'queued head wins' === $migrated['ai_step_42']['prompt_queue'][0]['prompt'] -); -assert_collapse( - 'queue_enabled=true preserved as drain (NOT forced to static)', - 'drain' === $migrated['ai_step_42']['queue_mode'] -); -assert_collapse( 'dropped counter incremented', 1 === $dropped ); -assert_collapse( 'seeded counter not touched', 0 === $seeded ); -assert_collapse( - 'user_message dropped', - ! array_key_exists( 'user_message', $migrated['ai_step_42'] ) -); - -echo "\n[migration:4b] queue_enabled=false + non-empty queue + user_message → static mode preserved\n"; -$flow_config = array( - 'ai_step_42' => array( - 'step_type' => 'ai', - 'user_message' => 'shadowed legacy message', - 'queue_enabled' => false, - 'prompt_queue' => array( - array( 'prompt' => 'pinned head', 'added_at' => 'x' ), - ), - ), -); -[ $migrated, $dropped, $seeded ] = migrate_collapse_for_test( $flow_config ); -assert_collapse( - 'queue preserved as-is (case 4b)', - 1 === count( $migrated['ai_step_42']['prompt_queue'] ) -); -assert_collapse( - 'queue_enabled=false preserved as static (case 4b)', - 'static' === $migrated['ai_step_42']['queue_mode'] -); -assert_collapse( - 'user_message dropped (case 4b)', - ! array_key_exists( 'user_message', $migrated['ai_step_42'] ) -); - -echo "\n[migration:5] FetchStep with queue_enabled=true → queue_mode=drain, no user_message handling\n"; -$flow_config = array( - 'fetch_step_99' => array( - 'step_type' => 'fetch', - 'queue_enabled' => true, - 'config_patch_queue' => array( - array( 'patch' => array( 'after' => '2017-01-01' ), 'added_at' => 'x' ), - ), - ), -); -[ $migrated ] = migrate_collapse_for_test( $flow_config ); -assert_collapse( - 'fetch step gets drain mode', - 'drain' === ( $migrated['fetch_step_99']['queue_mode'] ?? '' ) -); -assert_collapse( - 'fetch step queue_enabled stripped', - ! array_key_exists( 'queue_enabled', $migrated['fetch_step_99'] ) -); -assert_collapse( - 'fetch step config_patch_queue preserved', - 1 === count( $migrated['fetch_step_99']['config_patch_queue'] ) -); - -echo "\n[migration:6] idempotent — running migration twice = same result\n"; -$flow_config = array( - 'ai_step_42' => array( - 'step_type' => 'ai', - 'user_message' => 'seed me', - 'queue_enabled' => false, - 'prompt_queue' => array(), - ), -); -[ $first ] = migrate_collapse_for_test( $flow_config ); -[ $second ] = migrate_collapse_for_test( $first ); -assert_collapse( 'second run is no-op (no queue_enabled to flip)', $first === $second ); - -echo "\n[migration:7] non-queueable steps left untouched\n"; -$flow_config = array( - 'publish_step_1' => array( - 'step_type' => 'publish', - 'handler_slugs' => array( 'wordpress' ), - 'handler_configs' => array( 'wordpress' => array( 'post_type' => 'post' ) ), - ), -); -[ $migrated ] = migrate_collapse_for_test( $flow_config ); -assert_collapse( - 'publish step config unchanged', - $flow_config === $migrated -); - -// ===================================================================== -// Mode-aware consumption tests -// ===================================================================== - -echo "\n[consume:1] drain mode pops queue head per tick\n"; -$queue = array( - array( 'prompt' => 'a', 'added_at' => '1' ), - array( 'prompt' => 'b', 'added_at' => '2' ), - array( 'prompt' => 'c', 'added_at' => '3' ), -); -[ $q1, $entry1, $mut1 ] = consume_for_test( $queue, 'drain' ); -assert_collapse( 'first drain consumes a', 'a' === $entry1['prompt'] ); -assert_collapse( 'first drain mutates', true === $mut1 ); -[ $q2, $entry2 ] = consume_for_test( $q1, 'drain' ); -assert_collapse( 'second drain consumes b', 'b' === $entry2['prompt'] ); -[ $q3, $entry3 ] = consume_for_test( $q2, 'drain' ); -assert_collapse( 'third drain consumes c', 'c' === $entry3['prompt'] ); -[ $q4, $entry4 ] = consume_for_test( $q3, 'drain' ); -assert_collapse( 'fourth drain returns null (empty)', null === $entry4 ); - -echo "\n[consume:2] loop mode pops + appends — queue rotates back to original after N ticks\n"; -$queue = array( - array( 'prompt' => 'a', 'added_at' => '1' ), - array( 'prompt' => 'b', 'added_at' => '2' ), - array( 'prompt' => 'c', 'added_at' => '3' ), -); -$current = $queue; -$entries = array(); -for ( $i = 0; $i < 6; ++$i ) { - [ $current, $e ] = consume_for_test( $current, 'loop' ); - $entries[] = $e['prompt']; -} -assert_collapse( - 'loop sequence cycles a,b,c,a,b,c after 6 ticks', - array( 'a', 'b', 'c', 'a', 'b', 'c' ) === $entries -); -assert_collapse( - 'loop queue back to original shape after 3 ticks', - count( $current ) === count( $queue ) -); - -echo "\n[consume:3] static mode peeks — queue unchanged after tick\n"; -$queue = array( - array( 'prompt' => 'pinned', 'added_at' => '1' ), - array( 'prompt' => 'staged_b', 'added_at' => '2' ), -); -[ $after, $entry, $mut ] = consume_for_test( $queue, 'static' ); -assert_collapse( 'static consumes head', 'pinned' === $entry['prompt'] ); -assert_collapse( 'static does not mutate', false === $mut ); -assert_collapse( 'static queue unchanged', $after === $queue ); - -echo "\n[consume:4] static mode + multi-entry queue: position 0 fires every tick\n"; -$queue = array( - array( 'prompt' => 'iterating', 'added_at' => '1' ), - array( 'prompt' => 'staged_next', 'added_at' => '2' ), - array( 'prompt' => 'staged_after', 'added_at' => '3' ), -); -$picked = array(); -for ( $i = 0; $i < 4; ++$i ) { - [ $queue, $e ] = consume_for_test( $queue, 'static' ); - $picked[] = $e['prompt']; -} -assert_collapse( - 'iterative-dev pattern: head fires forever', - array( 'iterating', 'iterating', 'iterating', 'iterating' ) === $picked -); - -echo "\n[consume:5] empty queue + drain → null + mutated:true (signal no-items)\n"; -[ $q, $e, $mut ] = consume_for_test( array(), 'drain' ); -assert_collapse( 'empty drain returns null entry', null === $e ); -assert_collapse( 'empty drain signals mutation intent (caller should skip)', true === $mut ); - -echo "\n[consume:6] empty queue + static → null + mutated:false (fallthrough)\n"; -[ $q, $e, $mut ] = consume_for_test( array(), 'static' ); -assert_collapse( 'empty static returns null entry', null === $e ); -assert_collapse( 'empty static signals no mutation (caller falls through)', false === $mut ); - -// ===================================================================== -// updateUserMessage shim tests -// ===================================================================== - -echo "\n[shim:1] user_message=\"X\" writes 1-entry static queue\n"; -$step = array( - 'step_type' => 'ai', -); -$updated = update_user_message_for_test( $step, 'My new message' ); -assert_collapse( - 'prompt_queue is 1-entry list', - 1 === count( $updated['prompt_queue'] ) -); -assert_collapse( - 'queue head matches input', - 'My new message' === $updated['prompt_queue'][0]['prompt'] -); -assert_collapse( - 'queue mode is static', - 'static' === $updated['queue_mode'] -); -assert_collapse( - 'no user_message field stored', - ! array_key_exists( 'user_message', $updated ) -); - -echo "\n[shim:2] user_message=\"\" clears queue (legacy unset semantics)\n"; -$step = array( - 'step_type' => 'ai', - 'prompt_queue' => array( - array( 'prompt' => 'old prompt', 'added_at' => 'x' ), - ), -); -$updated = update_user_message_for_test( $step, '' ); -assert_collapse( 'queue emptied', array() === $updated['prompt_queue'] ); -assert_collapse( 'mode set to static', 'static' === $updated['queue_mode'] ); - -echo "\n[shim:3] update overwrites existing queue\n"; -$step = array( - 'step_type' => 'ai', - 'prompt_queue' => array( - array( 'prompt' => 'first', 'added_at' => 'a' ), - array( 'prompt' => 'second', 'added_at' => 'b' ), - ), -); -$updated = update_user_message_for_test( $step, 'replacement' ); -assert_collapse( - 'queue replaced with single entry', - 1 === count( $updated['prompt_queue'] ) - && 'replacement' === $updated['prompt_queue'][0]['prompt'] -); - -// ===================================================================== -// QueueAbility::executeQueueMode validation tests -// ===================================================================== - -echo "\n[mode-validate:1] enum validation rejects unknown\n"; -$result = queue_mode_validate_for_test( 'invalid' ); -assert_collapse( 'unknown mode rejected', false === $result['success'] ); - -echo "\n[mode-validate:2] each valid enum accepted\n"; -foreach ( array( 'drain', 'loop', 'static' ) as $mode ) { - $result = queue_mode_validate_for_test( $mode ); - assert_collapse( "mode={$mode} accepted", true === $result['success'] ); -} - -echo "\n[mode-validate:3] non-string rejected\n"; -$result = queue_mode_validate_for_test( true ); -assert_collapse( 'boolean rejected', false === $result['success'] ); - -// ===================================================================== -// AIStep collapsed precedence tests -// ===================================================================== - -echo "\n[aistep:1] empty queue + drain → COMPLETED_NO_ITEMS branch\n"; -// AIStep collapsed logic: $queue_mode picks consumption; empty queue + -// (drain|loop) sets job_status=COMPLETED_NO_ITEMS. -$queue_mode = 'drain'; -$queue = array(); -[ , $entry ] = consume_for_test( $queue, $queue_mode ); -$user_msg = null !== $entry ? $entry['prompt'] : ''; -$should_skip = '' === $user_msg && in_array( $queue_mode, array( 'drain', 'loop' ), true ); -assert_collapse( 'empty drain triggers no-items skip branch', true === $should_skip ); - -echo "\n[aistep:2] empty queue + static → fall through with empty user_message\n"; -$queue_mode = 'static'; -$queue = array(); -[ , $entry ] = consume_for_test( $queue, $queue_mode ); -$user_msg = null !== $entry ? $entry['prompt'] : ''; -$should_skip = '' === $user_msg && in_array( $queue_mode, array( 'drain', 'loop' ), true ); -assert_collapse( 'empty static does not trigger skip', false === $should_skip ); -assert_collapse( 'static fallthrough yields empty user_message', '' === $user_msg ); - -echo "\n[aistep:3] non-empty queue + drain → user_message comes from popped head\n"; -$queue = array( - array( 'prompt' => 'tick-1 work', 'added_at' => 'x' ), - array( 'prompt' => 'tick-2 work', 'added_at' => 'y' ), -); -[ $after, $entry ] = consume_for_test( $queue, 'drain' ); -assert_collapse( 'drain returns first entry', 'tick-1 work' === $entry['prompt'] ); -assert_collapse( 'drain mutates queue (length 1 remaining)', 1 === count( $after ) ); - -// ===================================================================== - -echo "\n"; -if ( 0 === $failed ) { - echo "=== queue-mode-collapse-smoke: ALL PASS ({$total}) ===\n"; - exit( 0 ); -} -echo "=== queue-mode-collapse-smoke: {$failed} FAIL of {$total} ===\n"; -exit( 1 ); diff --git a/tests/queue-payload-split-smoke.php b/tests/queue-payload-split-smoke.php deleted file mode 100644 index 14df9534a..000000000 --- a/tests/queue-payload-split-smoke.php +++ /dev/null @@ -1,564 +0,0 @@ - array(), - 'from_queue' => false, - 'added_at' => null, - ); - } - - $queue = $step_config['config_patch_queue'] ?? array(); - if ( empty( $queue ) ) { - return array( - 'patch' => array(), - 'from_queue' => false, - 'added_at' => null, - ); - } - - $entry = array_shift( $queue ); - $patch = isset( $entry['patch'] ) && is_array( $entry['patch'] ) ? $entry['patch'] : array(); - - return array( - 'patch' => $patch, - 'from_queue' => true, - 'added_at' => $entry['added_at'] ?? null, - ); -} - -/** - * Inline mirror of AIStep's prompt_queue read path — reads `prompt` - * field as a string from `prompt_queue`. - * - * Mirrors inc/Core/Steps/AI/AIStep.php::execute() lines 140-173. - */ -function pop_prompt_for_test( array $step_config, bool $queue_enabled ): array { - $queue = $step_config['prompt_queue'] ?? array(); - $queued_head = $queue[0]['prompt'] ?? ''; - - if ( $queue_enabled ) { - // drain mode: pop the head. - $entry = array_shift( $queue ); - return array( - 'value' => $entry['prompt'] ?? '', - 'from_queue' => null !== $entry, - 'added_at' => $entry['added_at'] ?? null, - ); - } - - // Static peek mode. - return array( - 'value' => $queued_head, - 'from_queue' => false, - 'added_at' => null, - ); -} - -/** - * Inline mirror of inc/migrations/split-queue-payload.php::datamachine_migrate_split_queue_payload. - * - * Walks one flow_config and migrates fetch-step prompt_queue entries - * (with JSON-encoded prompt fields) into config_patch_queue entries - * (with decoded patch fields). AI step queues are left untouched. - * - * Returns [ migrated_flow_config, entries_migrated, entries_skipped ]. - */ -function migrate_flow_config_for_test( array $flow_config ): array { - $entries_migrated = 0; - $entries_skipped = 0; - - foreach ( $flow_config as $step_id => &$step ) { - if ( ! is_array( $step ) ) { - continue; - } - if ( 'fetch' !== ( $step['step_type'] ?? '' ) ) { - continue; - } - - $legacy_queue = $step['prompt_queue'] ?? null; - if ( ! is_array( $legacy_queue ) || empty( $legacy_queue ) ) { - if ( array_key_exists( 'prompt_queue', $step ) ) { - unset( $step['prompt_queue'] ); - } - continue; - } - - $existing_patch_queue = $step['config_patch_queue'] ?? array(); - if ( ! is_array( $existing_patch_queue ) ) { - $existing_patch_queue = array(); - } - - $migrated_for_step = array(); - foreach ( $legacy_queue as $entry ) { - if ( ! is_array( $entry ) || ! isset( $entry['prompt'] ) ) { - ++$entries_skipped; - continue; - } - $decoded = json_decode( (string) $entry['prompt'], true ); - if ( ! is_array( $decoded ) || empty( $decoded ) ) { - ++$entries_skipped; - continue; - } - $migrated_for_step[] = array( - 'patch' => $decoded, - 'added_at' => $entry['added_at'] ?? '2026-04-26T00:00:00+00:00', - ); - ++$entries_migrated; - } - - $step['config_patch_queue'] = array_merge( $existing_patch_queue, $migrated_for_step ); - unset( $step['prompt_queue'] ); - } - unset( $step ); - - return array( $flow_config, $entries_migrated, $entries_skipped ); -} - -/** - * Inline mirror of QueueAbility::executeConfigPatchAdd validation. - * Returns whether the input is accepted, plus an error string when not. - * - * Mirrors inc/Abilities/Flow/QueueAbility.php::executeConfigPatchAdd. - */ -function validate_config_patch_add_for_test( $patch ): array { - if ( ! is_array( $patch ) ) { - return array( 'success' => false, 'error' => 'patch is required and must be an object' ); - } - if ( empty( $patch ) ) { - return array( 'success' => false, 'error' => 'patch must be a non-empty object' ); - } - return array( 'success' => true ); -} - -/** - * Inline mirror of CLI consumer-aware routing: given step_type and - * input shape, decide whether the operation is allowed and which - * slot it targets. - * - * Mirrors inc/Cli/Commands/Flows/QueueCommand.php::add(). - */ -function cli_route_for_test( string $step_type, $patch_json, $prompt ): array { - if ( null !== $patch_json && null !== $prompt ) { - return array( 'allowed' => false, 'error' => 'cannot use both' ); - } - if ( null === $patch_json && null === $prompt ) { - return array( 'allowed' => false, 'error' => 'provide one' ); - } - - if ( null !== $patch_json ) { - if ( 'fetch' !== $step_type ) { - return array( 'allowed' => false, 'error' => '--patch only valid for fetch' ); - } - return array( 'allowed' => true, 'slot' => 'config_patch_queue' ); - } - - // Prompt path. - if ( 'fetch' === $step_type ) { - return array( 'allowed' => false, 'error' => 'fetch consumes patches, not prompts' ); - } - return array( 'allowed' => true, 'slot' => 'prompt_queue' ); -} - -// --- Case 1: AIStep reads prompt_queue post-split (drain mode). -echo "Case 1: AIStep prompt_queue drain mode\n"; - -$ai_step_drain = array( - 'step_type' => 'ai', - 'queue_enabled' => true, - 'prompt_queue' => array( - array( 'prompt' => 'First task', 'added_at' => '2026-04-26T01:00:00+00:00' ), - array( 'prompt' => 'Second task', 'added_at' => '2026-04-26T02:00:00+00:00' ), - ), -); - -$result = pop_prompt_for_test( $ai_step_drain, true ); - -assert_split( - 'AI drain pops the first prompt as a string', - 'First task' === $result['value'], - 'got: ' . var_export( $result['value'], true ) -); -assert_split( - 'AI drain reports from_queue=true', - true === $result['from_queue'] -); -assert_split( - 'AI drain returns added_at', - '2026-04-26T01:00:00+00:00' === $result['added_at'] -); - -// --- Case 2: AIStep reads prompt_queue post-split (peek mode, queue_enabled=false). -echo "\nCase 2: AIStep prompt_queue peek mode\n"; - -$ai_step_peek = array( - 'step_type' => 'ai', - 'queue_enabled' => false, - 'prompt_queue' => array( - array( 'prompt' => 'Static peek', 'added_at' => '2026-04-26T01:00:00+00:00' ), - ), -); - -$result = pop_prompt_for_test( $ai_step_peek, false ); - -assert_split( - 'AI peek returns head value', - 'Static peek' === $result['value'] -); -assert_split( - 'AI peek does NOT report from_queue=true', - false === $result['from_queue'] -); - -// --- Case 3: FetchStep reads config_patch_queue (post-split). -echo "\nCase 3: FetchStep config_patch_queue read\n"; - -$fetch_step = array( - 'step_type' => 'fetch', - 'queue_enabled' => true, - 'config_patch_queue' => array( - array( - 'patch' => array( 'params' => array( 'after' => '2015-05-01', 'before' => '2015-06-01' ) ), - 'added_at' => '2026-04-26T01:00:00+00:00', - ), - array( - 'patch' => array( 'params' => array( 'after' => '2015-06-01' ) ), - 'added_at' => '2026-04-26T02:00:00+00:00', - ), - ), -); - -$result = pop_config_patch_for_test( $fetch_step, true ); - -assert_split( - 'FetchStep pops patch as a decoded array', - is_array( $result['patch'] ) && isset( $result['patch']['params'] ) -); -assert_split( - 'FetchStep patch contains expected keys', - '2015-05-01' === ( $result['patch']['params']['after'] ?? null ) -); -assert_split( - 'FetchStep patch is NOT a JSON-encoded string', - is_array( $result['patch'] ) && ! is_string( $result['patch'] ), - 'patch type: ' . gettype( $result['patch'] ) -); -assert_split( - 'FetchStep reports from_queue=true', - true === $result['from_queue'] -); - -// --- Case 4: FetchStep with no queued patch returns empty. -echo "\nCase 4: FetchStep with empty config_patch_queue\n"; - -$fetch_empty = array( - 'step_type' => 'fetch', - 'queue_enabled' => true, - 'config_patch_queue' => array(), -); - -$result = pop_config_patch_for_test( $fetch_empty, true ); - -assert_split( - 'Empty queue returns empty patch', - array() === $result['patch'] -); -assert_split( - 'Empty queue reports from_queue=false', - false === $result['from_queue'] -); - -// --- Case 5: FetchStep with queue_enabled=false skips pop. -echo "\nCase 5: FetchStep with queue_enabled=false\n"; - -$result = pop_config_patch_for_test( $fetch_step, false ); -assert_split( - 'queue_enabled=false returns empty patch', - array() === $result['patch'] && false === $result['from_queue'] -); - -// --- Case 6: Migration moves fetch-step JSON-encoded prompts to config_patch_queue. -echo "\nCase 6: Migration — fetch step JSON prompts → config_patch_queue\n"; - -$pre_migration = array( - 'pstep_fetch_uuid_1' => array( - 'step_type' => 'fetch', - 'prompt_queue' => array( - array( - 'prompt' => '{"params":{"after":"2015-05-01","before":"2015-06-01"}}', - 'added_at' => '2026-04-26T01:00:00+00:00', - ), - array( - 'prompt' => '{"params":{"after":"2015-06-01","before":"2015-07-01"}}', - 'added_at' => '2026-04-26T02:00:00+00:00', - ), - ), - ), - 'pstep_ai_uuid_1' => array( - 'step_type' => 'ai', - 'prompt_queue' => array( - array( 'prompt' => 'AI task one', 'added_at' => '2026-04-26T03:00:00+00:00' ), - ), - ), -); - -list( $migrated, $count_migrated, $count_skipped ) = migrate_flow_config_for_test( $pre_migration ); - -assert_split( - 'Migration creates config_patch_queue on fetch step', - isset( $migrated['pstep_fetch_uuid_1']['config_patch_queue'] ) -); -assert_split( - 'Migration removes prompt_queue from fetch step', - ! isset( $migrated['pstep_fetch_uuid_1']['prompt_queue'] ) -); -assert_split( - 'Migration moves all 2 fetch entries', - 2 === count( $migrated['pstep_fetch_uuid_1']['config_patch_queue'] ?? array() ) -); -assert_split( - 'Migrated patch is a decoded object', - isset( $migrated['pstep_fetch_uuid_1']['config_patch_queue'][0]['patch']['params']['after'] ) - && '2015-05-01' === $migrated['pstep_fetch_uuid_1']['config_patch_queue'][0]['patch']['params']['after'] -); -assert_split( - 'Migration preserves added_at on each entry', - '2026-04-26T01:00:00+00:00' === ( $migrated['pstep_fetch_uuid_1']['config_patch_queue'][0]['added_at'] ?? null ) -); -assert_split( - 'Migration leaves AI step prompt_queue untouched', - isset( $migrated['pstep_ai_uuid_1']['prompt_queue'] ) - && 1 === count( $migrated['pstep_ai_uuid_1']['prompt_queue'] ) - && 'AI task one' === $migrated['pstep_ai_uuid_1']['prompt_queue'][0]['prompt'] -); -assert_split( - 'Migration does NOT create config_patch_queue on AI step', - ! isset( $migrated['pstep_ai_uuid_1']['config_patch_queue'] ) -); -assert_split( 'Migration count: 2 entries migrated', 2 === $count_migrated ); -assert_split( 'Migration count: 0 entries skipped', 0 === $count_skipped ); - -// --- Case 7: Migration skips misshaped fetch entries (non-JSON prompts). -echo "\nCase 7: Migration — misshaped fetch entries are skipped, not crashed on\n"; - -$misshaped = array( - 'pstep_fetch_uuid_1' => array( - 'step_type' => 'fetch', - 'prompt_queue' => array( - array( 'prompt' => 'this is not JSON', 'added_at' => '2026-04-26T01:00:00+00:00' ), - array( 'prompt' => '{"valid":true}', 'added_at' => '2026-04-26T02:00:00+00:00' ), - array( 'prompt' => '', 'added_at' => '2026-04-26T03:00:00+00:00' ), - array( 'prompt' => '"just a string"', 'added_at' => '2026-04-26T04:00:00+00:00' ), - ), - ), -); - -list( $migrated, $count_migrated, $count_skipped ) = migrate_flow_config_for_test( $misshaped ); - -assert_split( - 'Misshaped: only the valid JSON object entry is migrated', - 1 === count( $migrated['pstep_fetch_uuid_1']['config_patch_queue'] ?? array() ) -); -assert_split( - 'Misshaped: surviving entry decoded correctly', - true === ( $migrated['pstep_fetch_uuid_1']['config_patch_queue'][0]['patch']['valid'] ?? null ) -); -assert_split( - 'Misshaped: 3 entries skipped (non-JSON, empty, JSON-but-not-object)', - 3 === $count_skipped -); -assert_split( - 'Misshaped: 1 entry migrated', - 1 === $count_migrated -); -assert_split( - 'Misshaped: prompt_queue still removed from fetch step', - ! isset( $migrated['pstep_fetch_uuid_1']['prompt_queue'] ) -); - -// --- Case 8: Migration is idempotent. -echo "\nCase 8: Migration idempotency\n"; - -$first_pass_input = array( - 'pstep_fetch_uuid_1' => array( - 'step_type' => 'fetch', - 'prompt_queue' => array( - array( 'prompt' => '{"x":1}', 'added_at' => '2026-04-26T01:00:00+00:00' ), - ), - ), -); - -list( $after_first, , ) = migrate_flow_config_for_test( $first_pass_input ); -list( $after_second, $second_count, $second_skipped ) = migrate_flow_config_for_test( $after_first ); - -assert_split( - 'Idempotency: second pass migrates 0 entries', - 0 === $second_count -); -assert_split( - 'Idempotency: second pass skips 0 entries', - 0 === $second_skipped -); -assert_split( - 'Idempotency: shape unchanged after second pass', - $after_first === $after_second, - 'first: ' . wp_json_encode_test( $after_first ) . ' second: ' . wp_json_encode_test( $after_second ) -); - -function wp_json_encode_test( $v ) { - return json_encode( $v ); -} - -// --- Case 9: Validation — config-patch-add rejects non-object input. -echo "\nCase 9: config-patch-add validation\n"; - -$result = validate_config_patch_add_for_test( 'a plain string' ); -assert_split( - 'Strings rejected on config-patch-add', - false === $result['success'] - && false !== strpos( $result['error'], 'object' ) -); - -$result = validate_config_patch_add_for_test( null ); -assert_split( - 'Null rejected on config-patch-add', - false === $result['success'] -); - -$result = validate_config_patch_add_for_test( array() ); -assert_split( - 'Empty arrays rejected on config-patch-add', - false === $result['success'] - && false !== strpos( $result['error'], 'non-empty' ) -); - -$result = validate_config_patch_add_for_test( array( 'params' => array( 'x' => 1 ) ) ); -assert_split( - 'Valid object accepted on config-patch-add', - true === $result['success'] -); - -// --- Case 10: CLI routing — fetch step blocks string prompt. -echo "\nCase 10: CLI consumer-aware routing\n"; - -$result = cli_route_for_test( 'fetch', null, 'a prompt' ); -assert_split( - 'fetch + positional prompt → blocked', - false === $result['allowed'] - && false !== strpos( $result['error'], 'fetch' ) -); - -$result = cli_route_for_test( 'fetch', '{"x":1}', null ); -assert_split( - 'fetch + --patch → routes to config_patch_queue', - true === $result['allowed'] - && 'config_patch_queue' === ( $result['slot'] ?? null ) -); - -$result = cli_route_for_test( 'ai', '{"x":1}', null ); -assert_split( - 'ai + --patch → blocked', - false === $result['allowed'] - && false !== strpos( $result['error'], 'fetch' ) -); - -$result = cli_route_for_test( 'ai', null, 'a prompt' ); -assert_split( - 'ai + positional prompt → routes to prompt_queue', - true === $result['allowed'] - && 'prompt_queue' === ( $result['slot'] ?? null ) -); - -$result = cli_route_for_test( 'ai', '{"x":1}', 'a prompt' ); -assert_split( - 'both flags supplied → blocked with helpful error', - false === $result['allowed'] - && false !== strpos( $result['error'], 'cannot use both' ) -); - -$result = cli_route_for_test( 'ai', null, null ); -assert_split( - 'neither flag supplied → blocked', - false === $result['allowed'] -); - -// --- Case 11: Storage shape contract — slot keys are distinct. -echo "\nCase 11: Storage shape contract\n"; - -$config = array( - 'step_type' => 'fetch', - 'prompt_queue' => array(), - 'config_patch_queue' => array( - array( 'patch' => array( 'a' => 1 ), 'added_at' => '2026-04-26T01:00:00+00:00' ), - ), -); - -assert_split( - 'prompt_queue and config_patch_queue are independent storage slots', - array_key_exists( 'prompt_queue', $config ) - && array_key_exists( 'config_patch_queue', $config ) - && count( $config['prompt_queue'] ) !== count( $config['config_patch_queue'] ) -); - -assert_split( - 'config_patch_queue entries use `patch` field, not `prompt`', - isset( $config['config_patch_queue'][0]['patch'] ) - && ! isset( $config['config_patch_queue'][0]['prompt'] ) -); - -// --- Final report. -echo "\n"; -echo "===========================================\n"; -echo sprintf( "Smoke test: %d / %d passed\n", $total - $failed, $total ); -echo "===========================================\n"; - -if ( $failed > 0 ) { - exit( 1 ); -} - -exit( 0 ); diff --git a/tests/settings-mode-models-migration-smoke.php b/tests/settings-mode-models-migration-smoke.php deleted file mode 100644 index 69beecf3c..000000000 --- a/tests/settings-mode-models-migration-smoke.php +++ /dev/null @@ -1,201 +0,0 @@ - array( - 'pipeline' => array( 'provider' => 'openai', 'model' => 'gpt-5.4' ), - ), - 'default_model' => 'gpt-5.4-mini', - 'unrelated_value' => true, -); -$site_options['datamachine_network_settings'] = array(); - -datamachine_migrate_settings_mode_models(); - -assert_equals( - array( - 'default_model' => 'gpt-5.4-mini', - 'unrelated_value' => true, - 'mode_models' => array( - 'pipeline' => array( 'provider' => 'openai', 'model' => 'gpt-5.4' ), - ), - ), - $options['datamachine_settings'], - 'site context_models moved and legacy key removed', - $failures, - $passes -); - -echo "\n[settings:2] Migrates network agent_models to mode_models\n"; -$options = array(); -$site_options = array( - 'datamachine_network_settings' => array( - 'agent_models' => array( - 'chat' => array( 'provider' => 'anthropic', 'model' => 'claude-sonnet-4-20250514' ), - ), - 'default_provider' => 'openai', - ), -); - -datamachine_migrate_settings_mode_models(); - -assert_equals( - array( - 'default_provider' => 'openai', - 'mode_models' => array( - 'chat' => array( 'provider' => 'anthropic', 'model' => 'claude-sonnet-4-20250514' ), - ), - ), - $site_options['datamachine_network_settings'], - 'network agent_models moved and legacy key removed', - $failures, - $passes -); - -echo "\n[settings:3] Existing mode_models wins over legacy keys\n"; -$options = array( - 'datamachine_settings' => array( - 'mode_models' => array( - 'pipeline' => array( 'provider' => 'openai', 'model' => 'gpt-5.4' ), - ), - 'context_models' => array( - 'pipeline' => array( 'provider' => 'openai', 'model' => 'gpt-5.4-mini' ), - ), - ), -); -$site_options = array(); - -datamachine_migrate_settings_mode_models(); - -assert_equals( - array( - 'mode_models' => array( - 'pipeline' => array( 'provider' => 'openai', 'model' => 'gpt-5.4' ), - ), - ), - $options['datamachine_settings'], - 'canonical mode_models preserved and legacy key removed', - $failures, - $passes -); - -echo "\n[settings:4] Migration is gated\n"; -$options = array( - 'datamachine_settings_mode_models_migrated' => true, - 'datamachine_settings' => array( - 'context_models' => array( - 'pipeline' => array( 'provider' => 'openai', 'model' => 'gpt-5.4' ), - ), - ), -); -$site_options = array(); - -datamachine_migrate_settings_mode_models(); - -assert_equals( - array( - 'context_models' => array( - 'pipeline' => array( 'provider' => 'openai', 'model' => 'gpt-5.4' ), - ), - ), - $options['datamachine_settings'], - 'gate prevents re-entry', - $failures, - $passes -); - -echo "\n[settings:5] Direct array helper sanitizes extension modes\n"; -$normalized = datamachine_migrate_settings_mode_models_array( - array( - 'agent_models' => array( - 'Pipeline!' => array( 'provider' => ' openai ', 'model' => ' gpt-5.4 ' ), - 'invalid' => 'not an array', - ), - ) -); - -assert_equals( - array( - 'mode_models' => array( - 'pipeline' => array( 'provider' => 'openai', 'model' => 'gpt-5.4' ), - ), - ), - $normalized, - 'helper normalizes mode keys and drops invalid entries', - $failures, - $passes -); - -echo "\n------------------------------------\n"; -if ( ! empty( $failures ) ) { - echo count( $failures ) . " failure(s)\n"; - exit( 1 ); -} - -echo "All {$passes} assertions passed.\n"; diff --git a/tests/system-task-config-passthrough-smoke.php b/tests/system-task-config-passthrough-smoke.php index 8bcaaa1af..dae816d48 100644 --- a/tests/system-task-config-passthrough-smoke.php +++ b/tests/system-task-config-passthrough-smoke.php @@ -166,59 +166,37 @@ function assert_absent( string $key, array $array, string $name, array &$failure assert_absent( 'handler_slugs', $step0, 'AI has no handler_slugs', $failures, $passes ); assert_equals( 'be helpful', $built['pipeline_config']['ephemeral_pipeline_0']['system_prompt'] ?? null, 'AI system_prompt lands in pipeline_config', $failures, $passes ); -echo "\n[6] normalize legacy rows to canonical storage:\n"; +echo "\n[6] normalize canonical rows without cross-shape inference:\n"; $system_task = FlowStepConfig::normalizeHandlerShape( array( - 'step_type' => 'system_task', - 'handler_slugs' => array( 'system_task' ), - 'handler_configs' => array( 'system_task' => array( 'task' => 'agent_call' ) ), + 'step_type' => 'system_task', + 'handler_config' => array( 'task' => 'agent_call' ), ) ); -assert_equals( array( 'task' => 'agent_call' ), $system_task['handler_config'] ?? array(), 'legacy synthetic system_task config collapses', $failures, $passes ); -assert_absent( 'handler_slugs', $system_task, 'legacy synthetic system_task slugs removed', $failures, $passes ); +assert_equals( array( 'task' => 'agent_call' ), $system_task['handler_config'] ?? array(), 'system_task config preserved', $failures, $passes ); +assert_absent( 'handler_slugs', $system_task, 'system_task slugs remain absent', $failures, $passes ); $fetch = FlowStepConfig::normalizeHandlerShape( array( - 'step_type' => 'fetch', - 'handler_slugs' => array( 'rss' ), - 'handler_configs' => array( 'rss' => array( 'url' => 'https://example.com/feed.xml' ) ), + 'step_type' => 'fetch', + 'handler_slug' => 'rss', + 'handler_config' => array( 'url' => 'https://example.com/feed.xml' ), ) ); -assert_equals( 'rss', $fetch['handler_slug'] ?? null, 'legacy fetch slug becomes scalar', $failures, $passes ); -assert_equals( array( 'url' => 'https://example.com/feed.xml' ), $fetch['handler_config'] ?? array(), 'legacy fetch config becomes scalar', $failures, $passes ); -assert_absent( 'handler_slugs', $fetch, 'legacy fetch plural slugs removed', $failures, $passes ); - -$fetch_configs_only = FlowStepConfig::normalizeHandlerShape( - array( - 'step_type' => 'fetch', - 'handler_configs' => array( 'rss' => array( 'url' => 'https://example.com/feed.xml' ) ), - ) -); -assert_equals( 'rss', $fetch_configs_only['handler_slug'] ?? null, 'legacy fetch infers scalar slug from config key', $failures, $passes ); -assert_equals( array( 'url' => 'https://example.com/feed.xml' ), $fetch_configs_only['handler_config'] ?? array(), 'legacy fetch keeps config when slug list missing', $failures, $passes ); +assert_equals( 'rss', $fetch['handler_slug'] ?? null, 'fetch scalar slug preserved', $failures, $passes ); +assert_equals( array( 'url' => 'https://example.com/feed.xml' ), $fetch['handler_config'] ?? array(), 'fetch scalar config preserved', $failures, $passes ); +assert_absent( 'handler_slugs', $fetch, 'fetch plural slugs remain absent', $failures, $passes ); $publish = FlowStepConfig::normalizeHandlerShape( - array( - 'step_type' => 'publish', - 'handler_slug' => 'wordpress_publish', - 'handler_config' => array( 'post_type' => 'post' ), - ) -); -assert_equals( array( 'wordpress_publish' ), $publish['handler_slugs'] ?? array(), 'legacy publish scalar slug becomes list', $failures, $passes ); -assert_equals( array( 'wordpress_publish' => array( 'post_type' => 'post' ) ), $publish['handler_configs'] ?? array(), 'legacy publish scalar config becomes map', $failures, $passes ); -assert_absent( 'handler_slug', $publish, 'legacy publish scalar slug removed', $failures, $passes ); - -$publish_configs_only = FlowStepConfig::normalizeHandlerShape( array( 'step_type' => 'publish', - 'handler_configs' => array( - 'wordpress_publish' => array( 'post_type' => 'post' ), - 'email_publish' => array( 'to' => 'ops@example.com' ), - ), + 'handler_slugs' => array( 'wordpress_publish' ), + 'handler_configs' => array( 'wordpress_publish' => array( 'post_type' => 'post' ) ), ) ); -assert_equals( array( 'wordpress_publish', 'email_publish' ), $publish_configs_only['handler_slugs'] ?? array(), 'legacy publish infers slugs from config keys', $failures, $passes ); -assert_equals( array( 'to' => 'ops@example.com' ), $publish_configs_only['handler_configs']['email_publish'] ?? array(), 'legacy publish keeps all inferred configs', $failures, $passes ); +assert_equals( array( 'wordpress_publish' ), $publish['handler_slugs'] ?? array(), 'publish slugs preserved', $failures, $passes ); +assert_equals( array( 'wordpress_publish' => array( 'post_type' => 'post' ) ), $publish['handler_configs'] ?? array(), 'publish configs preserved', $failures, $passes ); +assert_absent( 'handler_slug', $publish, 'publish scalar slug remains absent', $failures, $passes ); echo "\n-----------------------------------\n"; $total = $passes + count( $failures ); diff --git a/tests/webhook-auth-v2-migration-smoke.php b/tests/webhook-auth-v2-migration-smoke.php deleted file mode 100644 index 624f54994..000000000 --- a/tests/webhook-auth-v2-migration-smoke.php +++ /dev/null @@ -1,209 +0,0 @@ ->> */ - public array $rows = array(); - - public function prepare( string $query, ...$args ): string { - return vsprintf( str_replace( '%s', "'%s'", $query ), $args ); - } - - public function get_var( string $query ) { - foreach ( array_keys( $this->rows ) as $table ) { - if ( str_contains( $query, $table ) ) { - return $table; - } - } - return null; - } - - public function get_results( string $query, $_output ) { - foreach ( $this->rows as $table => $rows ) { - if ( str_contains( $query, $table ) ) { - return $rows; - } - } - return array(); - } - - public function update( string $table, array $data, array $where, array $formats, array $where_formats ): bool { - $GLOBALS['__webhook_auth_v2_migration_update_formats'][] = array( $formats, $where_formats ); - $id_column = array_key_first( $where ); - if ( null === $id_column ) { - return false; - } - $id_value = $where[ $id_column ]; - - foreach ( $this->rows[ $table ] as &$row ) { - if ( array_key_exists( $id_column, $row ) && (string) $row[ $id_column ] === (string) $id_value ) { - $row = array_merge( $row, $data ); - return true; - } - } - unset( $row ); - - return false; - } -} - -require_once __DIR__ . '/../inc/Api/WebhookVerificationResult.php'; -require_once __DIR__ . '/../inc/Api/WebhookVerifier.php'; -require_once __DIR__ . '/../inc/migrations/webhook-auth-v2.php'; - -$failures = array(); -$passes = 0; - -function assert_equals( $expected, $actual, string $name, array &$failures, int &$passes ): void { - if ( $expected === $actual ) { - ++$passes; - echo " ✓ {$name}\n"; - return; - } - - $failures[] = $name; - echo " ✗ {$name}\n"; - echo ' expected: ' . var_export( $expected, true ) . "\n"; - echo ' actual: ' . var_export( $actual, true ) . "\n"; -} - -echo "webhook auth v2 migration smoke\n"; -echo "--------------------------------\n"; - -$wpdb = new WebhookAuthV2MigrationWpdb(); -$wpdb->rows['wp_datamachine_flows'] = array( - array( - 'flow_id' => 10, - 'scheduling_config' => wp_json_encode( - array( - 'webhook_enabled' => true, - 'webhook_auth_mode' => 'hmac_sha256', - 'webhook_signature_header' => 'X-Hub-Signature-256', - 'webhook_signature_format' => 'sha256=hex', - 'webhook_secret' => 'legacy-secret', - ) - ), - ), - array( - 'flow_id' => 11, - 'scheduling_config' => wp_json_encode( - array( - 'webhook_enabled' => true, - 'webhook_auth_mode' => 'hmac', - 'webhook_auth' => array( 'mode' => 'hmac' ), - 'webhook_secrets' => array( array( 'id' => 'current', 'value' => 'canonical' ) ), - 'webhook_signature_header' => 'stale', - 'webhook_secret' => 'stale', - ) - ), - ), - array( - 'flow_id' => 12, - 'scheduling_config' => wp_json_encode( - array( - 'webhook_enabled' => true, - 'webhook_auth_mode' => 'hmac', - 'webhook_auth' => array( - 'mode' => 'hmac', - 'signed_template' => '{body}', - 'signature_source' => array( - 'header' => 'X-Sig', - 'extract' => array( 'kind' => 'raw' ), - 'encoding' => 'base64', - ), - ), - 'webhook_secrets' => array( array( 'id' => 'current', 'value' => 'canon' ) ), - ) - ), - ), - array( - 'flow_id' => 13, - 'scheduling_config' => wp_json_encode( array() ), - ), - array( - 'flow_id' => 14, - 'scheduling_config' => '{not-json', - ), -); - -datamachine_migrate_webhook_auth_v2(); - -$legacy = json_decode( $wpdb->rows['wp_datamachine_flows'][0]['scheduling_config'], true ); -$orphaned = json_decode( $wpdb->rows['wp_datamachine_flows'][1]['scheduling_config'], true ); -$canon = json_decode( $wpdb->rows['wp_datamachine_flows'][2]['scheduling_config'], true ); -$empty = json_decode( $wpdb->rows['wp_datamachine_flows'][3]['scheduling_config'], true ); - -assert_equals( true, get_option( 'datamachine_webhook_auth_v2_migrated' ), 'migration gate set', $failures, $passes ); -assert_equals( 'hmac', $legacy['webhook_auth_mode'] ?? null, 'legacy hmac_sha256 row converted to hmac', $failures, $passes ); -assert_equals( '{body}', $legacy['webhook_auth']['signed_template'] ?? null, 'legacy row gets v2 signed template', $failures, $passes ); -assert_equals( 'X-Hub-Signature-256', $legacy['webhook_auth']['signature_source']['header'] ?? null, 'legacy header preserved', $failures, $passes ); -assert_equals( 'prefix', $legacy['webhook_auth']['signature_source']['extract']['kind'] ?? null, 'legacy sha256 prefix extraction preserved', $failures, $passes ); -assert_equals( 'sha256=', $legacy['webhook_auth']['signature_source']['extract']['key'] ?? null, 'legacy sha256 prefix key preserved', $failures, $passes ); -assert_equals( 'hex', $legacy['webhook_auth']['signature_source']['encoding'] ?? null, 'legacy encoding preserved', $failures, $passes ); -assert_equals( 'current', $legacy['webhook_secrets'][0]['id'] ?? null, 'legacy singular secret promoted to roster id', $failures, $passes ); -assert_equals( 'legacy-secret', $legacy['webhook_secrets'][0]['value'] ?? null, 'legacy singular secret promoted to roster value', $failures, $passes ); -assert_equals( false, array_key_exists( 'webhook_signature_header', $legacy ), 'legacy header removed', $failures, $passes ); -assert_equals( false, array_key_exists( 'webhook_signature_format', $legacy ), 'legacy format removed', $failures, $passes ); -assert_equals( false, array_key_exists( 'webhook_secret', $legacy ), 'legacy secret removed', $failures, $passes ); - -assert_equals( 'hmac', $orphaned['webhook_auth_mode'] ?? null, 'orphaned row keeps canonical mode', $failures, $passes ); -assert_equals( 'canonical', $orphaned['webhook_secrets'][0]['value'] ?? null, 'orphaned row preserves canonical roster', $failures, $passes ); -assert_equals( false, array_key_exists( 'webhook_signature_header', $orphaned ), 'orphaned header removed', $failures, $passes ); -assert_equals( false, array_key_exists( 'webhook_secret', $orphaned ), 'orphaned secret removed', $failures, $passes ); -assert_equals( 'base64', $canon['webhook_auth']['signature_source']['encoding'] ?? null, 'already-canonical row unchanged', $failures, $passes ); -assert_equals( array(), $empty, 'empty scheduling config unchanged', $failures, $passes ); -assert_equals( 2, count( $GLOBALS['__webhook_auth_v2_migration_update_formats'] ?? array() ), 'only changed rows persisted', $failures, $passes ); - -datamachine_migrate_webhook_auth_v2(); -assert_equals( 2, count( $GLOBALS['__webhook_auth_v2_migration_update_formats'] ?? array() ), 'second run is gated and does not persist rows again', $failures, $passes ); - -echo "\n--------------------------------\n"; -$total = $passes + count( $failures ); -echo "{$passes} / {$total} passed\n"; - -if ( ! empty( $failures ) ) { - echo "\nFailures:\n"; - foreach ( $failures as $failure ) { - echo " - {$failure}\n"; - } - exit( 1 ); -} - -echo "\nAll assertions passed.\n"; diff --git a/tests/wp-ai-client-provider-admin-smoke.php b/tests/wp-ai-client-provider-admin-smoke.php index 9a055dda4..f40aebd4e 100644 --- a/tests/wp-ai-client-provider-admin-smoke.php +++ b/tests/wp-ai-client-provider-admin-smoke.php @@ -161,7 +161,6 @@ public static function modelMetadataDirectory(): ModelDirectoryDouble { namespace { require_once __DIR__ . '/../inc/Engine/AI/WpAiClientProviderAdmin.php'; - require_once __DIR__ . '/../inc/migrations/ai-provider-keys.php'; use DataMachine\Engine\AI\WpAiClientProviderAdmin; @@ -198,18 +197,6 @@ public static function modelMetadataDirectory(): ModelDirectoryDouble { $assert( 'sk-new-secret' === get_option( 'connectors_ai_openai_api_key' ), 'settings update writes unmasked key to connector option' ); $assert( '' === get_option( 'connectors_ai_gemini_api_key', '' ), 'settings update ignores masked round-trip value' ); - $GLOBALS['datamachine_provider_admin_site_options']['chubes_ai_http_shared_api_keys'] = serialize( - array( - 'openai' => 'legacy-openai', - 'gemini' => 'legacy-gemini', - ) - ); - $GLOBALS['datamachine_provider_admin_options']['connectors_ai_openai_api_key'] = 'already-present'; - datamachine_migrate_ai_provider_keys_to_connectors(); - $assert( 'already-present' === get_option( 'connectors_ai_openai_api_key' ), 'migration preserves existing connector key' ); - $assert( 'legacy-gemini' === get_option( 'connectors_ai_gemini_api_key' ), 'migration copies missing serialized legacy key' ); - $assert( true === get_option( 'datamachine_ai_provider_keys_migrated' ), 'migration records completion flag' ); - $production_files = array( 'inc/Api/Providers.php', 'inc/Api/Chat/Chat.php', diff --git a/uninstall.php b/uninstall.php index 094dd1c10..2a54a30ac 100644 --- a/uninstall.php +++ b/uninstall.php @@ -47,27 +47,6 @@ function datamachine_uninstall_site() { // Unified auth data. delete_option( 'datamachine_auth_data' ); - // Legacy per-provider auth data. - $datamachine_auth_providers = array( 'twitter', 'facebook', 'threads', 'googlesheets', 'reddit', 'bluesky', 'wordpress_publish', 'wordpress_posts' ); - foreach ( $datamachine_auth_providers as $datamachine_provider ) { - delete_option( "{$datamachine_provider}_auth_data" ); - } - - // Legacy meta/key options. - delete_option( 'datamachine_openai_api_key' ); - delete_option( 'datamachine_openai_user_meta' ); - delete_option( 'datamachine_bluesky_user_meta' ); - delete_option( 'datamachine_twitter_user_meta' ); - delete_option( 'datamachine_reddit_user_meta' ); - delete_option( 'datamachine_threads_user_meta' ); - delete_option( 'datamachine_facebook_user_meta' ); - - // Log level options. - $datamachine_log_types = array( 'pipeline', 'system', 'chat' ); - foreach ( $datamachine_log_types as $datamachine_log_type ) { - delete_option( "datamachine_log_level_{$datamachine_log_type}" ); - } - // --- User meta --- $datamachine_pattern = 'datamachine_%'; From 0d2e915846913adf7376f2c62626473bff6a1278 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Tue, 5 May 2026 11:23:26 -0400 Subject: [PATCH 2/6] Drop bounded pre-1.0 runtime shims Removes the bounded compatibility branches that only existed to clean up state from older Data Machine versions. None of these are public contracts. SystemAgentServiceProvider: drop LEGACY_DAILY_MEMORY_HOOK, LEGACY_TASK_HANDLE_HOOK, and LEGACY_RETENTION_HOOKS plus the manageRecurringTaskSchedules() Action Scheduler unschedule pass that swept them on every request. OAuth2Handler::verify_state: drop the plain-string transient fallback that handled in-flight OAuth states from the deploy window between transient formats. Structured array is the only shape now. TaskScheduler::resolveParentJobId: drop the pre-0.82.0 batch_id string lookup path. BatchScheduler always passes the parent job ID; numeric strings from Action Scheduler are still accepted, the rest is dropped. summarizeBatchJob also drops the legacy 'total' fallback. Chat::ensure_mode_column: drop the rename branch for legacy 'context' / 'agent_type' columns and the legacy index drops. Mode column is created fresh when missing. WebhookAuthResolver / WebhookTrigger: drop the doc references to v1 hmac_sha256 + webhook_signature_* + webhook_secret normalization that the deleted webhook-auth-v2 migration handled. Tests updated to assert these branches are gone instead of exercising them. --- inc/Api/WebhookAuthResolver.php | 4 -- inc/Api/WebhookTrigger.php | 7 ++- inc/Core/Database/Chat/Chat.php | 29 +++------- inc/Core/OAuth/OAuth2Handler.php | 16 ------ .../AI/System/SystemAgentServiceProvider.php | 54 ++----------------- inc/Engine/Tasks/TaskScheduler.php | 44 +++------------ .../System/SystemAgentServiceProviderTest.php | 8 +-- .../Core/OAuth/OAuth2HandlerStateTest.php | 36 ------------- tests/retention-system-tasks-smoke.php | 6 +-- ...task-scheduler-batch-parent-link-smoke.php | 9 ++-- 10 files changed, 27 insertions(+), 186 deletions(-) diff --git a/inc/Api/WebhookAuthResolver.php b/inc/Api/WebhookAuthResolver.php index c764f00bc..f2b4e6502 100644 --- a/inc/Api/WebhookAuthResolver.php +++ b/inc/Api/WebhookAuthResolver.php @@ -9,10 +9,6 @@ * scheduling_config[ 'webhook_auth' ] =