#!/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())