Files
tai/taicpp/tests/agent_sched.py
T

78 lines
4.2 KiB
Python

#!/usr/bin/env python3
"""Differential test of scheduler_tick: tai.py over a real sqlite vault versus
the C++ scheduler over an in-memory table, same rows, stub runner."""
import json
import os
import subprocess
import sys
import tempfile
import time
import types
from datetime import datetime, timedelta, timezone
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.abspath(os.path.join(HERE, "..", ".."))
sys.path.insert(0, ROOT)
sys.dont_write_bytecode = True
import tai # noqa: E402
def main():
binary = os.path.join(HERE, "..", "build", "agent_sched")
home = tempfile.mkdtemp(prefix="agent-sched-")
for key in list(os.environ):
if key.startswith("TAI_"):
del os.environ[key]
os.environ["TAI_HOME"] = home
config = tai.Config(types.SimpleNamespace(yes=True, profile="default", no_opencode=True))
seal = tai.Seal(config.home, tai.resolve_passphrase()[0])
store = tai.Store(config, seal)
now = datetime.now(timezone.utc)
past = lambda **kw: (now - timedelta(**kw)).isoformat() # noqa: E731
rows = [
{"id": 1, "name": "once", "prompt": "do once", "profile": "default", "every_sec": 0, "next_run": past(minutes=5), "timeout": 60, "last_status": "", "updated": past(hours=1)},
{"id": 2, "name": "hourly", "prompt": "do hourly", "profile": "default", "every_sec": 3600, "next_run": "2026-01-01T00:00:00+00:00", "timeout": 60, "last_status": "", "updated": past(hours=1)},
{"id": 3, "name": "busy", "prompt": "still running", "profile": "default", "every_sec": 600, "next_run": past(minutes=1), "timeout": 600, "last_status": "running", "updated": past(seconds=30)},
{"id": 4, "name": "stale", "prompt": "lease expired", "profile": "default", "every_sec": 600, "next_run": past(seconds=1500), "timeout": 60, "last_status": "running", "updated": past(hours=3)},
{"id": 5, "name": "future", "prompt": "later", "profile": "default", "every_sec": 0, "next_run": (now + timedelta(hours=2)).isoformat(), "timeout": 60, "last_status": "", "updated": past(hours=1)},
{"id": 6, "name": "badtime", "prompt": "bad", "profile": "default", "every_sec": 60, "next_run": "2020-01-01 nonsense", "timeout": 60, "last_status": "", "updated": past(hours=1)},
{"id": 7, "name": "micro", "prompt": "micro", "profile": "default", "every_sec": 90, "next_run": "2026-03-01T10:00:00.250000+02:00", "timeout": 60, "last_status": "", "updated": past(hours=1)},
]
db = sqlite3_connect(config.db_path)
for r in rows:
db.execute("INSERT INTO schedules (id, name, prompt, profile, every_sec, next_run, timeout, status, last_status, last_result, created, updated) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', ?, '', ?, ?)",
(r["id"], r["name"], r["prompt"], r["profile"], r["every_sec"], r["next_run"], r["timeout"], r["last_status"], r["updated"], r["updated"]))
db.commit()
db.close()
fired = tai.scheduler_tick(config, seal, runner=lambda task, profile, timeout: "ran: " + task)
deadline = time.time() + 5
while tai.running_agents() and time.time() < deadline:
time.sleep(0.02)
time.sleep(0.4)
db = sqlite3_connect(config.db_path)
got = db.execute("SELECT id, status, next_run, last_status, last_result FROM schedules ORDER BY id").fetchall()
db.close()
expected = {"fired": fired, "rows": [{"id": r[0], "status": r[1], "next_run": r[2], "last_status": r[3], "last_result": r[4]} for r in got]}
done = subprocess.run([binary], input=json.dumps({"rows": rows}).encode(), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if done.returncode != 0:
print("binary failed", done.stderr.decode()[-500:])
return 2
actual = json.loads(done.stdout)
ok = json.dumps(expected, sort_keys=True) == json.dumps(actual, sort_keys=True)
if not ok:
for e, a in zip(expected["rows"], actual["rows"]):
if e != a:
print("row differs\n python:", e, "\n cpp: ", a)
print("fired python=%s cpp=%s" % (expected["fired"], actual["fired"]))
print("scheduler: %s (fired %d)" % ("ok" if ok else "MISMATCH", expected["fired"]))
return 0 if ok else 1
def sqlite3_connect(path):
import sqlite3
return sqlite3.connect(path, timeout=30)
if __name__ == "__main__":
sys.exit(main())