"""Faelle fuer verbrauch_reihe.py: baut je Fall einen kuenstlichen Session-Speicher und faehrt das Skript als eigenen Prozess. Die Grundlinie laeuft zuerst; scheitert sie, messen die uebrigen Faelle nichts. Der Prozess laeuft ohne PYTHONIOENCODING, damit eine Ausgabe ausserhalb von ASCII auf cp1252 auffaellt. Aufruf: python faelle_verbrauch_reihe.py Exit 0, wenn alle Faelle bestehen, sonst 1. """ import json import os import subprocess import sys import tempfile from pathlib import Path SKRIPT = Path(__file__).with_name("verbrauch_reihe.py") def fenster(fuenf, woche, reset5="2026-09-24T18:30:00Z", resetw="2026-09-28T00:00:00Z"): return [ {"label": "5-hour limit", "percentUsed": fuenf, "resetsAt": reset5}, {"label": "Weekly · all models", "percentUsed": woche, "resetsAt": resetw}, ] def baue(speicher, laeufe): """laeufe: {lauf: [agent]}; agent: {id, label, start, ende, modell, ergebnis, ohne_zeit, kaputt}.""" for lauf, agenten in laeufe.items(): ordner = speicher / "sitzung-1" / "subagents" / "workflows" / lauf ordner.mkdir(parents=True) with (ordner / "journal.jsonl").open("w", encoding="utf-8") as journal: journal.write(json.dumps({"type": "launched"}) + "\n") for a in agenten: journal.write(json.dumps({"type": "started", "agentId": a["id"], "label": a["label"]}) + "\n") if a.get("kaputt"): journal.write("{kein json\n") if "ergebnis" in a: journal.write(json.dumps({"type": "result", "agentId": a["id"], "result": a["ergebnis"]}) + "\n") for a in agenten: (ordner / f"agent-{a['id']}.meta.json").write_text(json.dumps({"model": a.get("modell", "haiku")}), encoding="utf-8") if a.get("ohne_zeit"): continue with (ordner / f"agent-{a['id']}.jsonl").open("w", encoding="utf-8") as d: d.write(json.dumps({"timestamp": a["start"], "type": "user"}) + "\n") d.write(json.dumps({"timestamp": a["ende"], "type": "assistant"}) + "\n") def ablesung(nr, anlass, zeitpunkt, fuenf, woche, **extra): ende = extra.pop("bis", None) or zeitpunkt return {"id": f"v{nr}", "label": "verbrauch:" + anlass, "start": zeitpunkt, "ende": ende, "modell": "haiku", "ergebnis": {"verfuegbar": True, "status": "ok", "plan": "Pro", "fenster": extra.pop("liste", None) or fenster(fuenf, woche, **extra), "fehler": ""}} # resetsAt schwankt bei echten Ablesungen um Millisekunden und springt ueber Sekunden- und # Datumsgrenzen (gemessen 2026-09-24); die Grundlinie traegt deshalb solche Werte. ZITTER = {"reset5": "2026-09-24T18:29:59.664Z", "resetw": "2026-09-27T23:59:59.664Z"} def opus(nr, start, ende, modell="opus"): return {"id": f"o{nr}", "label": f"plankritik:linse{nr}", "start": start, "ende": ende, "modell": modell, "ergebnis": {}} def fahre(speicher, *zusatz): umgebung = {k: v for k, v in os.environ.items() if k not in ("PYTHONIOENCODING", "PYTHONUTF8")} lauf = subprocess.run([sys.executable, str(SKRIPT), "--speicher", str(speicher), *zusatz], capture_output=True, env=umgebung) return lauf.returncode, lauf.stdout.decode("ascii", "replace"), lauf.stderr.decode("utf-8", "replace") GRUND = { "wf_a": [ ablesung(1, "plankritik:vor Linsen", "2026-09-24T10:00:00Z", 10, 5), opus(1, "2026-09-24T10:00:10Z", "2026-09-24T10:30:10Z"), ablesung(2, "plankritik:Ende", "2026-09-24T10:30:10Z", 14, 6, **ZITTER), ] } def f_grundlinie(tmp): baue(tmp, GRUND) code, aus, err = fahre(tmp) fehler = [] if code != 0: fehler.append(f"Exit {code}: {err[-300:]}") for erwartet in ["2 Ablesungen, 1 Paare", "| wf_a | plankritik:vor Linsen -> plankritik:Ende | 30.2 | opus 30.0 | 4.0 | 1.0 |", "Naeherung: 8.0 % des 5-Stunden-Fensters je Opus-Agentenstunde (1 Paare, 0.5 Opus-Stunden"]: if erwartet not in aus: fehler.append("fehlt: " + erwartet) if "Hinweise" in aus: fehler.append("unerwartete Hinweise") return fehler def f_reset(tmp): baue(tmp, {"wf_r": [ablesung(1, "a", "2026-09-24T10:00:00Z", 80, 5), opus(1, "2026-09-24T10:01:00Z", "2026-09-24T10:20:00Z"), ablesung(2, "b", "2026-09-24T10:30:00Z", 3, 6, reset5="2026-09-24T15:30:00Z")]}) code, aus, _ = fahre(tmp) fehler = [] if code != 0 or "| Reset | 1.0 |" not in aus: fehler.append("Reset nicht erkannt: " + aus[-300:]) if "Keine auswertbaren Paare mit Opus-Agenten." not in aus: fehler.append("Reset-Paar wurde mitgerechnet") return fehler def f_speicher_fehlt(tmp): code, aus, _ = fahre(tmp / "gibt-es-nicht") return [] if code == 2 and "Speicher fehlt" in aus else [f"Exit {code}, Ausgabe {aus!r}"] def f_null_ablesungen(tmp): baue(tmp, {"wf_n": [opus(1, "2026-09-24T10:00:00Z", "2026-09-24T10:10:00Z")]}) code, aus, _ = fahre(tmp) return [] if code == 0 and "0 Ablesungen, 0 Paare" in aus else [f"Exit {code}, Ausgabe {aus!r}"] def f_kaputte_zeile(tmp): agenten = [dict(GRUND["wf_a"][0], kaputt=True), GRUND["wf_a"][1], GRUND["wf_a"][2]] baue(tmp, {"wf_k": agenten}) code, aus, _ = fahre(tmp) fehler = [] if code != 0 or "1 Paare" not in aus: fehler.append("Auswertung trotz kaputter Zeile gescheitert: " + aus[-300:]) if "ist kein JSON" not in aus: fehler.append("kaputte Zeile nicht als Hinweis gemeldet") return fehler def f_ohne_zeitstempel(tmp, position): agenten = [dict(a) for a in GRUND["wf_a"]] agenten[position]["ohne_zeit"] = True baue(tmp, {"wf_z": agenten}) code, aus, _ = fahre(tmp) fehler = [] if code != 0 or "1 Ablesungen, 0 Paare" not in aus: fehler.append(f"Exit {code}, Ausgabe {aus[-300:]!r}") if "ohne Zeitstempel" not in aus: fehler.append("fehlender Zeitstempel nicht gemeldet") return fehler def f_fenster_kaputt(tmp, liste, erwartet="| unbekannt | unbekannt |"): baue(tmp, {"wf_f": [ablesung(1, "a", "2026-09-24T10:00:00Z", 10, 5, liste=liste), opus(1, "2026-09-24T10:01:00Z", "2026-09-24T10:20:00Z"), ablesung(2, "b", "2026-09-24T10:30:00Z", 12, 6)]}) code, aus, _ = fahre(tmp) return [] if code == 0 and erwartet in aus else [f"Exit {code}, Ausgabe {aus[-300:]!r}"] def f_umlaut_im_anlass(tmp): baue(tmp, {"wf_u": [ablesung(1, "pruefung:vor Prüfung", "2026-09-24T10:00:00Z", 10, 5), ablesung(2, "pruefung:nach Prüfung", "2026-09-24T10:30:00Z", 12, 5)]}) code, aus, err = fahre(tmp) return [] if code == 0 and "pruefung:vor Pruefung -> pruefung:nach Pruefung" in aus else [f"Exit {code}, Ausgabe {aus[-300:]!r}, stderr {err[-200:]!r}"] def f_seit(tmp): baue(tmp, {"wf_s": [ablesung(1, "alt", "2026-09-20T10:00:00Z", 10, 5), ablesung(2, "a", "2026-09-24T10:00:00Z", 10, 5), ablesung(3, "b", "2026-09-24T11:00:00Z", 12, 5)]}) code, aus, _ = fahre(tmp, "--seit", "2026-09-24") return [] if code == 0 and "2 Ablesungen, 1 Paare" in aus and "alt ->" not in aus else [f"Exit {code}, Ausgabe {aus[-300:]!r}"] def f_seit_ungueltig(tmp): baue(tmp, GRUND) code, aus, _ = fahre(tmp, "--seit", "gestern") return [] if code == 2 and "kein Datum" in aus else [f"Exit {code}, Ausgabe {aus!r}"] def f_voller_modellname(tmp): baue(tmp, {"wf_m": [GRUND["wf_a"][0], opus(1, "2026-09-24T10:00:10Z", "2026-09-24T10:30:10Z", modell="claude-opus-5-5"), GRUND["wf_a"][2]]}) code, aus, _ = fahre(tmp) return [] if code == 0 and "opus 30.0" in aus else [f"Exit {code}, Ausgabe {aus[-300:]!r}"] def f_agent_ausserhalb(tmp): # Ein Agent, der vor der ersten Ablesung endet, zaehlt nicht mit; einer, der halb hineinreicht, anteilig. baue(tmp, {"wf_x": [opus(1, "2026-09-24T09:00:00Z", "2026-09-24T09:50:00Z"), GRUND["wf_a"][0], opus(2, "2026-09-24T09:55:00Z", "2026-09-24T10:10:00Z"), GRUND["wf_a"][2]]}) code, aus, _ = fahre(tmp) return [] if code == 0 and "| opus 10.0 |" in aus else [f"Exit {code}, Ausgabe {aus[-400:]!r}"] def f_absturz_ist_befund(tmp): # Ein Zeitstempel ohne Zeitzone neben einem mit ihr laesst den Vergleich mit TypeError scheitern. # Der Lauf muss das als Hinweis melden und die anderen Laeufe trotzdem auswerten. kaputt = [dict(GRUND["wf_a"][0]), dict(GRUND["wf_a"][1], start="2026-09-24T10:00:10"), dict(GRUND["wf_a"][2])] baue(tmp, {"wf_a": GRUND["wf_a"], "wf_b": kaputt}) code, aus, err = fahre(tmp) fehler = [] if code != 0: fehler.append(f"Exit {code}: {err[-300:]}") if "wf_b: Auswertung abgebrochen (TypeError" not in aus: fehler.append("Absturz nicht als Hinweis gemeldet") if "| wf_a |" not in aus: fehler.append("der gesunde Lauf fehlt") return fehler def f_verschiebung(tmp, reset5, erwartet): # Nur resetsAt verschiebt sich, der Wert steigt; entscheidend ist allein die Toleranz. baue(tmp, {"wf_v": [ablesung(1, "a", "2026-09-24T10:00:00Z", 10, 5), ablesung(2, "b", "2026-09-24T10:20:00Z", 12, 5, reset5=reset5)]}) code, aus, _ = fahre(tmp) return [] if code == 0 and erwartet in aus else [f"Exit {code}, Ausgabe {aus[-300:]!r}"] def f_fallender_wert(tmp): baue(tmp, {"wf_d": [ablesung(1, "a", "2026-09-24T10:00:00Z", 30, 5), ablesung(2, "b", "2026-09-24T10:20:00Z", 5, 5)]}) code, aus, _ = fahre(tmp) return [] if code == 0 and "| Reset | 0.0 |" in aus else [f"Exit {code}, Ausgabe {aus[-300:]!r}"] def f_reset_unlesbar(tmp): baue(tmp, {"wf_u": [ablesung(1, "a", "2026-09-24T10:00:00Z", 10, 5, reset5="bald"), ablesung(2, "b", "2026-09-24T10:20:00Z", 12, 6)]}) code, aus, _ = fahre(tmp) return [] if code == 0 and "| unbekannt | 1.0 |" in aus else [f"Exit {code}, Ausgabe {aus[-300:]!r}"] def f_hoechstes_fenster(tmp): liste = [{"label": "5-hour limit", "percentUsed": 10, "resetsAt": "2026-09-24T18:30:00Z"}, {"label": "5-hour limit", "percentUsed": 30, "resetsAt": "2026-09-24T18:30:00Z"}, {"label": "Weekly · all models", "percentUsed": 5, "resetsAt": "2026-09-28T00:00:00Z"}] baue(tmp, {"wf_h": [ablesung(1, "a", "2026-09-24T10:00:00Z", 0, 0, liste=liste), ablesung(2, "b", "2026-09-24T10:20:00Z", 35, 6)]}) code, aus, _ = fahre(tmp) return [] if code == 0 and "| 5.0 | 1.0 |" in aus else [f"Exit {code}, Ausgabe {aus[-300:]!r}"] def f_ablesezeit_ist_ende(tmp): # Der Opus-Agent laeuft ganz innerhalb des ersten Ableseagenten, also vor der Ablesung. baue(tmp, {"wf_t": [ablesung(1, "a", "2026-09-24T10:00:00Z", 10, 5, bis="2026-09-24T10:00:10Z"), opus(1, "2026-09-24T10:00:02Z", "2026-09-24T10:00:08Z"), ablesung(2, "b", "2026-09-24T10:30:00Z", 12, 5)]}) code, aus, _ = fahre(tmp) return [] if code == 0 and "| 29.8 | - | 2.0 | 0.0 |" in aus else [f"Exit {code}, Ausgabe {aus[-300:]!r}"] def f_agent_ueber_das_paar_hinaus(tmp): baue(tmp, {"wf_b": [ablesung(1, "a", "2026-09-24T10:00:00Z", 10, 5), ablesung(2, "b", "2026-09-24T10:30:00Z", 12, 5), opus(1, "2026-09-24T10:20:00Z", "2026-09-24T10:50:00Z"), ablesung(3, "c", "2026-09-24T11:00:00Z", 15, 5)]}) code, aus, _ = fahre(tmp) fehler = [] if code != 0 or "| a -> b | 30.0 | opus 10.0 |" not in aus: fehler.append("erstes Paar nicht am Ende gekappt: " + aus[-400:]) if "| b -> c | 30.0 | opus 20.0 |" not in aus: fehler.append("zweites Paar falsch: " + aus[-400:]) return fehler def f_ueber_hundert(tmp): baue(tmp, {"wf_g": [ablesung(1, "a", "2026-09-24T10:00:00Z", 98, 5), ablesung(2, "b", "2026-09-24T10:20:00Z", 103, 5)]}) code, aus, _ = fahre(tmp) return [] if code == 0 and "| 5.0 | 0.0 |" in aus else [f"Exit {code}, Ausgabe {aus[-300:]!r}"] FAELLE = [ ("Grundlinie: zwei Ablesungen, ein Opus-Agent dazwischen", f_grundlinie), ("Reset zwischen zwei Ablesungen", f_reset), ("Speicher fehlt: Exit 2", f_speicher_fehlt), ("null Ablesungen: Exit 0 mit Zaehlung", f_null_ablesungen), ("kaputte Journalzeile: Hinweis, kein Absturz", f_kaputte_zeile), ("erste Ablesung ohne Zeitstempel", lambda t: f_ohne_zeitstempel(t, 0)), ("letzte Ablesung ohne Zeitstempel", lambda t: f_ohne_zeitstempel(t, 2)), ("5-Stunden-Fenster fehlt", lambda t: f_fenster_kaputt(t, [{"label": "Weekly · all models", "percentUsed": 5, "resetsAt": "2026-09-28T00:00:00Z"}], "| unbekannt | 1.0 |")), ("Umlaut im Anlass auf cp1252", f_umlaut_im_anlass), ("Prozent als Text", lambda t: f_fenster_kaputt(t, [{"label": "5-hour limit", "percentUsed": "10", "resetsAt": "2026-09-24T18:30:00Z"}, {"label": "Weekly · all models", "percentUsed": 5, "resetsAt": "2026-09-28T00:00:00Z"}])), ("Prozent als Wahrheitswert", lambda t: f_fenster_kaputt(t, [{"label": "5-hour limit", "percentUsed": True, "resetsAt": "2026-09-24T18:30:00Z"}, {"label": "Weekly · all models", "percentUsed": 5, "resetsAt": "2026-09-28T00:00:00Z"}])), ("fenster kein Array", lambda t: f_fenster_kaputt(t, {"a": 1})), ("--seit filtert aeltere Ablesungen", f_seit), ("--seit ungueltig: Exit 2", f_seit_ungueltig), ("voller Modellname zaehlt als opus", f_voller_modellname), ("Agent ausserhalb und halb innerhalb des Paars", f_agent_ausserhalb), ("Absturz in einem Lauf ist ein Hinweis", f_absturz_ist_befund), ("resetsAt 5 Stunden weiter, Wert steigt: Reset", lambda t: f_verschiebung(t, "2026-09-24T23:30:00Z", "| Reset | 0.0 |")), ("resetsAt 4:59 verschoben: dasselbe Fenster", lambda t: f_verschiebung(t, "2026-09-24T18:34:59Z", "| 2.0 | 0.0 |")), ("resetsAt 5:01 verschoben: Reset", lambda t: f_verschiebung(t, "2026-09-24T18:35:01Z", "| Reset | 0.0 |")), ("resetsAt 5:01 frueher: Reset", lambda t: f_verschiebung(t, "2026-09-24T18:24:59Z", "| Reset | 0.0 |")), ("fallender Wert im selben Fenster ist ein Reset", f_fallender_wert), ("resetsAt ohne Datum: unbekannt", f_reset_unlesbar), ("doppeltes 5-Stunden-Fenster: das hoechste zaehlt", f_hoechstes_fenster), ("Ablesezeit ist das Ende des Ableseagenten", f_ablesezeit_ist_ende), ("Agent ueber das Paar hinaus wird am Ende gekappt", f_agent_ueber_das_paar_hinaus), ("ueber 100 Prozent ist ein Wert, kein Fehler", f_ueber_hundert), ] def main(): # Fehlertexte koennen Zeichen ausserhalb der Konsolenkodierung tragen; der Treiber darf daran nicht sterben. sys.stdout.reconfigure(errors="replace") gescheitert = 0 for name, fall in FAELLE: with tempfile.TemporaryDirectory() as tmp: try: fehler = fall(Path(tmp)) except Exception as e: # ein Absturz im Fall ist selbst ein Befund fehler = [f"Absturz im Fall: {type(e).__name__}: {e}"] if fehler: gescheitert += 1 print(f"FEHLER {name}\n " + "\n ".join(fehler)) else: print(f"OK {name}") print(f"{len(FAELLE)} Faelle, {gescheitert} gescheitert") return 1 if gescheitert else 0 if __name__ == "__main__": sys.exit(main())