decnet/realism/naming._home and decnet/canary/cultivator._persona_login both normalised "John Smith"→"johnsmith" with identical logic. Lift to decnet.realism.personas.login_for(persona) and have both consumers import it. Drift between the two would have left canary placement and realism path naming using different login derivations.
128 lines
3.7 KiB
Python
128 lines
3.7 KiB
Python
"""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
|