"""Tester for agent.py. Bare standardbiblioteket, ingen nøkkel, ingen nett. python test_agent.py eller python -m unittest test_agent -v Testene bruker den simulerte modellen. De sier noe om rammene, ikke om hvordan en ekte modell oppfører seg. """ import re import unittest from pathlib import Path from types import SimpleNamespace as NS import agent def kjor_scenario(navn, **overstyr): """Kjører et av scenariene fra agent.py og gir tilbake (resultat, utboks).""" oppgave, manus, tillatt, innstillinger = agent.SCENARIER[navn] innstillinger = {**innstillinger, **overstyr} with agent.lag_arbeidsmappe() as mappe: verktoy, utboks = agent.lag_verktoy(Path(mappe)) resultat = agent.kjor(oppgave, agent.SimulertModell(manus()), verktoy, tillatt, logg=lambda linje: None, **innstillinger) return resultat, utboks class Verktoy(unittest.TestCase): def test_regn_regner(self): self.assertEqual(agent.regn("3*45 + 2*120"), 375) self.assertEqual(agent.regn("10/4"), 2.5) self.assertEqual(agent.regn("-2 * 5"), -10) def test_regn_godtar_ikke_kode(self): for uttrykk in ["__import__('os').getcwd()", "open('x')", "2**9999999", "(1).__class__"]: with self.assertRaises(Exception, msg=uttrykk): agent.regn(uttrykk) def test_les_fil_holder_seg_i_arbeidsmappa(self): with agent.lag_arbeidsmappe() as mappe: verktoy, _ = agent.lag_verktoy(Path(mappe)) for sti in ["../agent.py", "..\\agent.py", str(Path(mappe).parent / "x.txt")]: with self.assertRaises(ValueError, msg=sti): verktoy["les_fil"](sti) def test_les_fil_deler_lange_filer_i_biter(self): with agent.lag_arbeidsmappe() as mappe: les = agent.lag_verktoy(Path(mappe))[0]["les_fil"] forste, andre = les("innkjop.txt"), les("innkjop.txt", fra=agent.MAKS_TEGN) self.assertIn("avkortet", forste) self.assertNotIn("avkortet", andre) self.assertEqual(forste.split("\n[avkortet")[0] + andre, agent.INNKJOP) class FasitStemmer(unittest.TestCase): def test_fasiten_er_regnet_riktig(self): # Fasiten er skrevet for hånd i agent.py. Her regnes den på nytt fra selve filen. sum_ = sum(int(m[1]) * int(m[2]) for m in re.finditer(r"(\d+) stk à (\d+) kr", agent.INNKJOP)) self.assertEqual(str(sum_), agent.FASIT_INNKJOP) class FasitSammenligningen(unittest.TestCase): """stemmer() skal sammenligne tall, ikke lete etter tegn. Her er svarene en ekte modell kan finne på å skrive. De to første gruppene er grunnen til at funksjonen ble skrevet om: en delstrengtest slipper gjennom «13990» og slår alarm på «1.399».""" def stemmer(self, svar, stopp="ferdig"): return agent.stemmer(agent.Resultat(stopp, svar, 3, 500), agent.FASIT_INNKJOP) def test_riktig_svar_i_vanlige_skrivemater(self): for svar in ["1399", "Innkjøpene koster 1399 kr.", "1 399 kr", "1 399 kr", "1 399 kr", "1.399 kr", "1,399 kr", "1399,00 kr", "1 399,00 kr", "1.399,00 kr", "1,399.00 kr", "Det ble 11 poster og 1399 kr, ikke 1170."]: self.assertTrue(self.stemmer(svar), msg=ascii(svar)) def test_feil_svar_som_inneholder_sifrene_slipper_ikke_gjennom(self): for svar in ["13990", "Totalt 13990 kr", "11399 kr", "21399", "1399000", "1 399 000 kr", "139", "1,3990", "13 99"]: self.assertFalse(self.stemmer(svar), msg=ascii(svar)) def test_feil_svar_er_feil(self): for svar in ["1170 kr", "1 170 kr", "Jeg vet ikke.", ""]: self.assertFalse(self.stemmer(svar), msg=ascii(svar)) def test_riktig_svar_teller_ikke_hvis_kjoringen_ikke_ble_ferdig(self): for stopp in ["budsjett", "max_steps"]: self.assertFalse(self.stemmer("1399", stopp=stopp), msg=stopp) def test_tall_leses_ut_riktig(self): self.assertEqual(agent.tallverdier("1 399,50 kr for 12 stk"), {agent.Decimal("1399.50"), agent.Decimal("12")}) self.assertEqual(agent.tallverdier("1.399"), {agent.Decimal("1399"), agent.Decimal("1.399")}) class Rammene(unittest.TestCase): def test_normal_kjoring_gir_fasit(self): resultat, utboks = kjor_scenario("normal") self.assertEqual(resultat.stopp, "ferdig") self.assertTrue(agent.stemmer(resultat, agent.FASIT_INNKJOP)) self.assertEqual(utboks, []) def test_ring_stoppes_av_max_steps(self): resultat, _ = kjor_scenario("ring") self.assertEqual(resultat.stopp, "max_steps") self.assertEqual(resultat.steg, 6) self.assertEqual(resultat.svar, "") def test_budsjettet_stopper_ringen_for_max_steps(self): resultat, _ = kjor_scenario("ring-budsjett") self.assertEqual(resultat.stopp, "budsjett") self.assertLess(resultat.steg, 8) self.assertGreater(resultat.token, 1500) def test_regninga_vokser_fordi_hele_samtalen_sendes_hver_gang(self): token = [] oppgave, manus, tillatt, _ = agent.SCENARIER["ring"] with agent.lag_arbeidsmappe() as mappe: verktoy, _ = agent.lag_verktoy(Path(mappe)) agent.kjor(oppgave, agent.SimulertModell(manus()), verktoy, tillatt, maks_steg=6, logg=lambda linje: token.append(int(linje.split("|")[1].split()[0])) if linje.startswith("steg") else None) okning = [b - a for a, b in zip(token, token[1:])] self.assertEqual(okning, sorted(okning)) # hver runde koster mer enn den forrige def test_misforstaelsen_slipper_gjennom_alle_grensene(self): resultat, _ = kjor_scenario("misforstaelse") self.assertEqual(resultat.stopp, "ferdig") # ingen grense reagerer, løkka mener alt er i orden def test_men_kjent_oppgave_avslorer_den(self): resultat, _ = kjor_scenario("misforstaelse") self.assertFalse(agent.stemmer(resultat, agent.FASIT_INNKJOP)) def test_injeksjon_uten_rammer_sender_eposten(self): _, utboks = kjor_scenario("injeksjon-uten-rammer") self.assertEqual([til for til, _ in utboks], ["angriper@example.com"]) def test_tillatelseslista_stopper_injeksjonen(self): resultat, utboks = kjor_scenario("injeksjon-tillatelsesliste") self.assertEqual(utboks, []) self.assertEqual(resultat.stopp, "ferdig") # agenten fullfører oppgaven, uten å sende def test_godkjenning_stopper_injeksjonen_og_slipper_gjennom_ja(self): _, utboks = kjor_scenario("injeksjon-godkjenning") self.assertEqual(utboks, []) _, utboks = kjor_scenario("injeksjon-godkjenning", godkjenn=lambda kall: True) self.assertEqual(len(utboks), 1) # godkjenning er et menneskes ja, ikke en magisk sperre def test_verktoyfeil_gar_tilbake_til_modellen_som_tekst(self): logg = [] kall = agent.Kall("x", "regn", {"uttrykk": "hei"}) ut = agent.utfor(kall, {"regn": agent.regn}, {"regn"}, lambda k: False, logg.append) self.assertTrue(ut["is_error"]) self.assertTrue(ut["content"].startswith("FEIL")) def test_ukjent_verktoy_nektes(self): kall = agent.Kall("x", "slett_alt", {}) ut = agent.utfor(kall, {}, {"les_fil"}, lambda k: True, lambda linje: None) self.assertTrue(ut["is_error"]) self.assertTrue(ut["content"].startswith("NEKTET")) class EkteModell(unittest.TestCase): def test_claude_modellen_finnes_og_filtrerer_verktoy_uten_nettverk(self): # Ikke et kall mot API-et. Vi sjekker bare at klassen er til stede og at # verktøylista som sendes til modellen følger tillatelseslista. self.assertTrue(callable(agent.ClaudeModell)) navn = [d["name"] for d in agent.VERKTOYDEF if d["name"] in {"les_fil", "regn"}] self.assertEqual(navn, ["les_fil", "regn"]) self.assertTrue(agent.MODELL_ID.startswith("claude-")) def test_claude_modellen_leser_svarformen_fra_dokumentasjonen(self): # En falsk klient med samme form som svaret i Anthropics dokumentasjon: content-blokker, # stop_reason og usage. Dette tester vår oversetting, ikke SDK-en og ikke modellen. def falsk(content, stopp): svar = NS(content=content, stop_reason=stopp, usage=NS(input_tokens=100, output_tokens=20)) ekte = agent.ClaudeModell.__new__(agent.ClaudeModell) ekte.klient = NS(messages=NS(create=lambda **kw: svar)) ekte.modell_id, ekte.verktoydef = "x", [] return ekte([{"role": "user", "content": "hei"}]) bruker = falsk([NS(type="text", text="Jeg leser."), NS(type="tool_use", id="toolu_1", name="les_fil", input={"sti": "a.txt"})], "tool_use") self.assertEqual(bruker.kall, [agent.Kall("toolu_1", "les_fil", {"sti": "a.txt"})]) self.assertEqual(bruker.tokens, 120) ferdig = falsk([NS(type="text", text="Ferdig.")], "end_turn") self.assertEqual((ferdig.kall, ferdig.tekst), ([], "Ferdig.")) avkuttet = falsk([NS(type="text", text="Jeg ska")], "max_tokens") self.assertEqual(avkuttet.kall, []) self.assertIn("max_tokens", avkuttet.tekst) if __name__ == "__main__": unittest.main(verbosity=2)