homelab-codex-ws/jobs/mail-body-ingest/tests/test_ingest.py
oskar fc5c698898 fix(mail-body-ingest): html_to_text loses entire body on unclosed void elements
The naive LIFO tag stack pushed every start tag and popped on any end tag by
position, not name. Real email HTML almost always writes void elements
(<meta>, <br>, <img>, ...) without a self-closing slash -- e.g. <head><meta
charset=...><meta name=viewport ...></head> pops a "meta" frame for the
literal </head>, leaving skip_depth stuck at 1 for the rest of the document.

Caught live during the faza-mailowa Etap A dry-run spot-check (plan §7,
Krok 4 calibration step) against real archived mail: a genuine HTML-only
promotional email extracted to '' entirely. Re-running the Etap A dry-run
after the fix dropped body_empty from 3253/13300 (24.5%) to 291/13300
(2.2%) -- the bug was silently discarding real content from a meaningful
slice of HTML-only mail.

Fix: void elements are never pushed onto the stack (so they can never
desync it); every other closing tag searches backward for its matching
open tag and truncates the stack from there, which also self-heals other
malformed nesting instead of only the void-tag case. 3 new regression
tests reproduce the exact <head><meta>...</head> pattern, br/hr line
breaks, and a stray unmatched closing tag.
2026-07-23 17:06:49 +02:00

601 lines
25 KiB
Python

"""Unit tests for the mail body ingest job — no DB, no real HTTP, no real Ollama.
Fixture .eml bytes are built inline (email.mime helpers / raw byte literals), matching the
convention in jobs/gmail-header-backfill/tests/test_backfill.py rather than separate fixture
files -- easier to see exactly what each test is asserting against."""
from __future__ import annotations
import json
from datetime import datetime, timezone
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import aiohttp
import pytest
from kb_retrieval.embed import EmbeddingDimensionError
from mail_body_ingest.ingest import (
_CHUNK_INSERT_SQL,
_decode_jsonb,
_has_threading,
build_prefix,
extract_body,
extract_threading,
fetch_envelopes,
fetch_existing_chunk_keys,
html_to_text,
insert_chunk,
is_newsletter,
parse_message,
run,
strip_quotes,
)
def _plain_eml(headers: dict, body: str) -> bytes:
msg = MIMEText(body, "plain", "utf-8")
for k, v in headers.items():
msg[k] = v
return msg.as_bytes()
def _html_eml(headers: dict, html_body: str) -> bytes:
msg = MIMEText(html_body, "html", "utf-8")
for k, v in headers.items():
msg[k] = v
return msg.as_bytes()
def _multipart_with_attachment_eml(headers: dict, body: str) -> bytes:
msg = MIMEMultipart()
for k, v in headers.items():
msg[k] = v
msg.attach(MIMEText(body, "plain", "utf-8"))
attachment = MIMEApplication(b"%PDF-fake", Name="doc.pdf")
attachment["Content-Disposition"] = 'attachment; filename="doc.pdf"'
msg.attach(attachment)
return msg.as_bytes()
# Pattern 1 from the gmail-header-backfill diagnosis (2026-07-14): RFC 2047 encoded-word whose
# decoded text contains a newline in a display name -- policy.default raises ValueError.
_CRLF_ENCODED_WORD_EML = (
b"From: Rekrutacja z =?utf-8?Q?ExampleCorp=0A?= <mailing@example.pl>\r\n"
b"To: Jan Kowalski <jan@example.com>\r\n"
b"Subject: Nowe oferty\r\n"
b"Date: Mon, 15 Sep 2014 13:24:18 +0000\r\n"
b"\r\nbody"
)
class TestStripQuotes:
def test_no_marker_no_quotes_returns_unchanged(self):
clean, stripped = strip_quotes("Hello,\n\nJust checking in.\n\nRegards,\nA")
assert clean == "Hello,\n\nJust checking in.\n\nRegards,\nA"
assert stripped == 0
def test_gmail_wrote_marker_truncates_everything_after(self):
text = "Sure, sounds good.\n\nOn Tue, 10 Jun 2025 at 12:00, Alice <a@b.com> wrote:\n> old text\n> more old"
clean, stripped = strip_quotes(text)
assert clean == "Sure, sounds good."
assert stripped > 0
def test_polish_dnia_napisal_marker(self):
text = "OK, dzieki.\n\nDnia 10 czerwca 2025 Jan napisał(a):\n> stara tresc"
clean, _ = strip_quotes(text)
assert clean == "OK, dzieki."
def test_polish_w_dniu_pisze_marker(self):
text = "Dobrze.\n\nW dniu 10.06.2025 Anna pisze:\n> cos tam"
clean, _ = strip_quotes(text)
assert clean == "Dobrze."
def test_outlook_original_message_marker(self):
text = "Ok, dzieki za info.\n\n-----Original Message-----\nFrom: bob@example.com\nold content"
clean, _ = strip_quotes(text)
assert clean == "Ok, dzieki za info."
def test_outlook_underscore_separator(self):
text = "Widziane, dzieki.\n\n________________________________\nFrom: c@example.com\nstuff"
clean, _ = strip_quotes(text)
assert clean == "Widziane, dzieki."
def test_bare_quote_lines_without_marker_are_dropped(self):
text = "My reply here.\n> quoted line one\n> quoted line two\nNot quoted trailing line"
clean, stripped = strip_quotes(text)
assert ">" not in clean
assert "My reply here." in clean
assert stripped > 0
def test_chars_stripped_counts_removed_characters(self):
text = "short reply\n\nOn 1 Jan 2020 X wrote:\n> a very long quoted paragraph indeed"
_clean, stripped = strip_quotes(text)
assert stripped == len(text) - len("short reply")
class TestHtmlToText:
def test_skips_style_and_script(self):
html = "<html><head><style>.x{color:red}</style></head><body><script>evil()</script><p>Real text</p></body></html>"
assert "color:red" not in html_to_text(html)
assert "evil()" not in html_to_text(html)
assert "Real text" in html_to_text(html)
def test_skips_blockquote_subtree(self):
html = "<div>Reply text</div><blockquote><div>quoted old text</div></blockquote>"
text = html_to_text(html)
assert "Reply text" in text
assert "quoted old text" not in text
def test_skips_gmail_quote_div(self):
html = (
'<div>My reply</div>'
'<div class="gmail_quote">On Tue, X wrote:<div>nested quoted content</div></div>'
)
text = html_to_text(html)
assert "My reply" in text
assert "nested quoted content" not in text
def test_plain_div_without_gmail_quote_class_is_not_skipped(self):
html = '<div class="not_a_quote">Still visible</div>'
assert "Still visible" in html_to_text(html)
def test_unclosed_void_elements_in_head_do_not_blank_the_rest_of_the_document(self):
# Regression: a naive LIFO stack pops the wrong "frame" for the literal </head> when
# <meta> tags inside <head> are written without a self-closing slash (the overwhelming
# majority of real-world email HTML) -- confirmed live against an Etap A pilot mail
# where this bug silently blanked the entire body to ''.
html = (
"<!DOCTYPE html><html><head>"
'<meta charset="utf-8"><meta name="viewport" content="width=device-width">'
"<title>promo</title>"
"</head><body><p>Kod rabatowy dla Ciebie!</p></body></html>"
)
assert "Kod rabatowy dla Ciebie!" in html_to_text(html)
def test_br_and_hr_void_elements_still_produce_line_breaks(self):
html = "<p>line one<br>line two</p><hr><p>line three</p>"
text = html_to_text(html)
assert "line one" in text and "line two" in text and "line three" in text
def test_stray_unmatched_closing_tag_is_ignored_not_fatal(self):
html = "<p>hello</blockquote><p>world</p>"
text = html_to_text(html)
assert "hello" in text and "world" in text
class TestIsNewsletter:
def test_list_unsubscribe_present(self):
msg = parse_message(_plain_eml({"From": "a@b.com", "List-Unsubscribe": "<mailto:x@y.com>"}, "hi"))
assert is_newsletter(msg) is True
def test_list_id_present(self):
msg = parse_message(_plain_eml({"From": "a@b.com", "List-Id": "newsletter.example.com"}, "hi"))
assert is_newsletter(msg) is True
def test_precedence_bulk(self):
msg = parse_message(_plain_eml({"From": "a@b.com", "Precedence": "bulk"}, "hi"))
assert is_newsletter(msg) is True
def test_precedence_list_case_insensitive(self):
msg = parse_message(_plain_eml({"From": "a@b.com", "Precedence": "LIST"}, "hi"))
assert is_newsletter(msg) is True
def test_ordinary_mail_is_not_newsletter(self):
msg = parse_message(_plain_eml({"From": "a@b.com", "Subject": "hi"}, "hi"))
assert is_newsletter(msg) is False
class TestExtractThreading:
def test_in_reply_to_and_references_stripped_of_brackets(self):
msg = parse_message(_plain_eml(
{"From": "a@b.com", "In-Reply-To": "<msg1@x.com>",
"References": "<msg0@x.com> <msg1@x.com>"}, "hi",
))
threading = extract_threading(msg)
assert threading == {
"type": "threading", "in_reply_to": "msg1@x.com",
"references": ["msg0@x.com", "msg1@x.com"],
}
def test_missing_headers_yield_none_and_empty_list(self):
msg = parse_message(_plain_eml({"From": "a@b.com"}, "hi"))
threading = extract_threading(msg)
assert threading == {"type": "threading", "in_reply_to": None, "references": []}
class TestBuildPrefix:
def test_full_headers(self):
entities = [{"type": "headers", "subject": "Twoja polisa", "from": {"name": "PZU", "address": "x@pzu.pl"}}]
ts = datetime(2025, 8, 3, tzinfo=timezone.utc)
assert build_prefix(entities, ts) == "Temat: Twoja polisa | Od: PZU | Data: 2025-08-03"
def test_missing_subject_and_from_fallback(self):
entities = [{"type": "headers", "subject": None, "from": None}]
ts = datetime(2025, 1, 1, tzinfo=timezone.utc)
prefix = build_prefix(entities, ts)
assert "(brak tematu)" in prefix
assert "Od: ?" in prefix
def test_from_without_name_uses_address(self):
entities = [{"type": "headers", "subject": "s", "from": {"name": None, "address": "a@b.com"}}]
ts = datetime(2025, 1, 1, tzinfo=timezone.utc)
assert "Od: a@b.com" in build_prefix(entities, ts)
def test_no_headers_entity_at_all(self):
ts = datetime(2025, 1, 1, tzinfo=timezone.utc)
prefix = build_prefix([], ts)
assert "(brak tematu)" in prefix
assert "Od: ?" in prefix
class TestExtractBody:
def test_plain_text_preferred(self):
msg = parse_message(_plain_eml({"From": "a@b.com"}, "hello plain body"))
assert extract_body(msg) == "hello plain body"
def test_html_only_falls_back_to_html_to_text(self):
msg = parse_message(_html_eml({"From": "a@b.com"}, "<p>hello <b>html</b> body</p>"))
body = extract_body(msg)
assert "hello" in body and "html" in body and "body" in body
assert "<" not in body
def test_attachment_only_yields_empty_body(self):
msg = MIMEApplication(b"%PDF-fake", Name="doc.pdf")
msg["From"] = "a@b.com"
assert extract_body(msg) == ""
def test_multipart_with_attachment_extracts_only_the_text_part(self):
msg = parse_message(_multipart_with_attachment_eml({"From": "a@b.com"}, "the actual message"))
assert extract_body(msg) == "the actual message"
class TestParseMessage:
def test_typed_parse_succeeds_for_ordinary_mail(self):
msg = parse_message(_plain_eml({"From": "a@b.com", "Subject": "hi"}, "body"))
assert msg.get("Subject") == "hi"
def test_falls_back_to_compat32_on_typed_parse_failure(self):
# This exact byte pattern raises ValueError under policy.default (see backfill's
# diagnosis) -- parse_message must still return a usable Message via the fallback.
msg = parse_message(_CRLF_ENCODED_WORD_EML)
assert msg.get("Subject") is not None
class TestDecodeJsonbAndThreadingHelpers:
def test_decode_jsonb_handles_str_and_object(self):
assert _decode_jsonb('[{"type": "headers"}]') == [{"type": "headers"}]
assert _decode_jsonb([{"type": "headers"}]) == [{"type": "headers"}]
assert _decode_jsonb(None) is None
def test_has_threading(self):
assert _has_threading([{"type": "threading"}]) is True
assert _has_threading([{"type": "headers"}]) is False
assert _has_threading([]) is False
class TestInsertSql:
def test_on_conflict_target_includes_model(self):
assert "ON CONFLICT (envelope_id, chunk_index, model)" in _CHUNK_INSERT_SQL
class TestInsertChunk:
async def test_newsletter_chunk_inserts_null_embedding_with_reason(self):
conn = _FakeConn()
await insert_chunk(conn, "msgid@x", 0, "junk text", None, "bge-m3", excluded_reason="newsletter")
assert conn.execute_calls == [("msgid@x", 0, "junk text", None, "bge-m3", "newsletter")]
async def test_normal_chunk_inserts_vector_with_no_reason(self):
conn = _FakeConn()
await insert_chunk(conn, "msgid@x", 0, "real text", [0.1, 0.2], "bge-m3")
assert conn.execute_calls == [("msgid@x", 0, "real text", "[0.1,0.2]", "bge-m3", None)]
class TestFetchHelpers:
async def test_fetch_envelopes_no_filters(self):
conn = _FakeConn(envelopes=[_env_row("m1@x", "hi")])
rows = await fetch_envelopes(conn, since=None, limit=None, offset=None)
assert rows == [_env_row("m1@x", "hi")]
query, params = conn.queries[-1]
assert "ORDER BY id" in query
assert "LIMIT" not in query and "OFFSET" not in query
async def test_fetch_envelopes_with_since_limit_offset(self):
conn = _FakeConn(envelopes=[])
await fetch_envelopes(conn, since=datetime(2025, 7, 1, tzinfo=timezone.utc), limit=10, offset=5)
query, params = conn.queries[-1]
assert "ts >= $1" in query
assert "LIMIT $2" in query
assert "OFFSET $3" in query
assert params == (datetime(2025, 7, 1, tzinfo=timezone.utc), 10, 5)
async def test_fetch_existing_chunk_keys(self):
conn = _FakeConn(existing_keys=[("m1@x", 0), ("m1@x", 1)])
keys = await fetch_existing_chunk_keys(conn, model="bge-m3")
assert keys == {("m1@x", 0), ("m1@x", 1)}
def _env_row(envelope_id: str, subject: str, ts: datetime = datetime(2025, 8, 1, tzinfo=timezone.utc)):
entities = [{"type": "headers", "subject": subject, "from": {"name": "A", "address": "a@b.com"}}]
return {"id": envelope_id, "ts": ts, "raw_ref": f"gmail/2025/08/{envelope_id}.eml", "entities": json.dumps(entities)}
class _FakeConn:
"""Mirrors documents_ingest's/backfill's `_FakeConn` test style. `existing_keys` were
"written" under `existing_model` -- a fetch for a different model must not see them,
same as the real `WHERE model = $1` filter."""
def __init__(self, envelopes=None, existing_keys=None, existing_model="bge-m3", execute_results=None):
self._envelopes = envelopes or []
self._existing_keys = list(existing_keys or [])
self._existing_model = existing_model
self._execute_results = list(execute_results) if execute_results is not None else None
self.execute_calls: list[tuple] = []
self.executemany_calls: list[tuple] = []
self.queries: list[tuple] = []
async def fetch(self, query, *params):
self.queries.append((query, params))
if "FROM document_chunk" in query:
if params and params[0] != self._existing_model:
return []
return [{"envelope_id": eid, "chunk_index": idx} for eid, idx in self._existing_keys]
if "FROM envelope" in query:
return self._envelopes
raise AssertionError(f"unexpected query: {query}")
async def execute(self, query, *params):
self.execute_calls.append(params)
if self._execute_results is not None:
return self._execute_results.pop(0)
return "INSERT 0 1"
async def executemany(self, query, rows):
self.executemany_calls.append((query, list(rows)))
async def close(self):
pass
class _FakeEmbedResponse:
def __init__(self, payload, status=200):
self._payload = payload
self._status = status
async def __aenter__(self):
return self
async def __aexit__(self, *exc):
return False
async def json(self):
return self._payload
def raise_for_status(self):
if self._status >= 400:
raise aiohttp.ClientConnectionError(f"simulated HTTP {self._status}")
class _FakeTagsResponse:
def __init__(self, status=200):
self.status = status
async def __aenter__(self):
return self
async def __aexit__(self, *exc):
return False
class _FakeOllamaSession:
def __init__(self, dim=1024, fail_batches=False, health_up=True):
self._dim = dim
self._fail_batches = fail_batches
self._health_up = health_up
self.requests: list[dict] = []
self.closed = False
def post(self, url, json):
assert url.endswith("/api/embed")
self.requests.append({"url": url, "json": json})
if self._fail_batches:
return _FakeEmbedResponse({}, status=500)
n = len(json["input"])
return _FakeEmbedResponse({"embeddings": [[0.01] * self._dim for _ in range(n)]})
def get(self, url, timeout=None):
return _FakeTagsResponse(200 if self._health_up else 500)
async def close(self):
self.closed = True
def _write_eml(archive_root, rel_path: str, raw: bytes) -> None:
dest = archive_root / rel_path
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_bytes(raw)
class TestRun:
def _patch(self, monkeypatch, conn, ollama_session):
async def _fake_connect(dsn):
return conn
monkeypatch.setattr("mail_body_ingest.ingest.asyncpg.connect", _fake_connect)
def _fake_session_factory(*args, **kwargs):
return ollama_session
monkeypatch.setattr("mail_body_ingest.ingest.aiohttp.ClientSession", _fake_session_factory)
async def test_dry_run_counts_without_calling_ollama_or_db(self, tmp_path, monkeypatch):
_write_eml(tmp_path, "gmail/2025/08/m1@x.eml", _plain_eml(
{"From": "a@b.com", "Subject": "hi", "Date": "Fri, 01 Aug 2025 10:00:00 +0000"}, "hello there"
))
conn = _FakeConn(envelopes=[_env_row("m1@x", "hi")])
ollama = _FakeOllamaSession()
self._patch(monkeypatch, conn, ollama)
stats = await run(dsn="postgresql://fake", archive_root=tmp_path, apply=False)
assert stats["mails_scanned"] == 1
assert stats["mails_chunked"] == 1
assert stats["chunks_total"] == 1
assert stats["chunks_inserted"] == 1
assert stats["threading_updated"] == 1 # would-update, not applied
assert ollama.requests == []
assert conn.execute_calls == []
assert conn.executemany_calls == []
async def test_apply_embeds_inserts_and_updates_threading(self, tmp_path, monkeypatch):
_write_eml(tmp_path, "gmail/2025/08/m1@x.eml", _plain_eml(
{"From": "a@b.com", "Subject": "hi", "Date": "Fri, 01 Aug 2025 10:00:00 +0000"}, "hello there"
))
conn = _FakeConn(envelopes=[_env_row("m1@x", "hi")])
ollama = _FakeOllamaSession(dim=1024)
self._patch(monkeypatch, conn, ollama)
stats = await run(dsn="postgresql://fake", archive_root=tmp_path, apply=True)
assert stats["chunks_inserted"] == 1
assert stats["embed_calls"] == 1
assert len(conn.execute_calls) == 1
assert len(ollama.requests) == 1
assert len(conn.executemany_calls) == 1 # threading update batch flushed
async def test_idempotent_skips_already_inserted_chunks(self, tmp_path, monkeypatch):
_write_eml(tmp_path, "gmail/2025/08/m1@x.eml", _plain_eml(
{"From": "a@b.com", "Subject": "hi", "Date": "Fri, 01 Aug 2025 10:00:00 +0000"}, "hello there"
))
conn = _FakeConn(envelopes=[_env_row("m1@x", "hi")], existing_keys=[("m1@x", 0)])
ollama = _FakeOllamaSession(dim=1024)
self._patch(monkeypatch, conn, ollama)
stats = await run(dsn="postgresql://fake", archive_root=tmp_path, apply=True)
assert stats["chunks_already_embedded"] == 1
assert stats["chunks_inserted"] == 0
assert ollama.requests == []
async def test_rerun_after_apply_inserts_nothing_new(self, tmp_path, monkeypatch):
_write_eml(tmp_path, "gmail/2025/08/m1@x.eml", _plain_eml(
{"From": "a@b.com", "Subject": "hi", "Date": "Fri, 01 Aug 2025 10:00:00 +0000"}, "hello there"
))
conn1 = _FakeConn(envelopes=[_env_row("m1@x", "hi")])
ollama1 = _FakeOllamaSession(dim=1024)
self._patch(monkeypatch, conn1, ollama1)
first = await run(dsn="postgresql://fake", archive_root=tmp_path, apply=True)
assert first["chunks_inserted"] == 1
conn2 = _FakeConn(envelopes=[_env_row("m1@x", "hi")], existing_keys=[("m1@x", 0)])
ollama2 = _FakeOllamaSession(dim=1024)
self._patch(monkeypatch, conn2, ollama2)
second = await run(dsn="postgresql://fake", archive_root=tmp_path, apply=True)
assert second["chunks_inserted"] == 0
assert second["chunks_already_embedded"] == 1
assert ollama2.requests == []
async def test_newsletter_chunk_flagged_and_not_embedded(self, tmp_path, monkeypatch):
_write_eml(tmp_path, "gmail/2025/08/nl@x.eml", _plain_eml(
{"From": "a@b.com", "Subject": "Promo", "List-Unsubscribe": "<mailto:x@y.com>",
"Date": "Fri, 01 Aug 2025 10:00:00 +0000"},
"Kup teraz z rabatem!",
))
conn = _FakeConn(envelopes=[_env_row("nl@x", "Promo")])
ollama = _FakeOllamaSession(dim=1024)
self._patch(monkeypatch, conn, ollama)
stats = await run(dsn="postgresql://fake", archive_root=tmp_path, apply=True)
assert stats["chunks_newsletter_flagged"] == 1
assert stats["chunks_inserted"] == 0
assert ollama.requests == [] # no Ollama call for newsletter chunks
assert conn.execute_calls[0][3] is None # embedding column is NULL
assert conn.execute_calls[0][5] == "newsletter"
async def test_body_empty_after_quote_strip_still_updates_threading(self, tmp_path, monkeypatch):
_write_eml(tmp_path, "gmail/2025/08/empty@x.eml", _plain_eml(
{"From": "a@b.com", "Subject": "Re: x", "In-Reply-To": "<orig@x.com>",
"Date": "Fri, 01 Aug 2025 10:00:00 +0000"},
"On Tue wrote:\n> everything is quoted",
))
conn = _FakeConn(envelopes=[_env_row("empty@x", "Re: x")])
ollama = _FakeOllamaSession(dim=1024)
self._patch(monkeypatch, conn, ollama)
stats = await run(dsn="postgresql://fake", archive_root=tmp_path, apply=True)
assert stats["body_empty"] == 1
assert stats["chunks_total"] == 0
assert stats["threading_updated"] == 1
assert len(conn.executemany_calls) == 1
patch = json.loads(conn.executemany_calls[0][1][0][1])
assert patch[0]["in_reply_to"] == "orig@x.com"
async def test_missing_file_counted(self, tmp_path, monkeypatch):
conn = _FakeConn(envelopes=[_env_row("missing@x", "hi")])
ollama = _FakeOllamaSession()
self._patch(monkeypatch, conn, ollama)
stats = await run(dsn="postgresql://fake", archive_root=tmp_path, apply=False)
assert stats["missing_file"] == 1
assert stats["mails_scanned"] == 1
async def test_ollama_offline_batch_error_isolated_not_aborting(self, tmp_path, monkeypatch):
_write_eml(tmp_path, "gmail/2025/08/m1@x.eml", _plain_eml(
{"From": "a@b.com", "Subject": "hi", "Date": "Fri, 01 Aug 2025 10:00:00 +0000"}, "hello there"
))
conn = _FakeConn(envelopes=[_env_row("m1@x", "hi")])
ollama = _FakeOllamaSession(dim=1024, fail_batches=True)
self._patch(monkeypatch, conn, ollama)
stats = await run(dsn="postgresql://fake", archive_root=tmp_path, apply=True)
assert stats["chunks_errors"] == 1
assert stats["chunks_inserted"] == 0
assert stats["chunks_total"] == (
stats["chunks_inserted"] + stats["chunks_newsletter_flagged"] + stats["chunks_already_embedded"]
+ stats["chunks_conflict_skipped"] + stats["chunks_errors"]
)
async def test_dimension_mismatch_aborts(self, tmp_path, monkeypatch):
_write_eml(tmp_path, "gmail/2025/08/m1@x.eml", _plain_eml(
{"From": "a@b.com", "Subject": "hi", "Date": "Fri, 01 Aug 2025 10:00:00 +0000"}, "hello there"
))
conn = _FakeConn(envelopes=[_env_row("m1@x", "hi")])
ollama = _FakeOllamaSession(dim=768) # wrong dim
self._patch(monkeypatch, conn, ollama)
with pytest.raises(EmbeddingDimensionError):
await run(dsn="postgresql://fake", archive_root=tmp_path, apply=True)
async def test_conflict_skipped_counted_separately(self, tmp_path, monkeypatch):
_write_eml(tmp_path, "gmail/2025/08/m1@x.eml", _plain_eml(
{"From": "a@b.com", "Subject": "hi", "Date": "Fri, 01 Aug 2025 10:00:00 +0000"}, "hello there"
))
conn = _FakeConn(envelopes=[_env_row("m1@x", "hi")], execute_results=["INSERT 0 0"])
ollama = _FakeOllamaSession(dim=1024)
self._patch(monkeypatch, conn, ollama)
stats = await run(dsn="postgresql://fake", archive_root=tmp_path, apply=True)
assert stats["chunks_conflict_skipped"] == 1
assert stats["chunks_inserted"] == 0
async def test_threading_already_present_is_skipped(self, tmp_path, monkeypatch):
_write_eml(tmp_path, "gmail/2025/08/m1@x.eml", _plain_eml(
{"From": "a@b.com", "Subject": "hi", "Date": "Fri, 01 Aug 2025 10:00:00 +0000"}, "hello there"
))
row = _env_row("m1@x", "hi")
entities = json.loads(row["entities"]) + [{"type": "threading", "in_reply_to": None, "references": []}]
row["entities"] = json.dumps(entities)
conn = _FakeConn(envelopes=[row])
ollama = _FakeOllamaSession(dim=1024)
self._patch(monkeypatch, conn, ollama)
stats = await run(dsn="postgresql://fake", archive_root=tmp_path, apply=True)
assert stats["threading_already_present"] == 1
assert stats["threading_updated"] == 0
assert conn.executemany_calls == []