feat(classifier): label 'drop' via hard-rule + score threshold (-5 default)
- Add DEFAULT_THRESHOLDS['drop'] = -5 and DEFAULT_HARD_DROP_RULES const
- Add evaluate_hard_drop(): fires when no positive signals + short latin name OR ≥3 placeholder fields
- Wire into classify(): drop overrides all other labels; synthetic reason explains trigger
- Add drop_reason_label() and 'drop' => '⛔ авто-дроп' to MessageFormatter::LABEL_MAP
- Update ClassifierTest/MessageFormatterTest to extend Cf7stgTestCase (filter isolation)
- Adjust pre-existing assertions that now correctly yield 'drop' instead of 'spam'
This commit is contained in:
@@ -156,4 +156,131 @@ final class ClassifierDropTest extends Cf7stgTestCase
|
||||
$fields = ['a' => 'Ия', 'b' => 'a b', 'c' => 'XY'];
|
||||
self::assertSame(1, Classifier::count_placeholder_fields($fields));
|
||||
}
|
||||
|
||||
public function test_screenshot_1_yields_drop(): void
|
||||
{
|
||||
$fields = require __DIR__ . '/fixtures/spam-screenshot-1.php';
|
||||
$r = Classifier::classify($fields, ['reason' => 'Akismet']);
|
||||
self::assertSame('drop', $r['label']);
|
||||
}
|
||||
|
||||
public function test_screenshot_2_yields_drop(): void
|
||||
{
|
||||
$fields = require __DIR__ . '/fixtures/spam-screenshot-2.php';
|
||||
$r = Classifier::classify($fields, ['reason' => 'Akismet']);
|
||||
self::assertSame('drop', $r['label']);
|
||||
}
|
||||
|
||||
public function test_screenshot_3_yields_drop(): void
|
||||
{
|
||||
$fields = require __DIR__ . '/fixtures/spam-screenshot-3.php';
|
||||
$r = Classifier::classify($fields, ['reason' => 'Akismet']);
|
||||
self::assertSame('drop', $r['label']);
|
||||
}
|
||||
|
||||
public function test_drop_skipped_when_cyrillic_name_present(): void
|
||||
{
|
||||
$fields = [
|
||||
'your-name' => 'Иван',
|
||||
'text-1' => 'AB', 'text-2' => 'CD', 'text-3' => 'EF',
|
||||
];
|
||||
$r = Classifier::classify($fields, ['reason' => 'Akismet']);
|
||||
self::assertNotSame('drop', $r['label']);
|
||||
}
|
||||
|
||||
public function test_drop_skipped_when_meaningful_text_present(): void
|
||||
{
|
||||
$fields = [
|
||||
'your-name' => 'AB',
|
||||
'your-message' => 'Здравствуйте, нужна уборка квартиры в субботу с 10 утра',
|
||||
'text-1' => 'AB', 'text-2' => 'CD', 'text-3' => 'EF',
|
||||
];
|
||||
$r = Classifier::classify($fields, ['reason' => 'Akismet']);
|
||||
self::assertNotSame('drop', $r['label']);
|
||||
}
|
||||
|
||||
/** @dataProvider valid_email_provider */
|
||||
public function test_drop_skipped_when_valid_email_present(string $email): void
|
||||
{
|
||||
$fields = [
|
||||
'your-name' => 'AB',
|
||||
'your-email' => $email,
|
||||
'text-1' => 'AB', 'text-2' => 'CD', 'text-3' => 'EF',
|
||||
];
|
||||
$r = Classifier::classify($fields, ['reason' => 'Akismet']);
|
||||
self::assertNotSame('drop', $r['label']);
|
||||
}
|
||||
|
||||
public function valid_email_provider(): array
|
||||
{
|
||||
return [
|
||||
'gmail' => ['pasha@gmail.com'],
|
||||
'doménный' => ['info@some-domain.com'],
|
||||
'yandex' => ['name@yandex.ru'],
|
||||
];
|
||||
}
|
||||
|
||||
public function test_drop_invalid_email_does_not_protect(): void
|
||||
{
|
||||
$fields = [
|
||||
'your-name' => 'OB',
|
||||
'email' => 'not-an-email',
|
||||
'text-1' => 'AB', 'text-2' => 'CD', 'text-3' => 'EF',
|
||||
];
|
||||
$r = Classifier::classify($fields, ['reason' => 'Akismet']);
|
||||
self::assertSame('drop', $r['label']);
|
||||
}
|
||||
|
||||
public function test_drop_skipped_on_form_without_name_when_no_placeholder_rule(): void
|
||||
{
|
||||
// Форма «обратный звонок»: только телефон, имя нет, < 3 placeholder
|
||||
$fields = ['tel-1' => '+79991234567'];
|
||||
$r = Classifier::classify($fields, ['reason' => 'Akismet']);
|
||||
self::assertNotSame('drop', $r['label']);
|
||||
}
|
||||
|
||||
public function test_drop_triggers_on_form_without_name_via_placeholder_rule(): void
|
||||
{
|
||||
$fields = ['a' => 'AB', 'b' => 'CD', 'c' => 'EF', 'd' => 'GH'];
|
||||
$r = Classifier::classify($fields, ['reason' => 'Akismet']);
|
||||
self::assertSame('drop', $r['label']);
|
||||
}
|
||||
|
||||
public function test_score_drop_threshold_minus_5(): void
|
||||
{
|
||||
// keyword_cap (-6) + latin_only (-1) = -7, без позитивных → score-based drop
|
||||
// Имя длинное (Melissa Smith), сообщение latin_only → НЕ hard-rule, чисто по score.
|
||||
$fields = [
|
||||
'your-name' => 'Melissa Smith',
|
||||
'your-message' => 'SEO, backlinks, crypto, casino, займ, bitcoin, viagra, porno',
|
||||
];
|
||||
$r = Classifier::classify($fields, ['reason' => 'Akismet']);
|
||||
self::assertLessThanOrEqual(-5, $r['score']);
|
||||
self::assertSame('drop', $r['label']);
|
||||
}
|
||||
|
||||
public function test_filter_disables_short_latin_name_rule(): void
|
||||
{
|
||||
add_filter('cf7stg_hard_drop_rules', static function ($rules) {
|
||||
$rules['short_latin_name'] = false;
|
||||
return $rules;
|
||||
});
|
||||
// Только короткое имя, < 3 placeholder, нет позитивных → правило отключено → не drop
|
||||
$fields = ['your-name' => 'OB', 'tel' => '+79991234567'];
|
||||
$r = Classifier::classify($fields, ['reason' => 'Akismet']);
|
||||
self::assertNotSame('drop', $r['label']);
|
||||
}
|
||||
|
||||
public function test_filter_thresholds_can_change_drop_score(): void
|
||||
{
|
||||
add_filter('cf7stg_classifier_thresholds', static function ($t) {
|
||||
$t['drop'] = -3;
|
||||
return $t;
|
||||
});
|
||||
// URL даёт -3, нет hard-rule (нет короткого латинского имени, нет 3+ placeholder),
|
||||
// но -3 ≤ -3 → drop по score-порогу.
|
||||
$fields = ['msg' => 'http://x.com'];
|
||||
$r = Classifier::classify($fields, ['reason' => 'Akismet']);
|
||||
self::assertSame('drop', $r['label']);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user