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>
This commit is contained in:
parent
4669cc363d
commit
8d577dc3ca
1 changed files with 170 additions and 0 deletions
170
tests/test_sak_status_silence.py
Normal file
170
tests/test_sak_status_silence.py
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
"""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,
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue