feat(m2): add dagens text view with golden output
This commit is contained in:
parent
06b45e9168
commit
bdb6291042
4 changed files with 578 additions and 0 deletions
253
scripts/dagens.py
Normal file
253
scripts/dagens.py
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
"""The daily operating view, as deterministic text (plan Step 23).
|
||||
|
||||
Build-brief 6 says the silence flags "surface in `dagens`". This is where they
|
||||
surface, and at this milestone that is plain text -- the HTML dashboard belongs
|
||||
to M6, and building it now would put a rendering layer between the operator and
|
||||
the first thing the status machine ever produced for them.
|
||||
|
||||
Four sections, in the order the brief names them: what needs action, what has
|
||||
gone silent, upcoming deadlines, and the pipeline by state.
|
||||
|
||||
**It writes nothing.** Not the cache, not a log, not a temporary file. The
|
||||
operator reads this several times a day against a workspace they are working
|
||||
in, and a view that refreshed something as a side effect would make reading a
|
||||
write. `sak_status.py --oppdater` stays the only writer of the four cached
|
||||
keys; when this view disagrees with a `sak.md`, it says so and names the
|
||||
command rather than fixing it in passing.
|
||||
|
||||
**Two kinds of deadline, told apart rather than merged.** The status machine
|
||||
derives `neste_frist` as the date a case goes silent, and derives nothing at
|
||||
all in `vurderer` and `soker` -- where the date in the frontmatter is the
|
||||
operator's own application deadline. Both are deadlines and neither is the
|
||||
other, so each entry carries which kind it is. Printing a single unlabelled
|
||||
date would have quietly told the operator that an application deadline is
|
||||
something the machine computed.
|
||||
|
||||
**An absent inbound event is a fact about the log.** With the mail server
|
||||
deferred to M4 nothing reads the inbox, so "nothing inbound recorded" cannot
|
||||
mean "nobody answered" (risk H9). Those cases carry the status machine's own
|
||||
note, verbatim, and never turn up in the silence section on that basis alone.
|
||||
|
||||
Determinism is a property of the interface. The date is injected, never read
|
||||
off the wall clock, and nothing that reaches stdout is iterated out of a dict
|
||||
or a set: two runs are byte-identical, including under different hash seeds.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
import sak_status
|
||||
from jobbsok_lib import frontmatter as frontmatter_lib
|
||||
from jobbsok_lib import jsonl, paths
|
||||
|
||||
#: Section headings, in build-brief 6's order. Norwegian, because everything
|
||||
#: the operator reads is Norwegian.
|
||||
KREVER_HANDLING = "Krever handling"
|
||||
GATT_STILLE = "Gått stille"
|
||||
KOMMENDE_FRISTER = "Kommende frister"
|
||||
PER_TILSTAND = "Saker per tilstand"
|
||||
|
||||
#: What a deadline is, when there is one. The derived one is the date silence
|
||||
#: trips; the cached one is the operator's own, and no machine here can
|
||||
#: recompute it.
|
||||
STILLHETSFRIST = "stillhetsfrist"
|
||||
SOKNADSFRIST = "søknadsfrist"
|
||||
|
||||
TOM = " (ingen)"
|
||||
|
||||
|
||||
def samle(root, today):
|
||||
"""Everything the view shows, as one ordered structure. Pure reader."""
|
||||
data = sak_status.rapport(root, today)
|
||||
saker = []
|
||||
for sak in data["saker"]:
|
||||
meta, _body = frontmatter_lib.read(root, "saker", sak["sak_id"], "sak.md")
|
||||
frist, frist_kilde = _frist(sak, meta)
|
||||
beriket = dict(sak)
|
||||
beriket["frist"] = frist
|
||||
beriket["frist_kilde"] = frist_kilde
|
||||
saker.append(beriket)
|
||||
return {"dato": data["dato"], "saker": saker}
|
||||
|
||||
|
||||
def _frist(sak, meta):
|
||||
"""The date this case is next measured against, and which kind it is."""
|
||||
utledet = sak.get("neste_frist")
|
||||
if utledet:
|
||||
return utledet, STILLHETSFRIST
|
||||
bufret = meta.get("neste_frist")
|
||||
if bufret in (None, "", sak_status.NULL):
|
||||
return None, None
|
||||
return str(bufret), SOKNADSFRIST
|
||||
|
||||
|
||||
def krever_handling(saker):
|
||||
"""Non-terminal cases the operator is the one holding up."""
|
||||
return [s for s in saker
|
||||
if s["status"] not in sak_status.TERMINALE and s["ventende_part"] == "meg"]
|
||||
|
||||
|
||||
def gatt_stille(saker):
|
||||
"""Cases a silence rule has actually fired on -- and only those.
|
||||
|
||||
Membership is the flag list the status machine derived, never the absence
|
||||
of an inbound event: those two look alike and mean opposite things.
|
||||
"""
|
||||
return [s for s in saker if s["flagg"]]
|
||||
|
||||
|
||||
def kommende_frister(saker, dagen):
|
||||
"""Deadlines still ahead, earliest first. A passed one is in the section
|
||||
above, and repeating it here would double-count the same fact."""
|
||||
ut = [s for s in saker
|
||||
if s["status"] not in sak_status.TERMINALE and s["frist"] and s["frist"] >= dagen]
|
||||
# ISO-8601 dates sort correctly as text, and sorting on the text keeps the
|
||||
# ordering independent of any locale.
|
||||
return sorted(ut, key=lambda s: (s["frist"], s["sak_id"]))
|
||||
|
||||
|
||||
def per_tilstand(saker):
|
||||
"""(state, count) for all nine states, in the brief's order.
|
||||
|
||||
All nine, including the empty ones: a pipeline with nothing in `tilbud` is
|
||||
something the operator should be able to see rather than infer from a
|
||||
missing line. Iterating the fixed tuple is also what keeps this out of a
|
||||
dict's ordering.
|
||||
"""
|
||||
return [(tilstand, len([s for s in saker if s["status"] == tilstand]))
|
||||
for tilstand in sak_status.TILSTANDER]
|
||||
|
||||
|
||||
def render(data):
|
||||
"""The whole view as text. Every ordering here is fixed, none is a dict's."""
|
||||
saker = data["saker"]
|
||||
dagen = data["dato"]
|
||||
|
||||
linjer = ["dagens %s (%d saker)" % (dagen, len(saker)), ""]
|
||||
|
||||
avvikende = [s["sak_id"] for s in saker if s["divergens"]]
|
||||
if avvikende:
|
||||
# Reported, never repaired: this view does not write, and a cache
|
||||
# quietly fixed by a reader is a divergence nobody ever learns about.
|
||||
linjer.append(
|
||||
"NB: %d %s en hurtigbuffer som er uenig med loggen: %s."
|
||||
% (len(avvikende), "sak har" if len(avvikende) == 1 else "saker har",
|
||||
", ".join(avvikende))
|
||||
)
|
||||
linjer.append(
|
||||
" Tallene nedenfor er utledet fra loggen. Kjør sak_status.py "
|
||||
"--oppdater for å rette sak.md."
|
||||
)
|
||||
linjer.append("")
|
||||
|
||||
linjer.extend(_saksseksjon(KREVER_HANDLING, krever_handling(saker)))
|
||||
linjer.extend(_saksseksjon(GATT_STILLE, gatt_stille(saker)))
|
||||
linjer.extend(_fristseksjon(kommende_frister(saker, dagen)))
|
||||
linjer.extend(_tilstandsseksjon(saker))
|
||||
return "\n".join(linjer)
|
||||
|
||||
|
||||
def _saksseksjon(tittel, utvalg):
|
||||
linjer = ["## %s (%d)" % (tittel, len(utvalg)), ""]
|
||||
if not utvalg:
|
||||
linjer.extend([TOM, ""])
|
||||
return linjer
|
||||
for sak in utvalg:
|
||||
linjer.extend(_sakslinjer(sak))
|
||||
return linjer
|
||||
|
||||
|
||||
def _sakslinjer(sak):
|
||||
linjer = [
|
||||
"- %s -- %s, %s" % (sak["sak_id"], sak["arbeidsgiver"], sak["rolle"]),
|
||||
" %s, ventende: %s, sist aktivitet: %s"
|
||||
% (sak["status"], sak["ventende_part"], sak["sist_aktivitet"] or "-"),
|
||||
]
|
||||
if sak["flagg"]:
|
||||
linjer.append(
|
||||
" flagg: %s (terskel passert %s)"
|
||||
% (", ".join(sak["flagg"]), sak["neste_frist"] or "-")
|
||||
)
|
||||
# The flag line above already states a passed threshold by its date, so a
|
||||
# `frist:` line repeating the same number is noise rather than a second
|
||||
# fact. A deadline the flag line does not carry is still printed.
|
||||
if sak["frist"] and not (sak["flagg"] and sak["frist"] == sak["neste_frist"]):
|
||||
linjer.append(" frist: %s (%s)" % (sak["frist"], sak["frist_kilde"]))
|
||||
if not sak["innkommende"]["registrert"]:
|
||||
linjer.append(" merknad: %s" % sak["innkommende"]["merknad"])
|
||||
linjer.append("")
|
||||
return linjer
|
||||
|
||||
|
||||
def _fristseksjon(utvalg):
|
||||
linjer = ["## %s (%d)" % (KOMMENDE_FRISTER, len(utvalg)), ""]
|
||||
if not utvalg:
|
||||
linjer.extend([TOM, ""])
|
||||
return linjer
|
||||
for sak in utvalg:
|
||||
linjer.append("- %s %s -- %s" % (sak["frist"], sak["sak_id"], sak["frist_kilde"]))
|
||||
linjer.append("")
|
||||
return linjer
|
||||
|
||||
|
||||
def _tilstandsseksjon(saker):
|
||||
linjer = ["## %s" % PER_TILSTAND, ""]
|
||||
for tilstand, antall in per_tilstand(saker):
|
||||
linjer.append(" %-11s %d" % (tilstand, antall))
|
||||
linjer.append(" %-11s %d" % ("i alt", len(saker)))
|
||||
linjer.append("")
|
||||
return linjer
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Command line
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
EXIT_OK = 0
|
||||
EXIT_FEIL = 2
|
||||
|
||||
|
||||
def build_parser():
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="dagens.py",
|
||||
description=(
|
||||
"Dagens arbeidsbilde som ren tekst: hva som krever handling, hva "
|
||||
"som har gaatt stille, kommende frister og saker per tilstand. "
|
||||
"Skriver ingenting."
|
||||
),
|
||||
)
|
||||
parser.add_argument("--workspace", required=True,
|
||||
help="arbeidsomraadets rot (ingen underforstaatt standard)")
|
||||
parser.add_argument("--today", default=None, metavar="YYYY-MM-DD",
|
||||
help="klokka som skal brukes; injisert, aldri veggklokka")
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv=None, stdout=None, stderr=None):
|
||||
import sys as _sys
|
||||
|
||||
argv = _sys.argv[1:] if argv is None else argv
|
||||
stdout = _sys.stdout if stdout is None else stdout
|
||||
stderr = _sys.stderr if stderr is None else stderr
|
||||
|
||||
args = build_parser().parse_args(argv)
|
||||
today = args.today or datetime.date.today().isoformat()
|
||||
|
||||
try:
|
||||
root = paths.workspace_root(args.workspace)
|
||||
tekst = render(samle(root, today))
|
||||
except (sak_status.StatusError, paths.WorkspaceError, jsonl.JsonlError,
|
||||
frontmatter_lib.FrontmatterError, OSError) as feil:
|
||||
stderr.write("dagens: %s\n" % feil)
|
||||
return EXIT_FEIL
|
||||
|
||||
stdout.write(tekst)
|
||||
stdout.flush()
|
||||
return EXIT_OK
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys as _sys
|
||||
|
||||
_sys.exit(main())
|
||||
86
skills/dagens/SKILL.md
Normal file
86
skills/dagens/SKILL.md
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
---
|
||||
name: dagens
|
||||
description: "Viser dagens arbeidsbilde fra arbeidsmappa: hva som krever handling, hva som har gaatt stille, kommende frister og saker per tilstand. Skriver ingenting, og eier ogsaa versjonsekkoet paa forespoersel. Utloeses av vis dagens, hva skjer i dag, hva har gått stille, hva har gatt stille, hvilke frister kommer og vis byggestempelet."
|
||||
triggers:
|
||||
- vis dagens
|
||||
- hva skjer i dag
|
||||
- hva har gått stille
|
||||
- hva har gatt stille
|
||||
- hvilke frister kommer
|
||||
- vis byggestempelet
|
||||
---
|
||||
|
||||
# dagens
|
||||
|
||||
Operatørens daglige inngang. **Skriver ingenting** — ingen sak, ingen logg,
|
||||
ingen hurtigbuffer. Visningen leses flere ganger om dagen mens arbeidsmappa er
|
||||
i bruk, og en visning som oppdaterte noe underveis ville gjort lesing til
|
||||
skriving.
|
||||
|
||||
All operatørrettet tekst er norsk bokmål.
|
||||
|
||||
## Kjør den
|
||||
|
||||
```
|
||||
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/dagens.py \
|
||||
--workspace <arbeidsmappa> --today <YYYY-MM-DD>
|
||||
```
|
||||
|
||||
På Windows heter tolken `python`, ikke `python3`.
|
||||
|
||||
Datoen injiseres. Utelates `--today`, brukes dagens dato — men i alt som skal
|
||||
kunne reproduseres, oppgi den. Utdata er ren tekst og er byte-stabil: samme
|
||||
arbeidsmappe og samme dato gir samme bytes hver gang.
|
||||
|
||||
Regn aldri ut noe av dette selv. Alt kommer fra statusmaskinen, og en visning
|
||||
som resonnerte seg fram til en tilstand ved siden av scriptet ville vært en
|
||||
andre statusmaskin ingen har testet.
|
||||
|
||||
## De fire seksjonene
|
||||
|
||||
| Seksjon | Hva som står der |
|
||||
|---|---|
|
||||
| **Krever handling** | Ikke-terminale saker der ventende part er `meg` |
|
||||
| **Gått stille** | Saker der en av de tre stillhetsreglene faktisk har slått til |
|
||||
| **Kommende frister** | Frister som ennå ikke har passert, tidligst først |
|
||||
| **Saker per tilstand** | Alle ni tilstandene, også de tomme |
|
||||
|
||||
**«Gått stille» er flagg, ikke fravær.** En sak havner der bare fordi
|
||||
statusmaskinen har satt et flagg — aldri fordi det mangler en innkommende
|
||||
hendelse. De to ser like ut og betyr det motsatte.
|
||||
|
||||
**To slags frister, og de er merket.** `stillhetsfrist` er datoen saken går
|
||||
stille, utledet av maskinen. `søknadsfrist` er operatørens egen dato fra
|
||||
saksfila, som ingen maskin kan regne ut på nytt. Gjengi merkingen — en
|
||||
umerket dato ville fortalt operatøren at maskinen står inne for begge.
|
||||
|
||||
## «Ingen innkommende registrert»
|
||||
|
||||
Står den merknaden på en sak, gjengi den som den er. E-postserveren er utsatt
|
||||
til M4, så ingenting leser innboksen ennå: fraværet er i loggen, ikke i verden.
|
||||
Ikke oppsummer det til «ingen har svart».
|
||||
|
||||
## Står det NB øverst
|
||||
|
||||
Da er en saksfil uenig med sin egen logg. Visningen viser det loggen sier, og
|
||||
retter ingenting. Si fra om avviket og foreslå
|
||||
`${CLAUDE_PLUGIN_ROOT}/scripts/sak_status.py --oppdater` — la `sak` gjøre
|
||||
selve rettingen.
|
||||
|
||||
## Versjonsekko — bare på forespørsel
|
||||
|
||||
Blir du spurt om hvilken versjon som kjører, skriv ut begge deler: innholdet i
|
||||
`${CLAUDE_PLUGIN_ROOT}/BUILD_STAMP` og `version` fra plugin-manifestet
|
||||
`.claude-plugin/plugin.json`. Samme form som `kandidatprofil` bruker. Mangler
|
||||
stempelfila, si det rett ut i stedet for å oppgi manifestversjonen alene som om
|
||||
den var hele svaret.
|
||||
|
||||
**Aldri i den daglige visningen.** Stempelet endrer seg ved hver commit, og et
|
||||
stempel inni tekstutdataen ville gjort visningen ustabil fra commit til commit.
|
||||
Ekkoet kommer fra skillen, ikke fra scriptet, og bare når noen spør.
|
||||
|
||||
## Uten verktøyserveren
|
||||
|
||||
Er `jobbsok-tools` ikke tilgjengelig, degraderer skillen i stedet for å stoppe:
|
||||
be operatøren kjøre kommandoen over fra kommandolinja, og les teksten som ble
|
||||
skrevet ut. Innholdet er det samme uansett hvilken vei det kom.
|
||||
52
tests/golden/dagens-2026-09-15.txt
Normal file
52
tests/golden/dagens-2026-09-15.txt
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
dagens 2026-09-15 (8 saker)
|
||||
|
||||
NB: 1 sak har en hurtigbuffer som er uenig med loggen: 2026-09-havbris-energi-as-losningsarkitekt.
|
||||
Tallene nedenfor er utledet fra loggen. Kjør sak_status.py --oppdater for å rette sak.md.
|
||||
|
||||
## Krever handling (3)
|
||||
|
||||
- 2026-09-nordlys-data-as-ai-radgiver -- Nordlys Data AS, AI-rådgiver
|
||||
vurderer, ventende: meg, sist aktivitet: 2026-09-13
|
||||
frist: 2026-09-25 (søknadsfrist)
|
||||
merknad: ingen innkommende hendelse er registrert. Med e-postserveren utsatt betyr det ikke at ingen finnes.
|
||||
|
||||
- 2026-09-solvang-helse-as-teknisk-radgiver -- Solvang Helse AS, Teknisk rådgiver
|
||||
tilbud, ventende: meg, sist aktivitet: 2026-09-12
|
||||
|
||||
- 2026-09-vaeroy-sjomat-as-dataingenior -- Værøy Sjømat AS, Dataingeniør
|
||||
soker, ventende: meg, sist aktivitet: 2026-09-10
|
||||
frist: 2026-09-20 (søknadsfrist)
|
||||
merknad: ingen innkommende hendelse er registrert. Med e-postserveren utsatt betyr det ikke at ingen finnes.
|
||||
|
||||
## Gått stille (3)
|
||||
|
||||
- 2026-09-bratthaug-industri-as-systemarkitekt -- Bratthaug Industri AS, Systemarkitekt
|
||||
intervju, ventende: dem, sist aktivitet: 2026-08-26
|
||||
flagg: intervju_10 (terskel passert 2026-09-05)
|
||||
|
||||
- 2026-09-havbris-energi-as-losningsarkitekt -- Havbris Energi AS, Løsningsarkitekt
|
||||
sendt, ventende: dem, sist aktivitet: 2026-08-27
|
||||
flagg: sendt_14 (terskel passert 2026-09-09)
|
||||
merknad: ingen innkommende hendelse er registrert. Med e-postserveren utsatt betyr det ikke at ingen finnes.
|
||||
|
||||
- 2026-09-storelva-kommune-fagleder-digitalisering -- Storelva kommune, Fagleder digitalisering
|
||||
dialog, ventende: dem, sist aktivitet: 2026-08-22
|
||||
flagg: dialog_7 (terskel passert 2026-08-28)
|
||||
|
||||
## Kommende frister (2)
|
||||
|
||||
- 2026-09-20 2026-09-vaeroy-sjomat-as-dataingenior -- søknadsfrist
|
||||
- 2026-09-25 2026-09-nordlys-data-as-ai-radgiver -- søknadsfrist
|
||||
|
||||
## Saker per tilstand
|
||||
|
||||
vurderer 1
|
||||
soker 1
|
||||
sendt 1
|
||||
dialog 1
|
||||
intervju 1
|
||||
tilbud 1
|
||||
avsluttet 0
|
||||
avslag 1
|
||||
trukket 1
|
||||
i alt 8
|
||||
187
tests/test_dagens_text.py
Normal file
187
tests/test_dagens_text.py
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
"""The daily operating view as plain text, and what it must not become
|
||||
(plan Step 23).
|
||||
|
||||
`dagens` is build-brief 6's consumer: the flags the status machine derives
|
||||
surface here or nowhere. At this milestone it is text -- the HTML dashboard is
|
||||
M6's job -- and text pinned to a golden file, because a daily view whose
|
||||
wording drifts is a daily view nobody can diff.
|
||||
|
||||
Three properties carry the section, and each is here for a measured reason.
|
||||
|
||||
**It writes nothing.** The view is read on a workspace the operator is working
|
||||
in, often several times a day. A view that refreshed a cache as a side effect
|
||||
would make reading the pipeline a write, and `sak_status.py --oppdater` is
|
||||
deliberately the only writer of those four keys.
|
||||
|
||||
**"No inbound recorded" is not "gone silent".** With the mail server deferred
|
||||
to M4, nothing reads the inbox, so an absent inbound event is a fact about the
|
||||
log and not about the world (risk H9). A view that read the absence as silence
|
||||
would flag most of the pipeline on its first run and be switched off by the
|
||||
second.
|
||||
|
||||
**The version echo lives in the skill, never in this output.** Step 9 promised
|
||||
the echo from `dagens` and Step 23 is where that promise is kept -- but the
|
||||
stamp changes with every commit, so a stamp inside the text would break this
|
||||
step's own golden at the next commit. The echo is on request, from the skill.
|
||||
|
||||
Style note: this file follows tests/test_sak_status_cli.py.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
import sak_status
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
CLI = os.path.join(REPO, "scripts", "dagens.py")
|
||||
KORPUS = os.path.join(REPO, "tests", "fixtures", "workspace")
|
||||
GOLDEN = os.path.join(REPO, "tests", "golden", "dagens-2026-09-15.txt")
|
||||
SKILL = os.path.join(REPO, "skills", "dagens", "SKILL.md")
|
||||
STEMPEL = os.path.join(REPO, "BUILD_STAMP")
|
||||
TODAY = "2026-09-15"
|
||||
|
||||
#: The three the status machine flags at the frozen date, one per rule.
|
||||
FLAGGEDE = {
|
||||
"2026-09-bratthaug-industri-as-systemarkitekt": "intervju_10",
|
||||
"2026-09-havbris-energi-as-losningsarkitekt": "sendt_14",
|
||||
"2026-09-storelva-kommune-fagleder-digitalisering": "dialog_7",
|
||||
}
|
||||
|
||||
#: A case with no inbound recorded and no flag. It must be labelled, and it
|
||||
#: must not be presented as having gone quiet.
|
||||
UTEN_INNKOMMENDE = "2026-09-nordlys-data-as-ai-radgiver"
|
||||
|
||||
|
||||
def kjor(*argv, **kwargs):
|
||||
env = dict(os.environ)
|
||||
env.pop("JOBBSOK_WORKSPACE", None)
|
||||
env.pop("CLAUDE_PLUGIN_DATA", None)
|
||||
env.update(kwargs.get("env", {}))
|
||||
proc = subprocess.run(
|
||||
[sys.executable, CLI] + list(argv),
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env,
|
||||
)
|
||||
return proc.returncode, proc.stdout, proc.stderr
|
||||
|
||||
|
||||
def visning(workspace=KORPUS, today=TODAY):
|
||||
kode, ut, feil = kjor("--workspace", workspace, "--today", today)
|
||||
assert kode == 0, feil.decode("utf-8")
|
||||
return ut.decode("utf-8")
|
||||
|
||||
|
||||
def seksjoner(tekst):
|
||||
"""Split the view into {heading: body}, so a claim about a section is one."""
|
||||
deler = re.split(r"^## ", tekst, flags=re.M)
|
||||
ut = {}
|
||||
for del_ in deler[1:]:
|
||||
linjer = del_.split("\n")
|
||||
overskrift = re.sub(r"\s*\(\d+\)\s*$", "", linjer[0]).strip()
|
||||
ut[overskrift] = "\n".join(linjer[1:])
|
||||
return ut
|
||||
|
||||
|
||||
def digest(root):
|
||||
ut = {}
|
||||
for dirpath, dirnames, filenames in os.walk(root):
|
||||
dirnames.sort()
|
||||
for navn in sorted(filenames):
|
||||
full = os.path.join(dirpath, navn)
|
||||
with open(full, "rb") as handle:
|
||||
ut[os.path.relpath(full, root)] = hashlib.sha256(handle.read()).hexdigest()
|
||||
return ut
|
||||
|
||||
|
||||
def test_the_text_view_matches_the_golden_for_the_fixture_workspace(golden):
|
||||
golden(GOLDEN, visning())
|
||||
|
||||
|
||||
def test_the_three_flagged_cases_appear_under_the_silence_section():
|
||||
deler = seksjoner(visning())
|
||||
stille = [navn for navn in deler if "stille" in navn.lower()]
|
||||
assert len(stille) == 1, "expected exactly one silence section, found %r" % (list(deler),)
|
||||
kropp = deler[stille[0]]
|
||||
|
||||
for sak_id, flagg in FLAGGEDE.items():
|
||||
assert sak_id in kropp, "%s is flagged but not in the silence section" % sak_id
|
||||
assert flagg in kropp, "the section never names the rule %r that fired" % flagg
|
||||
|
||||
# And nothing else: a section that listed the whole pipeline would be a
|
||||
# section the operator learns to skip. Membership is checked against the
|
||||
# corpus's own case list rather than a pattern, because a pattern loose
|
||||
# enough to catch a sak-id also catches the ISO dates in the same block.
|
||||
oppforte = [sak_id for sak_id in sak_status.alle_saker(KORPUS) if sak_id in kropp]
|
||||
assert sorted(oppforte) == sorted(FLAGGEDE), (
|
||||
"the silence section lists %r; only the flagged three belong there"
|
||||
% (sorted(oppforte),)
|
||||
)
|
||||
|
||||
|
||||
def test_a_case_with_no_recorded_inbound_is_labelled_and_not_called_silent():
|
||||
tekst = visning()
|
||||
deler = seksjoner(tekst)
|
||||
stille = deler[[navn for navn in deler if "stille" in navn.lower()][0]]
|
||||
|
||||
assert UTEN_INNKOMMENDE not in stille, (
|
||||
"a case with nothing recorded inbound was presented as having gone "
|
||||
"quiet; with the mail server deferred that is a fact about the log"
|
||||
)
|
||||
assert UTEN_INNKOMMENDE in tekst, "the case is missing from the view entirely"
|
||||
|
||||
# Labelled, and in the status machine's own words rather than a second
|
||||
# paraphrase that could drift from it.
|
||||
rapport = sak_status.rapport(KORPUS, TODAY, UTEN_INNKOMMENDE)
|
||||
merknad = rapport["saker"][0]["innkommende"]["merknad"]
|
||||
assert merknad and merknad in tekst, (
|
||||
"the view never carries the %r note" % (merknad,)
|
||||
)
|
||||
|
||||
|
||||
def test_the_run_writes_nothing(workspace):
|
||||
forst = digest(workspace)
|
||||
kode, _ut, feil = kjor("--workspace", workspace, "--today", TODAY)
|
||||
assert kode == 0, feil.decode("utf-8")
|
||||
assert digest(workspace) == forst, "the daily view wrote into the workspace"
|
||||
|
||||
|
||||
def test_two_runs_are_byte_identical():
|
||||
forste = visning()
|
||||
andre = visning()
|
||||
assert forste == andre
|
||||
|
||||
# And under two hash seeds, which is what turns "no dict reaches stdout"
|
||||
# from an intention into a measurement.
|
||||
_kode, null, _ = kjor("--workspace", KORPUS, "--today", TODAY,
|
||||
env={"PYTHONHASHSEED": "0"})
|
||||
_kode, mange, _ = kjor("--workspace", KORPUS, "--today", TODAY,
|
||||
env={"PYTHONHASHSEED": "1048573"})
|
||||
assert null == mange, (
|
||||
"output moved with PYTHONHASHSEED, so a dict or a set reached stdout"
|
||||
)
|
||||
|
||||
|
||||
def test_the_skill_carries_the_version_echo_and_the_golden_does_not():
|
||||
with open(SKILL, "r", encoding="utf-8") as handle:
|
||||
skill = handle.read()
|
||||
assert "BUILD_STAMP" in skill, (
|
||||
"Step 9 promised the echo from dagens in two places and kept it in "
|
||||
"none; this is the step that keeps it"
|
||||
)
|
||||
assert "plugin.json" in skill, "the echo prints the stamp and the version, not one"
|
||||
|
||||
tekst = visning()
|
||||
assert "BUILD_STAMP" not in tekst, (
|
||||
"the daily view carries the build stamp; it changes with every commit "
|
||||
"and would break this step's own golden at the next one"
|
||||
)
|
||||
if os.path.isfile(STEMPEL):
|
||||
with open(STEMPEL, "r", encoding="utf-8") as handle:
|
||||
stempel = handle.read().strip()
|
||||
assert stempel and stempel not in tekst, (
|
||||
"the stamp %r leaked into the daily view" % (stempel,)
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue