--workspace is required with no environment fall-back: this process runs unsandboxed, and a status run that quietly picked a workspace is one nobody can audit. --check exits 1 on a stale cache, distinct from 2 for a failed run. Byte-stability is measured, not asserted: the CLI is run twice under two PYTHONHASHSEED values and the bytes compared, in both output formats. The write boundary gets two digest tests rather than one -- without --oppdater every byte stays put, with it exactly the diverging case's four cached keys move and logg.jsonl, the body and every other file are byte-identical. Co-Authored-By: Claude <claude-opus-5>
154 lines
6.2 KiB
Python
154 lines
6.2 KiB
Python
"""The sak_status command line, and what it is allowed to write (plan Step 20).
|
|
|
|
Two properties are asserted here that no unit test can reach, because both are
|
|
about the process rather than the function.
|
|
|
|
**Byte-stability.** The golden file is only evidence if the same workspace and
|
|
the same `--today` produce the same bytes every time, including under a
|
|
different `PYTHONHASHSEED`. Python randomises string hashing per process, so a
|
|
dictionary or a set that reached stdout would reorder between runs and the
|
|
golden would fail intermittently -- the worst way for a determinism bug to
|
|
present itself. Running the CLI twice under two seeds and comparing bytes is
|
|
what turns "no dict iteration order reaches stdout" from an intention into a
|
|
measurement.
|
|
|
|
**The write boundary.** The read paths write nothing, and `--oppdater` is the
|
|
one exception -- not a caveat, a named mode. So there are two digest tests,
|
|
not one: a run without the flag leaves every byte in the workspace where it
|
|
was, and a run with it changes exactly the four cached frontmatter keys and
|
|
leaves `logg.jsonl`, the `sak.md` body and every other file byte-identical.
|
|
Saying "the script writes nothing" without that second test is what let the
|
|
write-back go unnoticed in the first place.
|
|
|
|
Style note: this file follows tests/test_sak_status_silence.py.
|
|
"""
|
|
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
import sak_status
|
|
from jobbsok_lib import frontmatter as frontmatter_lib
|
|
|
|
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
CLI = os.path.join(REPO, "scripts", "sak_status.py")
|
|
KORPUS = os.path.join(REPO, "tests", "fixtures", "workspace")
|
|
GOLDEN = os.path.join(REPO, "tests", "golden", "sak-status-2026-09-15.json")
|
|
TODAY = "2026-09-15"
|
|
|
|
#: The case whose cached frontmatter is deliberately wrong (Step 19).
|
|
UENIG = "2026-09-havbris-energi-as-losningsarkitekt"
|
|
|
|
|
|
def kjor(*argv, **kwargs):
|
|
"""Run the CLI as a process and return (exit code, stdout, stderr) bytes."""
|
|
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 digest(root):
|
|
"""Every file under ``root`` mapped to the sha256 of its bytes."""
|
|
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_json_output_matches_the_golden_file(golden):
|
|
kode, ut, feil = kjor("--workspace", KORPUS, "--today", TODAY, "--format", "json")
|
|
assert kode == 0, feil.decode("utf-8")
|
|
golden(GOLDEN, ut.decode("utf-8"))
|
|
|
|
|
|
def test_two_consecutive_runs_are_byte_identical():
|
|
_kode, forste, _ = kjor("--workspace", KORPUS, "--today", TODAY, "--format", "json")
|
|
_kode, andre, _ = kjor("--workspace", KORPUS, "--today", TODAY, "--format", "json")
|
|
assert forste == andre
|
|
|
|
|
|
def test_two_different_hash_seeds_produce_the_same_bytes():
|
|
kwargs = ("--workspace", KORPUS, "--today", TODAY, "--format", "json")
|
|
_kode, null, _ = kjor(*kwargs, env={"PYTHONHASHSEED": "0"})
|
|
_kode, mange, _ = kjor(*kwargs, env={"PYTHONHASHSEED": "1048573"})
|
|
assert null == mange, (
|
|
"output moved with PYTHONHASHSEED, so a dict or a set reached stdout"
|
|
)
|
|
# And the text form too, which is the one the operator actually reads.
|
|
tekst = ("--workspace", KORPUS, "--today", TODAY)
|
|
_kode, a, _ = kjor(*tekst, env={"PYTHONHASHSEED": "0"})
|
|
_kode, b, _ = kjor(*tekst, env={"PYTHONHASHSEED": "1048573"})
|
|
assert a == b
|
|
|
|
|
|
def test_omitting_workspace_is_an_error_and_never_a_guess_at_home():
|
|
kode, ut, feil = kjor("--today", TODAY)
|
|
assert kode != 0
|
|
assert b"--workspace" in feil
|
|
assert os.path.expanduser("~").encode("utf-8") not in ut + feil, (
|
|
"the refusal leaked a home directory; there is no default workspace"
|
|
)
|
|
|
|
|
|
def test_a_traversing_case_identifier_is_refused():
|
|
kode, _ut, feil = kjor(
|
|
"--workspace", KORPUS, "--today", TODAY, "--sak", "../../etc/passwd"
|
|
)
|
|
assert kode != 0
|
|
assert b"../../etc/passwd" in feil
|
|
|
|
|
|
def test_a_run_without_oppdater_leaves_every_byte_where_it_was(workspace):
|
|
forst = digest(workspace)
|
|
kode, _ut, feil = kjor("--workspace", workspace, "--today", TODAY, "--format", "json")
|
|
assert kode == 0, feil.decode("utf-8")
|
|
assert digest(workspace) == forst, "the read path wrote something"
|
|
|
|
|
|
def test_a_run_with_oppdater_changes_only_the_four_cached_keys(workspace):
|
|
sti = os.path.join(workspace, "saker", UENIG, "sak.md")
|
|
with open(sti, "r", encoding="utf-8") as handle:
|
|
for_meta, for_body = frontmatter_lib.parse(handle.read())
|
|
forst = digest(workspace)
|
|
|
|
kode, _ut, feil = kjor("--workspace", workspace, "--today", TODAY, "--oppdater")
|
|
assert kode == 0, feil.decode("utf-8")
|
|
|
|
etter = digest(workspace)
|
|
endrede = sorted(navn for navn in forst if forst[navn] != etter[navn])
|
|
assert endrede == [os.path.join("saker", UENIG, "sak.md")], (
|
|
"--oppdater touched %r; only the diverging case's sak.md may move"
|
|
% (endrede,)
|
|
)
|
|
assert sorted(etter) == sorted(forst), "--oppdater added or removed a file"
|
|
|
|
with open(sti, "r", encoding="utf-8") as handle:
|
|
etter_meta, etter_body = frontmatter_lib.parse(handle.read())
|
|
assert etter_body == for_body
|
|
flyttet = sorted(k for k in for_meta if for_meta[k] != etter_meta.get(k))
|
|
assert flyttet == ["status", "ventende_part"]
|
|
assert set(flyttet) <= set(sak_status.CACHE_KEYS)
|
|
|
|
|
|
def test_check_exits_non_zero_on_the_deliberately_divergent_fixture(workspace):
|
|
kode, ut, _feil = kjor("--workspace", KORPUS, "--today", TODAY, "--check")
|
|
assert kode != 0
|
|
assert UENIG.encode("utf-8") in ut, "the check must name the case that diverged"
|
|
|
|
kjor("--workspace", workspace, "--today", TODAY, "--oppdater")
|
|
kode, _ut, _feil = kjor("--workspace", workspace, "--today", TODAY, "--check")
|
|
assert kode == 0, "a workspace that has just been refreshed must check clean"
|