homelab-codex-ws/jobs/documents-ingest/tests/test_cyclic_ingest.py

573 lines
26 KiB
Python
Raw Permalink Normal View History

feat(kb): faza 3 krok 5 — cykliczny ingest (systemd timer) + alerting documents-ingest-cyclic (jobs/documents-ingest/src/documents_ingest/cyclic_ingest.py): orkiestruje paperless_adapter -> chunk_embed -> summarize(--backend anthropic, claude-haiku-4-5) -> summarize(--embed-summaries) bez zmian w samych jobach. Ollama@SOLARIA (availability_target: medium) jest tolerowana offline: probe GET /api/tags przed obu etapami embed, brak -> pominięcie, nie fail (oba embed passy idempotentne, nadrobią się na kolejnym ticku). Czwarty etap (embed-summaries) dopisany ponad plan §7.1 (który wymieniał tylko 3 kroki) — bez niego nowe streszczenia miałyby embedding=NULL i byłyby niewidoczne dla cascade_query (bramka kroku 4, WHERE embedding IS NOT NULL); potwierdzone z Oskarem. Predykaty pass/fail każdego etapu 1:1 z exit-checkiem danego joba (chunks_errors, llm_errors, stats-balance itd.) — etapy izolowane, nie fail-fast (wcześniejszy fail nie blokuje kolejnych, tak jak joby izolują błędy per wiersz). Metryki .prom (atomowy zapis, last_success_timestamp trzymany z poprzedniego pliku przy failu) do /opt/homelab/state/node-exporter/kb-ingest.prom. 36 nowych testów (202/202 pakietu). systemd (jobs/documents-ingest/systemd/): pierwszy systemd-timer w repo — kb-ingest.timer (OnCalendar=*-*-* 03:30, Persistent=true, plan §7.1) + kb-ingest.service (host-level, User oskar, EnvironmentFile /opt/homelab/kb/.env) + kb-ingest-run.sh (log per-run do /opt/homelab/logs/kb-ingest/, konwencja repo). Instalacja i sekrety udokumentowane w README (Faza 3 krok 5) — instalacja na PIHA dopiero po merge. fleet-prometheus (rules/kb-ingest.yml): KbIngestStale (>172800s od last_success, critical) + KbEmbedBacklogGrowing (backlog>0 przez 72h, warning) — dostawa istniejącym torem brain-watchdog->Telegram, bez Alertmanagera (konwencja liveness.yml). node_exporter: owner_node vps -> per-host (service.yaml) + wpis + override (--collector.textfile.directory, bez nowego mountu — czyta przez istniejący /:/host:ro) + topology.yaml dla PIHA. Domyka pozycję z docs/backlog.md "stability-agent / node_exporter owner_node single, biegaja wielomiejscowo -> per-host" (połowę — node_exporter; stability-agent zostaje osobnym follow-upem) w ramach paczki B inwentaryzacji monitoringu dla PIHA. Test end-to-end na żywo na PIHA (2× --apply, po potwierdzeniu z Oskarem): pierwszy run złapał 5 dokumentów faktycznie nowych w Paperless (nieoczekiwane, niezwiązane z tym taskiem) -> 82 nowe chunki (2 ocr_junk), 5 nowych streszczeń, 5 embeddingów streszczeń, 0 błędów, metryki zapisane. Drugi run: pełna idempotencja, wszystko 0. ANTHROPIC_API_KEY dodany przez Oskara ręcznie do /opt/homelab/kb/.env (nigdy nie logowany/generowany). Co dalej: prawdziwa instalacja systemd (services.yaml już przygotowany, po merge), zdecydowanie czy stability-agent też idzie na per-host przy okazji. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 15:05:18 +02:00
"""Unit tests for the cyclic ingest wrapper — no DB, no real HTTP, no real
Ollama/Anthropic/Paperless. Every stage function (paperless_adapter.run, chunk_embed.run,
summarize.run_summarize, summarize.run_embed_summaries) and probe_ollama/fetch_embed_backlog
are monkeypatched with fakes; only cyclic_ingest's own orchestration, predicates, and
.prom rendering are under test."""
from __future__ import annotations
import sys
import pytest
from documents_ingest import cyclic_ingest
from documents_ingest.cyclic_ingest import (
PROM_METRIC_HELP,
_adapter_failed,
_chunk_embed_failed,
_embed_summaries_failed,
_read_prev_metric,
_summarize_failed,
build_metrics,
main,
probe_ollama,
render_prom,
run_cyclic,
write_prom_atomic,
)
# ---------------------------------------------------------------------------
# Per-stage failure predicates — pinned against each job's own main() exit check
# ---------------------------------------------------------------------------
class TestAdapterFailed:
def test_balanced_no_errors_passes(self):
stats = {"fetched": 3, "already_in_db": 1, "inserted": 2, "errors": 0}
assert _adapter_failed(stats) is False
def test_any_error_fails(self):
stats = {"fetched": 3, "already_in_db": 1, "inserted": 1, "errors": 1}
assert _adapter_failed(stats) is True
def test_imbalance_fails_even_with_zero_errors(self):
stats = {"fetched": 3, "already_in_db": 1, "inserted": 1, "errors": 0}
assert _adapter_failed(stats) is True
class TestChunkEmbedFailed:
def _stats(self, **overrides):
stats = {
"documents_fetched": 2, "empty_content": 0, "documents_chunked": 2,
"chunks_total": 4, "chunks_already_embedded": 0, "chunks_inserted": 4,
"chunks_junk_flagged": 0, "chunks_conflict_skipped": 0, "chunks_errors": 0,
}
stats.update(overrides)
return stats
def test_balanced_passes(self):
assert _chunk_embed_failed(self._stats()) is False
def test_chunks_errors_fails(self):
assert _chunk_embed_failed(self._stats(chunks_errors=1, chunks_inserted=3)) is True
def test_conflict_skipped_fails(self):
assert _chunk_embed_failed(self._stats(chunks_conflict_skipped=1, chunks_inserted=3)) is True
def test_doc_balance_break_fails(self):
assert _chunk_embed_failed(self._stats(documents_fetched=99)) is True
class TestSummarizeFailed:
def _stats(self, **overrides):
stats = {
"documents_fetched": 2, "duplicates_skipped": 0, "no_active_chunks": 0,
"already_summarized": 0, "summarized": 2, "llm_errors": 0,
}
stats.update(overrides)
return stats
def test_balanced_passes(self):
assert _summarize_failed(self._stats()) is False
def test_llm_errors_fails(self):
assert _summarize_failed(self._stats(llm_errors=1, summarized=1)) is True
def test_imbalance_fails(self):
assert _summarize_failed(self._stats(documents_fetched=5)) is True
class TestEmbedSummariesFailed:
def test_balanced_passes(self):
assert _embed_summaries_failed({"summaries_fetched": 2, "embedded": 2, "errors": 0}) is False
def test_errors_fails(self):
assert _embed_summaries_failed({"summaries_fetched": 2, "embedded": 1, "errors": 1}) is True
def test_imbalance_fails(self):
assert _embed_summaries_failed({"summaries_fetched": 5, "embedded": 2, "errors": 0}) is True
# ---------------------------------------------------------------------------
# probe_ollama — reachability probe, must never raise
# ---------------------------------------------------------------------------
class _FakeProbeResponse:
def __init__(self, status):
self.status = status
async def __aenter__(self):
return self
async def __aexit__(self, *exc):
return False
class _FakeProbeSession:
def __init__(self, status=200, raise_on_get=None):
self._status = status
self._raise_on_get = raise_on_get
async def __aenter__(self):
return self
async def __aexit__(self, *exc):
return False
def get(self, url):
if self._raise_on_get is not None:
raise self._raise_on_get
return _FakeProbeResponse(self._status)
class TestProbeOllama:
async def test_200_is_up(self, monkeypatch):
monkeypatch.setattr(
"documents_ingest.cyclic_ingest.aiohttp.ClientSession",
lambda *a, **kw: _FakeProbeSession(status=200),
)
assert await probe_ollama("http://solaria:11434") is True
async def test_non_200_is_down(self, monkeypatch):
monkeypatch.setattr(
"documents_ingest.cyclic_ingest.aiohttp.ClientSession",
lambda *a, **kw: _FakeProbeSession(status=500),
)
assert await probe_ollama("http://solaria:11434") is False
async def test_connection_error_is_down_not_raised(self, monkeypatch):
monkeypatch.setattr(
"documents_ingest.cyclic_ingest.aiohttp.ClientSession",
lambda *a, **kw: _FakeProbeSession(raise_on_get=ConnectionError("refused")),
)
assert await probe_ollama("http://solaria:11434") is False
# ---------------------------------------------------------------------------
# run_cyclic — sequencing, Ollama-offline tolerance, stage isolation
# ---------------------------------------------------------------------------
ADAPTER_OK = {"fetched": 1, "already_in_db": 0, "inserted": 1, "source_mail_linked": 0,
"empty_content": 0, "errors": 0}
CHUNK_OK = {"documents_fetched": 1, "empty_content": 0, "documents_chunked": 1,
"chunks_total": 1, "chunks_already_embedded": 0, "chunks_inserted": 1,
"chunks_junk_flagged": 0, "chunks_conflict_skipped": 0, "chunks_errors": 0,
"embed_calls": 1, "embed_seconds_total": 0.1}
SUMMARIZE_OK = {"documents_fetched": 1, "duplicates_skipped": 0, "no_active_chunks": 0,
"already_summarized": 0, "summarized": 1, "llm_errors": 0,
"documents_mapreduce": 0, "tags_truncated": 0}
EMBED_SUMMARIES_OK = {"summaries_fetched": 1, "embedded": 1, "errors": 0}
feat(mail-sync): scheduler PIHA, takt kb-ingest, runbook i dokumentacja Domkniecie Kroku 7. Realizuje Decyzje (d) reconu (host schedulera + korekta kadencji indeksowania) i doklada dokumentacje wg konwencji OKF. Scheduler (NIEAKTYWOWANY — wlacza operator): - jobs/mail-imap-sync/systemd/{service,timer,run.sh} — wzorzec 1:1 z kb-ingest, OnCalendar=hourly, Persistent=true, log do pliku (nigdy sam journal). - hosts/piha/jobs.yaml — deklaracja jednostek host-level na PIHA. Nowy plik, bo services.yaml jest dla kontenerow (supervisor dopasowuje jego wpisy do world-state i wpis niekontenerowy dryfowalby wiecznie jako missing_service). Nic tego pliku nie czyta — istnieje po to, zeby "shadow-deploy family" z otwartego pytania 5 reconu multiagentowego byla spisana, a nie tylko na nodzie. Takt indeksowania (Decyzja (d), recon §3.3): - kb-ingest.timer: 03:30 raz na dobe -> co 2 h. O 03:30 SOLARIA prawie na pewno spi (potwierdzone odczytem kb_ingest_embed_skipped 1 z 2026-08-06), a tick dostaje teraz etap mailowy: ~60 nowych chunkow na dobe pomijanych kazdej nocy sprawiloby, ze backlog rosnie monotonicznie i KbEmbedBacklogGrowing zapala sie NA STALE. Co 2 h zamiast stalej godziny — probe Ollamy sam wybiera okno, wiec ktorys tick w nie trafi niezaleznie od nawykow operatora. - cyclic_ingest: etap mailowy (mail_body_ingest --only-unchunked), import miekki, wiec venv bez tego pakietu pomija etap zamiast wywracac wrapper. Predykat bledu JEST luzniejszy niz wlasne main() tamtego joba i to jedyne takie miejsce w tym wrapperze: pojedynczy trwale nieparsowalny mail nie moze zamrozic last_success_timestamp i zapalic KbIngestStale na zawsze. Bledy per-mail sa publikowane jako kb_ingest_mail_parse_errors, nie chowane. Obserwowalnosc: KbMailSyncStale (6 h bez udanego ticku). Alert na cisze w skrzynce ODRZUCONY (decyzja operatora, zgodna z reconem §3.4) — zero nowych maili to legalny stan skrzynki, a alert zapalajacy sie na zdrowym systemie zostaje wyciszony i przestaje dzialac wtedy, gdy jest potrzebny. Dokumentacja: - kb/services/job-mail-imap-sync.md (OKF), kb/runbooks/mail-sync-run.md — 9 krokow pierwszego uruchomienia, w tym checklista 4 punktow [do weryfikacji na zywo] z reconu (polityki dostawcow — do sprawdzenia, nie do zgadniecia) oraz pomiar STATUS (MESSAGES) na Fastmailu, na ktorym zapada ODLOZONA decyzja o historii. - kb-mail-pillar.md: KOREKTA JMAP -> IMAP dla Fastmaila jako decyzja 2026-08-06; stary zapis zostaje jako historia z data. Zamkniete "unifikacja adaptera" i "sizing Gmaila"; otwarte zostaje "sizing Fastmaila" — celowo, bo rozstrzyga je pomiar, nie dyskusja. - kb-m5-faza-mailowa.md: Krok 7 IN PROGRESS + tabela zakresu wdrozonego, kb-m5-faza3.md: korekta harmonogramu i sekwencji wrappera, pkg-kb-mail.md: rozpisany ze stubu, kb-postgres.md: lista migracji + 005. Testy: 642 passed (calosc kb-mail, kb-retrieval i jobs). systemd-analyze verify na timerze przechodzi, OnCalendar=0/2:00:00 normalizuje sie do co 2 h. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 14:49:58 +02:00
MAIL_BODY_OK = {"mails_scanned": 1, "missing_file": 0, "read_errors": 0, "parse_errors": 0,
"body_empty": 0, "mails_chunked": 1, "chunks_total": 2, "chunks_inserted": 2,
"chunks_newsletter_flagged": 0, "chunks_already_embedded": 0,
"chunks_conflict_skipped": 0, "chunks_errors": 0}
feat(kb): faza 3 krok 5 — cykliczny ingest (systemd timer) + alerting documents-ingest-cyclic (jobs/documents-ingest/src/documents_ingest/cyclic_ingest.py): orkiestruje paperless_adapter -> chunk_embed -> summarize(--backend anthropic, claude-haiku-4-5) -> summarize(--embed-summaries) bez zmian w samych jobach. Ollama@SOLARIA (availability_target: medium) jest tolerowana offline: probe GET /api/tags przed obu etapami embed, brak -> pominięcie, nie fail (oba embed passy idempotentne, nadrobią się na kolejnym ticku). Czwarty etap (embed-summaries) dopisany ponad plan §7.1 (który wymieniał tylko 3 kroki) — bez niego nowe streszczenia miałyby embedding=NULL i byłyby niewidoczne dla cascade_query (bramka kroku 4, WHERE embedding IS NOT NULL); potwierdzone z Oskarem. Predykaty pass/fail każdego etapu 1:1 z exit-checkiem danego joba (chunks_errors, llm_errors, stats-balance itd.) — etapy izolowane, nie fail-fast (wcześniejszy fail nie blokuje kolejnych, tak jak joby izolują błędy per wiersz). Metryki .prom (atomowy zapis, last_success_timestamp trzymany z poprzedniego pliku przy failu) do /opt/homelab/state/node-exporter/kb-ingest.prom. 36 nowych testów (202/202 pakietu). systemd (jobs/documents-ingest/systemd/): pierwszy systemd-timer w repo — kb-ingest.timer (OnCalendar=*-*-* 03:30, Persistent=true, plan §7.1) + kb-ingest.service (host-level, User oskar, EnvironmentFile /opt/homelab/kb/.env) + kb-ingest-run.sh (log per-run do /opt/homelab/logs/kb-ingest/, konwencja repo). Instalacja i sekrety udokumentowane w README (Faza 3 krok 5) — instalacja na PIHA dopiero po merge. fleet-prometheus (rules/kb-ingest.yml): KbIngestStale (>172800s od last_success, critical) + KbEmbedBacklogGrowing (backlog>0 przez 72h, warning) — dostawa istniejącym torem brain-watchdog->Telegram, bez Alertmanagera (konwencja liveness.yml). node_exporter: owner_node vps -> per-host (service.yaml) + wpis + override (--collector.textfile.directory, bez nowego mountu — czyta przez istniejący /:/host:ro) + topology.yaml dla PIHA. Domyka pozycję z docs/backlog.md "stability-agent / node_exporter owner_node single, biegaja wielomiejscowo -> per-host" (połowę — node_exporter; stability-agent zostaje osobnym follow-upem) w ramach paczki B inwentaryzacji monitoringu dla PIHA. Test end-to-end na żywo na PIHA (2× --apply, po potwierdzeniu z Oskarem): pierwszy run złapał 5 dokumentów faktycznie nowych w Paperless (nieoczekiwane, niezwiązane z tym taskiem) -> 82 nowe chunki (2 ocr_junk), 5 nowych streszczeń, 5 embeddingów streszczeń, 0 błędów, metryki zapisane. Drugi run: pełna idempotencja, wszystko 0. ANTHROPIC_API_KEY dodany przez Oskara ręcznie do /opt/homelab/kb/.env (nigdy nie logowany/generowany). Co dalej: prawdziwa instalacja systemd (services.yaml już przygotowany, po merge), zdecydowanie czy stability-agent też idzie na per-host przy okazji. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 15:05:18 +02:00
def _patch_stages(monkeypatch, *, ollama_up=True, adapter=None, chunk=None, summarize=None,
feat(mail-sync): scheduler PIHA, takt kb-ingest, runbook i dokumentacja Domkniecie Kroku 7. Realizuje Decyzje (d) reconu (host schedulera + korekta kadencji indeksowania) i doklada dokumentacje wg konwencji OKF. Scheduler (NIEAKTYWOWANY — wlacza operator): - jobs/mail-imap-sync/systemd/{service,timer,run.sh} — wzorzec 1:1 z kb-ingest, OnCalendar=hourly, Persistent=true, log do pliku (nigdy sam journal). - hosts/piha/jobs.yaml — deklaracja jednostek host-level na PIHA. Nowy plik, bo services.yaml jest dla kontenerow (supervisor dopasowuje jego wpisy do world-state i wpis niekontenerowy dryfowalby wiecznie jako missing_service). Nic tego pliku nie czyta — istnieje po to, zeby "shadow-deploy family" z otwartego pytania 5 reconu multiagentowego byla spisana, a nie tylko na nodzie. Takt indeksowania (Decyzja (d), recon §3.3): - kb-ingest.timer: 03:30 raz na dobe -> co 2 h. O 03:30 SOLARIA prawie na pewno spi (potwierdzone odczytem kb_ingest_embed_skipped 1 z 2026-08-06), a tick dostaje teraz etap mailowy: ~60 nowych chunkow na dobe pomijanych kazdej nocy sprawiloby, ze backlog rosnie monotonicznie i KbEmbedBacklogGrowing zapala sie NA STALE. Co 2 h zamiast stalej godziny — probe Ollamy sam wybiera okno, wiec ktorys tick w nie trafi niezaleznie od nawykow operatora. - cyclic_ingest: etap mailowy (mail_body_ingest --only-unchunked), import miekki, wiec venv bez tego pakietu pomija etap zamiast wywracac wrapper. Predykat bledu JEST luzniejszy niz wlasne main() tamtego joba i to jedyne takie miejsce w tym wrapperze: pojedynczy trwale nieparsowalny mail nie moze zamrozic last_success_timestamp i zapalic KbIngestStale na zawsze. Bledy per-mail sa publikowane jako kb_ingest_mail_parse_errors, nie chowane. Obserwowalnosc: KbMailSyncStale (6 h bez udanego ticku). Alert na cisze w skrzynce ODRZUCONY (decyzja operatora, zgodna z reconem §3.4) — zero nowych maili to legalny stan skrzynki, a alert zapalajacy sie na zdrowym systemie zostaje wyciszony i przestaje dzialac wtedy, gdy jest potrzebny. Dokumentacja: - kb/services/job-mail-imap-sync.md (OKF), kb/runbooks/mail-sync-run.md — 9 krokow pierwszego uruchomienia, w tym checklista 4 punktow [do weryfikacji na zywo] z reconu (polityki dostawcow — do sprawdzenia, nie do zgadniecia) oraz pomiar STATUS (MESSAGES) na Fastmailu, na ktorym zapada ODLOZONA decyzja o historii. - kb-mail-pillar.md: KOREKTA JMAP -> IMAP dla Fastmaila jako decyzja 2026-08-06; stary zapis zostaje jako historia z data. Zamkniete "unifikacja adaptera" i "sizing Gmaila"; otwarte zostaje "sizing Fastmaila" — celowo, bo rozstrzyga je pomiar, nie dyskusja. - kb-m5-faza-mailowa.md: Krok 7 IN PROGRESS + tabela zakresu wdrozonego, kb-m5-faza3.md: korekta harmonogramu i sekwencji wrappera, pkg-kb-mail.md: rozpisany ze stubu, kb-postgres.md: lista migracji + 005. Testy: 642 passed (calosc kb-mail, kb-retrieval i jobs). systemd-analyze verify na timerze przechodzi, OnCalendar=0/2:00:00 normalizuje sie do co 2 h. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 14:49:58 +02:00
embed_summaries=None, mail_body=None, backlog=0, mail_archive=True):
calls = {"adapter": 0, "chunk_embed": 0, "summarize": 0, "embed_summaries": 0,
"mail_body": 0}
feat(kb): faza 3 krok 5 — cykliczny ingest (systemd timer) + alerting documents-ingest-cyclic (jobs/documents-ingest/src/documents_ingest/cyclic_ingest.py): orkiestruje paperless_adapter -> chunk_embed -> summarize(--backend anthropic, claude-haiku-4-5) -> summarize(--embed-summaries) bez zmian w samych jobach. Ollama@SOLARIA (availability_target: medium) jest tolerowana offline: probe GET /api/tags przed obu etapami embed, brak -> pominięcie, nie fail (oba embed passy idempotentne, nadrobią się na kolejnym ticku). Czwarty etap (embed-summaries) dopisany ponad plan §7.1 (który wymieniał tylko 3 kroki) — bez niego nowe streszczenia miałyby embedding=NULL i byłyby niewidoczne dla cascade_query (bramka kroku 4, WHERE embedding IS NOT NULL); potwierdzone z Oskarem. Predykaty pass/fail każdego etapu 1:1 z exit-checkiem danego joba (chunks_errors, llm_errors, stats-balance itd.) — etapy izolowane, nie fail-fast (wcześniejszy fail nie blokuje kolejnych, tak jak joby izolują błędy per wiersz). Metryki .prom (atomowy zapis, last_success_timestamp trzymany z poprzedniego pliku przy failu) do /opt/homelab/state/node-exporter/kb-ingest.prom. 36 nowych testów (202/202 pakietu). systemd (jobs/documents-ingest/systemd/): pierwszy systemd-timer w repo — kb-ingest.timer (OnCalendar=*-*-* 03:30, Persistent=true, plan §7.1) + kb-ingest.service (host-level, User oskar, EnvironmentFile /opt/homelab/kb/.env) + kb-ingest-run.sh (log per-run do /opt/homelab/logs/kb-ingest/, konwencja repo). Instalacja i sekrety udokumentowane w README (Faza 3 krok 5) — instalacja na PIHA dopiero po merge. fleet-prometheus (rules/kb-ingest.yml): KbIngestStale (>172800s od last_success, critical) + KbEmbedBacklogGrowing (backlog>0 przez 72h, warning) — dostawa istniejącym torem brain-watchdog->Telegram, bez Alertmanagera (konwencja liveness.yml). node_exporter: owner_node vps -> per-host (service.yaml) + wpis + override (--collector.textfile.directory, bez nowego mountu — czyta przez istniejący /:/host:ro) + topology.yaml dla PIHA. Domyka pozycję z docs/backlog.md "stability-agent / node_exporter owner_node single, biegaja wielomiejscowo -> per-host" (połowę — node_exporter; stability-agent zostaje osobnym follow-upem) w ramach paczki B inwentaryzacji monitoringu dla PIHA. Test end-to-end na żywo na PIHA (2× --apply, po potwierdzeniu z Oskarem): pierwszy run złapał 5 dokumentów faktycznie nowych w Paperless (nieoczekiwane, niezwiązane z tym taskiem) -> 82 nowe chunki (2 ocr_junk), 5 nowych streszczeń, 5 embeddingów streszczeń, 0 błędów, metryki zapisane. Drugi run: pełna idempotencja, wszystko 0. ANTHROPIC_API_KEY dodany przez Oskara ręcznie do /opt/homelab/kb/.env (nigdy nie logowany/generowany). Co dalej: prawdziwa instalacja systemd (services.yaml już przygotowany, po merge), zdecydowanie czy stability-agent też idzie na per-host przy okazji. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 15:05:18 +02:00
async def _fake_probe(url, timeout=5.0):
return ollama_up
async def _fake_adapter_run(**kwargs):
calls["adapter"] += 1
return adapter if adapter is not None else dict(ADAPTER_OK)
async def _fake_chunk_run(**kwargs):
calls["chunk_embed"] += 1
if chunk is not None and isinstance(chunk, Exception):
raise chunk
return chunk if chunk is not None else dict(CHUNK_OK)
async def _fake_run_summarize(**kwargs):
calls["summarize"] += 1
if summarize is not None and isinstance(summarize, Exception):
raise summarize
return summarize if summarize is not None else dict(SUMMARIZE_OK)
async def _fake_run_embed_summaries(**kwargs):
calls["embed_summaries"] += 1
return embed_summaries if embed_summaries is not None else dict(EMBED_SUMMARIES_OK)
feat(mail-sync): scheduler PIHA, takt kb-ingest, runbook i dokumentacja Domkniecie Kroku 7. Realizuje Decyzje (d) reconu (host schedulera + korekta kadencji indeksowania) i doklada dokumentacje wg konwencji OKF. Scheduler (NIEAKTYWOWANY — wlacza operator): - jobs/mail-imap-sync/systemd/{service,timer,run.sh} — wzorzec 1:1 z kb-ingest, OnCalendar=hourly, Persistent=true, log do pliku (nigdy sam journal). - hosts/piha/jobs.yaml — deklaracja jednostek host-level na PIHA. Nowy plik, bo services.yaml jest dla kontenerow (supervisor dopasowuje jego wpisy do world-state i wpis niekontenerowy dryfowalby wiecznie jako missing_service). Nic tego pliku nie czyta — istnieje po to, zeby "shadow-deploy family" z otwartego pytania 5 reconu multiagentowego byla spisana, a nie tylko na nodzie. Takt indeksowania (Decyzja (d), recon §3.3): - kb-ingest.timer: 03:30 raz na dobe -> co 2 h. O 03:30 SOLARIA prawie na pewno spi (potwierdzone odczytem kb_ingest_embed_skipped 1 z 2026-08-06), a tick dostaje teraz etap mailowy: ~60 nowych chunkow na dobe pomijanych kazdej nocy sprawiloby, ze backlog rosnie monotonicznie i KbEmbedBacklogGrowing zapala sie NA STALE. Co 2 h zamiast stalej godziny — probe Ollamy sam wybiera okno, wiec ktorys tick w nie trafi niezaleznie od nawykow operatora. - cyclic_ingest: etap mailowy (mail_body_ingest --only-unchunked), import miekki, wiec venv bez tego pakietu pomija etap zamiast wywracac wrapper. Predykat bledu JEST luzniejszy niz wlasne main() tamtego joba i to jedyne takie miejsce w tym wrapperze: pojedynczy trwale nieparsowalny mail nie moze zamrozic last_success_timestamp i zapalic KbIngestStale na zawsze. Bledy per-mail sa publikowane jako kb_ingest_mail_parse_errors, nie chowane. Obserwowalnosc: KbMailSyncStale (6 h bez udanego ticku). Alert na cisze w skrzynce ODRZUCONY (decyzja operatora, zgodna z reconem §3.4) — zero nowych maili to legalny stan skrzynki, a alert zapalajacy sie na zdrowym systemie zostaje wyciszony i przestaje dzialac wtedy, gdy jest potrzebny. Dokumentacja: - kb/services/job-mail-imap-sync.md (OKF), kb/runbooks/mail-sync-run.md — 9 krokow pierwszego uruchomienia, w tym checklista 4 punktow [do weryfikacji na zywo] z reconu (polityki dostawcow — do sprawdzenia, nie do zgadniecia) oraz pomiar STATUS (MESSAGES) na Fastmailu, na ktorym zapada ODLOZONA decyzja o historii. - kb-mail-pillar.md: KOREKTA JMAP -> IMAP dla Fastmaila jako decyzja 2026-08-06; stary zapis zostaje jako historia z data. Zamkniete "unifikacja adaptera" i "sizing Gmaila"; otwarte zostaje "sizing Fastmaila" — celowo, bo rozstrzyga je pomiar, nie dyskusja. - kb-m5-faza-mailowa.md: Krok 7 IN PROGRESS + tabela zakresu wdrozonego, kb-m5-faza3.md: korekta harmonogramu i sekwencji wrappera, pkg-kb-mail.md: rozpisany ze stubu, kb-postgres.md: lista migracji + 005. Testy: 642 passed (calosc kb-mail, kb-retrieval i jobs). systemd-analyze verify na timerze przechodzi, OnCalendar=0/2:00:00 normalizuje sie do co 2 h. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 14:49:58 +02:00
async def _fake_mail_body_run(**kwargs):
calls["mail_body"] += 1
if mail_body is not None and isinstance(mail_body, Exception):
raise mail_body
return mail_body if mail_body is not None else dict(MAIL_BODY_OK)
feat(kb): faza 3 krok 5 — cykliczny ingest (systemd timer) + alerting documents-ingest-cyclic (jobs/documents-ingest/src/documents_ingest/cyclic_ingest.py): orkiestruje paperless_adapter -> chunk_embed -> summarize(--backend anthropic, claude-haiku-4-5) -> summarize(--embed-summaries) bez zmian w samych jobach. Ollama@SOLARIA (availability_target: medium) jest tolerowana offline: probe GET /api/tags przed obu etapami embed, brak -> pominięcie, nie fail (oba embed passy idempotentne, nadrobią się na kolejnym ticku). Czwarty etap (embed-summaries) dopisany ponad plan §7.1 (który wymieniał tylko 3 kroki) — bez niego nowe streszczenia miałyby embedding=NULL i byłyby niewidoczne dla cascade_query (bramka kroku 4, WHERE embedding IS NOT NULL); potwierdzone z Oskarem. Predykaty pass/fail każdego etapu 1:1 z exit-checkiem danego joba (chunks_errors, llm_errors, stats-balance itd.) — etapy izolowane, nie fail-fast (wcześniejszy fail nie blokuje kolejnych, tak jak joby izolują błędy per wiersz). Metryki .prom (atomowy zapis, last_success_timestamp trzymany z poprzedniego pliku przy failu) do /opt/homelab/state/node-exporter/kb-ingest.prom. 36 nowych testów (202/202 pakietu). systemd (jobs/documents-ingest/systemd/): pierwszy systemd-timer w repo — kb-ingest.timer (OnCalendar=*-*-* 03:30, Persistent=true, plan §7.1) + kb-ingest.service (host-level, User oskar, EnvironmentFile /opt/homelab/kb/.env) + kb-ingest-run.sh (log per-run do /opt/homelab/logs/kb-ingest/, konwencja repo). Instalacja i sekrety udokumentowane w README (Faza 3 krok 5) — instalacja na PIHA dopiero po merge. fleet-prometheus (rules/kb-ingest.yml): KbIngestStale (>172800s od last_success, critical) + KbEmbedBacklogGrowing (backlog>0 przez 72h, warning) — dostawa istniejącym torem brain-watchdog->Telegram, bez Alertmanagera (konwencja liveness.yml). node_exporter: owner_node vps -> per-host (service.yaml) + wpis + override (--collector.textfile.directory, bez nowego mountu — czyta przez istniejący /:/host:ro) + topology.yaml dla PIHA. Domyka pozycję z docs/backlog.md "stability-agent / node_exporter owner_node single, biegaja wielomiejscowo -> per-host" (połowę — node_exporter; stability-agent zostaje osobnym follow-upem) w ramach paczki B inwentaryzacji monitoringu dla PIHA. Test end-to-end na żywo na PIHA (2× --apply, po potwierdzeniu z Oskarem): pierwszy run złapał 5 dokumentów faktycznie nowych w Paperless (nieoczekiwane, niezwiązane z tym taskiem) -> 82 nowe chunki (2 ocr_junk), 5 nowych streszczeń, 5 embeddingów streszczeń, 0 błędów, metryki zapisane. Drugi run: pełna idempotencja, wszystko 0. ANTHROPIC_API_KEY dodany przez Oskara ręcznie do /opt/homelab/kb/.env (nigdy nie logowany/generowany). Co dalej: prawdziwa instalacja systemd (services.yaml już przygotowany, po merge), zdecydowanie czy stability-agent też idzie na per-host przy okazji. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 15:05:18 +02:00
async def _fake_backlog(dsn):
return backlog
feat(mail-sync): scheduler PIHA, takt kb-ingest, runbook i dokumentacja Domkniecie Kroku 7. Realizuje Decyzje (d) reconu (host schedulera + korekta kadencji indeksowania) i doklada dokumentacje wg konwencji OKF. Scheduler (NIEAKTYWOWANY — wlacza operator): - jobs/mail-imap-sync/systemd/{service,timer,run.sh} — wzorzec 1:1 z kb-ingest, OnCalendar=hourly, Persistent=true, log do pliku (nigdy sam journal). - hosts/piha/jobs.yaml — deklaracja jednostek host-level na PIHA. Nowy plik, bo services.yaml jest dla kontenerow (supervisor dopasowuje jego wpisy do world-state i wpis niekontenerowy dryfowalby wiecznie jako missing_service). Nic tego pliku nie czyta — istnieje po to, zeby "shadow-deploy family" z otwartego pytania 5 reconu multiagentowego byla spisana, a nie tylko na nodzie. Takt indeksowania (Decyzja (d), recon §3.3): - kb-ingest.timer: 03:30 raz na dobe -> co 2 h. O 03:30 SOLARIA prawie na pewno spi (potwierdzone odczytem kb_ingest_embed_skipped 1 z 2026-08-06), a tick dostaje teraz etap mailowy: ~60 nowych chunkow na dobe pomijanych kazdej nocy sprawiloby, ze backlog rosnie monotonicznie i KbEmbedBacklogGrowing zapala sie NA STALE. Co 2 h zamiast stalej godziny — probe Ollamy sam wybiera okno, wiec ktorys tick w nie trafi niezaleznie od nawykow operatora. - cyclic_ingest: etap mailowy (mail_body_ingest --only-unchunked), import miekki, wiec venv bez tego pakietu pomija etap zamiast wywracac wrapper. Predykat bledu JEST luzniejszy niz wlasne main() tamtego joba i to jedyne takie miejsce w tym wrapperze: pojedynczy trwale nieparsowalny mail nie moze zamrozic last_success_timestamp i zapalic KbIngestStale na zawsze. Bledy per-mail sa publikowane jako kb_ingest_mail_parse_errors, nie chowane. Obserwowalnosc: KbMailSyncStale (6 h bez udanego ticku). Alert na cisze w skrzynce ODRZUCONY (decyzja operatora, zgodna z reconem §3.4) — zero nowych maili to legalny stan skrzynki, a alert zapalajacy sie na zdrowym systemie zostaje wyciszony i przestaje dzialac wtedy, gdy jest potrzebny. Dokumentacja: - kb/services/job-mail-imap-sync.md (OKF), kb/runbooks/mail-sync-run.md — 9 krokow pierwszego uruchomienia, w tym checklista 4 punktow [do weryfikacji na zywo] z reconu (polityki dostawcow — do sprawdzenia, nie do zgadniecia) oraz pomiar STATUS (MESSAGES) na Fastmailu, na ktorym zapada ODLOZONA decyzja o historii. - kb-mail-pillar.md: KOREKTA JMAP -> IMAP dla Fastmaila jako decyzja 2026-08-06; stary zapis zostaje jako historia z data. Zamkniete "unifikacja adaptera" i "sizing Gmaila"; otwarte zostaje "sizing Fastmaila" — celowo, bo rozstrzyga je pomiar, nie dyskusja. - kb-m5-faza-mailowa.md: Krok 7 IN PROGRESS + tabela zakresu wdrozonego, kb-m5-faza3.md: korekta harmonogramu i sekwencji wrappera, pkg-kb-mail.md: rozpisany ze stubu, kb-postgres.md: lista migracji + 005. Testy: 642 passed (calosc kb-mail, kb-retrieval i jobs). systemd-analyze verify na timerze przechodzi, OnCalendar=0/2:00:00 normalizuje sie do co 2 h. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 14:49:58 +02:00
class _FakeMailBodyModule:
run = staticmethod(_fake_mail_body_run)
monkeypatch.setattr(cyclic_ingest, "mail_body_ingest", _FakeMailBodyModule)
# The archive root check is a real filesystem probe; pin it so the suite does not depend
# on whether the host running the tests happens to have a mail archive.
monkeypatch.setattr(cyclic_ingest.Path, "is_dir", lambda self: mail_archive)
feat(kb): faza 3 krok 5 — cykliczny ingest (systemd timer) + alerting documents-ingest-cyclic (jobs/documents-ingest/src/documents_ingest/cyclic_ingest.py): orkiestruje paperless_adapter -> chunk_embed -> summarize(--backend anthropic, claude-haiku-4-5) -> summarize(--embed-summaries) bez zmian w samych jobach. Ollama@SOLARIA (availability_target: medium) jest tolerowana offline: probe GET /api/tags przed obu etapami embed, brak -> pominięcie, nie fail (oba embed passy idempotentne, nadrobią się na kolejnym ticku). Czwarty etap (embed-summaries) dopisany ponad plan §7.1 (który wymieniał tylko 3 kroki) — bez niego nowe streszczenia miałyby embedding=NULL i byłyby niewidoczne dla cascade_query (bramka kroku 4, WHERE embedding IS NOT NULL); potwierdzone z Oskarem. Predykaty pass/fail każdego etapu 1:1 z exit-checkiem danego joba (chunks_errors, llm_errors, stats-balance itd.) — etapy izolowane, nie fail-fast (wcześniejszy fail nie blokuje kolejnych, tak jak joby izolują błędy per wiersz). Metryki .prom (atomowy zapis, last_success_timestamp trzymany z poprzedniego pliku przy failu) do /opt/homelab/state/node-exporter/kb-ingest.prom. 36 nowych testów (202/202 pakietu). systemd (jobs/documents-ingest/systemd/): pierwszy systemd-timer w repo — kb-ingest.timer (OnCalendar=*-*-* 03:30, Persistent=true, plan §7.1) + kb-ingest.service (host-level, User oskar, EnvironmentFile /opt/homelab/kb/.env) + kb-ingest-run.sh (log per-run do /opt/homelab/logs/kb-ingest/, konwencja repo). Instalacja i sekrety udokumentowane w README (Faza 3 krok 5) — instalacja na PIHA dopiero po merge. fleet-prometheus (rules/kb-ingest.yml): KbIngestStale (>172800s od last_success, critical) + KbEmbedBacklogGrowing (backlog>0 przez 72h, warning) — dostawa istniejącym torem brain-watchdog->Telegram, bez Alertmanagera (konwencja liveness.yml). node_exporter: owner_node vps -> per-host (service.yaml) + wpis + override (--collector.textfile.directory, bez nowego mountu — czyta przez istniejący /:/host:ro) + topology.yaml dla PIHA. Domyka pozycję z docs/backlog.md "stability-agent / node_exporter owner_node single, biegaja wielomiejscowo -> per-host" (połowę — node_exporter; stability-agent zostaje osobnym follow-upem) w ramach paczki B inwentaryzacji monitoringu dla PIHA. Test end-to-end na żywo na PIHA (2× --apply, po potwierdzeniu z Oskarem): pierwszy run złapał 5 dokumentów faktycznie nowych w Paperless (nieoczekiwane, niezwiązane z tym taskiem) -> 82 nowe chunki (2 ocr_junk), 5 nowych streszczeń, 5 embeddingów streszczeń, 0 błędów, metryki zapisane. Drugi run: pełna idempotencja, wszystko 0. ANTHROPIC_API_KEY dodany przez Oskara ręcznie do /opt/homelab/kb/.env (nigdy nie logowany/generowany). Co dalej: prawdziwa instalacja systemd (services.yaml już przygotowany, po merge), zdecydowanie czy stability-agent też idzie na per-host przy okazji. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 15:05:18 +02:00
monkeypatch.setattr(cyclic_ingest, "probe_ollama", _fake_probe)
monkeypatch.setattr(cyclic_ingest.paperless_adapter, "run", _fake_adapter_run)
monkeypatch.setattr(cyclic_ingest.chunk_embed, "run", _fake_chunk_run)
monkeypatch.setattr(cyclic_ingest.summarize, "run_summarize", _fake_run_summarize)
monkeypatch.setattr(cyclic_ingest.summarize, "run_embed_summaries", _fake_run_embed_summaries)
monkeypatch.setattr(cyclic_ingest, "fetch_embed_backlog", _fake_backlog)
return calls
class TestRunCyclic:
async def test_all_stages_succeed_ollama_up(self, monkeypatch):
calls = _patch_stages(monkeypatch, ollama_up=True)
result = await run_cyclic(
dsn="dsn", paperless_url="http://paperless", paperless_token="tok",
anthropic_api_key="key", apply=True,
)
assert result["failed"] is False
assert result["ollama_up"] is True
feat(mail-sync): scheduler PIHA, takt kb-ingest, runbook i dokumentacja Domkniecie Kroku 7. Realizuje Decyzje (d) reconu (host schedulera + korekta kadencji indeksowania) i doklada dokumentacje wg konwencji OKF. Scheduler (NIEAKTYWOWANY — wlacza operator): - jobs/mail-imap-sync/systemd/{service,timer,run.sh} — wzorzec 1:1 z kb-ingest, OnCalendar=hourly, Persistent=true, log do pliku (nigdy sam journal). - hosts/piha/jobs.yaml — deklaracja jednostek host-level na PIHA. Nowy plik, bo services.yaml jest dla kontenerow (supervisor dopasowuje jego wpisy do world-state i wpis niekontenerowy dryfowalby wiecznie jako missing_service). Nic tego pliku nie czyta — istnieje po to, zeby "shadow-deploy family" z otwartego pytania 5 reconu multiagentowego byla spisana, a nie tylko na nodzie. Takt indeksowania (Decyzja (d), recon §3.3): - kb-ingest.timer: 03:30 raz na dobe -> co 2 h. O 03:30 SOLARIA prawie na pewno spi (potwierdzone odczytem kb_ingest_embed_skipped 1 z 2026-08-06), a tick dostaje teraz etap mailowy: ~60 nowych chunkow na dobe pomijanych kazdej nocy sprawiloby, ze backlog rosnie monotonicznie i KbEmbedBacklogGrowing zapala sie NA STALE. Co 2 h zamiast stalej godziny — probe Ollamy sam wybiera okno, wiec ktorys tick w nie trafi niezaleznie od nawykow operatora. - cyclic_ingest: etap mailowy (mail_body_ingest --only-unchunked), import miekki, wiec venv bez tego pakietu pomija etap zamiast wywracac wrapper. Predykat bledu JEST luzniejszy niz wlasne main() tamtego joba i to jedyne takie miejsce w tym wrapperze: pojedynczy trwale nieparsowalny mail nie moze zamrozic last_success_timestamp i zapalic KbIngestStale na zawsze. Bledy per-mail sa publikowane jako kb_ingest_mail_parse_errors, nie chowane. Obserwowalnosc: KbMailSyncStale (6 h bez udanego ticku). Alert na cisze w skrzynce ODRZUCONY (decyzja operatora, zgodna z reconem §3.4) — zero nowych maili to legalny stan skrzynki, a alert zapalajacy sie na zdrowym systemie zostaje wyciszony i przestaje dzialac wtedy, gdy jest potrzebny. Dokumentacja: - kb/services/job-mail-imap-sync.md (OKF), kb/runbooks/mail-sync-run.md — 9 krokow pierwszego uruchomienia, w tym checklista 4 punktow [do weryfikacji na zywo] z reconu (polityki dostawcow — do sprawdzenia, nie do zgadniecia) oraz pomiar STATUS (MESSAGES) na Fastmailu, na ktorym zapada ODLOZONA decyzja o historii. - kb-mail-pillar.md: KOREKTA JMAP -> IMAP dla Fastmaila jako decyzja 2026-08-06; stary zapis zostaje jako historia z data. Zamkniete "unifikacja adaptera" i "sizing Gmaila"; otwarte zostaje "sizing Fastmaila" — celowo, bo rozstrzyga je pomiar, nie dyskusja. - kb-m5-faza-mailowa.md: Krok 7 IN PROGRESS + tabela zakresu wdrozonego, kb-m5-faza3.md: korekta harmonogramu i sekwencji wrappera, pkg-kb-mail.md: rozpisany ze stubu, kb-postgres.md: lista migracji + 005. Testy: 642 passed (calosc kb-mail, kb-retrieval i jobs). systemd-analyze verify na timerze przechodzi, OnCalendar=0/2:00:00 normalizuje sie do co 2 h. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 14:49:58 +02:00
assert calls == {"adapter": 1, "chunk_embed": 1, "summarize": 1, "embed_summaries": 1,
"mail_body": 1}
feat(kb): faza 3 krok 5 — cykliczny ingest (systemd timer) + alerting documents-ingest-cyclic (jobs/documents-ingest/src/documents_ingest/cyclic_ingest.py): orkiestruje paperless_adapter -> chunk_embed -> summarize(--backend anthropic, claude-haiku-4-5) -> summarize(--embed-summaries) bez zmian w samych jobach. Ollama@SOLARIA (availability_target: medium) jest tolerowana offline: probe GET /api/tags przed obu etapami embed, brak -> pominięcie, nie fail (oba embed passy idempotentne, nadrobią się na kolejnym ticku). Czwarty etap (embed-summaries) dopisany ponad plan §7.1 (który wymieniał tylko 3 kroki) — bez niego nowe streszczenia miałyby embedding=NULL i byłyby niewidoczne dla cascade_query (bramka kroku 4, WHERE embedding IS NOT NULL); potwierdzone z Oskarem. Predykaty pass/fail każdego etapu 1:1 z exit-checkiem danego joba (chunks_errors, llm_errors, stats-balance itd.) — etapy izolowane, nie fail-fast (wcześniejszy fail nie blokuje kolejnych, tak jak joby izolują błędy per wiersz). Metryki .prom (atomowy zapis, last_success_timestamp trzymany z poprzedniego pliku przy failu) do /opt/homelab/state/node-exporter/kb-ingest.prom. 36 nowych testów (202/202 pakietu). systemd (jobs/documents-ingest/systemd/): pierwszy systemd-timer w repo — kb-ingest.timer (OnCalendar=*-*-* 03:30, Persistent=true, plan §7.1) + kb-ingest.service (host-level, User oskar, EnvironmentFile /opt/homelab/kb/.env) + kb-ingest-run.sh (log per-run do /opt/homelab/logs/kb-ingest/, konwencja repo). Instalacja i sekrety udokumentowane w README (Faza 3 krok 5) — instalacja na PIHA dopiero po merge. fleet-prometheus (rules/kb-ingest.yml): KbIngestStale (>172800s od last_success, critical) + KbEmbedBacklogGrowing (backlog>0 przez 72h, warning) — dostawa istniejącym torem brain-watchdog->Telegram, bez Alertmanagera (konwencja liveness.yml). node_exporter: owner_node vps -> per-host (service.yaml) + wpis + override (--collector.textfile.directory, bez nowego mountu — czyta przez istniejący /:/host:ro) + topology.yaml dla PIHA. Domyka pozycję z docs/backlog.md "stability-agent / node_exporter owner_node single, biegaja wielomiejscowo -> per-host" (połowę — node_exporter; stability-agent zostaje osobnym follow-upem) w ramach paczki B inwentaryzacji monitoringu dla PIHA. Test end-to-end na żywo na PIHA (2× --apply, po potwierdzeniu z Oskarem): pierwszy run złapał 5 dokumentów faktycznie nowych w Paperless (nieoczekiwane, niezwiązane z tym taskiem) -> 82 nowe chunki (2 ocr_junk), 5 nowych streszczeń, 5 embeddingów streszczeń, 0 błędów, metryki zapisane. Drugi run: pełna idempotencja, wszystko 0. ANTHROPIC_API_KEY dodany przez Oskara ręcznie do /opt/homelab/kb/.env (nigdy nie logowany/generowany). Co dalej: prawdziwa instalacja systemd (services.yaml już przygotowany, po merge), zdecydowanie czy stability-agent też idzie na per-host przy okazji. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 15:05:18 +02:00
assert result["chunk_embed"]["skipped"] is False
assert result["embed_summaries"]["skipped"] is False
assert result["embed_backlog"] == 0
async def test_ollama_down_skips_embed_stages_not_a_failure(self, monkeypatch):
calls = _patch_stages(monkeypatch, ollama_up=False)
result = await run_cyclic(
dsn="dsn", paperless_url="http://paperless", paperless_token="tok",
anthropic_api_key="key", apply=True,
)
assert result["failed"] is False
assert result["ollama_up"] is False
# chunk_embed and embed_summaries must never even be invoked when Ollama is down.
assert calls["chunk_embed"] == 0
assert calls["embed_summaries"] == 0
assert calls["adapter"] == 1
assert calls["summarize"] == 1
assert result["chunk_embed"]["skipped"] is True
assert result["chunk_embed"]["stats"] is None
assert result["embed_summaries"]["skipped"] is True
feat(mail-sync): scheduler PIHA, takt kb-ingest, runbook i dokumentacja Domkniecie Kroku 7. Realizuje Decyzje (d) reconu (host schedulera + korekta kadencji indeksowania) i doklada dokumentacje wg konwencji OKF. Scheduler (NIEAKTYWOWANY — wlacza operator): - jobs/mail-imap-sync/systemd/{service,timer,run.sh} — wzorzec 1:1 z kb-ingest, OnCalendar=hourly, Persistent=true, log do pliku (nigdy sam journal). - hosts/piha/jobs.yaml — deklaracja jednostek host-level na PIHA. Nowy plik, bo services.yaml jest dla kontenerow (supervisor dopasowuje jego wpisy do world-state i wpis niekontenerowy dryfowalby wiecznie jako missing_service). Nic tego pliku nie czyta — istnieje po to, zeby "shadow-deploy family" z otwartego pytania 5 reconu multiagentowego byla spisana, a nie tylko na nodzie. Takt indeksowania (Decyzja (d), recon §3.3): - kb-ingest.timer: 03:30 raz na dobe -> co 2 h. O 03:30 SOLARIA prawie na pewno spi (potwierdzone odczytem kb_ingest_embed_skipped 1 z 2026-08-06), a tick dostaje teraz etap mailowy: ~60 nowych chunkow na dobe pomijanych kazdej nocy sprawiloby, ze backlog rosnie monotonicznie i KbEmbedBacklogGrowing zapala sie NA STALE. Co 2 h zamiast stalej godziny — probe Ollamy sam wybiera okno, wiec ktorys tick w nie trafi niezaleznie od nawykow operatora. - cyclic_ingest: etap mailowy (mail_body_ingest --only-unchunked), import miekki, wiec venv bez tego pakietu pomija etap zamiast wywracac wrapper. Predykat bledu JEST luzniejszy niz wlasne main() tamtego joba i to jedyne takie miejsce w tym wrapperze: pojedynczy trwale nieparsowalny mail nie moze zamrozic last_success_timestamp i zapalic KbIngestStale na zawsze. Bledy per-mail sa publikowane jako kb_ingest_mail_parse_errors, nie chowane. Obserwowalnosc: KbMailSyncStale (6 h bez udanego ticku). Alert na cisze w skrzynce ODRZUCONY (decyzja operatora, zgodna z reconem §3.4) — zero nowych maili to legalny stan skrzynki, a alert zapalajacy sie na zdrowym systemie zostaje wyciszony i przestaje dzialac wtedy, gdy jest potrzebny. Dokumentacja: - kb/services/job-mail-imap-sync.md (OKF), kb/runbooks/mail-sync-run.md — 9 krokow pierwszego uruchomienia, w tym checklista 4 punktow [do weryfikacji na zywo] z reconu (polityki dostawcow — do sprawdzenia, nie do zgadniecia) oraz pomiar STATUS (MESSAGES) na Fastmailu, na ktorym zapada ODLOZONA decyzja o historii. - kb-mail-pillar.md: KOREKTA JMAP -> IMAP dla Fastmaila jako decyzja 2026-08-06; stary zapis zostaje jako historia z data. Zamkniete "unifikacja adaptera" i "sizing Gmaila"; otwarte zostaje "sizing Fastmaila" — celowo, bo rozstrzyga je pomiar, nie dyskusja. - kb-m5-faza-mailowa.md: Krok 7 IN PROGRESS + tabela zakresu wdrozonego, kb-m5-faza3.md: korekta harmonogramu i sekwencji wrappera, pkg-kb-mail.md: rozpisany ze stubu, kb-postgres.md: lista migracji + 005. Testy: 642 passed (calosc kb-mail, kb-retrieval i jobs). systemd-analyze verify na timerze przechodzi, OnCalendar=0/2:00:00 normalizuje sie do co 2 h. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 14:49:58 +02:00
async def test_mail_stage_drains_only_unchunked_envelopes(self, monkeypatch):
# The handoff from mail-imap-sync is the unchunked-envelope queue, not a timestamp.
captured = {}
async def _capture(**kwargs):
captured.update(kwargs)
return dict(MAIL_BODY_OK)
_patch_stages(monkeypatch, ollama_up=True)
monkeypatch.setattr(cyclic_ingest.mail_body_ingest, "run", staticmethod(_capture))
await run_cyclic(
dsn="dsn", paperless_url="http://paperless", paperless_token="tok",
anthropic_api_key="key", apply=True,
)
assert captured["only_unchunked"] is True
assert captured["sources"] == ("gmail", "fastmail")
assert captured["apply"] is True
async def test_mail_stage_is_skipped_when_ollama_is_down(self, monkeypatch):
calls = _patch_stages(monkeypatch, ollama_up=False)
result = await run_cyclic(
dsn="dsn", paperless_url="http://paperless", paperless_token="tok",
anthropic_api_key="key", apply=True,
)
assert calls["mail_body"] == 0
assert result["mail_body"]["skipped"] is True
assert result["mail_skip_reason"] == "ollama_unreachable"
assert result["failed"] is False
async def test_mail_stage_is_skipped_when_the_package_is_not_installed(self, monkeypatch):
# PIHA's venv may predate mail-body-ingest; that must not stop the whole wrapper.
calls = _patch_stages(monkeypatch, ollama_up=True)
monkeypatch.setattr(cyclic_ingest, "mail_body_ingest", None)
result = await run_cyclic(
dsn="dsn", paperless_url="http://paperless", paperless_token="tok",
anthropic_api_key="key", apply=True,
)
assert result["mail_skip_reason"] == "mail_body_ingest_not_installed"
assert result["failed"] is False
assert calls["adapter"] == 1
async def test_mail_stage_is_skipped_without_an_archive(self, monkeypatch):
_patch_stages(monkeypatch, ollama_up=True, mail_archive=False)
result = await run_cyclic(
dsn="dsn", paperless_url="http://paperless", paperless_token="tok",
anthropic_api_key="key", apply=True,
)
assert result["mail_skip_reason"] == "archive_root_missing"
assert result["failed"] is False
async def test_mail_stage_is_skipped_when_no_sources_are_configured(self, monkeypatch):
_patch_stages(monkeypatch, ollama_up=True)
result = await run_cyclic(
dsn="dsn", paperless_url="http://paperless", paperless_token="tok",
anthropic_api_key="key", apply=True, mail_sources=(),
)
assert result["mail_skip_reason"] == "no_mail_sources_configured"
async def test_per_mail_parse_errors_do_not_fail_the_wrapper(self, monkeypatch):
# Looser than mail-body-ingest's own CLI predicate, on purpose: one permanently
# unparseable mail must not keep last_success frozen and light KbIngestStale forever.
flaky = dict(MAIL_BODY_OK, mails_scanned=2, parse_errors=1)
_patch_stages(monkeypatch, ollama_up=True, mail_body=flaky)
result = await run_cyclic(
dsn="dsn", paperless_url="http://paperless", paperless_token="tok",
anthropic_api_key="key", apply=True,
)
assert result["mail_body"]["failed"] is False
assert result["failed"] is False
async def test_unbalanced_mail_stats_do_fail_the_wrapper(self, monkeypatch):
_patch_stages(monkeypatch, ollama_up=True,
mail_body=dict(MAIL_BODY_OK, mails_scanned=9))
result = await run_cyclic(
dsn="dsn", paperless_url="http://paperless", paperless_token="tok",
anthropic_api_key="key", apply=True,
)
assert result["mail_body"]["failed"] is True
assert result["failed"] is True
async def test_silent_chunk_conflict_fails_the_wrapper(self, monkeypatch):
# An insert that no-op'd is a defect in the code, not bad input.
broken = dict(MAIL_BODY_OK, chunks_inserted=1, chunks_conflict_skipped=1)
_patch_stages(monkeypatch, ollama_up=True, mail_body=broken)
result = await run_cyclic(
dsn="dsn", paperless_url="http://paperless", paperless_token="tok",
anthropic_api_key="key", apply=True,
)
assert result["mail_body"]["failed"] is True
async def test_mail_stage_exception_is_isolated(self, monkeypatch):
calls = _patch_stages(monkeypatch, ollama_up=True,
mail_body=RuntimeError("archive vanished"))
result = await run_cyclic(
dsn="dsn", paperless_url="http://paperless", paperless_token="tok",
anthropic_api_key="key", apply=True,
)
assert result["mail_body"]["failed"] is True
assert result["failed"] is True
assert calls["summarize"] == 1 # later stages still ran
feat(kb): faza 3 krok 5 — cykliczny ingest (systemd timer) + alerting documents-ingest-cyclic (jobs/documents-ingest/src/documents_ingest/cyclic_ingest.py): orkiestruje paperless_adapter -> chunk_embed -> summarize(--backend anthropic, claude-haiku-4-5) -> summarize(--embed-summaries) bez zmian w samych jobach. Ollama@SOLARIA (availability_target: medium) jest tolerowana offline: probe GET /api/tags przed obu etapami embed, brak -> pominięcie, nie fail (oba embed passy idempotentne, nadrobią się na kolejnym ticku). Czwarty etap (embed-summaries) dopisany ponad plan §7.1 (który wymieniał tylko 3 kroki) — bez niego nowe streszczenia miałyby embedding=NULL i byłyby niewidoczne dla cascade_query (bramka kroku 4, WHERE embedding IS NOT NULL); potwierdzone z Oskarem. Predykaty pass/fail każdego etapu 1:1 z exit-checkiem danego joba (chunks_errors, llm_errors, stats-balance itd.) — etapy izolowane, nie fail-fast (wcześniejszy fail nie blokuje kolejnych, tak jak joby izolują błędy per wiersz). Metryki .prom (atomowy zapis, last_success_timestamp trzymany z poprzedniego pliku przy failu) do /opt/homelab/state/node-exporter/kb-ingest.prom. 36 nowych testów (202/202 pakietu). systemd (jobs/documents-ingest/systemd/): pierwszy systemd-timer w repo — kb-ingest.timer (OnCalendar=*-*-* 03:30, Persistent=true, plan §7.1) + kb-ingest.service (host-level, User oskar, EnvironmentFile /opt/homelab/kb/.env) + kb-ingest-run.sh (log per-run do /opt/homelab/logs/kb-ingest/, konwencja repo). Instalacja i sekrety udokumentowane w README (Faza 3 krok 5) — instalacja na PIHA dopiero po merge. fleet-prometheus (rules/kb-ingest.yml): KbIngestStale (>172800s od last_success, critical) + KbEmbedBacklogGrowing (backlog>0 przez 72h, warning) — dostawa istniejącym torem brain-watchdog->Telegram, bez Alertmanagera (konwencja liveness.yml). node_exporter: owner_node vps -> per-host (service.yaml) + wpis + override (--collector.textfile.directory, bez nowego mountu — czyta przez istniejący /:/host:ro) + topology.yaml dla PIHA. Domyka pozycję z docs/backlog.md "stability-agent / node_exporter owner_node single, biegaja wielomiejscowo -> per-host" (połowę — node_exporter; stability-agent zostaje osobnym follow-upem) w ramach paczki B inwentaryzacji monitoringu dla PIHA. Test end-to-end na żywo na PIHA (2× --apply, po potwierdzeniu z Oskarem): pierwszy run złapał 5 dokumentów faktycznie nowych w Paperless (nieoczekiwane, niezwiązane z tym taskiem) -> 82 nowe chunki (2 ocr_junk), 5 nowych streszczeń, 5 embeddingów streszczeń, 0 błędów, metryki zapisane. Drugi run: pełna idempotencja, wszystko 0. ANTHROPIC_API_KEY dodany przez Oskara ręcznie do /opt/homelab/kb/.env (nigdy nie logowany/generowany). Co dalej: prawdziwa instalacja systemd (services.yaml już przygotowany, po merge), zdecydowanie czy stability-agent też idzie na per-host przy okazji. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 15:05:18 +02:00
async def test_adapter_failure_does_not_skip_later_stages(self, monkeypatch):
broken_adapter = dict(ADAPTER_OK, errors=1)
calls = _patch_stages(monkeypatch, ollama_up=True, adapter=broken_adapter)
result = await run_cyclic(
dsn="dsn", paperless_url="http://paperless", paperless_token="tok",
anthropic_api_key="key", apply=True,
)
assert result["failed"] is True
assert result["adapter"]["failed"] is True
# every other stage still ran despite the adapter failing this tick.
feat(mail-sync): scheduler PIHA, takt kb-ingest, runbook i dokumentacja Domkniecie Kroku 7. Realizuje Decyzje (d) reconu (host schedulera + korekta kadencji indeksowania) i doklada dokumentacje wg konwencji OKF. Scheduler (NIEAKTYWOWANY — wlacza operator): - jobs/mail-imap-sync/systemd/{service,timer,run.sh} — wzorzec 1:1 z kb-ingest, OnCalendar=hourly, Persistent=true, log do pliku (nigdy sam journal). - hosts/piha/jobs.yaml — deklaracja jednostek host-level na PIHA. Nowy plik, bo services.yaml jest dla kontenerow (supervisor dopasowuje jego wpisy do world-state i wpis niekontenerowy dryfowalby wiecznie jako missing_service). Nic tego pliku nie czyta — istnieje po to, zeby "shadow-deploy family" z otwartego pytania 5 reconu multiagentowego byla spisana, a nie tylko na nodzie. Takt indeksowania (Decyzja (d), recon §3.3): - kb-ingest.timer: 03:30 raz na dobe -> co 2 h. O 03:30 SOLARIA prawie na pewno spi (potwierdzone odczytem kb_ingest_embed_skipped 1 z 2026-08-06), a tick dostaje teraz etap mailowy: ~60 nowych chunkow na dobe pomijanych kazdej nocy sprawiloby, ze backlog rosnie monotonicznie i KbEmbedBacklogGrowing zapala sie NA STALE. Co 2 h zamiast stalej godziny — probe Ollamy sam wybiera okno, wiec ktorys tick w nie trafi niezaleznie od nawykow operatora. - cyclic_ingest: etap mailowy (mail_body_ingest --only-unchunked), import miekki, wiec venv bez tego pakietu pomija etap zamiast wywracac wrapper. Predykat bledu JEST luzniejszy niz wlasne main() tamtego joba i to jedyne takie miejsce w tym wrapperze: pojedynczy trwale nieparsowalny mail nie moze zamrozic last_success_timestamp i zapalic KbIngestStale na zawsze. Bledy per-mail sa publikowane jako kb_ingest_mail_parse_errors, nie chowane. Obserwowalnosc: KbMailSyncStale (6 h bez udanego ticku). Alert na cisze w skrzynce ODRZUCONY (decyzja operatora, zgodna z reconem §3.4) — zero nowych maili to legalny stan skrzynki, a alert zapalajacy sie na zdrowym systemie zostaje wyciszony i przestaje dzialac wtedy, gdy jest potrzebny. Dokumentacja: - kb/services/job-mail-imap-sync.md (OKF), kb/runbooks/mail-sync-run.md — 9 krokow pierwszego uruchomienia, w tym checklista 4 punktow [do weryfikacji na zywo] z reconu (polityki dostawcow — do sprawdzenia, nie do zgadniecia) oraz pomiar STATUS (MESSAGES) na Fastmailu, na ktorym zapada ODLOZONA decyzja o historii. - kb-mail-pillar.md: KOREKTA JMAP -> IMAP dla Fastmaila jako decyzja 2026-08-06; stary zapis zostaje jako historia z data. Zamkniete "unifikacja adaptera" i "sizing Gmaila"; otwarte zostaje "sizing Fastmaila" — celowo, bo rozstrzyga je pomiar, nie dyskusja. - kb-m5-faza-mailowa.md: Krok 7 IN PROGRESS + tabela zakresu wdrozonego, kb-m5-faza3.md: korekta harmonogramu i sekwencji wrappera, pkg-kb-mail.md: rozpisany ze stubu, kb-postgres.md: lista migracji + 005. Testy: 642 passed (calosc kb-mail, kb-retrieval i jobs). systemd-analyze verify na timerze przechodzi, OnCalendar=0/2:00:00 normalizuje sie do co 2 h. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 14:49:58 +02:00
assert calls == {"adapter": 1, "chunk_embed": 1, "summarize": 1, "embed_summaries": 1,
"mail_body": 1}
feat(kb): faza 3 krok 5 — cykliczny ingest (systemd timer) + alerting documents-ingest-cyclic (jobs/documents-ingest/src/documents_ingest/cyclic_ingest.py): orkiestruje paperless_adapter -> chunk_embed -> summarize(--backend anthropic, claude-haiku-4-5) -> summarize(--embed-summaries) bez zmian w samych jobach. Ollama@SOLARIA (availability_target: medium) jest tolerowana offline: probe GET /api/tags przed obu etapami embed, brak -> pominięcie, nie fail (oba embed passy idempotentne, nadrobią się na kolejnym ticku). Czwarty etap (embed-summaries) dopisany ponad plan §7.1 (który wymieniał tylko 3 kroki) — bez niego nowe streszczenia miałyby embedding=NULL i byłyby niewidoczne dla cascade_query (bramka kroku 4, WHERE embedding IS NOT NULL); potwierdzone z Oskarem. Predykaty pass/fail każdego etapu 1:1 z exit-checkiem danego joba (chunks_errors, llm_errors, stats-balance itd.) — etapy izolowane, nie fail-fast (wcześniejszy fail nie blokuje kolejnych, tak jak joby izolują błędy per wiersz). Metryki .prom (atomowy zapis, last_success_timestamp trzymany z poprzedniego pliku przy failu) do /opt/homelab/state/node-exporter/kb-ingest.prom. 36 nowych testów (202/202 pakietu). systemd (jobs/documents-ingest/systemd/): pierwszy systemd-timer w repo — kb-ingest.timer (OnCalendar=*-*-* 03:30, Persistent=true, plan §7.1) + kb-ingest.service (host-level, User oskar, EnvironmentFile /opt/homelab/kb/.env) + kb-ingest-run.sh (log per-run do /opt/homelab/logs/kb-ingest/, konwencja repo). Instalacja i sekrety udokumentowane w README (Faza 3 krok 5) — instalacja na PIHA dopiero po merge. fleet-prometheus (rules/kb-ingest.yml): KbIngestStale (>172800s od last_success, critical) + KbEmbedBacklogGrowing (backlog>0 przez 72h, warning) — dostawa istniejącym torem brain-watchdog->Telegram, bez Alertmanagera (konwencja liveness.yml). node_exporter: owner_node vps -> per-host (service.yaml) + wpis + override (--collector.textfile.directory, bez nowego mountu — czyta przez istniejący /:/host:ro) + topology.yaml dla PIHA. Domyka pozycję z docs/backlog.md "stability-agent / node_exporter owner_node single, biegaja wielomiejscowo -> per-host" (połowę — node_exporter; stability-agent zostaje osobnym follow-upem) w ramach paczki B inwentaryzacji monitoringu dla PIHA. Test end-to-end na żywo na PIHA (2× --apply, po potwierdzeniu z Oskarem): pierwszy run złapał 5 dokumentów faktycznie nowych w Paperless (nieoczekiwane, niezwiązane z tym taskiem) -> 82 nowe chunki (2 ocr_junk), 5 nowych streszczeń, 5 embeddingów streszczeń, 0 błędów, metryki zapisane. Drugi run: pełna idempotencja, wszystko 0. ANTHROPIC_API_KEY dodany przez Oskara ręcznie do /opt/homelab/kb/.env (nigdy nie logowany/generowany). Co dalej: prawdziwa instalacja systemd (services.yaml już przygotowany, po merge), zdecydowanie czy stability-agent też idzie na per-host przy okazji. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 15:05:18 +02:00
assert result["chunk_embed"]["failed"] is False
assert result["summarize"]["failed"] is False
assert result["embed_summaries"]["failed"] is False
async def test_chunk_embed_exception_is_isolated(self, monkeypatch):
calls = _patch_stages(monkeypatch, ollama_up=True, chunk=RuntimeError("ollama 500"))
result = await run_cyclic(
dsn="dsn", paperless_url="http://paperless", paperless_token="tok",
anthropic_api_key="key", apply=True,
)
assert result["failed"] is True
assert result["chunk_embed"]["failed"] is True
assert "ollama 500" in result["chunk_embed"]["error"]
# summarize + embed_summaries still ran despite chunk_embed raising.
assert calls["summarize"] == 1
assert calls["embed_summaries"] == 1
assert result["summarize"]["failed"] is False
async def test_stats_mismatch_marks_stage_failed_without_exception(self, monkeypatch):
imbalanced_summarize = dict(SUMMARIZE_OK, documents_fetched=99)
_patch_stages(monkeypatch, ollama_up=True, summarize=imbalanced_summarize)
result = await run_cyclic(
dsn="dsn", paperless_url="http://paperless", paperless_token="tok",
anthropic_api_key="key", apply=True,
)
assert result["failed"] is True
assert result["summarize"]["failed"] is True
assert result["summarize"]["error"] is None # no exception, just a failed predicate
# ---------------------------------------------------------------------------
# .prom rendering + carry-forward
# ---------------------------------------------------------------------------
class TestRenderProm:
def test_includes_help_and_type_for_every_metric(self):
metrics = {name: 1 for name in PROM_METRIC_HELP}
content = render_prom(metrics)
for name in PROM_METRIC_HELP:
assert f"# HELP {name} " in content
assert f"# TYPE {name} gauge" in content
assert f"\n{name} 1\n" in content
def test_omits_metrics_not_present(self):
content = render_prom({"kb_ingest_last_run_timestamp": 123.0})
assert "kb_ingest_last_run_timestamp 123.0" in content
assert "kb_ingest_embed_backlog" not in content
class TestWriteAndReadPromFile:
def test_write_then_read_prev_metric(self, tmp_path):
path = tmp_path / "kb-ingest.prom"
content = render_prom({"kb_ingest_last_success_timestamp": 1700000000.0})
write_prom_atomic(path, content)
assert path.read_text() == content
assert list(tmp_path.iterdir()) == [path] # no leftover .tmp file
assert _read_prev_metric(path, "kb_ingest_last_success_timestamp") == 1700000000.0
assert _read_prev_metric(path, "kb_ingest_missing_metric") is None
def test_read_prev_metric_missing_file_returns_none(self, tmp_path):
assert _read_prev_metric(tmp_path / "nope.prom", "kb_ingest_last_success_timestamp") is None
class TestBuildMetrics:
def _result(self, failed, ollama_up=True, embed_backlog=0):
return {
"ollama_up": ollama_up,
"adapter": {"stats": dict(ADAPTER_OK), "failed": False, "skipped": False, "error": None},
"chunk_embed": {"stats": dict(CHUNK_OK), "failed": False, "skipped": False, "error": None},
"summarize": {"stats": dict(SUMMARIZE_OK), "failed": False, "skipped": False, "error": None},
"embed_summaries": {"stats": dict(EMBED_SUMMARIES_OK), "failed": False, "skipped": False, "error": None},
feat(mail-sync): scheduler PIHA, takt kb-ingest, runbook i dokumentacja Domkniecie Kroku 7. Realizuje Decyzje (d) reconu (host schedulera + korekta kadencji indeksowania) i doklada dokumentacje wg konwencji OKF. Scheduler (NIEAKTYWOWANY — wlacza operator): - jobs/mail-imap-sync/systemd/{service,timer,run.sh} — wzorzec 1:1 z kb-ingest, OnCalendar=hourly, Persistent=true, log do pliku (nigdy sam journal). - hosts/piha/jobs.yaml — deklaracja jednostek host-level na PIHA. Nowy plik, bo services.yaml jest dla kontenerow (supervisor dopasowuje jego wpisy do world-state i wpis niekontenerowy dryfowalby wiecznie jako missing_service). Nic tego pliku nie czyta — istnieje po to, zeby "shadow-deploy family" z otwartego pytania 5 reconu multiagentowego byla spisana, a nie tylko na nodzie. Takt indeksowania (Decyzja (d), recon §3.3): - kb-ingest.timer: 03:30 raz na dobe -> co 2 h. O 03:30 SOLARIA prawie na pewno spi (potwierdzone odczytem kb_ingest_embed_skipped 1 z 2026-08-06), a tick dostaje teraz etap mailowy: ~60 nowych chunkow na dobe pomijanych kazdej nocy sprawiloby, ze backlog rosnie monotonicznie i KbEmbedBacklogGrowing zapala sie NA STALE. Co 2 h zamiast stalej godziny — probe Ollamy sam wybiera okno, wiec ktorys tick w nie trafi niezaleznie od nawykow operatora. - cyclic_ingest: etap mailowy (mail_body_ingest --only-unchunked), import miekki, wiec venv bez tego pakietu pomija etap zamiast wywracac wrapper. Predykat bledu JEST luzniejszy niz wlasne main() tamtego joba i to jedyne takie miejsce w tym wrapperze: pojedynczy trwale nieparsowalny mail nie moze zamrozic last_success_timestamp i zapalic KbIngestStale na zawsze. Bledy per-mail sa publikowane jako kb_ingest_mail_parse_errors, nie chowane. Obserwowalnosc: KbMailSyncStale (6 h bez udanego ticku). Alert na cisze w skrzynce ODRZUCONY (decyzja operatora, zgodna z reconem §3.4) — zero nowych maili to legalny stan skrzynki, a alert zapalajacy sie na zdrowym systemie zostaje wyciszony i przestaje dzialac wtedy, gdy jest potrzebny. Dokumentacja: - kb/services/job-mail-imap-sync.md (OKF), kb/runbooks/mail-sync-run.md — 9 krokow pierwszego uruchomienia, w tym checklista 4 punktow [do weryfikacji na zywo] z reconu (polityki dostawcow — do sprawdzenia, nie do zgadniecia) oraz pomiar STATUS (MESSAGES) na Fastmailu, na ktorym zapada ODLOZONA decyzja o historii. - kb-mail-pillar.md: KOREKTA JMAP -> IMAP dla Fastmaila jako decyzja 2026-08-06; stary zapis zostaje jako historia z data. Zamkniete "unifikacja adaptera" i "sizing Gmaila"; otwarte zostaje "sizing Fastmaila" — celowo, bo rozstrzyga je pomiar, nie dyskusja. - kb-m5-faza-mailowa.md: Krok 7 IN PROGRESS + tabela zakresu wdrozonego, kb-m5-faza3.md: korekta harmonogramu i sekwencji wrappera, pkg-kb-mail.md: rozpisany ze stubu, kb-postgres.md: lista migracji + 005. Testy: 642 passed (calosc kb-mail, kb-retrieval i jobs). systemd-analyze verify na timerze przechodzi, OnCalendar=0/2:00:00 normalizuje sie do co 2 h. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 14:49:58 +02:00
"mail_body": {"stats": dict(MAIL_BODY_OK), "failed": False, "skipped": False, "error": None},
"mail_skip_reason": None,
feat(kb): faza 3 krok 5 — cykliczny ingest (systemd timer) + alerting documents-ingest-cyclic (jobs/documents-ingest/src/documents_ingest/cyclic_ingest.py): orkiestruje paperless_adapter -> chunk_embed -> summarize(--backend anthropic, claude-haiku-4-5) -> summarize(--embed-summaries) bez zmian w samych jobach. Ollama@SOLARIA (availability_target: medium) jest tolerowana offline: probe GET /api/tags przed obu etapami embed, brak -> pominięcie, nie fail (oba embed passy idempotentne, nadrobią się na kolejnym ticku). Czwarty etap (embed-summaries) dopisany ponad plan §7.1 (który wymieniał tylko 3 kroki) — bez niego nowe streszczenia miałyby embedding=NULL i byłyby niewidoczne dla cascade_query (bramka kroku 4, WHERE embedding IS NOT NULL); potwierdzone z Oskarem. Predykaty pass/fail każdego etapu 1:1 z exit-checkiem danego joba (chunks_errors, llm_errors, stats-balance itd.) — etapy izolowane, nie fail-fast (wcześniejszy fail nie blokuje kolejnych, tak jak joby izolują błędy per wiersz). Metryki .prom (atomowy zapis, last_success_timestamp trzymany z poprzedniego pliku przy failu) do /opt/homelab/state/node-exporter/kb-ingest.prom. 36 nowych testów (202/202 pakietu). systemd (jobs/documents-ingest/systemd/): pierwszy systemd-timer w repo — kb-ingest.timer (OnCalendar=*-*-* 03:30, Persistent=true, plan §7.1) + kb-ingest.service (host-level, User oskar, EnvironmentFile /opt/homelab/kb/.env) + kb-ingest-run.sh (log per-run do /opt/homelab/logs/kb-ingest/, konwencja repo). Instalacja i sekrety udokumentowane w README (Faza 3 krok 5) — instalacja na PIHA dopiero po merge. fleet-prometheus (rules/kb-ingest.yml): KbIngestStale (>172800s od last_success, critical) + KbEmbedBacklogGrowing (backlog>0 przez 72h, warning) — dostawa istniejącym torem brain-watchdog->Telegram, bez Alertmanagera (konwencja liveness.yml). node_exporter: owner_node vps -> per-host (service.yaml) + wpis + override (--collector.textfile.directory, bez nowego mountu — czyta przez istniejący /:/host:ro) + topology.yaml dla PIHA. Domyka pozycję z docs/backlog.md "stability-agent / node_exporter owner_node single, biegaja wielomiejscowo -> per-host" (połowę — node_exporter; stability-agent zostaje osobnym follow-upem) w ramach paczki B inwentaryzacji monitoringu dla PIHA. Test end-to-end na żywo na PIHA (2× --apply, po potwierdzeniu z Oskarem): pierwszy run złapał 5 dokumentów faktycznie nowych w Paperless (nieoczekiwane, niezwiązane z tym taskiem) -> 82 nowe chunki (2 ocr_junk), 5 nowych streszczeń, 5 embeddingów streszczeń, 0 błędów, metryki zapisane. Drugi run: pełna idempotencja, wszystko 0. ANTHROPIC_API_KEY dodany przez Oskara ręcznie do /opt/homelab/kb/.env (nigdy nie logowany/generowany). Co dalej: prawdziwa instalacja systemd (services.yaml już przygotowany, po merge), zdecydowanie czy stability-agent też idzie na per-host przy okazji. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 15:05:18 +02:00
"embed_backlog": embed_backlog,
"failed": failed,
}
feat(mail-sync): scheduler PIHA, takt kb-ingest, runbook i dokumentacja Domkniecie Kroku 7. Realizuje Decyzje (d) reconu (host schedulera + korekta kadencji indeksowania) i doklada dokumentacje wg konwencji OKF. Scheduler (NIEAKTYWOWANY — wlacza operator): - jobs/mail-imap-sync/systemd/{service,timer,run.sh} — wzorzec 1:1 z kb-ingest, OnCalendar=hourly, Persistent=true, log do pliku (nigdy sam journal). - hosts/piha/jobs.yaml — deklaracja jednostek host-level na PIHA. Nowy plik, bo services.yaml jest dla kontenerow (supervisor dopasowuje jego wpisy do world-state i wpis niekontenerowy dryfowalby wiecznie jako missing_service). Nic tego pliku nie czyta — istnieje po to, zeby "shadow-deploy family" z otwartego pytania 5 reconu multiagentowego byla spisana, a nie tylko na nodzie. Takt indeksowania (Decyzja (d), recon §3.3): - kb-ingest.timer: 03:30 raz na dobe -> co 2 h. O 03:30 SOLARIA prawie na pewno spi (potwierdzone odczytem kb_ingest_embed_skipped 1 z 2026-08-06), a tick dostaje teraz etap mailowy: ~60 nowych chunkow na dobe pomijanych kazdej nocy sprawiloby, ze backlog rosnie monotonicznie i KbEmbedBacklogGrowing zapala sie NA STALE. Co 2 h zamiast stalej godziny — probe Ollamy sam wybiera okno, wiec ktorys tick w nie trafi niezaleznie od nawykow operatora. - cyclic_ingest: etap mailowy (mail_body_ingest --only-unchunked), import miekki, wiec venv bez tego pakietu pomija etap zamiast wywracac wrapper. Predykat bledu JEST luzniejszy niz wlasne main() tamtego joba i to jedyne takie miejsce w tym wrapperze: pojedynczy trwale nieparsowalny mail nie moze zamrozic last_success_timestamp i zapalic KbIngestStale na zawsze. Bledy per-mail sa publikowane jako kb_ingest_mail_parse_errors, nie chowane. Obserwowalnosc: KbMailSyncStale (6 h bez udanego ticku). Alert na cisze w skrzynce ODRZUCONY (decyzja operatora, zgodna z reconem §3.4) — zero nowych maili to legalny stan skrzynki, a alert zapalajacy sie na zdrowym systemie zostaje wyciszony i przestaje dzialac wtedy, gdy jest potrzebny. Dokumentacja: - kb/services/job-mail-imap-sync.md (OKF), kb/runbooks/mail-sync-run.md — 9 krokow pierwszego uruchomienia, w tym checklista 4 punktow [do weryfikacji na zywo] z reconu (polityki dostawcow — do sprawdzenia, nie do zgadniecia) oraz pomiar STATUS (MESSAGES) na Fastmailu, na ktorym zapada ODLOZONA decyzja o historii. - kb-mail-pillar.md: KOREKTA JMAP -> IMAP dla Fastmaila jako decyzja 2026-08-06; stary zapis zostaje jako historia z data. Zamkniete "unifikacja adaptera" i "sizing Gmaila"; otwarte zostaje "sizing Fastmaila" — celowo, bo rozstrzyga je pomiar, nie dyskusja. - kb-m5-faza-mailowa.md: Krok 7 IN PROGRESS + tabela zakresu wdrozonego, kb-m5-faza3.md: korekta harmonogramu i sekwencji wrappera, pkg-kb-mail.md: rozpisany ze stubu, kb-postgres.md: lista migracji + 005. Testy: 642 passed (calosc kb-mail, kb-retrieval i jobs). systemd-analyze verify na timerze przechodzi, OnCalendar=0/2:00:00 normalizuje sie do co 2 h. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 14:49:58 +02:00
def test_mail_metrics_are_published(self, tmp_path):
metrics = build_metrics(self._result(failed=False), now_ts=1000.0,
prom_path=tmp_path / "kb-ingest.prom")
assert metrics["kb_ingest_mail_chunks_inserted"] == 2
assert metrics["kb_ingest_mail_parse_errors"] == 0
assert metrics["kb_ingest_mail_skipped"] == 0
def test_tolerated_mail_parse_errors_are_still_published(self, tmp_path):
# Tolerated by the exit code, but not invisible — a rising count is a real signal.
result = self._result(failed=False)
result["mail_body"]["stats"] = dict(MAIL_BODY_OK, parse_errors=2, missing_file=1)
metrics = build_metrics(result, now_ts=1000.0, prom_path=tmp_path / "kb-ingest.prom")
assert metrics["kb_ingest_mail_parse_errors"] == 3
def test_skipped_mail_stage_reports_zero_chunks_and_a_skip_flag(self, tmp_path):
result = self._result(failed=False)
result["mail_body"] = {"stats": None, "failed": False, "skipped": True, "error": None}
metrics = build_metrics(result, now_ts=1000.0, prom_path=tmp_path / "kb-ingest.prom")
assert metrics["kb_ingest_mail_skipped"] == 1
assert metrics["kb_ingest_mail_chunks_inserted"] == 0
feat(kb): faza 3 krok 5 — cykliczny ingest (systemd timer) + alerting documents-ingest-cyclic (jobs/documents-ingest/src/documents_ingest/cyclic_ingest.py): orkiestruje paperless_adapter -> chunk_embed -> summarize(--backend anthropic, claude-haiku-4-5) -> summarize(--embed-summaries) bez zmian w samych jobach. Ollama@SOLARIA (availability_target: medium) jest tolerowana offline: probe GET /api/tags przed obu etapami embed, brak -> pominięcie, nie fail (oba embed passy idempotentne, nadrobią się na kolejnym ticku). Czwarty etap (embed-summaries) dopisany ponad plan §7.1 (który wymieniał tylko 3 kroki) — bez niego nowe streszczenia miałyby embedding=NULL i byłyby niewidoczne dla cascade_query (bramka kroku 4, WHERE embedding IS NOT NULL); potwierdzone z Oskarem. Predykaty pass/fail każdego etapu 1:1 z exit-checkiem danego joba (chunks_errors, llm_errors, stats-balance itd.) — etapy izolowane, nie fail-fast (wcześniejszy fail nie blokuje kolejnych, tak jak joby izolują błędy per wiersz). Metryki .prom (atomowy zapis, last_success_timestamp trzymany z poprzedniego pliku przy failu) do /opt/homelab/state/node-exporter/kb-ingest.prom. 36 nowych testów (202/202 pakietu). systemd (jobs/documents-ingest/systemd/): pierwszy systemd-timer w repo — kb-ingest.timer (OnCalendar=*-*-* 03:30, Persistent=true, plan §7.1) + kb-ingest.service (host-level, User oskar, EnvironmentFile /opt/homelab/kb/.env) + kb-ingest-run.sh (log per-run do /opt/homelab/logs/kb-ingest/, konwencja repo). Instalacja i sekrety udokumentowane w README (Faza 3 krok 5) — instalacja na PIHA dopiero po merge. fleet-prometheus (rules/kb-ingest.yml): KbIngestStale (>172800s od last_success, critical) + KbEmbedBacklogGrowing (backlog>0 przez 72h, warning) — dostawa istniejącym torem brain-watchdog->Telegram, bez Alertmanagera (konwencja liveness.yml). node_exporter: owner_node vps -> per-host (service.yaml) + wpis + override (--collector.textfile.directory, bez nowego mountu — czyta przez istniejący /:/host:ro) + topology.yaml dla PIHA. Domyka pozycję z docs/backlog.md "stability-agent / node_exporter owner_node single, biegaja wielomiejscowo -> per-host" (połowę — node_exporter; stability-agent zostaje osobnym follow-upem) w ramach paczki B inwentaryzacji monitoringu dla PIHA. Test end-to-end na żywo na PIHA (2× --apply, po potwierdzeniu z Oskarem): pierwszy run złapał 5 dokumentów faktycznie nowych w Paperless (nieoczekiwane, niezwiązane z tym taskiem) -> 82 nowe chunki (2 ocr_junk), 5 nowych streszczeń, 5 embeddingów streszczeń, 0 błędów, metryki zapisane. Drugi run: pełna idempotencja, wszystko 0. ANTHROPIC_API_KEY dodany przez Oskara ręcznie do /opt/homelab/kb/.env (nigdy nie logowany/generowany). Co dalej: prawdziwa instalacja systemd (services.yaml już przygotowany, po merge), zdecydowanie czy stability-agent też idzie na per-host przy okazji. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 15:05:18 +02:00
def test_success_sets_last_success_to_now(self, tmp_path):
prom_path = tmp_path / "kb-ingest.prom"
metrics = build_metrics(self._result(failed=False), now_ts=1000.0, prom_path=prom_path)
assert metrics["kb_ingest_last_success_timestamp"] == 1000.0
assert metrics["kb_ingest_last_exit_code"] == 0
assert metrics["kb_ingest_documents_inserted"] == 1
assert metrics["kb_ingest_embed_skipped"] == 0
def test_failure_carries_forward_previous_success(self, tmp_path):
prom_path = tmp_path / "kb-ingest.prom"
write_prom_atomic(prom_path, render_prom({"kb_ingest_last_success_timestamp": 500.0}))
metrics = build_metrics(self._result(failed=True), now_ts=1000.0, prom_path=prom_path)
assert metrics["kb_ingest_last_success_timestamp"] == 500.0
assert metrics["kb_ingest_last_run_timestamp"] == 1000.0
assert metrics["kb_ingest_last_exit_code"] == 1
def test_failure_with_no_previous_file_defaults_to_zero(self, tmp_path):
prom_path = tmp_path / "kb-ingest.prom"
metrics = build_metrics(self._result(failed=True), now_ts=1000.0, prom_path=prom_path)
assert metrics["kb_ingest_last_success_timestamp"] == 0.0
def test_ollama_down_sets_embed_skipped(self, tmp_path):
result = self._result(failed=False, ollama_up=False)
result["chunk_embed"] = {"stats": None, "failed": False, "skipped": True, "error": None}
metrics = build_metrics(result, now_ts=1000.0, prom_path=tmp_path / "x.prom")
assert metrics["kb_ingest_embed_skipped"] == 1
assert metrics["kb_ingest_chunks_inserted"] == 0
def test_embed_backlog_none_is_omitted(self, tmp_path):
metrics = build_metrics(
self._result(failed=False, embed_backlog=None), now_ts=1000.0, prom_path=tmp_path / "x.prom",
)
assert "kb_ingest_embed_backlog" not in metrics
# ---------------------------------------------------------------------------
# main() — CLI-level guardrails and exit-code propagation
# ---------------------------------------------------------------------------
class TestMainGuards:
def test_missing_dsn_exits_1(self, monkeypatch):
monkeypatch.setattr(sys, "argv", ["documents-ingest-cyclic",
"--paperless-token", "tok"])
monkeypatch.delenv("KB_DSN", raising=False)
with pytest.raises(SystemExit) as exc:
main()
assert exc.value.code == 1
def test_missing_paperless_token_exits_1(self, monkeypatch):
monkeypatch.setattr(sys, "argv", ["documents-ingest-cyclic", "--dsn", "dsn"])
monkeypatch.delenv("PAPERLESS_API_TOKEN", raising=False)
with pytest.raises(SystemExit) as exc:
main()
assert exc.value.code == 1
def test_apply_without_anthropic_key_exits_1(self, monkeypatch):
monkeypatch.setattr(sys, "argv", [
"documents-ingest-cyclic", "--dsn", "dsn", "--paperless-token", "tok", "--apply",
])
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
with pytest.raises(SystemExit) as exc:
main()
assert exc.value.code == 1
class TestMainExitCodePropagation:
def test_exit_0_on_success(self, monkeypatch, tmp_path):
_patch_stages(monkeypatch, ollama_up=True)
monkeypatch.setattr(sys, "argv", [
"documents-ingest-cyclic", "--dsn", "dsn", "--paperless-token", "tok",
"--anthropic-api-key", "key", "--prom-path", str(tmp_path / "kb-ingest.prom"),
])
with pytest.raises(SystemExit) as exc:
main()
assert exc.value.code == 0
assert (tmp_path / "kb-ingest.prom").exists()
def test_exit_1_on_stage_failure(self, monkeypatch, tmp_path):
_patch_stages(monkeypatch, ollama_up=True, adapter=dict(ADAPTER_OK, errors=1))
monkeypatch.setattr(sys, "argv", [
"documents-ingest-cyclic", "--dsn", "dsn", "--paperless-token", "tok",
"--anthropic-api-key", "key", "--prom-path", str(tmp_path / "kb-ingest.prom"),
])
with pytest.raises(SystemExit) as exc:
main()
assert exc.value.code == 1
def test_exit_0_when_ollama_down_apply_still_true(self, monkeypatch, tmp_path):
_patch_stages(monkeypatch, ollama_up=False)
monkeypatch.setattr(sys, "argv", [
"documents-ingest-cyclic", "--dsn", "dsn", "--paperless-token", "tok",
"--anthropic-api-key", "key", "--apply",
"--prom-path", str(tmp_path / "kb-ingest.prom"),
])
with pytest.raises(SystemExit) as exc:
main()
assert exc.value.code == 0
prom_content = (tmp_path / "kb-ingest.prom").read_text()
assert "kb_ingest_embed_skipped 1" in prom_content