"""`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: seniorkonsulent maks: sjefkonsulent/avdelingsdirektør --- ## 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