diff --git a/changedetectionio/tests/unit/test_content_type_detection.py b/changedetectionio/tests/unit/test_content_type_detection.py
new file mode 100644
index 00000000..e5746bb0
--- /dev/null
+++ b/changedetectionio/tests/unit/test_content_type_detection.py
@@ -0,0 +1,131 @@
+#!/usr/bin/env python3
+# coding=utf-8
+
+# run from dir above changedetectionio/ dir
+# python3 -m unittest changedetectionio.tests.unit.test_content_type_detection
+
+"""Unit tests for guess_stream_type() content-type classification.
+
+Regression cover for #4302: a normal HTML page served as 'text/html;charset=utf-8' was
+classified as plaintext, which made processor.py skip html_to_text() entirely and store the
+raw filtered block as the snapshot (visible as literal '
' separators in every diff).
+
+Two independent failures had to line up:
+ 1. The header test was an exact comparison ("text/html"), so any charset parameter missed it.
+ 2. has_html_patterns only sniffs content[:200], and the page put 241 bytes of HTML comments
+ before is_plaintext.
+"""
+
+import unittest
+
+from changedetectionio.processors.magic import guess_stream_type
+
+# Real-world shape from https://www.newbalancemexico.com/c/lo-mas-nuevo (Salesforce/ISML templates):
+# blank lines and two long comments push \n"
+ "\n\n\n"
+ '\n\n
hi
' + + +class TestGuessStreamType(unittest.TestCase): + + def _flags(self, header, content): + st = guess_stream_type(http_content_header=header, content=content) + return {n: getattr(st, n) for n in + ('is_html', 'is_plaintext', 'is_rss', 'is_xml', 'is_json', 'is_pdf', 'is_csv')} + + def test_preamble_pushes_doctype_past_sniff_window(self): + # Guard the premise of the test below - if this ever shrinks, the regression case is no + # longer exercising the "content sniff cannot help" path. + self.assertGreater(COMMENT_PREAMBLE_HTML.index('\n