Files
TKfA/tests/test-temps-session.py
T
2026-10-21 23:56:10 +02:00

203 lines
8.9 KiB
Python

#!/usr/bin/env python3
"""Test de outils/temps-session.py, hors ligne.
python3 -I tests/test-temps-session.py
Un transcript fabriqué dans un HOME jetable, et un Gitea simulé (l'appel
réseau est remplacé) : on vérifie ce que l'outil propose de saisir.
"""
import contextlib, datetime, importlib.util, io, json, os, sys, tempfile
from pathlib import Path
DEPOT = Path(__file__).resolve().parent.parent
OUTIL = DEPOT / "outils" / "temps-session.py"
SESSION = "00000000-0000-0000-0000-000000000000"
echecs = []
def verifier(condition, message):
if not condition:
echecs.append(message)
def charger():
spec = importlib.util.spec_from_file_location("temps_session", OUTIL)
m = importlib.util.module_from_spec(spec)
spec.loader.exec_module(m)
return m
def humain(t, texte):
return {"type": "user", "timestamp": t, "message": {"role": "user", "content": texte}}
def agent(t):
return {"type": "assistant", "timestamp": t, "message": {"role": "assistant",
"content": [{"type": "text", "text": "…"}]}}
# Deux tours : A de 10:00 à 10:05 (300 s), B de 10:08 à 10:10 (120 s).
TRANSCRIPT = [humain("2026-10-21T10:00:00Z", "A"), agent("2026-10-21T10:02:00Z"),
agent("2026-10-21T10:05:00Z"),
humain("2026-10-21T10:08:00Z", "B"), agent("2026-10-21T10:10:00Z")]
# (api, jeton) de chaque appel au Gitea simulé, pour le dernier lancement.
APPELS = []
def lancer(ts, saisies, *options, jeton=True, refus=False):
"""Lance main() avec un Gitea qui renvoie `saisies` ; rend les lignes JSON.
jeton=False n'ajoute pas --jeton ; refus=True attend que l'outil refuse,
sans avoir appelé Gitea."""
def gitea(api, chemin, j):
APPELS.append((api, j))
if chemin == "/user":
return {"login": "agent"}
return saisies if chemin.endswith("&page=1") else []
ts.gitea = gitea
APPELS.clear()
sys.argv = ["temps-session.py", "--depot", "organisation/depot", "--ticket", "1",
"--session", SESSION, *(["--jeton", str(JETON)] if jeton else []), *options]
sortie = io.StringIO()
erreur = io.StringIO()
try:
with contextlib.redirect_stdout(sortie), contextlib.redirect_stderr(erreur):
ts.main()
except SystemExit:
verifier(refus, "l'outil refuse %s : %s" % (" ".join(options), erreur.getvalue().strip()[-120:]))
verifier(not APPELS, "l'outil refuse %s après avoir appelé %r" % (" ".join(options), APPELS))
return []
verifier(not refus, "l'outil accepte %s" % " ".join(options))
return [json.loads(l) for l in sortie.getvalue().splitlines() if l.strip()]
with tempfile.TemporaryDirectory() as t:
t = Path(t)
os.environ["HOME"] = str(t)
projet = t / ".claude" / "projects" / "-espace"
projet.mkdir(parents=True)
(projet / (SESSION + ".jsonl")).write_text("".join(json.dumps(d) + "\n" for d in TRANSCRIPT))
for v in ("XDG_CONFIG_HOME", "TKFA_CONFIG", "GITEA_TOKEN_FILE"):
os.environ.pop(v, None)
JETON = t / "jeton"
JETON.write_text("factice\n")
ts = charger()
# Configuration des essais : `autre` par défaut (son jeton, chemin relatif
# au fichier), `variable` (jeton par GITEA_TOKEN_FILE), et deux instances
# à refuser. Les essais 1 à 7 passent --jeton, qui prime.
conf = t / "conf"
conf.mkdir()
(conf / "autre.jeton").write_text("autre\n")
config = conf / "tkfa.toml"
config.write_text('''defaut = "autre"
[instances.autre]
api = "https://gitea.example.org/api/v1/"
jeton = "autre.jeton"
[instances.variable]
api = "https://gitea.example.com/api/v1"
jeton_env = "GITEA_TOKEN_FILE"
[instances.sans-jeton]
api = "https://gitea.example.net/api/v1"
[instances.en-clair]
api = "http://gitea.example.org/api/v1"
jeton = "autre.jeton"
''')
os.environ["TKFA_CONFIG"] = str(config)
# 1. Rien de saisi : une séance (tours à moins de --seance), 420 s, datée de sa fin.
lignes = lancer(ts, [])
verifier([l["time"] for l in lignes] == [420], "sans saisie : %r au lieu de 420 s" % lignes)
verifier(lignes and lignes[0]["created"].startswith("2026-10-21T10:10:00"),
"la saisie n'est pas datée de la fin de la séance : %r" % lignes)
# 2. A saisi, Gitea a ignoré la date envoyée et daté la saisie de son envoi
# (10:06, après A, avant B) : seul B reste, 120 s.
lignes = lancer(ts, [{"created": "2026-10-21T10:06:00Z", "time": 300}])
verifier([l["time"] for l in lignes] == [120],
"saisie datée de son envoi : %r au lieu de 120 s (le temps de B est perdu)" % lignes)
# 3. A saisi, Gitea a gardé la date envoyée (la fin de A) : seul B reste.
lignes = lancer(ts, [{"created": "2026-10-21T10:05:00Z", "time": 300}])
verifier([l["time"] for l in lignes] == [120], "saisie datée de la fin de A : %r au lieu de 120 s" % lignes)
# 4. Tout saisi : relancer ne double rien.
lignes = lancer(ts, [{"created": "2026-10-21T10:06:00Z", "time": 300},
{"created": "2026-10-21T10:11:00Z", "time": 120}])
verifier(lignes == [], "tout est saisi, et l'outil propose encore %r" % lignes)
# 5. --jusqua : le travail d'après la borne (B, sur un autre ticket) n'est pas compté.
lignes = lancer(ts, [], "--jusqua", "2026-10-21T10:07:00Z")
verifier([l["time"] for l in lignes] == [300], "--jusqua entre A et B : %r au lieu de 300 s" % lignes)
verifier(lignes and lignes[0]["created"].startswith("2026-10-21T10:05:00"),
"--jusqua : la saisie n'est pas datée de la fin de A : %r" % lignes)
# 6. --jusqua au milieu d'un tour : seul ce qui précède la borne compte.
lignes = lancer(ts, [], "--jusqua", "2026-10-21T10:03:00Z")
verifier([l["time"] for l in lignes] == [120], "--jusqua dans A : %r au lieu de 120 s" % lignes)
# 7. --depuis et --jusqua ensemble, sans fuseau (celui de la machine) : B seul.
def local(h):
return datetime.datetime.fromisoformat(h).astimezone().replace(tzinfo=None).isoformat()
lignes = lancer(ts, [], "--depuis", local("2026-10-21T10:07:00+00:00"),
"--jusqua", local("2026-10-21T10:30:00+00:00"))
verifier([l["time"] for l in lignes] == [120], "--depuis/--jusqua sans fuseau : %r au lieu de 120 s" % lignes)
# 8. Sans configuration, aucune instance : refus, même avec --jeton et
# GITEA_TOKEN_FILE.
del os.environ["TKFA_CONFIG"]
os.environ["GITEA_TOKEN_FILE"] = str(JETON)
lancer(ts, [], refus=True)
lancer(ts, [], jeton=False, refus=True)
# 9. L'instance par défaut : son api, son jeton, et pas GITEA_TOKEN_FILE,
# resté défini ; `jeton_env` sert l'instance qui le nomme.
lancer(ts, [], "--config", str(config), jeton=False)
verifier(APPELS and all(a == ("https://gitea.example.org/api/v1", "autre") for a in APPELS),
"instance par défaut : appels %r" % APPELS)
lancer(ts, [], "--config", str(config), "--instance", "variable", jeton=False)
verifier(APPELS and all(a == ("https://gitea.example.com/api/v1", "factice") for a in APPELS),
"jeton_env : appels %r" % APPELS)
# 10. TKFA_CONFIG, puis le chemin par défaut, sont lus aussi.
os.environ["TKFA_CONFIG"] = str(config)
lancer(ts, [], jeton=False)
verifier(APPELS and APPELS[0][0] == "https://gitea.example.org/api/v1", "TKFA_CONFIG : appels %r" % APPELS)
del os.environ["TKFA_CONFIG"]
(t / ".config" / "tkfa").mkdir(parents=True)
(t / ".config" / "tkfa" / "config.toml").write_text(config.read_text().replace(
'"autre.jeton"', '"%s"' % (conf / "autre.jeton")))
lancer(ts, [], jeton=False)
verifier(APPELS and APPELS[0] == ("https://gitea.example.org/api/v1", "autre"),
"~/.config/tkfa/config.toml : appels %r" % APPELS)
(t / ".config" / "tkfa" / "config.toml").unlink()
# 11. Refus, avant tout appel : instance sans jeton (GITEA_TOKEN_FILE est
# défini mais ne part pas ailleurs), variable de `jeton_env` absente,
# api en http, instance ou fichier absents.
lancer(ts, [], "--config", str(config), "--instance", "sans-jeton", jeton=False, refus=True)
del os.environ["GITEA_TOKEN_FILE"]
lancer(ts, [], "--config", str(config), "--instance", "variable", jeton=False, refus=True)
lancer(ts, [], "--config", str(config), "--instance", "en-clair", refus=True)
lancer(ts, [], "--config", str(config), "--instance", "inconnue", refus=True)
lancer(ts, [], "--config", str(t / "absent.toml"), refus=True)
# 12. --version : le nom de l'outil et sa version, sans rien lire d'autre.
sortie = io.StringIO()
sys.argv = ["temps-session.py", "--version"]
try:
with contextlib.redirect_stdout(sortie):
ts.main()
except SystemExit:
pass
verifier(sortie.getvalue().strip() == "temps-session.py " + ts.VERSION,
"--version affiche %r" % sortie.getvalue())
if echecs:
print("%d échec(s) :" % len(echecs))
for e in echecs:
print(" ✗", e)
sys.exit(1)
print("ok : outil de saisie des temps")