187 lines
7.2 KiB
Python
187 lines
7.2 KiB
Python
"""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,)
|
|
)
|