feat(m2): add dagens text view with golden output

This commit is contained in:
Kjell Tore Guttormsen 2026-09-05 22:04:36 +02:00
commit bdb6291042
4 changed files with 578 additions and 0 deletions

View 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
View 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,)
)