jobbsok/tests/test_sak_status_silence.py
Kjell Tore Guttormsen 8d577dc3ca feat(m2): add silence flags at the 14, 7 and 10 day thresholds
Iron Law deviation, stated rather than hidden: these tests passed on the
first run. The silence computation landed in 4669cc3 because neste_frist is
one of Step 17's four cached keys and is derived from the same thresholds --
the plan entangles the two steps, so the code could not be written in the
order the steps are numbered. What this file adds is the boundary evidence:
13/14/15, 6/7/8, 9/10/11, an autoreply that must not move the clock, and the
H9 distinction between no inbound RECORDED and no inbound existing.

Co-Authored-By: Claude <claude-opus-5>
2026-09-05 21:35:11 +02:00

170 lines
6.6 KiB
Python

"""The three silence thresholds, measured against an injected clock (Step 18).
build-brief 6 gives three rules and three numbers: 14 days in `sendt`, 7 in
`dialog` when the other party owes the reply, 10 after an interview that has
produced no outcome. Numbers in prose are easy to agree with and easy to be
one day out on, so every one of them is tested on both sides of its boundary
and on the boundary itself. A rule tested only in the middle of its range is a
rule whose off-by-one nobody would notice.
Two rules are not about arithmetic at all.
`bekreftelse_mottatt` does not reset the clock. An automated receipt is not
the employer making contact, and a machine that treated it as contact would
quietly hide the case the operator most needs to see. The transition table
leaves it out of `INNKOMMENDE`, and the boundary cases here are what makes
that a fact.
"No inbound recorded" is not "no inbound exists" (risk H9). With the mail
server deferred nothing ingests replies, so an empty inbound history says
something about this log and nothing about the world. The result carries that
distinction in words, and `dagens` reads it from there rather than presenting
every sent case as gone quiet.
Cases here are built with `make_case` rather than from the fixture corpus:
the corpus arrives in Step 19, and a test that needs a fixture from a later
step cannot run when its own step is executed.
Style note: this file follows tests/test_sak_status_transitions.py.
"""
import datetime
import pytest
from helpers import workspace as workspace_helper
import sak_status
TODAY = workspace_helper.FROZEN_TODAY
def hendelse(navn, days_ago):
return workspace_helper.event(navn, days_ago=days_ago)
def beslutning_ja(days_ago):
moment = workspace_helper.FROZEN_NOON - datetime.timedelta(days=days_ago)
return {"ts": moment.isoformat(), "type": "beslutning", "beslutning": "ja"}
def sendt_for(dager, med_bekreftelse=False):
"""A case whose application went out ``dager`` days ago and got no reply."""
logg = [hendelse("opprettet", dager + 2), beslutning_ja(dager + 1),
hendelse("soknad_sendt", dager)]
if med_bekreftelse:
# One day after the send, and it must not move the clock.
logg.append(hendelse("bekreftelse_mottatt", dager - 1))
return logg
def dialog_med_dem_skyldig(siden):
"""`dialog`, the other party owing, their last message ``siden`` days ago."""
return [
hendelse("opprettet", siden + 20), beslutning_ja(siden + 19),
hendelse("soknad_sendt", siden + 10),
hendelse("henvendelse_mottatt", siden),
hendelse("svar_sendt", siden - 1),
]
def intervju_holdt_for(dager):
return [
hendelse("opprettet", dager + 20), beslutning_ja(dager + 19),
hendelse("soknad_sendt", dager + 10),
hendelse("intervju_avtalt", dager + 5),
hendelse("intervju_gjennomfort", dager),
]
@pytest.mark.parametrize("dager,flagget", [(13, False), (14, True), (15, True)])
def test_the_sendt_threshold_is_exactly_fourteen_days(dager, flagget):
resultat = sak_status.avgjor(sendt_for(dager), TODAY)
assert resultat["status"] == "sendt"
assert ("sendt_14" in resultat["flagg"]) is flagget
@pytest.mark.parametrize("dager,flagget", [(13, False), (14, True), (15, True)])
def test_an_automated_receipt_does_not_reset_the_sendt_clock(dager, flagget):
med = sak_status.avgjor(sendt_for(dager, med_bekreftelse=True), TODAY)
assert ("sendt_14" in med["flagg"]) is flagget, (
"bekreftelse_mottatt moved the clock; an autoreply is not contact"
)
@pytest.mark.parametrize("dager,flagget", [(6, False), (7, True), (8, True)])
def test_the_dialog_threshold_is_exactly_seven_days(dager, flagget):
resultat = sak_status.avgjor(dialog_med_dem_skyldig(dager), TODAY)
assert resultat["status"] == "dialog" and resultat["ventende_part"] == "dem"
assert ("dialog_7" in resultat["flagg"]) is flagget
@pytest.mark.parametrize("dager,flagget", [(9, False), (10, True), (11, True)])
def test_the_interview_threshold_is_exactly_ten_days(dager, flagget):
resultat = sak_status.avgjor(intervju_holdt_for(dager), TODAY)
assert resultat["status"] == "intervju"
assert ("intervju_10" in resultat["flagg"]) is flagget
def test_a_dialog_where_i_owe_the_reply_is_never_flagged():
logg = [
hendelse("opprettet", 60), beslutning_ja(59), hendelse("soknad_sendt", 50),
hendelse("henvendelse_mottatt", 30),
]
resultat = sak_status.avgjor(logg, TODAY)
assert resultat["ventende_part"] == "meg"
assert resultat["flagg"] == [], (
"thirty days of my own silence is a to-do, not the employer going quiet"
)
@pytest.mark.parametrize("slutt", ("avslag", "trukket"))
def test_terminal_states_are_never_flagged(slutt):
logg = sendt_for(60) + [hendelse(slutt, 50)]
resultat = sak_status.avgjor(logg, TODAY)
assert resultat["status"] == slutt
assert resultat["flagg"] == []
assert resultat["neste_frist"] is None
def test_an_interview_with_an_outcome_stops_the_ten_day_clock():
logg = intervju_holdt_for(30) + [hendelse("tilbud_mottatt", 2)]
resultat = sak_status.avgjor(logg, TODAY)
assert resultat["status"] == "tilbud"
assert resultat["flagg"] == []
def test_two_rules_at_once_produce_both_flags_in_a_stable_order():
logg = [
hendelse("opprettet", 60), beslutning_ja(59), hendelse("soknad_sendt", 50),
hendelse("intervju_avtalt", 30),
hendelse("intervju_gjennomfort", 20),
hendelse("henvendelse_mottatt", 12),
hendelse("svar_sendt", 11),
]
resultat = sak_status.avgjor(logg, TODAY)
assert resultat["status"] == "dialog" and resultat["ventende_part"] == "dem"
assert resultat["flagg"] == ["dialog_7", "intervju_10"], (
"flags must come out in FLAGG order, never in the order the rules fired"
)
# The nearer of the two deadlines is the one the daily view has to show.
assert resultat["neste_frist"] == "2026-09-05"
def test_no_inbound_recorded_is_reported_as_such_and_not_as_bare_silence():
resultat = sak_status.avgjor(sendt_for(20, med_bekreftelse=True), TODAY)
innkommende = resultat["innkommende"]
assert innkommende["registrert"] is False
assert innkommende["sist"] is None
assert "registrert" in innkommende["merknad"], (
"the case is flagged on an empty inbound history, and with the mail "
"server deferred that has to be said out loud (risk H9)"
)
assert "sendt_14" in resultat["flagg"]
def test_a_recorded_inbound_carries_its_date_and_no_caveat():
resultat = sak_status.avgjor(dialog_med_dem_skyldig(9), TODAY)
assert resultat["innkommende"] == {
"registrert": True, "sist": "2026-09-06", "merknad": None,
}