From 714b4712adc8ec64ea84b8b745aeb8eb90a3d679 Mon Sep 17 00:00:00 2001 From: Vladimir Bryzgalov Date: Tue, 21 Apr 2026 17:25:20 +0500 Subject: [PATCH] fix: wire is_hard_drop_enabled + drop_score_threshold settings into Classifier filter chain Settings::register_classifier_filters() registers priority-5 callbacks on cf7stg_classifier_thresholds and cf7stg_hard_drop_rules so the UI options actually influence Classifier behaviour. Called from Plugin::boot(). User filters at priority 10 override these defaults as expected. Adds 4 tests covering the wired behaviour and override priority. Also adds dry_run_drop? to MessageFormatter::build() @param docblock. Co-Authored-By: Claude Sonnet 4.6 --- includes/class-message-formatter.php | 3 +- includes/class-plugin.php | 3 ++ includes/class-settings.php | 23 ++++++++++++ tests/ClassifierDropTest.php | 52 ++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 1 deletion(-) diff --git a/includes/class-message-formatter.php b/includes/class-message-formatter.php index 681a915..08ac3ec 100644 --- a/includes/class-message-formatter.php +++ b/includes/class-message-formatter.php @@ -24,7 +24,8 @@ final class MessageFormatter * reason:string, * ip:string, * user_agent:string, - * is_retro:bool + * is_retro:bool, + * dry_run_drop?:bool * } $ctx * * @return array{text:string,reply_markup:array|null} diff --git a/includes/class-plugin.php b/includes/class-plugin.php index 6769099..2b6728f 100644 --- a/includes/class-plugin.php +++ b/includes/class-plugin.php @@ -43,6 +43,9 @@ final class Plugin public function boot(): void { + // Inject silent-drop options into Classifier filter chain + Settings::register_classifier_filters(); + // Register runtime hooks Dispatcher::register_hooks(); (new CF7Source())->register(); diff --git a/includes/class-settings.php b/includes/class-settings.php index b86426d..284739c 100644 --- a/includes/class-settings.php +++ b/includes/class-settings.php @@ -171,4 +171,27 @@ final class Settings 'reset_at' => $today, ]; } + + /** + * Register default callbacks that inject silent-drop options into the + * Classifier's filter chain. Runs at plugin boot. User filters at the + * default priority (10) override these (priority 5). + */ + public static function register_classifier_filters(): void + { + add_filter('cf7stg_classifier_thresholds', static function ($thresholds) { + if (is_array($thresholds)) { + $thresholds['drop'] = self::get_drop_score_threshold(); + } + return $thresholds; + }, 5); + + add_filter('cf7stg_hard_drop_rules', static function ($rules) { + if (is_array($rules) && !self::is_hard_drop_enabled()) { + $rules['short_latin_name'] = false; + $rules['placeholder_fields'] = false; + } + return $rules; + }, 5); + } } diff --git a/tests/ClassifierDropTest.php b/tests/ClassifierDropTest.php index 907dfef..163cc3d 100644 --- a/tests/ClassifierDropTest.php +++ b/tests/ClassifierDropTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Cf7stg\Tests; use Cf7stg\Classifier; +use Cf7stg\Settings; final class ClassifierDropTest extends Cf7stgTestCase { @@ -283,4 +284,55 @@ final class ClassifierDropTest extends Cf7stgTestCase $r = Classifier::classify($fields, ['reason' => 'Akismet']); self::assertSame('drop', $r['label']); } + + public function test_hard_drop_disabled_via_settings_skips_drop(): void + { + Settings::set_hard_drop_enabled(false); + Settings::register_classifier_filters(); + $fields = require __DIR__ . '/fixtures/spam-screenshot-1.php'; + $r = Classifier::classify($fields, ['reason' => 'Akismet']); + // With hard-rule disabled, screenshot-1 has no positive signals AND score isn't ≤ -5 + // (placeholders by themselves give no score penalty), so label should NOT be 'drop'. + self::assertNotSame('drop', $r['label']); + } + + public function test_drop_score_threshold_from_settings_applied(): void + { + Settings::set_drop_score_threshold(-3); + Settings::register_classifier_filters(); + // URL gives -3 score, no positive signals, no hard-rule trigger. + // With threshold lowered to -3, this should fire score-based drop. + $fields = ['msg' => 'http://x.com']; + $r = Classifier::classify($fields, ['reason' => 'Akismet']); + self::assertSame('drop', $r['label']); + } + + public function test_settings_threshold_can_be_overridden_by_user_filter(): void + { + Settings::set_drop_score_threshold(-3); + Settings::register_classifier_filters(); + // User filter at priority 10 should override Settings default at priority 5. + add_filter('cf7stg_classifier_thresholds', static function ($t) { + $t['drop'] = -10; + return $t; + }, 10); + $fields = ['msg' => 'http://x.com']; // score = -3 + $r = Classifier::classify($fields, ['reason' => 'Akismet']); + // -3 is NOT ≤ -10, so no drop + self::assertNotSame('drop', $r['label']); + } + + public function test_filter_disables_placeholder_fields_rule(): void + { + add_filter('cf7stg_hard_drop_rules', static function ($rules) { + $rules['placeholder_fields'] = false; + return $rules; + }); + // Form WITHOUT name field — digit-only values are valid placeholders (^[A-Za-z0-9]{1,2}$) + // but FieldDetector::find_name rejects them (has digits), so short_latin_name cannot fire. + // With placeholder_fields rule also disabled → no hard-rule fires → no drop. + $fields = ['a' => '11', 'b' => '22', 'c' => '33', 'd' => '44']; + $r = Classifier::classify($fields, ['reason' => 'Akismet']); + self::assertNotSame('drop', $r['label']); + } }