jobbsok/tests/test_kandidatvurdering_nowrite.py

146 lines
5.3 KiB
Python

"""`kandidatvurdering` writes nothing, and this is the proof (plan Step 11).
Build-brief 7 says the skill's Writes column is `nothing`, and in M1 that is
not a tidiness preference. M1 scores pasted listing text with no ingestion
guard in front of it -- the guard arrives in M3 -- so the only thing standing
between an untrusted paste and the workspace is that nothing on this path
writes at all. A test that merely checked the score would leave that property
unasserted.
So the check is a content digest of the whole workspace tree, taken before and
after a full scoring run, including reading the profile off disk. Content
rather than size and mtime: a rewrite of the same length inside the same
second is exactly the kind of write this test exists to catch.
Style note: this file follows tests/test_skills_contract.py.
"""
import hashlib
import os
import kandidat_schema
import test_skills_contract as kontrakt
import vurdering
from jobbsok_lib import frontmatter
SKILL = "kandidatvurdering"
PROFIL = """---
oppdatert: 2026-09-04
geografi:
base: Tromsø
maks_reisetid_min: 45
hjemmekontor_min_dager: 2
vurderer_flytting: nei
lonn:
gulv_nok: 850000
onsket_nok: 950000
kommentar: gulvet avviser mekanisk
ansettelsesform:
aksepterer: fast, konsulent via byrå
avviser: åremål/engasjement, vikariat
absolutte_nei:
- turnus
senioritet:
min: seniorrådgiver
maks: fagdirektør/sjefsarkitekt
---
## Kjerne
## Kompetanse
## Retning
## Signaturprosjekter
## Kjente svakheter
## Formuleringer som virker
## Profilgap
"""
def digest(root):
"""A content digest of every file under ``root``, path included."""
summer = hashlib.sha256()
for dirpath, dirnames, filenames in os.walk(root):
dirnames.sort()
for name in sorted(filenames):
full = os.path.join(dirpath, name)
summer.update(os.path.relpath(full, root).encode("utf-8"))
with open(full, "rb") as handle:
summer.update(handle.read())
return summer.hexdigest()
def test_scoring_a_listing_leaves_the_workspace_byte_identical(
empty_workspace, fixtures_dir
):
profil_sti = os.path.join(empty_workspace, "profil", "kandidat.md")
with open(profil_sti, "w", encoding="utf-8") as handle:
handle.write(PROFIL)
annonse = os.path.join(fixtures_dir, "listings", "01-alt-passer.md")
with open(annonse, "r", encoding="utf-8") as handle:
meta, body = frontmatter.parse(handle.read())
last = {
"delscore": {k: 70 for k in kandidat_schema.STANDARDVEKTER},
"bekymringer": ["konseptfase, ikke drift"],
}
for_ = digest(empty_workspace)
profil = vurdering.les_profil_fil(empty_workspace, "profil", "kandidat.md")
resultat = vurdering.vurder(profil, meta, body, last)
etter = digest(empty_workspace)
assert resultat["score"] == 70
assert etter == for_, "kandidatvurdering wrote into the workspace"
# And it did not create the workspace-adjacent files either: the tree is
# exactly what the scaffold left.
assert sorted(os.listdir(os.path.join(empty_workspace, "profil"))) == ["cv", "kandidat.md"]
def test_the_skill_contract_covers_this_skill():
assert SKILL in kontrakt.ALLE, "%s is not discovered by the skills contract" % SKILL
meta, body, text = kontrakt.load(SKILL)
skill = {"name": SKILL, "meta": meta, "body": body, "text": text}
kontrakt.test_frontmatter_parses_and_name_matches_the_directory(skill)
kontrakt.test_description_is_third_person_and_names_every_trigger(skill)
kontrakt.test_every_accented_trigger_has_an_ascii_twin(skill)
kontrakt.test_body_is_under_the_word_budget(skill)
kontrakt.test_every_intra_plugin_path_uses_the_plugin_root(skill)
kontrakt.test_no_absolute_home_path_is_baked_into_a_skill(skill)
kontrakt.test_reference_files_resolve_in_both_directions(skill)
kontrakt.test_skill_files_are_valid_utf8_and_carry_norwegian(skill)
kontrakt.test_trigger_phrases_are_unique_across_skills()
def test_the_skill_states_that_it_writes_nothing():
_meta, _body, text = kontrakt.load(SKILL)
assert "skriver ingenting" in text.lower()
# The degradation is part of the contract: with the host MCP server
# absent the skill still reads the profile and asks for the CLI, rather
# than guessing at the arithmetic.
assert "jobbsok-tools" in text
def test_vekter_reference_documents_every_weight_the_script_reads():
sti = os.path.join(kontrakt.SKILLS, SKILL, "references", "vekter.md")
with open(sti, "r", encoding="utf-8") as handle:
tekst = handle.read()
# Criterion and its default on the same line, not merely somewhere in
# the file: every criterion name also appears in the override example, so
# a token search would pass over a criterion whose row had been deleted.
linjer = tekst.split("\n")
for kriterium, standard in kandidat_schema.STANDARDVEKTER.items():
rader = [line for line in linjer if kriterium in line and str(standard) in line]
assert rader, (
"vekter.md has no line documenting %s with its shipped default %d"
% (kriterium, standard)
)
# The frontmatter contract, not only the numbers: a reader has to know
# that a profile may override them and what the report then says.
assert "vekter_kilde" in tekst
assert "profil" in tekst and "default" in tekst