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
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