203 lines
8.9 KiB
Python
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")
|