|
| 1 | +"""E2E: cloud-routine state persistence across FRESH CLONES. |
| 2 | +
|
| 3 | +A `/schedule` cloud routine clones the default branch fresh every run with no |
| 4 | +local state carried between runs. OODA-loop's claim is that committing |
| 5 | +agent/state/ each cycle (Step 6-D) makes the loop's memory survive that. This |
| 6 | +test proves the mechanism with REAL git: run 1 in one clone commits + pushes its |
| 7 | +state; run 2 in a SEPARATE fresh clone of the same remote must SEE run 1's state |
| 8 | +(cycle_count continuity, decision_log carried) — exactly what a cloud routine |
| 9 | +relies on. If this passed only by reusing a working dir it would be meaningless, |
| 10 | +so each "run" is a distinct clone of a bare remote. |
| 11 | +""" |
| 12 | +import subprocess |
| 13 | +import sys |
| 14 | +import tempfile |
| 15 | +import unittest |
| 16 | +from pathlib import Path |
| 17 | + |
| 18 | +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) |
| 19 | +from driver.engine import Engine, read_json # noqa: E402 |
| 20 | +from driver.sandbox import make_project # noqa: E402 |
| 21 | + |
| 22 | + |
| 23 | +def git(*a, cwd): |
| 24 | + return subprocess.run(["git", *a], cwd=cwd, capture_output=True, text=True, check=True) |
| 25 | + |
| 26 | + |
| 27 | +class CloudStatePersistence(unittest.TestCase): |
| 28 | + def setUp(self): |
| 29 | + self.tmp = tempfile.TemporaryDirectory() |
| 30 | + self.root = Path(self.tmp.name) |
| 31 | + self.remote = self.root / "remote.git" |
| 32 | + git("init", "--bare", "-b", "main", str(self.remote), cwd=self.root) |
| 33 | + |
| 34 | + def tearDown(self): |
| 35 | + self.tmp.cleanup() |
| 36 | + |
| 37 | + def _clone(self, name): |
| 38 | + d = self.root / name |
| 39 | + git("clone", str(self.remote), str(d), cwd=self.root) |
| 40 | + git("config", "user.email", "e2e@ooda.local", cwd=d) |
| 41 | + git("config", "user.name", "OODA E2E", cwd=d) |
| 42 | + return d |
| 43 | + |
| 44 | + def test_state_survives_fresh_clone(self): |
| 45 | + # --- Run 1: a fresh clone, seed the project, run a cycle, push state --- |
| 46 | + run1 = self._clone("run1") |
| 47 | + make_project(run1, safety={"min_cycle_interval_minutes": 0, "halt_file": "agent/safety/HALT", |
| 48 | + "lock_timeout_minutes": 30, "max_silent_failures": 99}) |
| 49 | + git("add", "config.json", "agent", cwd=run1) |
| 50 | + git("commit", "-q", "-m", "seed", cwd=run1) |
| 51 | + git("push", "-q", "origin", "main", cwd=run1) |
| 52 | + |
| 53 | + e1 = Engine(run1) |
| 54 | + e1.run_cycle("2026-08-01T06:00:00", {"selected_domain": "test_coverage", |
| 55 | + "selected_skill": "/check-tests", |
| 56 | + "result": "success", "had_output": True}) |
| 57 | + self.assertEqual(e1.state()["cycle_count"], 1) |
| 58 | + # 6-D committed agent/state; push it (what a cloud routine does at cycle end) |
| 59 | + git("add", "agent/state", cwd=run1) |
| 60 | + # commit may be a no-op if engine already committed; tolerate |
| 61 | + subprocess.run(["git", "commit", "-q", "-m", "cycle 1 state"], cwd=run1, capture_output=True) |
| 62 | + git("push", "-q", "origin", "main", cwd=run1) |
| 63 | + |
| 64 | + # --- Run 2: a SEPARATE fresh clone (a new cloud run) --- |
| 65 | + run2 = self._clone("run2") |
| 66 | + self.assertFalse((run2 / "run1").exists()) # genuinely separate working dir |
| 67 | + st2 = read_json(run2 / "agent" / "state" / "evolve" / "state.json") |
| 68 | + self.assertEqual(st2["cycle_count"], 1, |
| 69 | + "run 2's fresh clone must SEE run 1's committed cycle_count") |
| 70 | + self.assertEqual(len(st2["decision_log"]), 1, "decision_log carried across the clone") |
| 71 | + self.assertEqual(st2["decision_log"][-1]["selected_domain"], "test_coverage") |
| 72 | + |
| 73 | + # run 2 continues the loop from the persisted state → cycle 2, not 1 again |
| 74 | + e2 = Engine(run2) |
| 75 | + e2.run_cycle("2026-08-01T12:00:00", {"selected_domain": "backlog", |
| 76 | + "selected_skill": "/plan-backlog", |
| 77 | + "result": "success", "had_output": True}) |
| 78 | + self.assertEqual(e2.state()["cycle_count"], 2, |
| 79 | + "the loop accumulates across fresh-clone runs (no reset)") |
| 80 | + # outcomes.json (the measurement memory) also carried + grew |
| 81 | + outc = read_json(run2 / "agent" / "state" / "evolve" / "outcomes.json")["entries"] |
| 82 | + self.assertEqual(len(outc), 2, "Outcome Record accumulated across the two cloud runs") |
| 83 | + |
| 84 | + |
| 85 | +if __name__ == "__main__": |
| 86 | + unittest.main() |
0 commit comments