175 lines
9.7 KiB
Python
175 lines
9.7 KiB
Python
import contextlib
|
|
import io
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
import scheduler_maintenance as maintenance
|
|
import run_freecad_web_scheduler as scheduler
|
|
import watch_freecad_web_scheduler as watchdog
|
|
|
|
|
|
class ScheduleTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.directory = tempfile.TemporaryDirectory()
|
|
self.addCleanup(self.directory.cleanup)
|
|
self.root = Path(self.directory.name)
|
|
self.path = self.root / "docs/current/scheduler-maintenance.json"
|
|
self.enterContext(patch.object(maintenance, "ROOT", self.root))
|
|
self.enterContext(patch.object(maintenance, "STATE_PATH", self.path))
|
|
|
|
def test_disabled_schedule_never_runs(self):
|
|
self.assertIsNone(maintenance.claim_due(now=10000))
|
|
maintenance.configure(False, now=1000)
|
|
self.assertIsNone(maintenance.claim_due(now=10000))
|
|
|
|
def test_thirty_minute_schedule_is_idempotent_and_claims_only_once(self):
|
|
first = maintenance.configure(True, now=1000)
|
|
self.assertEqual(first["nextRunAt"], 2800)
|
|
self.assertEqual(maintenance.configure(True, now=1100)["nextRunAt"], 2800)
|
|
self.assertIsNone(maintenance.claim_due(now=2799))
|
|
claimed = maintenance.claim_due(now=2800)
|
|
self.assertEqual(claimed["nextRunAt"], 4600)
|
|
self.assertEqual(claimed["runCount"], 1)
|
|
self.assertIsNone(maintenance.claim_due(now=5000))
|
|
|
|
def test_missed_intervals_coalesce_without_catchup_loop(self):
|
|
maintenance.configure(True, now=1000)
|
|
claimed = maintenance.claim_due(now=8300)
|
|
self.assertEqual(claimed["nextRunAt"], 10000)
|
|
self.assertEqual(claimed["skippedIntervals"], 3)
|
|
self.assertEqual(claimed["runCount"], 1)
|
|
|
|
def test_restart_keeps_next_slot_and_marks_interrupted_run(self):
|
|
maintenance.configure(True, now=1000)
|
|
claimed = maintenance.claim_due(now=2800)
|
|
claimed["schedulerPid"] = 999999999
|
|
maintenance.write(self.path, claimed)
|
|
self.assertIsNone(maintenance.claim_due(now=3000))
|
|
state = maintenance.read(self.path)
|
|
self.assertEqual(state["lastOutcome"], "interrupted")
|
|
self.assertEqual(state["nextRunAt"], 4600)
|
|
self.assertEqual(maintenance.claim_due(now=4600)["runCount"], 2)
|
|
|
|
def test_surviving_child_blocks_duplicate_after_restart(self):
|
|
maintenance.configure(True, now=1000)
|
|
claimed = maintenance.claim_due(now=2800)
|
|
claimed["schedulerPid"] = 999999999
|
|
maintenance.write(self.path, claimed)
|
|
with patch.object(maintenance, "active_child", return_value=123):
|
|
self.assertIsNone(maintenance.claim_due(now=5000))
|
|
|
|
def test_result_records_real_command_outcomes_and_preserves_disabled_schedule(self):
|
|
maintenance.configure(True, now=1000)
|
|
claimed = maintenance.claim_due(now=2800)
|
|
def runner(prompt, log, metadata):
|
|
self.assertIn("functional module business and functional routes", prompt)
|
|
self.assertIn("Scoped builds and meaningful verification are allowed", prompt)
|
|
self.assertIn("Do not start, stop, kill or restart", prompt)
|
|
metadata.update(turnCompleted=True, fileChanges=1, commandsCompleted=2, commandsFailed=1,
|
|
commandResults=[{"command": "focused check", "exitCode": 1}], lastAgentMessage="Fix needs follow-up")
|
|
maintenance.configure(False, now=2850)
|
|
return 0, False, False, False, False
|
|
result = maintenance.execute(claimed, runner, self.root / "cycle.log", now=lambda: 2900, context={"observed": True})
|
|
self.assertEqual(result["commandsFailed"], 1)
|
|
self.assertTrue((self.root / result["reportPath"]).is_file())
|
|
self.assertEqual(result["reportSource"], "scheduler")
|
|
self.assertEqual(result["status"], "incomplete")
|
|
state = maintenance.read(self.path)
|
|
self.assertEqual(state["status"], "disabled")
|
|
self.assertEqual(state["nextRunAt"], 4600)
|
|
self.assertTrue((self.root / state["lastResultPath"]).is_file())
|
|
self.assertIsNone(maintenance.claim_due(now=5000))
|
|
|
|
def test_provider_failure_and_long_run_do_not_create_immediate_retry_loop(self):
|
|
maintenance.configure(True, now=1000)
|
|
claimed = maintenance.claim_due(now=2800)
|
|
result = maintenance.execute(claimed, lambda *args: (1, False, True, False, False),
|
|
self.root / "cycle.log", now=lambda: 8200, context={})
|
|
self.assertEqual(result["status"], "provider_wait")
|
|
self.assertTrue(result["providerFailed"])
|
|
self.assertFalse(result["modelResponded"])
|
|
state = maintenance.read(self.path)
|
|
self.assertEqual(state["nextRunAt"], 10000)
|
|
self.assertIsNone(maintenance.claim_due(now=8201))
|
|
|
|
def test_empty_clean_exit_is_not_a_completed_optimization(self):
|
|
maintenance.configure(True, now=1000)
|
|
claimed = maintenance.claim_due(now=2800)
|
|
result = maintenance.execute(claimed, lambda *args: (0, False, False, False, False),
|
|
self.root / "cycle.log", now=lambda: 2900, context={})
|
|
self.assertEqual(result["status"], "incomplete")
|
|
self.assertEqual(maintenance.read(self.path)["nextRunAt"], 4600)
|
|
|
|
def test_completed_inspection_has_a_report_and_returns_to_fixed_schedule(self):
|
|
maintenance.configure(True, now=1000)
|
|
claimed = maintenance.claim_due(now=2800)
|
|
def runner(prompt, log, metadata):
|
|
metadata.update(turnCompleted=True, commandsCompleted=1, commandsFailed=0)
|
|
report = self.root / "docs/current/periodic-optimization" / f"{claimed['runId']}.md"
|
|
report.write_text("Inspected current bottlenecks; no actionable regression found.\n")
|
|
return 0, False, False, False, False
|
|
result = maintenance.execute(claimed, runner, self.root / "cycle.log", now=lambda: 2900, context={})
|
|
self.assertEqual(result["status"], "completed")
|
|
self.assertTrue(result["modelResponded"])
|
|
self.assertFalse(result["providerFailed"])
|
|
self.assertEqual(maintenance.read(self.path)["nextRunAt"], 4600)
|
|
self.assertIsNone(maintenance.claim_due(now=4599))
|
|
self.assertEqual(maintenance.claim_due(now=4600)["runCount"], 2)
|
|
|
|
def test_unexpected_runner_failure_is_reported_and_rescheduled(self):
|
|
maintenance.configure(True, now=1000)
|
|
claimed = maintenance.claim_due(now=2800)
|
|
def fail(*args):
|
|
raise OSError("executable unavailable")
|
|
result = maintenance.execute(claimed, fail, self.root / "cycle.log", now=lambda: 2900, context={})
|
|
self.assertEqual(result["status"], "failed")
|
|
self.assertFalse(result["providerFailed"])
|
|
self.assertIn("executable unavailable", result["error"])
|
|
self.assertEqual(maintenance.read(self.path)["status"], "scheduled")
|
|
|
|
|
|
class ExecutionTests(unittest.TestCase):
|
|
def test_optimization_keeps_verification_after_edit_and_does_not_dirty_business_batch(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
events = [{"type": "turn.started"},
|
|
{"type": "item.completed", "item": {"type": "file_change", "changes": [{"path": "repair.py", "kind": "update"}]}}]
|
|
command = {"id": "check", "type": "command_execution", "command": "cmake --build fixture"}
|
|
tail = [{"type": "item.started", "item": command},
|
|
{"type": "item.completed", "item": {**command, "exit_code": 0}},
|
|
{"type": "item.completed", "item": {"type": "agent_message", "text": "Checked repair"}},
|
|
{"type": "turn.completed"}]
|
|
script = "import sys,time\nsys.stdin.read()\n" + "\n".join(f"print({json.dumps(event)!r}, flush=True)" for event in events)
|
|
script += "\ntime.sleep(1.5)\n" + "\n".join(f"print({json.dumps(event)!r}, flush=True)" for event in tail)
|
|
metadata = {}
|
|
with patch.object(scheduler, "ROOT", root), patch.object(scheduler, "POST_EDIT_FINALIZE_SECONDS", 0.05), \
|
|
patch.object(scheduler, "build_environment", return_value=dict(os.environ)), \
|
|
patch.object(scheduler, "load_failure_state", return_value={"failures": {}}), \
|
|
patch.object(scheduler, "update_timing_summary"), \
|
|
patch.object(scheduler, "record_unbuilt_edit", side_effect=AssertionError("must not alter business receipt")), \
|
|
patch.object(scheduler, "forbidden_implementation_command", return_value=True), \
|
|
contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
|
|
result = scheduler.run_codex_turn([sys.executable, "-c", script], "test optimization", 10, 1,
|
|
"UNATTENDED-OPTIMIZATION", root / "cycle.log", lambda **values: None,
|
|
turn_metadata=metadata, optimization=True)
|
|
self.assertEqual(result, (0, False, False, False, False))
|
|
self.assertTrue(metadata["turnCompleted"])
|
|
self.assertEqual(metadata["commandsCompleted"], 1)
|
|
self.assertEqual(metadata["lastAgentMessage"], "Checked repair")
|
|
self.assertEqual(metadata["commandResults"][0]["exitCode"], 0)
|
|
|
|
def test_watchdog_does_not_apply_business_cooldown_to_optimization(self):
|
|
with patch.object(watchdog, "scheduler_pid_alive", return_value=True), \
|
|
patch.object(watchdog, "stalled_launch", side_effect=AssertionError("wrong scope")):
|
|
self.assertEqual(watchdog.request_stall_repair(42, {"pid": 42, "executionMode": "periodic-optimization"}, {}, {}), {})
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|