jobbsok/tests/test_workspace_fixtures.py
Kjell Tore Guttormsen b80bd00781 test(m2): build the eight-case fixture workspace
Eight cases, one per build-brief 6 state, dated relative to 2026-09-15. One
of them (havbris) carries frontmatter that deliberately disagrees with its
log, so the derived-truth rule has a fixture instead of only a docstring. One
(vaeroy-sjomat) is named for an employer with ae, o-slash and a-ring, so the
ASCII path rule has teeth on macOS NFD.

Six beslutninger.jsonl fixtures for M6's counting rules are written now, in
M2, because the store is append-only: type, skjema, delscore and vekt_hash
have to be right at the first write or they are wrong forever.

Co-Authored-By: Claude <claude-opus-5>
2026-09-05 21:38:24 +02:00

133 lines
5.8 KiB
Python

"""The eight-case fixture workspace every later milestone reads (plan Step 19).
A corpus is a contract with the milestones that have not been written yet.
M3 ingests into it, M4 drafts against it, M6 counts from it, and each of those
will assume the corpus says what this file asserts it says. So the assertions
here are about the corpus itself -- that each case really does resolve to the
state its name claims, that the one case built to disagree with its own
frontmatter actually disagrees, and that nothing in it carries a real name, a
real employer or a resolvable host.
The decision fixtures are M6's, written now on purpose. `beslutninger.jsonl`
is append-only, so its schema is irreversible: `type` and `skjema` have to be
right at the first write or they are wrong forever (risk H10). Writing M6's
counting fixtures in M2 is what forces that schema to be decided while it can
still be changed.
The ASCII assertion is not decoration. macOS hands back filenames in NFD, so a
Norwegian employer name can produce two directories that look identical and
are different byte strings (risk H8). One case in the corpus is named for an
employer with three of the four characters that provoke it.
Style note: this file follows tests/test_fixture_hygiene.py.
"""
import os
import test_fixture_hygiene as hygiene
import sak_status
from jobbsok_lib import jsonl
FIXTURES = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fixtures")
KORPUS = os.path.join(FIXTURES, "workspace")
BESLUTNINGER = os.path.join(FIXTURES, "beslutninger")
TODAY = "2026-09-15"
#: sak-id -> (status, ventende_part, flags). One case per state in build-brief
#: 6, dated relative to the frozen today.
FORVENTET = {
"2026-09-aurland-maritim-as-prosjektleder": ("trukket", "ingen", []),
"2026-09-bratthaug-industri-as-systemarkitekt": ("intervju", "dem", ["intervju_10"]),
"2026-09-havbris-energi-as-losningsarkitekt": ("sendt", "dem", ["sendt_14"]),
"2026-09-myrvang-teknologi-as-utvikler": ("avslag", "ingen", []),
"2026-09-nordlys-data-as-ai-radgiver": ("vurderer", "meg", []),
"2026-09-solvang-helse-as-teknisk-radgiver": ("tilbud", "meg", []),
"2026-09-storelva-kommune-fagleder-digitalisering": ("dialog", "dem", ["dialog_7"]),
"2026-09-vaeroy-sjomat-as-dataingenior": ("soker", "meg", []),
}
#: The one case whose cached frontmatter is deliberately wrong, and the keys
#: it is wrong about.
UENIG = "2026-09-havbris-energi-as-losningsarkitekt"
UENIG_NOKLER = ["status", "ventende_part"]
#: M6's counting fixtures. The name says what each one is for, because a
#: counting rule tested against a file called `data2.jsonl` is a rule nobody
#: can check.
BESLUTNINGSFIKSTURER = (
"ni-beslutninger.jsonl",
"ti-beslutninger.jsonl",
"tjue-lonnsdominert.jsonl",
"tjue-faa-ja.jsonl",
"med-korrigering.jsonl",
"med-utfall.jsonl",
)
def test_each_of_the_eight_cases_resolves_to_the_state_its_name_claims():
funnet = sak_status.alle_saker(KORPUS)
assert funnet == sorted(FORVENTET), (
"the corpus holds %r, the contract names %r" % (funnet, sorted(FORVENTET))
)
for sak_id, (status, ventende, flagg) in sorted(FORVENTET.items()):
resultat = sak_status.avgjor(sak_status.les_sak(KORPUS, sak_id)[2], TODAY)
assert resultat["status"] == status, sak_id
assert resultat["ventende_part"] == ventende, sak_id
assert resultat["flagg"] == flagg, sak_id
def test_the_case_built_to_disagree_with_its_frontmatter_reports_the_divergence():
avvik = sak_status.divergenser(KORPUS, TODAY)
assert [post["sak_id"] for post in avvik] == [UENIG], (
"exactly one case is supposed to disagree; found %r"
% ([post["sak_id"] for post in avvik],)
)
nokler = sorted(d["nokkel"] for d in avvik[0]["divergens"])
assert nokler == UENIG_NOKLER
resultat = sak_status.status_for_sak(KORPUS, UENIG, TODAY)
assert resultat["status"] == "sendt", "the log wins over the cache (risk H3)"
def test_every_generated_path_component_in_the_corpus_is_pure_ascii():
komponenter = []
for dirpath, dirnames, filenames in os.walk(KORPUS):
dirnames.sort()
komponenter.extend(dirnames)
komponenter.extend(sorted(filenames))
assert komponenter, "walked an empty corpus; the assertion below proves nothing"
for navn in komponenter:
assert navn.encode("ascii", "ignore").decode("ascii") == navn, (
"%r is not pure ASCII; on macOS its NFD twin is a second directory "
"(risk H8)" % navn
)
# The corpus must actually contain a name that could have gone wrong.
assert any("vaeroy" in navn for navn in komponenter), (
"no Norwegian-derived slug in the corpus, so the rule has no teeth"
)
def test_the_hygiene_scan_passes_over_the_new_corpus_and_states_its_denominator():
resultat = hygiene.scan(KORPUS)
assert resultat["findings"] == [], "hygiene findings: %r" % (resultat["findings"],)
melding = hygiene.report(resultat, KORPUS)
assert resultat["files"] == 2 * len(FORVENTET), (
"expected a sak.md and a logg.jsonl per case, scanned %d files"
% resultat["files"]
)
assert resultat["bytes"] > 0 and str(resultat["files"]) in melding
def test_every_decision_fixture_parses_and_carries_type_and_skjema():
funnet = sorted(os.listdir(BESLUTNINGER))
assert funnet == sorted(BESLUTNINGSFIKSTURER), funnet
for navn in funnet:
linjer = jsonl.read_lines(os.path.join(BESLUTNINGER, navn))
assert linjer, "%s is empty" % navn
for nummer, linje in enumerate(linjer, start=1):
assert linje.get("type") in jsonl.TYPES, (navn, nummer, linje.get("type"))
assert linje.get("skjema") == jsonl.SKJEMA, (navn, nummer)
# fold() is M6's reader; a fixture it cannot fold is a fixture that
# will fail in M6 instead of here.
jsonl.fold(linjer)