146 lines
5.3 KiB
Python
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
|