feat(m2): add sak skill with case templates
This commit is contained in:
parent
52fd5c26a7
commit
4496c19045
4 changed files with 556 additions and 0 deletions
133
skills/sak/SKILL.md
Normal file
133
skills/sak/SKILL.md
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
---
|
||||
name: sak
|
||||
description: "Oppretter og vedlikeholder saksmapper i arbeidsmappa: sak-id, saksfila fra malen, en tom kontakter.md og den append-only hendelsesloggen. Delegerer hvert statusspoersmaal til statusmaskinen og utleder aldri status i prosa. Utloeses av opprett en sak, lag sak for denne stillingen, hva er status på saken, hva er status pa saken, legg til en hendelse i saken og oppdater saksmappa."
|
||||
triggers:
|
||||
- opprett en sak
|
||||
- lag sak for denne stillingen
|
||||
- hva er status på saken
|
||||
- hva er status pa saken
|
||||
- legg til en hendelse i saken
|
||||
- oppdater saksmappa
|
||||
---
|
||||
|
||||
# sak
|
||||
|
||||
Eier `saker/<sak-id>/` i arbeidsmappa. Skriver saksfila, den tomme
|
||||
`kontakter.md` og hendelsesloggen — og **utleder aldri status selv**.
|
||||
Statusmaskinen er et script, ikke en vurdering.
|
||||
|
||||
All operatørrettet tekst er norsk bokmål.
|
||||
|
||||
## Løs opp arbeidsmappa først
|
||||
|
||||
Samme rekkefølge som overalt ellers: `--workspace`, så `JOBBSOK_WORKSPACE`, så
|
||||
en `workspace:`-linje i `jobbsok.conf` under plugin-datamappa. Svarer ingen av
|
||||
de tre, si det og be om stien. Ingen hjemmekatalog gjettes.
|
||||
|
||||
## Sak-id
|
||||
|
||||
Formatet er `YYYY-MM-<arbeidsgiver-slug>-<rolle-slug>`, og året og måneden er
|
||||
den måneden saken opprettes. Bygg den aldri for hånd — kall `sak_id(...)` fra
|
||||
`${CLAUDE_PLUGIN_ROOT}/scripts/jobbsok_lib/paths.py`. Den folder `æ`, `ø` og
|
||||
`å` til ASCII og normaliserer først, slik at et navn skrevet dekomponert på
|
||||
macOS ikke blir en annen mappe enn det samme navnet skrevet komponert.
|
||||
|
||||
Finnes id-en fra før, er det samme sak. En sak som starter på nytt er en **ny**
|
||||
sak med ny id — statusmaskinen nekter å åpne en avsluttet sak igjen.
|
||||
|
||||
## Opprett saken
|
||||
|
||||
1. Lag `saker/<sak-id>/`.
|
||||
2. Skriv `sak.md` fra `${CLAUDE_PLUGIN_ROOT}/templates/sak.md`. Bytt ut hver
|
||||
`{{...}}`-plassholder. Det som ikke er kjent, skrives `null` — aldri et
|
||||
gjett, og aldri en tom verdi.
|
||||
3. Lag en tom `kontakter.md`. Den er tom med vilje: den fylles av
|
||||
`korrespondanse` og tømmes av `datahygiene`.
|
||||
4. Lag `logg.jsonl` og legg til `opprettet` som første hendelse.
|
||||
5. Kjør oppdateringen under.
|
||||
|
||||
Frontmatteren er kontrakten fra build-brief 5.2 og har alle tretten nøklene,
|
||||
også de som er `null`. `status` og `ventende_part` står som `vurderer` og `meg`
|
||||
i malen fordi det er nøyaktig det en sak med bare `opprettet` utleder — ikke
|
||||
fordi de er skrevet av skjønn.
|
||||
|
||||
Kroppen er operatørens. Fyll «Hvorfor denne» fra beslutningens begrunnelse, ikke
|
||||
fra annonsens egen selvbeskrivelse.
|
||||
|
||||
## Hendelsesloggen
|
||||
|
||||
`logg.jsonl` er append-only. En retting er en **ny** linje, aldri en redigering
|
||||
av en gammel.
|
||||
|
||||
Én hendelse er én linje:
|
||||
|
||||
```json
|
||||
{"ts": "2026-09-03T12:00:00+02:00", "hendelse": "soknad_sendt", "kilde": "e-post", "ref": "soknad-v1.md", "notat": null}
|
||||
```
|
||||
|
||||
`ts` bærer alltid UTC-forskyvning. Norsk lokaltid gjentar 02:30 den siste
|
||||
søndagen i oktober, og uten forskyvningen er to hendelser en time fra hverandre
|
||||
umulige å sortere i etterkant.
|
||||
|
||||
Vokabularet er lukket på elleve hendelser. Hele lista, tilstandene og de tre
|
||||
stillhetsreglene står i
|
||||
`${CLAUDE_PLUGIN_ROOT}/skills/sak/references/status.md`. Les den før du skriver
|
||||
en hendelse du ikke har skrevet før. En hendelse utenfor lista blir avvist ved
|
||||
navn når loggen leses — den blir ikke stille droppet.
|
||||
|
||||
Beslutningen `ja` speiles inn i saksloggen av `beslutning` som en
|
||||
beslutnings-post, ikke som en hendelse. Uten den kommer saken aldri videre fra
|
||||
`vurderer`.
|
||||
|
||||
## Etter hver tilføyde hendelse: oppdater hurtigbufferen
|
||||
|
||||
Dette steget hoppes aldri over:
|
||||
|
||||
```
|
||||
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/sak_status.py \
|
||||
--workspace <arbeidsmappa> --sak <sak-id> --oppdater
|
||||
```
|
||||
|
||||
Den skriver tilbake nøyaktig fire nøkler i frontmatteren — `status`,
|
||||
`ventende_part`, `sist_aktivitet` og `neste_frist` — og rører ingenting annet.
|
||||
Frontmatteren er en hurtigbuffer over loggen; en hurtigbuffer ingen oppdaterer
|
||||
er bare permanent avvik med bedre manerer.
|
||||
|
||||
En utledning som er tom overskriver ikke det som står der. `neste_frist` i
|
||||
`vurderer` og `soker` er søknadsfristen, som er operatørens egne data og som
|
||||
ingen maskin kan regne ut på nytt.
|
||||
|
||||
## Statusspørsmål besvares av scriptet
|
||||
|
||||
Spør operatøren om status, ventende part, frist eller stillhet, så **kjør
|
||||
scriptet og gjengi det**. Ikke les loggen og resonner deg fram til et svar:
|
||||
|
||||
```
|
||||
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/sak_status.py \
|
||||
--workspace <arbeidsmappa> --today <YYYY-MM-DD> --format json
|
||||
```
|
||||
|
||||
På Windows heter tolken `python`, ikke `python3`; alt annet i kommandoen er
|
||||
likt.
|
||||
|
||||
`--check` svarer på et annet spørsmål: den avslutter med kode 1 hvis en
|
||||
hurtigbuffer er uenig med loggen, og navngir saken. Er svaret 1, kjør
|
||||
`--oppdater` og si hva som flyttet seg — ikke rett i `sak.md` for hånd.
|
||||
|
||||
Står det `ingen innkommende hendelse er registrert`, gjengi det som det er.
|
||||
Det betyr ikke at ingen har svart; e-postserveren er utsatt, og fraværet av en
|
||||
registrering er ikke et fravær i verden.
|
||||
|
||||
## Grenser
|
||||
|
||||
- Ingen overgang utledes av tonen i en e-post. En tilstand endrer seg på en
|
||||
eksplisitt bekreftelse fra operatøren eller på et entydig strukturelt signal.
|
||||
- Ingenting eskalerer, sendes eller lukkes av seg selv.
|
||||
- Ingenting skrives utenfor arbeidsmappa.
|
||||
|
||||
## Uten verktøyserveren
|
||||
|
||||
Er `jobbsok-tools` ikke tilgjengelig, degraderer skillen i stedet for å stoppe:
|
||||
be operatøren kjøre kommandoene over fra kommandolinja, og les svaret som ble
|
||||
skrevet ut. Ikke regn ut status selv som en mellomløsning — da er den ikke
|
||||
lenger reproduserbar, og hele poenget med å ha en statusmaskin er borte.
|
||||
89
skills/sak/references/status.md
Normal file
89
skills/sak/references/status.md
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
# Statusmaskinen: tilstander, hendelser og stillhet
|
||||
|
||||
Referanse for `sak`. Alt her er implementert i
|
||||
`${CLAUDE_PLUGIN_ROOT}/scripts/sak_status.py` og utledes derfra — denne fila
|
||||
beskriver, den bestemmer ikke. Er de to uenige, er scriptet fasit og uenigheten
|
||||
en feil som skal meldes.
|
||||
|
||||
## Tilstander
|
||||
|
||||
Hovedkjeden, i rekkefølge:
|
||||
|
||||
| Tilstand | Betyr | Ventende part ved ankomst |
|
||||
|---|---|---|
|
||||
| `vurderer` | Saken finnes, beslutningen er ikke tatt | `meg` |
|
||||
| `soker` | Beslutningen er `ja`, søknaden er ikke sendt | `meg` |
|
||||
| `sendt` | Søknaden er sendt | `dem` |
|
||||
| `dialog` | Det er en toveis samtale i gang | veksler |
|
||||
| `intervju` | Intervju er avtalt eller gjennomført | `ingen` / `dem` |
|
||||
| `tilbud` | Tilbud er mottatt | `meg` |
|
||||
| `avsluttet` | Operatøren har lukket saken | `ingen` |
|
||||
|
||||
To terminale sidetilstander: `avslag` og `trukket`.
|
||||
|
||||
Ingenting går ut av en terminal tilstand. En hendelse etter `avsluttet`,
|
||||
`avslag` eller `trukket` er en feil, ikke en gjenåpning: en sak som starter på
|
||||
nytt er en ny sak med ny sak-id.
|
||||
|
||||
## Hendelser — lukket vokabular på elleve
|
||||
|
||||
Build-brief 5.3. Ingen tolvte finnes, og en hendelse utenfor lista blir avvist
|
||||
ved navn når loggen leses.
|
||||
|
||||
| Hendelse | Skriver seg fra | Lander på |
|
||||
|---|---|---|
|
||||
| `opprettet` | saken lages | `vurderer` |
|
||||
| `soknad_sendt` | søknaden er sendt | `sendt` |
|
||||
| `bekreftelse_mottatt` | automatisk kvittering | `sendt` |
|
||||
| `henvendelse_mottatt` | de tok kontakt | `dialog` |
|
||||
| `svar_sendt` | operatøren svarte | `dialog` |
|
||||
| `intervju_avtalt` | tidspunkt er satt | `intervju` |
|
||||
| `intervju_gjennomfort` | intervjuet er holdt | `intervju` |
|
||||
| `tilbud_mottatt` | tilbud foreligger | `tilbud` |
|
||||
| `avslag` | de sa nei | `avslag` |
|
||||
| `trukket` | operatøren trakk seg | `trukket` |
|
||||
| `stille` | operatøren noterer at det er stille | ingen overgang |
|
||||
|
||||
`stille` flytter ingenting. Den er en notering, ikke en tilstandsendring, og
|
||||
den legges bare til av operatøren — ingenting i denne pluginen skriver den av
|
||||
seg selv.
|
||||
|
||||
## To poster som ikke er hendelser
|
||||
|
||||
To rader i overgangstabellen kommer ikke fra hendelses-enumen, og de skrives
|
||||
med `type` i stedet for `hendelse`:
|
||||
|
||||
| Post | Form i saksloggen | Lander på |
|
||||
|---|---|---|
|
||||
| Beslutning `ja` | `{"type": "beslutning", "beslutning": "ja", ...}` | `soker` |
|
||||
| Operatøren lukker | `{"type": "utfall", "utfall": "avsluttet", ...}` | `avsluttet` |
|
||||
|
||||
Begge er poster build-brief 5.7 uansett speiler inn i saksloggen. `beslutning`
|
||||
skriver den første; uten den kommer ingen sak videre fra `vurderer`. En
|
||||
`beslutning: nei` er ingen overgang — et nei skaper ingen sak.
|
||||
|
||||
## Stillhet: tre regler
|
||||
|
||||
Terskler i dager, målt mot den datoen som faktisk står i loggen:
|
||||
|
||||
| Flagg | Regel | Terskel |
|
||||
|---|---|---|
|
||||
| `sendt_14` | `sendt` uten noe innkommende | 14 dager |
|
||||
| `dialog_7` | `dialog` med ventende part `dem` uten noe innkommende | 7 dager |
|
||||
| `intervju_10` | intervju gjennomført uten utfall | 10 dager |
|
||||
|
||||
**En automatisk kvittering er ikke kontakt.** `bekreftelse_mottatt` teller ikke
|
||||
som innkommende og nullstiller derfor ikke stillhetsklokka. Det er hele grunnen
|
||||
til at den står utenfor lista over innkommende hendelser.
|
||||
|
||||
`neste_frist` som statusmaskinen utleder, er den tidligste terskeldatoen blant
|
||||
reglene som gjelder nå — altså **datoen saken går stille**. I `vurderer` og
|
||||
`soker` gjelder ingen regel, og da står operatørens egen søknadsfrist urørt i
|
||||
frontmatteren.
|
||||
|
||||
## «Ingen innkommende registrert» er ikke «ingen har svart»
|
||||
|
||||
Rapporterer scriptet at ingen innkommende hendelse er registrert, er det et
|
||||
utsagn om denne loggen og ikke om verden. E-postserveren er utsatt til M4, så
|
||||
ingenting leser innboksen ennå. Gjengi merknaden som den er, og ikke presenter
|
||||
saken som stille på grunnlag av den.
|
||||
38
templates/sak.md
Normal file
38
templates/sak.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
---
|
||||
sak_id: {{sak_id}}
|
||||
arbeidsgiver: {{arbeidsgiver}}
|
||||
rolle: {{rolle}}
|
||||
kilde: {{kilde}}
|
||||
url: {{url}}
|
||||
opprettet: {{opprettet}}
|
||||
status: vurderer
|
||||
neste_frist: {{neste_frist}}
|
||||
ventende_part: meg
|
||||
sist_aktivitet: {{opprettet}}
|
||||
score: {{score}}
|
||||
cv_variant: {{cv_variant}}
|
||||
soknad_versjon: null
|
||||
---
|
||||
|
||||
## Hvorfor denne
|
||||
|
||||
<!-- Én til tre setninger, hentet fra beslutningens begrunnelse. Ikke annonsens
|
||||
egen selvbeskrivelse. Skriv hva som gjorde at denne ble en sak. -->
|
||||
|
||||
## Status nå
|
||||
|
||||
<!-- Hva som faktisk har skjedd sist. Ikke gjenta feltet `status` i
|
||||
frontmatteren -- det utledes av logg.jsonl og oppdateres av sak_status. -->
|
||||
|
||||
## Neste handling
|
||||
|
||||
<!-- Én handling, med hvem den venter på. Ingen automatikk gjør den for deg. -->
|
||||
|
||||
## Åpne spørsmål
|
||||
|
||||
- <!-- Det som må avklares før neste steg. «Ingen» er et gyldig svar. -->
|
||||
|
||||
## Tidslinje
|
||||
|
||||
<!-- Menneskelesbar, generert fra logg.jsonl. Legg til en linje per hendelse:
|
||||
YYYY-MM-DD hendelse kort merknad -->
|
||||
296
tests/test_sak_skill.py
Normal file
296
tests/test_sak_skill.py
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
"""What the `sak` skill promises, checked where a document can be checked
|
||||
(plan Step 21).
|
||||
|
||||
The skill is prose, so the same rule applies here as in
|
||||
`tests/test_skills_contract.py`: the invocation is a model turn and cannot run
|
||||
in pytest. What runs is the sequence the skill *documents* -- create the case
|
||||
from `templates/sak.md`, append the event, refresh the cache -- executed here
|
||||
step by step, plus the assertion that SKILL.md still says to do exactly that.
|
||||
A skill that stopped naming `--oppdater` fails the second half; a sequence that
|
||||
stopped refreshing fails the first.
|
||||
|
||||
Two properties are worth stating, because both are design decisions this step
|
||||
had to take rather than mechanics it inherited.
|
||||
|
||||
**The case log has no validated writer, and that is deliberate.**
|
||||
`jobbsok_lib.jsonl.append_line` validates `type` against the decision log's
|
||||
closed set (`beslutning`, `korrigering`, `utfall`), and a build-brief 5.3 case
|
||||
event carries `hendelse` instead. So a case event is written as the single
|
||||
`O_APPEND` line the module's contract describes, and the enum is enforced on
|
||||
the *read* side: `sak_status.trigger_of` refuses an event outside the eleven,
|
||||
by name. The mirrored decision record does go through `append_line`, because
|
||||
that one is a decision-log record living in the case log.
|
||||
|
||||
**The divergence test is the one that has teeth.** Asserting that the
|
||||
documented sequence leaves `--check` at exit 0 proves nothing on its own: a
|
||||
freshly created case agrees with its own template. So the same sequence is run
|
||||
with the refresh removed, and `--check` must then exit non-zero. Without that
|
||||
pair, a skill that dropped `--oppdater` entirely would pass.
|
||||
|
||||
Style note: this file follows tests/test_skills_contract.py.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
import sak_status
|
||||
from jobbsok_lib import frontmatter as frontmatter_lib
|
||||
from jobbsok_lib import jsonl, paths
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
SKILL = os.path.join(REPO, "skills", "sak", "SKILL.md")
|
||||
REFERANSE = os.path.join(REPO, "skills", "sak", "references", "status.md")
|
||||
MAL = os.path.join(REPO, "templates", "sak.md")
|
||||
CLI = os.path.join(REPO, "scripts", "sak_status.py")
|
||||
|
||||
#: build-brief 5.2, in the order the section lists them.
|
||||
FRONTMATTER_NOKLER = (
|
||||
"sak_id", "arbeidsgiver", "rolle", "kilde", "url", "opprettet", "status",
|
||||
"neste_frist", "ventende_part", "sist_aktivitet", "score", "cv_variant",
|
||||
"soknad_versjon",
|
||||
)
|
||||
|
||||
#: build-brief 5.2's body, in the order the section lists it.
|
||||
BODY_SEKSJONER = (
|
||||
"## Hvorfor denne",
|
||||
"## Status nå",
|
||||
"## Neste handling",
|
||||
"## Åpne spørsmål",
|
||||
"## Tidslinje",
|
||||
)
|
||||
|
||||
#: The files a case folder starts life with. `kontakter.md` is empty on
|
||||
#: purpose: build-brief 5 names it, and `datahygiene` deletes third-party
|
||||
#: personal data out of it in M6.
|
||||
SAKSFILER = ("sak.md", "logg.jsonl", "kontakter.md")
|
||||
|
||||
|
||||
def mal_verdier(sak_id, arbeidsgiver, rolle, opprettet, **felt):
|
||||
verdier = {
|
||||
"sak_id": sak_id,
|
||||
"arbeidsgiver": arbeidsgiver,
|
||||
"rolle": rolle,
|
||||
"kilde": "manuell",
|
||||
"url": "null",
|
||||
"opprettet": opprettet,
|
||||
"neste_frist": "null",
|
||||
"score": "null",
|
||||
"cv_variant": "null",
|
||||
}
|
||||
verdier.update(felt)
|
||||
return verdier
|
||||
|
||||
|
||||
def fyll_mal(verdier):
|
||||
"""Render `templates/sak.md` the way the skill documents rendering it."""
|
||||
with open(MAL, "r", encoding="utf-8") as handle:
|
||||
tekst = handle.read()
|
||||
for nokkel, verdi in verdier.items():
|
||||
tekst = tekst.replace("{{%s}}" % nokkel, str(verdi))
|
||||
assert "{{" not in tekst, (
|
||||
"the template still holds an unsubstituted placeholder: %r"
|
||||
% [bit for bit in tekst.split("{{")[1:]]
|
||||
)
|
||||
return tekst
|
||||
|
||||
|
||||
def legg_til_hendelse(root, sak_id, hendelse, dato, **felt):
|
||||
"""Append one build-brief 5.3 event as a single line, as the skill does."""
|
||||
record = {
|
||||
"ts": dato + "T12:00:00+02:00",
|
||||
"hendelse": hendelse,
|
||||
"kilde": "manuell",
|
||||
"ref": None,
|
||||
"notat": None,
|
||||
}
|
||||
record.update(felt)
|
||||
sti = paths.safe_join(root, "saker", sak_id, "logg.jsonl")
|
||||
payload = (json.dumps(record, ensure_ascii=False) + "\n").encode("utf-8")
|
||||
handle = os.open(sti, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
|
||||
try:
|
||||
os.write(handle, payload)
|
||||
finally:
|
||||
os.close(handle)
|
||||
return record
|
||||
|
||||
|
||||
def opprett_sak(root, arbeidsgiver, rolle, opprettet, **felt):
|
||||
"""The skill's documented creation step, executed."""
|
||||
sak_id = paths.sak_id(opprettet[:7], arbeidsgiver, rolle)
|
||||
katalog = paths.safe_join(root, "saker", sak_id)
|
||||
os.makedirs(katalog)
|
||||
|
||||
with open(os.path.join(katalog, "sak.md"), "w", encoding="utf-8") as handle:
|
||||
handle.write(fyll_mal(mal_verdier(sak_id, arbeidsgiver, rolle, opprettet, **felt)))
|
||||
open(os.path.join(katalog, "kontakter.md"), "w", encoding="utf-8").close()
|
||||
open(os.path.join(katalog, "logg.jsonl"), "a", encoding="utf-8").close()
|
||||
|
||||
legg_til_hendelse(root, sak_id, "opprettet", opprettet)
|
||||
return sak_id
|
||||
|
||||
|
||||
def kjor(*argv):
|
||||
proc = subprocess.run(
|
||||
[sys.executable, CLI] + list(argv),
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
)
|
||||
return proc.returncode, proc.stdout.decode("utf-8"), proc.stderr.decode("utf-8")
|
||||
|
||||
|
||||
def oppdater(root, sak_id, today):
|
||||
kode, _ut, feil = kjor(
|
||||
"--workspace", root, "--sak", sak_id, "--today", today, "--oppdater",
|
||||
"--format", "json",
|
||||
)
|
||||
assert kode == 0, feil
|
||||
return kode
|
||||
|
||||
|
||||
def sekvensen(root, today, oppdater_etter_hver_hendelse):
|
||||
"""Create a case, take it to `sendt`, refreshing the cache or not.
|
||||
|
||||
Three appends, because one is not enough to tell the two branches apart:
|
||||
a case that has only just been created agrees with its own template
|
||||
whether or not anything refreshed it.
|
||||
"""
|
||||
sak_id = opprett_sak(root, "Fjordtun Energi AS", "Løsningsarkitekt", "2026-09-01")
|
||||
if oppdater_etter_hver_hendelse:
|
||||
oppdater(root, sak_id, today)
|
||||
|
||||
# The decision mirrored into the case log -- a decision-log record, so it
|
||||
# goes through the validated writer (module docstring).
|
||||
jsonl.append_line(
|
||||
paths.safe_join(root, "saker", sak_id, "logg.jsonl"),
|
||||
{
|
||||
"type": "beslutning",
|
||||
"id": "b-2026-0901",
|
||||
"ts": "2026-09-02T12:00:00+02:00",
|
||||
"beslutning": "ja",
|
||||
},
|
||||
)
|
||||
if oppdater_etter_hver_hendelse:
|
||||
oppdater(root, sak_id, today)
|
||||
|
||||
legg_til_hendelse(root, sak_id, "soknad_sendt", "2026-09-03", ref="soknad-v1.md")
|
||||
if oppdater_etter_hver_hendelse:
|
||||
oppdater(root, sak_id, today)
|
||||
return sak_id
|
||||
|
||||
|
||||
def les_skill():
|
||||
with open(SKILL, "r", encoding="utf-8") as handle:
|
||||
return handle.read()
|
||||
|
||||
|
||||
def test_a_case_from_the_template_carries_5_2_whole_and_one_opprettet_line(
|
||||
empty_workspace,
|
||||
):
|
||||
sak_id = opprett_sak(
|
||||
empty_workspace, "Fjordtun Energi AS", "Løsningsarkitekt", "2026-09-01"
|
||||
)
|
||||
katalog = os.path.join(empty_workspace, "saker", sak_id)
|
||||
|
||||
for navn in SAKSFILER:
|
||||
assert os.path.isfile(os.path.join(katalog, navn)), (
|
||||
"the case folder has no %r; build-brief 5 names it" % navn
|
||||
)
|
||||
assert os.path.getsize(os.path.join(katalog, "kontakter.md")) == 0, (
|
||||
"kontakter.md must start empty -- it is the operator's, not a template's"
|
||||
)
|
||||
|
||||
with open(os.path.join(katalog, "sak.md"), "r", encoding="utf-8") as handle:
|
||||
meta, body = frontmatter_lib.parse(handle.read())
|
||||
assert list(meta) == list(FRONTMATTER_NOKLER), (
|
||||
"sak.md frontmatter is %r; build-brief 5.2 names %r"
|
||||
% (list(meta), list(FRONTMATTER_NOKLER))
|
||||
)
|
||||
assert meta["sak_id"] == sak_id
|
||||
assert meta["status"] == "vurderer" and meta["ventende_part"] == "meg", (
|
||||
"a case with only an `opprettet` event derives (vurderer, meg); the "
|
||||
"template must not start it anywhere else"
|
||||
)
|
||||
for overskrift in BODY_SEKSJONER:
|
||||
assert overskrift in body, "the case body has no %r section" % overskrift
|
||||
|
||||
records = jsonl.read_lines(os.path.join(katalog, "logg.jsonl"))
|
||||
assert len(records) == 1, "expected exactly one log line, got %r" % (records,)
|
||||
assert records[0]["hendelse"] == "opprettet"
|
||||
assert sak_status.trigger_of(records[0]) == "opprettet", (
|
||||
"the event does not read back as a trigger the status machine knows"
|
||||
)
|
||||
|
||||
|
||||
def test_the_case_identifier_is_pure_ascii_for_a_norwegian_employer(empty_workspace):
|
||||
sak_id = opprett_sak(
|
||||
empty_workspace, "Værøy Sjømat AS", "Dataingeniør", "2026-09-01"
|
||||
)
|
||||
assert sak_id == "2026-09-vaeroy-sjomat-as-dataingenior"
|
||||
sak_id.encode("ascii") # raises if a macOS NFD byte survived (risk H8)
|
||||
assert paths.SAK_ID_RE.match(sak_id), (
|
||||
"%r does not match the build-brief 5.2 format" % sak_id
|
||||
)
|
||||
assert os.path.isdir(os.path.join(empty_workspace, "saker", sak_id))
|
||||
|
||||
|
||||
def test_the_documented_sequence_leaves_the_cache_agreeing_with_the_log(
|
||||
empty_workspace, frozen_today
|
||||
):
|
||||
sak_id = sekvensen(empty_workspace, frozen_today, oppdater_etter_hver_hendelse=True)
|
||||
|
||||
kode, ut, feil = kjor(
|
||||
"--workspace", empty_workspace, "--today", frozen_today, "--check"
|
||||
)
|
||||
assert kode == 0, "--check found divergence after the documented sequence:\n%s%s" % (ut, feil)
|
||||
|
||||
meta, _body = frontmatter_lib.parse(
|
||||
open(os.path.join(empty_workspace, "saker", sak_id, "sak.md"),
|
||||
encoding="utf-8").read()
|
||||
)
|
||||
assert meta["status"] == "sendt" and meta["ventende_part"] == "dem"
|
||||
assert meta["sist_aktivitet"] == "2026-09-03"
|
||||
|
||||
|
||||
def test_dropping_the_oppdater_call_makes_the_same_sequence_diverge(
|
||||
empty_workspace, frozen_today
|
||||
):
|
||||
sak_id = sekvensen(empty_workspace, frozen_today, oppdater_etter_hver_hendelse=False)
|
||||
|
||||
kode, ut, _feil = kjor(
|
||||
"--workspace", empty_workspace, "--today", frozen_today, "--check"
|
||||
)
|
||||
assert kode == sak_status.EXIT_DIVERGENS, (
|
||||
"the same sequence without --oppdater checked clean, so the refresh "
|
||||
"step in the skill is unfalsifiable"
|
||||
)
|
||||
assert sak_id in ut
|
||||
|
||||
|
||||
def test_the_skill_delegates_every_status_question_to_the_script():
|
||||
tekst = les_skill()
|
||||
assert "${CLAUDE_PLUGIN_ROOT}/scripts/sak_status.py" in tekst, (
|
||||
"the skill never names the status script it is required to delegate to"
|
||||
)
|
||||
for flagg in ("--oppdater", "--check"):
|
||||
assert flagg in tekst, "the skill never names %s" % flagg
|
||||
assert "${CLAUDE_PLUGIN_ROOT}/templates/sak.md" in tekst, (
|
||||
"the skill never names the template it creates cases from"
|
||||
)
|
||||
assert "references/status.md" in tekst
|
||||
|
||||
|
||||
def test_the_status_reference_lists_the_closed_vocabulary_whole():
|
||||
with open(REFERANSE, "r", encoding="utf-8") as handle:
|
||||
tekst = handle.read()
|
||||
|
||||
mangler = [h for h in sak_status.HENDELSER if h not in tekst]
|
||||
assert mangler == [], (
|
||||
"references/status.md is missing %r of the eleven 5.3 events" % (mangler,)
|
||||
)
|
||||
mangler = [t for t in sak_status.TILSTANDER if t not in tekst]
|
||||
assert mangler == [], "references/status.md is missing the states %r" % (mangler,)
|
||||
for terskel in ("14", "7", "10"):
|
||||
assert terskel in tekst, "the silence rule at %s days is not written down" % terskel
|
||||
Loading…
Add table
Add a link
Reference in a new issue