132 lines
8.6 KiB
Python
132 lines
8.6 KiB
Python
import contextlib
|
|
from datetime import datetime, timezone
|
|
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 analyze_scheduler_timings as timing
|
|
import run_freecad_web_scheduler as scheduler
|
|
import scheduler_maintenance as maintenance
|
|
|
|
|
|
class StepTimingTests(unittest.TestCase):
|
|
def event(self, seconds, event_type, item=None):
|
|
return {"schedulerObservedAt": datetime.fromtimestamp(seconds, timezone.utc).isoformat(), "type": event_type, **({"item": item} if item else {})}
|
|
|
|
def test_waiting_is_not_reported_as_command_execution(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
path = Path(directory) / "cycle-000001.log"
|
|
command = {"id": "one", "type": "command_execution", "command": "cat source.py"}
|
|
events = [self.event(0, "scheduler.cycle.started"), self.event(65, "item.started", command),
|
|
self.event(65.01, "item.completed", {**command, "exit_code": 0}), self.event(131, "scheduler.cycle.completed")]
|
|
path.write_text("\n".join(json.dumps(e) for e in events))
|
|
result = timing.detailed_steps(path)
|
|
self.assertAlmostEqual(result["toolUnionSeconds"], 0.01)
|
|
self.assertAlmostEqual(result["betweenEventSeconds"], 130.99)
|
|
self.assertEqual(result["commands"][0]["waitBeforeSeconds"], 65)
|
|
self.assertEqual(result["observedSeconds"], 131)
|
|
|
|
def test_overlapping_commands_are_not_double_counted(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
path = Path(directory) / "cycle.log"
|
|
a = {"id": "a", "type": "command_execution", "command": "first"}
|
|
b = {"id": "b", "type": "command_execution", "command": "second"}
|
|
events = [self.event(0, "scheduler.cycle.started"), self.event(1, "item.started", a), self.event(2, "item.started", b),
|
|
self.event(5, "item.completed", {**a, "exit_code": 0}), self.event(7, "item.completed", {**b, "exit_code": 1}),
|
|
self.event(8, "scheduler.cycle.completed")]
|
|
path.write_text("\n".join(json.dumps(e) for e in events))
|
|
result = timing.detailed_steps(path)
|
|
self.assertEqual(result["toolUnionSeconds"], 6)
|
|
self.assertEqual(result["betweenEventSeconds"], 2)
|
|
self.assertEqual([r["runSeconds"] for r in result["commands"]], [4, 5])
|
|
self.assertEqual(result["unfinishedCommands"], 0)
|
|
|
|
|
|
class ExplorationTests(unittest.TestCase):
|
|
def test_budget_ignores_active_tools_edits_and_real_verification(self):
|
|
reads = [{"phase": "source/read", "command": "cat tools/draft-business-cases.mjs"}]
|
|
self.assertTrue(scheduler.exploration_expired(181, False, 0, reads, 180))
|
|
self.assertFalse(scheduler.exploration_expired(179, False, 0, reads, 180))
|
|
self.assertFalse(scheduler.exploration_expired(300, True, 0, reads, 180))
|
|
self.assertFalse(scheduler.exploration_expired(300, False, 1, reads, 180))
|
|
self.assertFalse(scheduler.exploration_expired(300, False, 0, [{"phase": "other", "command": "node tools/draft-business-cases.mjs"}], 180))
|
|
|
|
def test_runtime_business_retains_completed_reads_after_bounded_stop(self):
|
|
self.assertTrue(scheduler.retain_turn_context(business_turn=True, feature_turn=True, pipeline_eligible=False,
|
|
agent_successful=False, session_clean=False, retained_business_context=True))
|
|
|
|
def test_cumulative_exploration_stops_even_when_each_read_resets_idle(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
item = {"id": "read", "type": "command_execution", "command": "cat source.py"}
|
|
events = [{"type": "turn.started"}, {"type": "item.started", "item": item},
|
|
{"type": "item.completed", "item": {**item, "exit_code": 0}}]
|
|
script = "import sys,time\nsys.stdin.read()\n" + "\n".join(f"print({json.dumps(e)!r},flush=True)" for e in events) + "\ntime.sleep(20)\n"
|
|
metadata = {}
|
|
with patch.object(scheduler, "ROOT", root), 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"), \
|
|
contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
|
|
result = scheduler.run_codex_turn([sys.executable, "-c", script], "fixture", 10, 1, "test", root / "cycle.log",
|
|
lambda **v: None, turn_metadata=metadata, exploration_limit_seconds=0.2)
|
|
self.assertTrue(result[4])
|
|
self.assertFalse(result[1])
|
|
self.assertTrue(metadata["explorationBudgetExceeded"])
|
|
self.assertEqual(metadata["commandsCompleted"], 1)
|
|
|
|
def test_rejected_exploration_does_not_discard_following_edit(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
item = {"id": "read", "type": "command_execution", "command": "git status --short"}
|
|
events = [{"type": "turn.started"}, {"type": "item.started", "item": item},
|
|
{"type": "item.completed", "item": {**item, "exit_code": 0}},
|
|
{"type": "item.completed", "item": {"type": "file_change", "changes": [{"path": "source.py", "kind": "update"}]}},
|
|
{"type": "turn.completed"}]
|
|
script = "import sys\nsys.stdin.read()\n" + "\n".join(f"print({json.dumps(e)!r},flush=True)" for e in events)
|
|
metadata = {}
|
|
with patch.object(scheduler, "ROOT", root), 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"), 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], "fixture", 10, 1, "test", root / "cycle.log",
|
|
lambda **v: None, turn_metadata=metadata)
|
|
self.assertEqual(result[0], 0)
|
|
self.assertTrue(metadata["turnCompleted"])
|
|
self.assertEqual(metadata["fileChanges"], 1)
|
|
self.assertNotIn("forbiddenImplementationCommand", metadata)
|
|
|
|
|
|
class PreparedWorkTests(unittest.TestCase):
|
|
def test_periodic_work_uses_isolated_batch_and_existing_source(self):
|
|
with tempfile.TemporaryDirectory() as directory, patch.object(maintenance, "ROOT", Path(directory)):
|
|
root = Path(directory)
|
|
current = root / "docs/current"
|
|
current.mkdir(parents=True)
|
|
registry = current / "feature-capability-registry.json"
|
|
registry.write_text(json.dumps({"records": [{"id": "Draft:format", "command": "format", "module": "Draft", "worker": "draft.worker.ts"}]}))
|
|
global_batch = current / "module-batch-manifest.json"
|
|
global_batch.write_text('{"batchId":"business-original"}')
|
|
context = {"runtimeIssues": [{"capabilityId": "Draft:format"}], "batch": {"batchId": "other"}}
|
|
def prepare(command, **kwargs):
|
|
batch = json.loads(Path(command[command.index("--batch") + 1]).read_text())
|
|
output = Path(command[command.index("--output") + 1])
|
|
self.assertEqual(batch["selected"]["module"], "Draft")
|
|
output.write_text(json.dumps({"module": "Draft", "supportFiles": [{"path": "web/src/draft.worker.ts", "sha256": "hash"}],
|
|
"editableExcerpts": [{"path": "web/src/draft.worker.ts", "excerpt": "dispatchDraftFormat(request)"}]}))
|
|
return type("Result", (), {"returncode": 0})()
|
|
with patch.object(maintenance.subprocess, "run", side_effect=prepare):
|
|
work = maintenance.prepare_work(context, current, "optimization-test")
|
|
self.assertEqual(global_batch.read_text(), '{"batchId":"business-original"}')
|
|
self.assertEqual(work["module"], "Draft")
|
|
self.assertIn("dispatchDraftFormat", maintenance.prompt_for(current / "context.json", current / "report.md", work))
|
|
self.assertNotEqual(root / work["sourceBatch"], global_batch)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|