merge: testing → main (reconcile 2-week divergence)

This commit is contained in:
2026-04-28 18:36:00 -04:00
parent 499836c9e4
commit 862e4dbb31
1235 changed files with 160255 additions and 7996 deletions

View File

View File

@@ -0,0 +1,68 @@
"""Body templates produce realistic, non-empty output per content class."""
from __future__ import annotations
import secrets
import pytest
from decnet.realism.bodies import make_body
from decnet.realism.taxonomy import ContentClass
_INERT_CLASSES = (
ContentClass.NOTE,
ContentClass.TODO,
ContentClass.DRAFT,
ContentClass.SCRIPT,
ContentClass.LOG_CRON,
ContentClass.LOG_DAEMON,
ContentClass.CACHE_TMP,
)
@pytest.mark.parametrize("cls", _INERT_CLASSES)
def test_body_is_nonempty(cls: ContentClass) -> None:
body = make_body(cls, "admin", rand=secrets.SystemRandom())
assert isinstance(body, str)
assert body.strip()
def test_todo_body_uses_checkbox_markdown() -> None:
body = make_body(ContentClass.TODO, "admin")
# Each line should look like a markdown checkbox; we don't pin the
# exact distribution because the % checked is randomised.
for line in body.strip().splitlines():
assert line.startswith("- [")
def test_script_body_starts_with_shebang() -> None:
seen_shebangs: set[str] = set()
rng = secrets.SystemRandom()
for _ in range(20):
body = make_body(ContentClass.SCRIPT, "admin", rand=rng)
assert body.startswith("#!")
seen_shebangs.add(body.splitlines()[0])
# We should pick from at least two interpreter shebangs across 20
# trials; if not, the template list collapsed.
assert len(seen_shebangs) >= 2
def test_log_cron_body_has_cron_syslog_shape() -> None:
body = make_body(ContentClass.LOG_CRON, "admin", rand=secrets.SystemRandom())
for line in body.strip().splitlines():
assert "CRON[" in line
assert "CMD (" in line
@pytest.mark.parametrize(
"cls",
[c for c in ContentClass if c.value.startswith("canary_")],
)
def test_canary_classes_raise_in_bodies(cls: ContentClass) -> None:
with pytest.raises(NotImplementedError, match="canary"):
make_body(cls, "admin")
def test_email_class_raises_in_bodies() -> None:
with pytest.raises(NotImplementedError, match="email"):
make_body(ContentClass.EMAIL, "admin")

View File

@@ -0,0 +1,128 @@
"""LLM-enriched body generation with deterministic fallback."""
from __future__ import annotations
import asyncio
import pytest
from decnet.realism.bodies import make_body_with_llm
from decnet.realism.llm.base import LLMResult, LLMTimeout
from decnet.realism.llm.circuit import LLMCircuitBreaker
from decnet.realism.personas import EmailPersona
from decnet.realism.taxonomy import ContentClass
def _persona(uses_llms: bool = False) -> EmailPersona:
return EmailPersona(
name="admin", email="admin@corp.com", role="ops",
tone="direct", mannerisms=["uses bullets"],
active_hours="00:00-00:00",
uses_llms_heavily=uses_llms,
)
class _StubLLM:
"""Async stub: returns canned LLMResult; no subprocess work."""
def __init__(self, *, text: str = "stub body\n", success: bool = True):
self.model = "stub-model"
self.timeout = 1.0
self._result = LLMResult(
success=success, text=text, model=self.model, latency_ms=1,
)
self.calls = 0
async def generate(self, prompt: str) -> LLMResult:
self.calls += 1
return self._result
class _TimeoutLLM:
model = "timeout-model"
timeout = 0.05
async def generate(self, prompt: str) -> LLMResult:
raise LLMTimeout("simulated")
@pytest.mark.asyncio
async def test_no_llm_falls_back_to_template() -> None:
body = await make_body_with_llm(ContentClass.NOTE, _persona(), llm=None)
assert body.strip() # template path returns non-empty
@pytest.mark.asyncio
async def test_llm_success_returns_llm_text() -> None:
llm = _StubLLM(text="LLM-produced note body\n")
body = await make_body_with_llm(
ContentClass.NOTE, _persona(), llm=llm,
)
assert "LLM-produced note body" in body
assert llm.calls == 1
@pytest.mark.asyncio
async def test_em_dashes_are_stripped_for_default_persona() -> None:
llm = _StubLLM(text="Hi — quick update — see attached.\n")
body = await make_body_with_llm(
ContentClass.NOTE, _persona(uses_llms=False), llm=llm,
)
assert "" not in body
@pytest.mark.asyncio
async def test_em_dashes_pass_through_for_llm_heavy_persona() -> None:
llm = _StubLLM(text="Hi — quick update — see attached.\n")
body = await make_body_with_llm(
ContentClass.NOTE, _persona(uses_llms=True), llm=llm,
)
assert "" in body
@pytest.mark.asyncio
async def test_timeout_falls_back_to_template_and_records_failure() -> None:
breaker = LLMCircuitBreaker(failure_threshold=3, cooldown_seconds=10.0)
body = await make_body_with_llm(
ContentClass.NOTE, _persona(),
llm=_TimeoutLLM(), breaker=breaker, timeout=0.01,
)
assert body.strip() # template fallback returned non-empty
assert breaker.state == "closed" # one failure isn't enough to trip
@pytest.mark.asyncio
async def test_breaker_open_skips_llm_call() -> None:
breaker = LLMCircuitBreaker(failure_threshold=1, cooldown_seconds=60.0)
breaker.record_failure() # trip immediately
assert breaker.allow_call() is False
llm = _StubLLM()
body = await make_body_with_llm(
ContentClass.NOTE, _persona(),
llm=llm, breaker=breaker,
)
# LLM was NOT called (breaker open) — fallback to template.
assert llm.calls == 0
assert body.strip()
@pytest.mark.asyncio
async def test_system_class_never_invokes_llm() -> None:
llm = _StubLLM()
body = await make_body_with_llm(
ContentClass.LOG_CRON, _persona(), llm=llm,
)
# System-class content is supposed to look formulaic; LLM-authored
# cron logs would be a regression in realism.
assert llm.calls == 0
assert "CRON[" in body # template path
@pytest.mark.asyncio
async def test_empty_llm_response_falls_back() -> None:
llm = _StubLLM(text="", success=True)
body = await make_body_with_llm(
ContentClass.NOTE, _persona(), llm=llm,
)
# LLM ran but produced empty output → template fallback.
assert body.strip()

View File

@@ -0,0 +1,81 @@
"""LLMCircuitBreaker — process-local sliding-window breaker."""
from __future__ import annotations
from decnet.realism.llm.circuit import LLMCircuitBreaker
def test_starts_closed_and_allows_calls() -> None:
breaker = LLMCircuitBreaker()
assert breaker.state == "closed"
assert breaker.allow_call() is True
def test_trips_open_after_threshold_failures() -> None:
clock_value = [0.0]
breaker = LLMCircuitBreaker(
failure_threshold=3, cooldown_seconds=60.0,
clock=lambda: clock_value[0],
)
breaker.record_failure()
assert breaker.state == "closed"
breaker.record_failure()
assert breaker.state == "closed"
breaker.record_failure()
assert breaker.state == "open"
assert breaker.allow_call() is False
def test_success_resets_consecutive_failure_count() -> None:
breaker = LLMCircuitBreaker(failure_threshold=3)
breaker.record_failure()
breaker.record_failure()
breaker.record_success()
breaker.record_failure()
breaker.record_failure()
assert breaker.state == "closed" # only 2 since the success
def test_half_open_after_cooldown() -> None:
clock_value = [0.0]
breaker = LLMCircuitBreaker(
failure_threshold=2, cooldown_seconds=10.0,
clock=lambda: clock_value[0],
)
breaker.record_failure()
breaker.record_failure()
assert breaker.state == "open"
assert breaker.allow_call() is False
clock_value[0] = 11.0
assert breaker.allow_call() is True
assert breaker.state == "half_open"
def test_half_open_failure_re_opens() -> None:
clock_value = [0.0]
breaker = LLMCircuitBreaker(
failure_threshold=2, cooldown_seconds=5.0,
clock=lambda: clock_value[0],
)
breaker.record_failure()
breaker.record_failure()
clock_value[0] = 6.0
breaker.allow_call()
assert breaker.state == "half_open"
breaker.record_failure()
assert breaker.state == "open"
def test_half_open_success_closes() -> None:
clock_value = [0.0]
breaker = LLMCircuitBreaker(
failure_threshold=2, cooldown_seconds=5.0,
clock=lambda: clock_value[0],
)
breaker.record_failure()
breaker.record_failure()
clock_value[0] = 6.0
breaker.allow_call()
breaker.record_success()
assert breaker.state == "closed"
assert breaker.allow_call() is True

View File

@@ -0,0 +1,120 @@
"""Coverage for :mod:`decnet.realism.diurnal`.
Two functions to exercise:
* :func:`in_work_hours` — straightforward window membership including
the wrap-around (``22:00-06:00``) case and the fail-open behaviour
on malformed windows.
* :func:`sample_mtime` — must (a) return a ``datetime`` strictly in
the past, (b) clip to the configured backdate cap, and (c) snap the
hour-of-day into the persona's window when the unconstrained
candidate would land outside.
"""
from __future__ import annotations
import random
from datetime import datetime, timedelta, timezone
import pytest
from decnet.realism.diurnal import in_work_hours, sample_mtime
# Fixed 'now' for reproducible tests — Monday 2026-04-27 14:00 UTC.
_NOW = datetime(2026, 4, 27, 14, 0, tzinfo=timezone.utc)
# ---- in_work_hours -----------------------------------------------------
@pytest.mark.parametrize(
"now_hour,now_min,window,expected",
[
(10, 0, "09:00-18:00", True),
(8, 59, "09:00-18:00", False),
(9, 0, "09:00-18:00", True),
(18, 0, "09:00-18:00", False), # exclusive end
(17, 59, "09:00-18:00", True),
(23, 30, "22:00-06:00", True), # wrap-around: late
(3, 0, "22:00-06:00", True), # wrap-around: early
(12, 0, "22:00-06:00", False), # wrap-around: middle of day
],
)
def test_in_work_hours_window_membership(
now_hour: int, now_min: int, window: str, expected: bool,
) -> None:
now = _NOW.replace(hour=now_hour, minute=now_min)
assert in_work_hours(window, now) is expected
def test_in_work_hours_equal_start_end_means_always_on() -> None:
# A persona pegged "00:00-00:00" should never be silenced by the
# diurnal gate — interpreted as "no schedule".
assert in_work_hours("00:00-00:00", _NOW) is True
@pytest.mark.parametrize(
"garbage",
["not-a-window", "9-18", "09:00", "25:00-26:00", "09:00-18:99", ""],
)
def test_malformed_window_fails_open(garbage: str) -> None:
# The fleet must not silence on a typo — same fail-open semantics
# as decnet.realism.personas.in_active_hours.
assert in_work_hours(garbage, _NOW) is True
# ---- sample_mtime ------------------------------------------------------
def test_sample_mtime_is_in_the_past() -> None:
rng = random.Random(0)
for _ in range(20):
mt = sample_mtime("09:00-18:00", _NOW, rand=rng)
assert mt < _NOW, f"sample_mtime returned future: {mt} >= {_NOW}"
def test_sample_mtime_respects_backdate_cap() -> None:
rng = random.Random(0)
cap_days = 7.0
for _ in range(50):
mt = sample_mtime(
"09:00-18:00", _NOW, rand=rng,
backdate_max_days=cap_days, backdate_min_hours=0.5,
)
assert _NOW - mt <= timedelta(days=cap_days) + timedelta(hours=1)
assert _NOW - mt >= timedelta(hours=0.5) - timedelta(seconds=1)
def test_sample_mtime_snaps_hour_into_window() -> None:
# Force a tight window then assert the hour-of-day is always in it.
rng = random.Random(42)
window = "09:00-18:00"
for _ in range(60):
mt = sample_mtime(window, _NOW, rand=rng)
assert 9 <= mt.hour < 18, (
f"hour {mt.hour} fell outside {window} on {mt.isoformat()}"
)
def test_sample_mtime_handles_wrap_around_window() -> None:
rng = random.Random(123)
for _ in range(40):
mt = sample_mtime("22:00-06:00", _NOW, rand=rng)
assert mt.hour >= 22 or mt.hour < 6, (
f"hour {mt.hour} fell outside wrap window on {mt.isoformat()}"
)
def test_sample_mtime_malformed_window_does_not_snap() -> None:
# When the window can't be parsed, just return the unconstrained
# backdate. Belt-and-braces: shouldn't crash, shouldn't future-stamp.
rng = random.Random(0)
mt = sample_mtime("garbage", _NOW, rand=rng)
assert mt < _NOW
def test_sample_mtime_is_deterministic_per_seed() -> None:
# The diurnal sampler accepts a Random — pinning the seed must
# produce stable output, otherwise tests can't assert anything
# tighter than "returns a datetime in the past."
a = sample_mtime("09:00-18:00", _NOW, rand=random.Random(7))
b = sample_mtime("09:00-18:00", _NOW, rand=random.Random(7))
assert a == b

View File

@@ -0,0 +1,98 @@
"""next_iteration mutators per content class.
Stage 3b — read-modify-write contract: each editor takes a previous
body and returns a plausible next iteration. Append-only for logs;
small in-place edits for user content.
"""
from __future__ import annotations
import random
import pytest
from decnet.realism.bodies import next_iteration
from decnet.realism.taxonomy import ContentClass
def test_todo_edit_can_flip_an_unchecked_box() -> None:
prev = "- [ ] rotate keys\n- [ ] review pr\n"
seen_flip = False
for seed in range(40):
new = next_iteration(
ContentClass.TODO, "admin", prev, rand=random.Random(seed),
)
if "[x]" in new and "rotate" in new and "[x] rotate" in new:
seen_flip = True
if "[x]" in new and "[x] review" in new:
seen_flip = True
if seen_flip:
break
assert seen_flip, "no checkbox flip across 40 seeds — mutator broken"
def test_todo_edit_grows_or_holds_line_count() -> None:
prev = "- [ ] rotate keys\n"
new = next_iteration(
ContentClass.TODO, "admin", prev, rand=random.Random(0),
)
# Mutators may flip a box (same line count) or append (more lines)
# — but never shrink the file.
assert len(new.splitlines()) >= len(prev.splitlines())
def test_log_cron_edit_is_append_only() -> None:
prev = (
"Apr 27 09:00:01 hostname CRON[1234]: (root) CMD (run-parts /etc/cron.daily)\n"
)
new = next_iteration(
ContentClass.LOG_CRON, "admin", prev, rand=random.Random(0),
)
assert new.startswith(prev.rstrip())
assert len(new.splitlines()) > len(prev.splitlines())
def test_log_daemon_edit_is_append_only() -> None:
prev = "Apr 27 09:00:01 hostname systemd[1]: Started Daily apt download activities.\n"
new = next_iteration(
ContentClass.LOG_DAEMON, "admin", prev, rand=random.Random(0),
)
assert new.startswith(prev.rstrip())
def test_note_edit_grows_the_body() -> None:
prev = "remember to ping the on-call\n"
new = next_iteration(
ContentClass.NOTE, "admin", prev, rand=random.Random(0),
)
assert prev in new
assert len(new) > len(prev)
def test_draft_edit_appends_paragraph() -> None:
prev = "Hi team,\n\nQuick update.\n"
new = next_iteration(
ContentClass.DRAFT, "admin", prev, rand=random.Random(0),
)
assert new.startswith(prev.rstrip())
assert len(new) > len(prev)
def test_script_edit_appends_comment() -> None:
prev = "#!/usr/bin/env bash\nset -e\necho 'hi'\n"
new = next_iteration(
ContentClass.SCRIPT, "admin", prev, rand=random.Random(0),
)
assert new.startswith(prev.rstrip())
# New tail must be a comment (the editor's contract); never a
# silently-injected new exec line.
new_tail = new[len(prev.rstrip()):].strip()
assert new_tail.startswith("#")
@pytest.mark.parametrize("cls", [
ContentClass.CACHE_TMP, ContentClass.EMAIL,
ContentClass.CANARY_AWS_CREDS, ContentClass.CANARY_HONEYDOC,
])
def test_unsupported_classes_raise_in_edit(cls: ContentClass) -> None:
with pytest.raises(KeyError):
next_iteration(cls, "admin", "anything")

View File

@@ -0,0 +1,152 @@
"""Prompt builder behaviour: language constraint, em-dash suppression,
deterministic mannerism injection."""
from __future__ import annotations
import random
from decnet.realism.personas import EmailPersona
from decnet.realism.prompts.email import (
PromptInputs,
build,
select_mannerisms,
)
def _persona(**over) -> EmailPersona:
base = dict(
name="John Smith",
email="john@corp.com",
role="COO",
tone="formal",
mannerisms=[
"opens with 'I hope this finds you well'",
"uses 'Best regards' exclusively",
"references policy by number",
"ccs legal",
],
language="en",
)
base.update(over)
return EmailPersona(**base)
class _SeededRng:
"""Adapter so prompt code thinks it has a SystemRandom."""
def __init__(self, seed: int):
self._r = random.Random(seed)
def shuffle(self, seq):
self._r.shuffle(seq)
def random(self):
return self._r.random()
def choice(self, seq):
return self._r.choice(seq)
def test_select_mannerisms_returns_subset_of_pool():
persona = _persona()
picks = select_mannerisms(persona, rng=_SeededRng(0), n=2)
assert len(picks) == 2
assert all(m in persona.mannerisms for m in picks)
def test_select_mannerisms_deterministic_under_same_seed():
persona = _persona()
a = select_mannerisms(persona, rng=_SeededRng(42), n=2)
b = select_mannerisms(persona, rng=_SeededRng(42), n=2)
assert a == b
def test_select_mannerisms_returns_all_when_pool_smaller_than_n():
persona = _persona(mannerisms=["a"])
picks = select_mannerisms(persona, rng=_SeededRng(0), n=2)
assert picks == ["a"]
def test_select_mannerisms_empty_pool():
persona = _persona(mannerisms=[])
assert select_mannerisms(persona) == []
def test_build_includes_language_constraint_english():
sender = _persona(language="en")
recip = _persona(name="Sarah", email="sarah@corp.com", role="PM")
prompt, _ = build(
PromptInputs(sender=sender, recipient=recip, context_hint="budget"),
rng=_SeededRng(0),
)
assert "in English" in prompt
def test_build_includes_language_constraint_spanish():
sender = _persona(language="es")
recip = _persona(name="Sarah", email="sarah@corp.com", role="PM")
prompt, _ = build(
PromptInputs(sender=sender, recipient=recip, context_hint="budget"),
rng=_SeededRng(0),
)
assert "in Spanish" in prompt
def test_build_em_dash_suppression_default():
sender = _persona()
recip = _persona(name="Sarah", email="sarah@corp.com", role="PM")
prompt, _ = build(
PromptInputs(sender=sender, recipient=recip, context_hint="budget"),
rng=_SeededRng(0),
)
assert "Do NOT use em-dashes" in prompt
def test_build_em_dash_lifted_for_llm_heavy_persona():
sender = _persona(uses_llms_heavily=True)
recip = _persona(name="Sarah", email="sarah@corp.com", role="PM")
prompt, _ = build(
PromptInputs(sender=sender, recipient=recip, context_hint="budget"),
rng=_SeededRng(0),
)
assert "Do NOT use em-dashes" not in prompt
assert "fine" in prompt.lower()
def test_build_reply_thread_block_prefixes_re():
sender = _persona()
recip = _persona(name="Sarah", email="sarah@corp.com", role="PM")
prompt, _ = build(
PromptInputs(
sender=sender,
recipient=recip,
context_hint="budget",
parent_subject="Re: Q3 budget",
parent_excerpt="Numbers attached.",
),
rng=_SeededRng(0),
)
assert "REPLY in an ongoing thread" in prompt
assert "Re: Q3 budget" in prompt
assert "Numbers attached" in prompt
assert "prefixed with 'Re: '" in prompt
def test_build_returns_mannerisms_used_metadata():
sender = _persona()
recip = _persona(name="Sarah", email="sarah@corp.com", role="PM")
_, used = build(
PromptInputs(sender=sender, recipient=recip, context_hint="budget"),
rng=_SeededRng(7),
)
assert used
assert all(m in sender.mannerisms for m in used)
def test_build_uses_explicit_signature_when_provided():
sender = _persona(signature="-- John\\nCOO")
recip = _persona(name="Sarah", email="sarah@corp.com", role="PM")
prompt, _ = build(
PromptInputs(sender=sender, recipient=recip, context_hint="budget"),
rng=_SeededRng(0),
)
assert "Use this exact signature block" in prompt

137
tests/realism/test_llm.py Normal file
View File

@@ -0,0 +1,137 @@
"""LLM backend factory + Ollama implementation."""
from __future__ import annotations
import asyncio
import pytest
from decnet.realism.llm import LLMTimeout, get_llm
from decnet.realism.llm.impl.fake import FakeBackend
from decnet.realism.llm.impl.ollama import OllamaBackend
# ── factory dispatch ─────────────────────────────────────────────────────────
def test_factory_default_is_ollama(monkeypatch):
monkeypatch.delenv("DECNET_REALISM_LLM", raising=False)
backend = get_llm()
assert isinstance(backend, OllamaBackend)
def test_factory_selects_fake(monkeypatch):
monkeypatch.setenv("DECNET_REALISM_LLM", "fake")
backend = get_llm()
assert isinstance(backend, FakeBackend)
def test_factory_unknown_raises(monkeypatch):
monkeypatch.setenv("DECNET_REALISM_LLM", "vllm-someday")
with pytest.raises(ValueError, match="Unsupported"):
get_llm()
def test_factory_passes_model_through(monkeypatch):
monkeypatch.setenv("DECNET_REALISM_LLM", "ollama")
backend = get_llm(model="qwen2:7b")
assert backend.model == "qwen2:7b"
# ── FakeBackend ──────────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_fake_backend_returns_canned_output():
fb = FakeBackend(output="Subject: hi\n\nbody")
result = await fb.generate("any prompt")
assert result.success is True
assert result.text.startswith("Subject:")
assert result.model == "fake-model"
@pytest.mark.asyncio
async def test_fake_backend_can_simulate_failure():
fb = FakeBackend(success=False)
result = await fb.generate("prompt")
assert result.success is False
assert result.text == ""
# ── OllamaBackend (subprocess stubbed) ───────────────────────────────────────
@pytest.mark.asyncio
async def test_ollama_backend_success(monkeypatch):
"""Stub asyncio.create_subprocess_exec to return canned stdout."""
class _StubProc:
returncode = 0
async def communicate(self, _stdin):
return b"Subject: hi\n\nbody\n", b""
async def fake_create(*args, **kwargs): # noqa: ARG001
return _StubProc()
monkeypatch.setattr(asyncio, "create_subprocess_exec", fake_create)
backend = OllamaBackend(model="m1", timeout=1.0)
result = await backend.generate("hello")
assert result.success is True
assert "Subject:" in result.text
assert result.model == "m1"
@pytest.mark.asyncio
async def test_ollama_backend_non_zero_rc_marks_failure(monkeypatch):
class _StubProc:
returncode = 1
async def communicate(self, _stdin):
return b"", b"model not found"
async def fake_create(*args, **kwargs): # noqa: ARG001
return _StubProc()
monkeypatch.setattr(asyncio, "create_subprocess_exec", fake_create)
backend = OllamaBackend(model="m1", timeout=1.0)
result = await backend.generate("hello")
assert result.success is False
assert result.extra["rc"] == 1
assert "model not found" in result.extra["stderr"]
@pytest.mark.asyncio
async def test_ollama_backend_timeout_raises(monkeypatch):
class _StubProc:
returncode = None
async def communicate(self, _stdin):
await asyncio.sleep(10) # well past the timeout
return b"", b""
def kill(self):
pass
async def fake_create(*args, **kwargs): # noqa: ARG001
return _StubProc()
monkeypatch.setattr(asyncio, "create_subprocess_exec", fake_create)
backend = OllamaBackend(model="m1", timeout=0.05)
with pytest.raises(LLMTimeout):
await backend.generate("hello")
@pytest.mark.asyncio
async def test_ollama_backend_missing_binary_returns_failure(monkeypatch):
async def fake_create(*args, **kwargs): # noqa: ARG001
raise FileNotFoundError("ollama: not found")
monkeypatch.setattr(asyncio, "create_subprocess_exec", fake_create)
backend = OllamaBackend(model="m1", timeout=1.0)
result = await backend.generate("hello")
assert result.success is False
assert result.extra["rc"] == 127

View File

@@ -0,0 +1,95 @@
"""Filename realism contracts.
The pre-realism orchestrator emitted ``notes-1777315854.txt`` —
unix-epoch suffix, instant tell. This file pins the anti-regression:
no namer is allowed to drop a raw decimal timestamp into a filename.
"""
from __future__ import annotations
import re
import secrets
import pytest
from decnet.realism.naming import make_path
from decnet.realism.taxonomy import ContentClass
_USER_CLASSES = (
ContentClass.NOTE,
ContentClass.TODO,
ContentClass.DRAFT,
ContentClass.SCRIPT,
)
_SYSTEM_CLASSES = (
ContentClass.LOG_CRON,
ContentClass.LOG_DAEMON,
ContentClass.CACHE_TMP,
)
@pytest.mark.parametrize("cls", _USER_CLASSES)
def test_user_class_paths_live_under_persona_home(cls: ContentClass) -> None:
p = make_path(cls, "admin", rand=secrets.SystemRandom())
assert p.startswith("/home/admin/"), p
@pytest.mark.parametrize("cls", _SYSTEM_CLASSES)
def test_system_class_paths_have_no_epoch_suffix(cls: ContentClass) -> None:
rng = secrets.SystemRandom()
for _ in range(20):
p = make_path(cls, "admin", rand=rng)
# The realism failure today: filenames carry raw unix epochs.
# 8+ consecutive digits in the basename is the tell.
basename = p.rsplit("/", 1)[-1]
assert not re.search(r"\d{8,}", basename), (
f"epoch-shaped suffix found in {p!r}"
)
def test_log_cron_uses_logrotate_skeleton() -> None:
seen: set[str] = set()
rng = secrets.SystemRandom()
for _ in range(40):
seen.add(make_path(ContentClass.LOG_CRON, "admin", rand=rng))
# Real cron only ever writes a fixed set of names; anything outside
# the logrotate cycle is a realism bug.
expected = {"/var/log/cron.log", "/var/log/cron.log.1", "/var/log/cron.log.2.gz"}
assert seen <= expected
# And we should see at least the canonical name across 40 trials.
assert "/var/log/cron.log" in seen
def test_cache_tmp_uses_mkstemp_shape() -> None:
p = make_path(ContentClass.CACHE_TMP, "admin")
assert re.match(r"^/tmp/\.cache-[a-z0-9]{6}$", p), p
@pytest.mark.parametrize(
"cls",
[c for c in ContentClass if c.value.startswith("canary_")],
)
def test_canary_classes_raise_in_naming(cls: ContentClass) -> None:
with pytest.raises(NotImplementedError, match="canary"):
make_path(cls, "admin")
def test_email_class_raises_in_naming() -> None:
with pytest.raises(NotImplementedError, match="email"):
make_path(ContentClass.EMAIL, "admin")
def test_persona_with_spaces_normalises_to_login() -> None:
# "John Smith" → "johnsmith" is a plausible login, so the namer
# collapses spaces rather than falling back. This pins that
# behaviour against a future overcorrection.
p = make_path(ContentClass.NOTE, "John Smith")
assert p.startswith("/home/johnsmith/")
def test_persona_with_punctuation_falls_back_to_user_home() -> None:
# A persona name with punctuation (or non-ASCII letters) can't
# cleanly become a username; the namer must fall back to
# /home/user rather than leak weird chars onto the filesystem.
p = make_path(ContentClass.NOTE, "C-3PO!")
assert p.startswith("/home/user/")

View File

@@ -0,0 +1,127 @@
"""Persona schema parsing + active-hours window tests."""
from __future__ import annotations
import json
from decnet.realism.personas import (
EmailPersona,
in_active_hours,
login_for,
parse_personas,
)
def _persona(**over) -> dict:
base = {
"name": "John Smith",
"email": "john@corp.com",
"role": "COO",
"tone": "formal",
"mannerisms": ["uses 'Best regards'"],
}
base.update(over)
return base
def test_parse_empty_inputs():
assert parse_personas(None) == []
assert parse_personas("") == []
assert parse_personas([]) == []
def test_parse_invalid_json_returns_empty_no_raise():
assert parse_personas("{not json") == []
def test_parse_invalid_top_level_shape_returns_empty():
assert parse_personas('{"not": "a list"}') == []
def test_parse_drops_invalid_entry_keeps_valid():
raw = json.dumps([
_persona(),
{"name": "broken", "email": "not-an-email"},
_persona(name="Sarah", email="sarah@corp.com"),
])
parsed = parse_personas(raw)
assert len(parsed) == 2
assert {p.name for p in parsed} == {"John Smith", "Sarah"}
def test_parse_resolves_language_default_when_unset():
raw = json.dumps([_persona()])
parsed = parse_personas(raw, language_default="es")
assert parsed[0].language == "es"
def test_parse_persona_language_overrides_default():
raw = json.dumps([_persona(language="pt")])
parsed = parse_personas(raw, language_default="es")
assert parsed[0].language == "pt"
def test_parse_accepts_python_list_directly():
parsed = parse_personas([_persona()])
assert len(parsed) == 1
def test_uses_llms_heavily_default_false():
parsed = parse_personas([_persona()])
assert parsed[0].uses_llms_heavily is False
def test_uses_llms_heavily_can_be_set():
parsed = parse_personas([_persona(uses_llms_heavily=True)])
assert parsed[0].uses_llms_heavily is True
def test_active_hours_normal_window():
p = EmailPersona(**_persona(active_hours="09:00-18:00"))
assert in_active_hours(p, 12) is True
assert in_active_hours(p, 8) is False
assert in_active_hours(p, 18) is False
assert in_active_hours(p, 9) is True
def test_active_hours_wraparound_window():
p = EmailPersona(**_persona(active_hours="22:00-06:00"))
assert in_active_hours(p, 23) is True
assert in_active_hours(p, 0) is True
assert in_active_hours(p, 5) is True
assert in_active_hours(p, 7) is False
def test_active_hours_malformed_treats_as_always_on():
p = EmailPersona(**_persona(active_hours="garbage"))
assert in_active_hours(p, 0) is True
assert in_active_hours(p, 23) is True
def test_active_hours_equal_window_treated_as_always_on():
p = EmailPersona(**_persona(active_hours="10:00-10:00"))
assert in_active_hours(p, 5) is True
def test_login_for_normalises_display_name():
assert login_for("John Smith") == "johnsmith"
assert login_for("alice") == "alice"
def test_login_for_falls_back_to_user_on_punctuation():
# The realism namer and canary cultivator both rely on this so the
# decky filesystem doesn't end up with an unexpected username.
assert login_for("Mr. Robot") == "user"
assert login_for("") == "user"
assert login_for("Renée") == "user" # non-ASCII falls back
def test_login_for_shared_by_naming_and_cultivator():
"""Single source of truth: realism naming and canary cultivator
must agree on the persona→login mapping."""
from decnet.canary import cultivator
from decnet.realism import naming
persona = "John Smith"
expected = login_for(persona)
assert naming._home(persona) == f"/home/{expected}"
# cultivator imports login_for; not duplicated.
assert cultivator.login_for is login_for

View File

@@ -0,0 +1,99 @@
"""Global persona pool — disk-backed source for fleet/shard mail deckies."""
from __future__ import annotations
import json
import pytest
from decnet.realism import personas_pool as global_pool
@pytest.fixture(autouse=True)
def _reset():
global_pool.reset_cache()
yield
global_pool.reset_cache()
_TWO = [
{
"name": "John Smith",
"email": "john@corp.com",
"role": "COO",
"tone": "formal",
"mannerisms": ["uses 'Best regards'"],
},
{
"name": "Sarah Johnson",
"email": "sarah@corp.com",
"role": "PM",
"tone": "direct",
"mannerisms": ["uses bullets"],
},
]
def test_load_returns_empty_when_file_missing(tmp_path, monkeypatch):
monkeypatch.setenv(
"DECNET_REALISM_PERSONAS", str(tmp_path / "does-not-exist.json")
)
assert global_pool.load() == []
def test_load_returns_parsed_personas(tmp_path, monkeypatch):
f = tmp_path / "personas.json"
f.write_text(json.dumps(_TWO))
monkeypatch.setenv("DECNET_REALISM_PERSONAS", str(f))
personas = global_pool.load()
assert len(personas) == 2
assert {p.email for p in personas} == {"john@corp.com", "sarah@corp.com"}
def test_load_resolves_language_default(tmp_path, monkeypatch):
f = tmp_path / "personas.json"
f.write_text(json.dumps(_TWO))
monkeypatch.setenv("DECNET_REALISM_PERSONAS", str(f))
personas = global_pool.load(language_default="es")
assert all(p.language == "es" for p in personas)
def test_load_invalid_json_returns_empty(tmp_path, monkeypatch):
f = tmp_path / "personas.json"
f.write_text("{not valid")
monkeypatch.setenv("DECNET_REALISM_PERSONAS", str(f))
assert global_pool.load() == []
def test_load_caches_until_mtime_changes(tmp_path, monkeypatch):
f = tmp_path / "personas.json"
f.write_text(json.dumps(_TWO))
monkeypatch.setenv("DECNET_REALISM_PERSONAS", str(f))
first = global_pool.load()
assert len(first) == 2
# Re-write with a single persona; bump mtime so the cache invalidates.
import time as _time
_time.sleep(0.01)
f.write_text(json.dumps(_TWO[:1]))
import os
os.utime(f, None)
second = global_pool.load()
assert len(second) == 1
def test_resolve_path_honours_env_override(tmp_path, monkeypatch):
monkeypatch.setenv("DECNET_REALISM_PERSONAS", str(tmp_path / "x.json"))
assert global_pool.resolve_path() == tmp_path / "x.json"
def test_resolve_path_falls_back_to_user_path_when_system_missing(monkeypatch):
monkeypatch.delenv("DECNET_REALISM_PERSONAS", raising=False)
# In a typical dev box /etc/decnet/ doesn't exist; the resolver
# should pick ~/.decnet/email_personas.json.
p = global_pool.resolve_path()
# We don't assert the exact path (depends on whether /etc/decnet
# exists on the test host), only that it ends with the canonical
# filename and isn't an empty path.
assert p.name == "email_personas.json"

View File

@@ -0,0 +1,114 @@
"""Realism planner — picks (decky, persona, class, action, mtime).
Stage 3 ships create-only plans; the edit branch lands in 3b. Tests
pin the diurnal gate, the eligibility filter, and the create
contract.
"""
from __future__ import annotations
import random
from datetime import datetime, timezone
import pytest
from decnet.realism.personas import EmailPersona
from decnet.realism.planner import pick
from decnet.realism.taxonomy import ContentClass
def _persona(name: str = "admin", window: str = "00:00-00:00") -> EmailPersona:
return EmailPersona(
name=name,
email=f"{name}@corp.com",
role="ops",
tone="direct",
active_hours=window,
)
def _decky(uuid: str = "u1", name: str = "decky-01", personas=None) -> dict:
return {
"uuid": uuid,
"name": name,
"_realism_personas": personas or [_persona()],
}
_NOW = datetime(2026, 4, 27, 14, 0, tzinfo=timezone.utc)
def test_pick_returns_none_when_no_deckies() -> None:
assert pick([], _NOW) is None
def test_pick_returns_none_when_decky_has_no_personas() -> None:
assert pick([{"uuid": "u1", "name": "d", "_realism_personas": []}], _NOW) is None
def test_pick_filters_personas_outside_window() -> None:
# A persona pegged to 01:00-02:00 with now=14:00 must not be picked.
out_of_hours = _persona(window="01:00-02:00")
deckies = [_decky(personas=[out_of_hours])]
assert pick(deckies, _NOW) is None
def test_pick_returns_create_plan_with_mtime_in_past() -> None:
deckies = [_decky()]
plan = pick(deckies, _NOW, rand=random.Random(0))
assert plan is not None
assert plan.action == "create"
assert plan.decky_uuid == "u1"
assert plan.persona == "admin"
assert plan.target_path.startswith("/")
assert plan.body_hint
assert plan.mtime < _NOW
def test_pick_distributes_across_user_and_system_classes() -> None:
deckies = [_decky()]
seen: set[ContentClass] = set()
for seed in range(80):
plan = pick(deckies, _NOW, rand=random.Random(seed))
if plan is not None:
seen.add(plan.content_class)
# Across 80 seeds we should hit both buckets — at least one user
# class and at least one system class — otherwise the weights or
# the 70/30 split is broken.
user_classes = {c for c in seen if c.is_user_class()}
system_classes = {c for c in seen if c.is_system_class()}
assert user_classes, f"no user-class plans in 80 trials: {seen}"
assert system_classes, f"no system-class plans in 80 trials: {seen}"
def test_canary_picks_are_rare() -> None:
"""Stage 7: canary content_classes ARE picked, but bounded.
The documented rate is ~3% of file picks per
decnet/realism/planner.py:_CANARY_PROBABILITY. We trial a large
sample and assert the rate stays under a generous ceiling so a
typo bumping the constant to 30% explodes here loudly.
"""
deckies = [_decky()]
canary_count = 0
create_count = 0
for seed in range(500):
plan = pick(deckies, _NOW, rand=random.Random(seed))
if plan is None:
continue
create_count += 1
if plan.content_class.is_canary():
canary_count += 1
# 3% target with a 6% upper bound — sampling noise on 500 trials
# is comfortably below this for the documented rate.
rate = canary_count / max(1, create_count)
assert rate <= 0.06, f"canary rate {rate:.2%} exceeds 6% ceiling"
assert canary_count > 0, "expected at least one canary across 500 seeds"
def test_pick_persists_persona_window_in_notes() -> None:
plan = pick([_decky()], _NOW, rand=random.Random(0))
assert plan is not None
# The plan's notes carry the persona name and the window — useful
# for the dashboard's "why this file" inspector.
assert any("persona=admin" in n for n in plan.notes)
assert any("window=" in n for n in plan.notes)

View File

@@ -0,0 +1,119 @@
"""Operator-tunable planner knobs (apply_payload / current_payload).
§3c of the realism handoff: the planner reads mutable module globals
that an admin can override via PUT /api/v1/realism/config. These tests
pin the validation surface and the payload roundtrip so a regression
that breaks operator tunables surfaces here, not on a live fleet.
"""
from __future__ import annotations
import pytest
from decnet.realism import planner
from decnet.realism.taxonomy import ContentClass
@pytest.fixture(autouse=True)
def _reset_after_each_test():
yield
planner.reset_to_defaults()
def test_current_payload_returns_defaults_after_reset():
planner.reset_to_defaults()
payload = planner.current_payload()
assert payload["canary_probability"] == pytest.approx(0.03)
user = {e["content_class"]: e["weight"] for e in payload["user_class_weights"]}
assert user[ContentClass.NOTE.value] == 30
assert user[ContentClass.TODO.value] == 20
def test_apply_payload_overrides_user_weights():
planner.apply_payload({
"user_class_weights": [
{"content_class": "note", "weight": 5},
{"content_class": "todo", "weight": 95},
],
})
payload = planner.current_payload()
weights = {e["content_class"]: e["weight"] for e in payload["user_class_weights"]}
assert weights == {"note": 5, "todo": 95}
# System weights left untouched by a partial body.
assert payload["system_class_weights"]
def test_apply_payload_overrides_canary_probability():
planner.apply_payload({"canary_probability": 0.15})
assert planner.current_payload()["canary_probability"] == pytest.approx(0.15)
def test_apply_payload_rejects_bad_canary_probability():
with pytest.raises(ValueError, match="canary_probability"):
planner.apply_payload({"canary_probability": 1.5})
with pytest.raises(ValueError, match="canary_probability"):
planner.apply_payload({"canary_probability": -0.1})
with pytest.raises(ValueError, match="canary_probability"):
planner.apply_payload({"canary_probability": "high"})
def test_apply_payload_rejects_negative_weight():
with pytest.raises(ValueError, match="non-negative integer"):
planner.apply_payload({
"user_class_weights": [{"content_class": "note", "weight": -1}],
})
def test_apply_payload_rejects_unknown_content_class():
with pytest.raises(ValueError, match="unknown content_class"):
planner.apply_payload({
"user_class_weights": [{"content_class": "vibes", "weight": 1}],
})
def test_apply_payload_drops_class_from_wrong_list():
"""A canary class on the user list is silently dropped (operator
error), not raised — the partial save still applies the legit
entries. Roundtrip shows the operator their entry didn't land."""
planner.apply_payload({
"user_class_weights": [
{"content_class": "note", "weight": 10},
{"content_class": "canary_aws_creds", "weight": 100},
],
})
weights = {
e["content_class"]: e["weight"]
for e in planner.current_payload()["user_class_weights"]
}
assert weights == {"note": 10}
# canary class did NOT bleed onto the user list.
assert "canary_aws_creds" not in weights
def test_apply_payload_rejects_zero_total_weight():
with pytest.raises(ValueError, match="positive number"):
planner.apply_payload({
"user_class_weights": [{"content_class": "note", "weight": 0}],
})
def test_apply_payload_partial_failure_leaves_state_intact():
"""If validation rejects part of a payload, the planner's other
fields must not have been silently rebound."""
planner.apply_payload({"canary_probability": 0.10})
pre = planner.current_payload()
with pytest.raises(ValueError):
planner.apply_payload({
"user_class_weights": [{"content_class": "note", "weight": 5}],
"canary_probability": 9.0, # invalid
})
post = planner.current_payload()
assert post == pre # nothing rebound on failure
def test_apply_payload_ignores_unknown_keys():
"""Forward-compat: future fields land without breaking older clients."""
planner.apply_payload({"future_knob": "ignored"})
# Nothing changed.
assert planner.current_payload()["canary_probability"] == pytest.approx(0.03)

View File

@@ -0,0 +1,190 @@
"""record / update / list / pick-for-edit on the synthetic_files table.
Stage 3 of the realism migration introduces the synthetic_files
table for per-(decky, path) state. Tests pin the contract on a
real :class:`SQLiteRepository` so SQLModel schema bugs surface here
rather than in production.
"""
from __future__ import annotations
import hashlib
from datetime import datetime, timedelta, timezone
import pytest
import pytest_asyncio
from decnet.web.db.sqlite.repository import SQLiteRepository
@pytest_asyncio.fixture
async def repo(tmp_path):
r = SQLiteRepository(db_path=str(tmp_path / "decnet.db"))
await r.initialize()
yield r
await r.engine.dispose()
def _row(
decky: str = "d1",
path: str = "/home/admin/TODO.md",
persona: str = "admin",
cls: str = "todo",
body: str = "- [ ] rotate keys\n",
ts: datetime | None = None,
) -> dict:
now = ts or datetime.now(timezone.utc)
return {
"decky_uuid": decky,
"path": path,
"persona": persona,
"content_class": cls,
"created_at": now,
"last_modified": now,
"edit_count": 0,
"content_hash": hashlib.sha256(body.encode()).hexdigest(),
"last_body": body,
}
@pytest.mark.asyncio
async def test_record_returns_uuid(repo):
uuid = await repo.record_synthetic_file(_row())
assert isinstance(uuid, str) and uuid
@pytest.mark.asyncio
async def test_unique_constraint_on_decky_path(repo):
await repo.record_synthetic_file(_row())
with pytest.raises(Exception):
await repo.record_synthetic_file(_row())
@pytest.mark.asyncio
async def test_update_synthetic_file_patches_fields(repo):
uuid = await repo.record_synthetic_file(_row())
await repo.update_synthetic_file(
uuid,
{"edit_count": 1, "last_body": "- [x] rotate keys\n"},
)
listing = await repo.list_synthetic_files(decky_uuid="d1")
assert len(listing) == 1
assert listing[0]["edit_count"] == 1
assert listing[0]["last_body"].startswith("- [x]")
@pytest.mark.asyncio
async def test_list_filters_by_decky_and_persona(repo):
await repo.record_synthetic_file(_row(decky="d1", path="/a"))
await repo.record_synthetic_file(_row(decky="d1", path="/b", persona="ubuntu"))
await repo.record_synthetic_file(_row(decky="d2", path="/c"))
by_decky = await repo.list_synthetic_files(decky_uuid="d1")
assert {r["path"] for r in by_decky} == {"/a", "/b"}
by_persona = await repo.list_synthetic_files(decky_uuid="d1", persona="ubuntu")
assert {r["path"] for r in by_persona} == {"/b"}
@pytest.mark.asyncio
async def test_pick_random_returns_none_when_empty(repo):
assert await repo.pick_random_synthetic_file_for_edit("d-empty") is None
@pytest.mark.asyncio
async def test_pick_random_excludes_canary_classes(repo):
# Canary-class files are stored on the same table (stage 7) but
# the editor must skip them — their bodies are binary blobs.
await repo.record_synthetic_file(_row(cls="canary_aws_creds"))
picked = await repo.pick_random_synthetic_file_for_edit("d1")
assert picked is None
@pytest.mark.asyncio
async def test_pick_random_excludes_too_old_rows(repo):
old = datetime.now(timezone.utc) - timedelta(days=120)
await repo.record_synthetic_file(_row(ts=old))
picked = await repo.pick_random_synthetic_file_for_edit("d1", max_age_days=30)
assert picked is None
@pytest.mark.asyncio
async def test_pick_random_returns_eligible_row(repo):
await repo.record_synthetic_file(_row(cls="todo"))
picked = await repo.pick_random_synthetic_file_for_edit("d1")
assert picked is not None
assert picked["content_class"] == "todo"
assert picked["path"] == "/home/admin/TODO.md"
@pytest.mark.asyncio
async def test_count_synthetic_files_respects_filters(repo):
await repo.record_synthetic_file(_row(decky="d1", path="/a", cls="todo"))
await repo.record_synthetic_file(_row(decky="d1", path="/b", cls="note"))
await repo.record_synthetic_file(_row(decky="d2", path="/c", cls="todo"))
assert await repo.count_synthetic_files() == 3
assert await repo.count_synthetic_files(decky_uuid="d1") == 2
assert await repo.count_synthetic_files(content_class="todo") == 2
assert await repo.count_synthetic_files(
decky_uuid="d1", content_class="note",
) == 1
@pytest.mark.asyncio
async def test_list_filters_by_content_class(repo):
await repo.record_synthetic_file(_row(decky="d1", path="/a", cls="todo"))
await repo.record_synthetic_file(_row(decky="d1", path="/b", cls="note"))
rows = await repo.list_synthetic_files(content_class="todo")
assert len(rows) == 1
assert rows[0]["content_class"] == "todo"
@pytest.mark.asyncio
async def test_get_synthetic_file_returns_row(repo):
uuid = await repo.record_synthetic_file(_row(decky="d1", path="/a"))
got = await repo.get_synthetic_file(uuid)
assert got is not None
assert got["uuid"] == uuid
assert got["path"] == "/a"
@pytest.mark.asyncio
async def test_get_synthetic_file_returns_none_when_missing(repo):
assert await repo.get_synthetic_file("does-not-exist") is None
@pytest.mark.asyncio
async def test_realism_config_get_returns_none_when_unset(repo):
assert await repo.get_realism_config("weights") is None
@pytest.mark.asyncio
async def test_realism_config_set_then_get_roundtrips(repo):
await repo.set_realism_config("weights", '{"canary_probability": 0.07}')
row = await repo.get_realism_config("weights")
assert row is not None
assert row["key"] == "weights"
assert row["value"] == '{"canary_probability": 0.07}'
@pytest.mark.asyncio
async def test_realism_config_set_is_upsert(repo):
await repo.set_realism_config("weights", '{"a": 1}')
await repo.set_realism_config("weights", '{"a": 2}')
row = await repo.get_realism_config("weights")
assert row is not None
assert row["value"] == '{"a": 2}'
def test_path_max_length_fits_mysql_utf8mb4_index_limit():
"""The unique (decky_uuid, path) index has to fit MySQL's 3072-byte
utf8mb4 cap: (decky_uuid_len + path_len) * 4 <= 3072. A regression
that widens path past this triggers
``Specified key was too long`` on MySQL DB init."""
from decnet.web.db.models.realism import SyntheticFile
fields = SyntheticFile.model_fields
decky_len = fields["decky_uuid"].metadata[0].max_length # type: ignore[attr-defined]
path_len = fields["path"].metadata[0].max_length # type: ignore[attr-defined]
assert (decky_len + path_len) * 4 <= 3072, (
f"(decky_uuid={decky_len} + path={path_len}) * 4 = "
f"{(decky_len + path_len) * 4} exceeds MySQL utf8mb4 index cap"
)

View File

@@ -0,0 +1,91 @@
"""``synthetic_files.last_body`` is capped at 64 KB by the repo.
The repo clips on both insert and update so callers may pass the full
body. Large blobs (DOCX/PDF, canary artifacts) would bloat the table;
the decky filesystem holds the canonical bytes.
These tests pin the contract so a regression that drops the cap or
applies it inconsistently fails loudly. Note: callers pass the *full*
body — the worker no longer clips; the repo does.
"""
from __future__ import annotations
from datetime import datetime, timezone
import pytest
import pytest_asyncio
from decnet.web.db.models.realism import SYNTHETIC_FILE_BODY_LIMIT
from decnet.web.db.sqlite.repository import SQLiteRepository
_LIMIT = SYNTHETIC_FILE_BODY_LIMIT
@pytest_asyncio.fixture
async def repo(tmp_path):
r = SQLiteRepository(db_path=str(tmp_path / "decnet.db"))
await r.initialize()
yield r
await r.engine.dispose()
def _row(body: str) -> dict:
import hashlib
now = datetime.now(timezone.utc)
return {
"decky_uuid": "d1",
"path": "/home/admin/notes.txt",
"persona": "admin",
"content_class": "note",
"created_at": now,
"last_modified": now,
"edit_count": 0,
"content_hash": hashlib.sha256(body.encode("utf-8")).hexdigest(),
# Caller passes the full body — the repo clips.
"last_body": body,
}
@pytest.mark.asyncio
async def test_repo_clips_oversized_body_at_insert(repo):
body = "A" * (_LIMIT * 2)
uuid = await repo.record_synthetic_file(_row(body))
rows = await repo.list_synthetic_files(decky_uuid="d1")
assert len(rows) == 1
stored = rows[0]
assert stored["uuid"] == uuid
assert len(stored["last_body"]) == _LIMIT
@pytest.mark.asyncio
async def test_body_at_exact_limit_is_preserved(repo):
body = "B" * _LIMIT
await repo.record_synthetic_file(_row(body))
rows = await repo.list_synthetic_files(decky_uuid="d1")
assert len(rows[0]["last_body"]) == _LIMIT
@pytest.mark.asyncio
async def test_pick_for_edit_returns_clipped_body(repo):
body = "C" * (_LIMIT * 3)
await repo.record_synthetic_file(_row(body))
candidate = await repo.pick_random_synthetic_file_for_edit("d1")
assert candidate is not None
assert len(candidate["last_body"]) == _LIMIT
@pytest.mark.asyncio
async def test_repo_clips_oversized_body_at_update(repo):
uuid = await repo.record_synthetic_file(_row("seed"))
big = "D" * (_LIMIT * 4)
await repo.update_synthetic_file(
uuid,
{
"last_modified": datetime.now(timezone.utc),
"edit_count": 1,
"last_body": big,
},
)
rows = await repo.list_synthetic_files(decky_uuid="d1")
assert len(rows[0]["last_body"]) == _LIMIT

View File

@@ -0,0 +1,102 @@
"""Coverage for :mod:`decnet.realism.taxonomy`.
The enum values are persisted on ``synthetic_files.content_class`` and
flow through bus topics — renaming a member is a schema change, so the
stable-list test pins the wire format. ``Plan`` invariants (frozen,
edit requires previous_body) are tested too because the planner relies
on construction-time validation rather than a separate validator pass.
"""
from __future__ import annotations
from datetime import datetime, timezone
import pytest
from decnet.realism.taxonomy import ContentClass, Plan
def test_content_class_values_are_stable() -> None:
# If anyone renames or reorders, the assertion explodes — the
# enum is wire-visible (synthetic_files.content_class column,
# bus event payloads) so changes need a schema bump elsewhere.
assert {c.value for c in ContentClass} == {
"note", "todo", "draft", "script",
"log_cron", "log_daemon", "cache_tmp",
"email",
"canary_aws_creds", "canary_env_file", "canary_git_config",
"canary_ssh_key", "canary_honeydoc", "canary_honeydoc_docx",
"canary_honeydoc_pdf", "canary_mysql_dump",
}
@pytest.mark.parametrize("name", ["NOTE", "TODO", "DRAFT", "SCRIPT"])
def test_user_classes_classified(name: str) -> None:
cls = ContentClass[name]
assert cls.is_user_class()
assert not cls.is_system_class()
assert not cls.is_canary()
@pytest.mark.parametrize("name", ["LOG_CRON", "LOG_DAEMON", "CACHE_TMP"])
def test_system_classes_classified(name: str) -> None:
cls = ContentClass[name]
assert cls.is_system_class()
assert not cls.is_user_class()
assert not cls.is_canary()
def test_canary_members_all_classified() -> None:
canaries = [c for c in ContentClass if c.value.startswith("canary_")]
assert canaries, "expected at least one canary content_class"
for c in canaries:
assert c.is_canary()
assert not c.is_user_class()
assert not c.is_system_class()
def test_email_is_neither_user_nor_system_nor_canary() -> None:
# Email lives on its own track — same content engine but a
# different driver and a different table. Classification helpers
# must not falsely group it into file-class buckets.
assert ContentClass.EMAIL.value == "email"
assert not ContentClass.EMAIL.is_user_class()
assert not ContentClass.EMAIL.is_system_class()
assert not ContentClass.EMAIL.is_canary()
def _plan(**kw):
defaults = dict(
decky_uuid="d-1",
decky_name="alpha",
persona="admin",
content_class=ContentClass.NOTE,
action="create",
target_path="/home/admin/notes.txt",
mtime=datetime(2026, 4, 25, 11, 30, tzinfo=timezone.utc),
body_hint="todo: rotate keys",
)
defaults.update(kw)
return Plan(**defaults)
def test_plan_is_frozen() -> None:
p = _plan()
with pytest.raises(Exception): # FrozenInstanceError or AttributeError
p.persona = "ubuntu" # type: ignore[misc]
def test_edit_plan_requires_previous_body() -> None:
with pytest.raises(ValueError, match="previous_body"):
_plan(action="edit", previous_body=None)
def test_edit_plan_with_previous_body_succeeds() -> None:
p = _plan(action="edit", previous_body="- [ ] rotate keys\n")
assert p.action == "edit"
assert p.previous_body == "- [ ] rotate keys\n"
def test_create_plan_does_not_need_previous_body() -> None:
p = _plan(action="create", previous_body=None)
assert p.action == "create"
assert p.previous_body is None