Files
magnus919_agent-skills/technical-project-management/scripts/test_schedule.py
T
Magnus HedemarkandGitHub 22d141de75 feat: add research-grounded technical project management skill (#468)
* feat: add research-grounded technical project management skill

* test: retain isolated project coordination regression evidence
2026-09-04 23:56:40 -04:00

178 lines
6.7 KiB
Python

"""Hand-calculated networks, malformed-input boundaries, and CLI behavior."""
import copy
import importlib.util
import json
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
SCRIPT = Path(__file__).with_name("schedule.py")
spec = importlib.util.spec_from_file_location("tpm_schedule", SCRIPT)
schedule = importlib.util.module_from_spec(spec)
spec.loader.exec_module(schedule)
def task(key, duration, deps=(), **extra):
return {"id": key, "duration": duration, "depends_on": list(deps), **extra}
def network(tasks, **extra):
return {"schema_version": 1, "unit": "working_days", "tasks": tasks, **extra}
class ScheduleTests(unittest.TestCase):
def setUp(self):
self.data = network(
[task("A", 2), task("B", 3, ["A"]), task("C", 1, ["A"]), task("D", 1, ["B", "C"])]
)
def test_diamond_float_and_deadline(self):
self.data["deadline"] = 5
result = schedule.analyze(self.data)
self.assertEqual(result["finish"], 6)
self.assertEqual(result["deadline_gap"], 1)
self.assertEqual(result["critical_tasks"], ["A", "B", "D"])
timings = {t["id"]: t for t in result["tasks"]}
self.assertEqual(timings["C"]["total_float"], 2)
self.assertEqual(timings["D"]["early_start"], 5)
def test_tied_paths_and_disconnected_task(self):
result = schedule.analyze(
network([task("A", 2), task("B", 2), task("join", 0, ["A", "B"]), task("C", 1)])
)
self.assertEqual(result["critical_tasks"], ["A", "B", "join"])
self.assertEqual(result["finish"], 2)
self.assertIsNone(result["deadline_gap"])
def test_resource_conflict_not_silently_leveled(self):
result = schedule.analyze(
network([task("A", 2, resource="engineer"), task("B", 3, resource="engineer")])
)
self.assertEqual(result["finish"], 3)
self.assertEqual(result["resource_conflicts"][0]["overlap_finish"], 2)
self.assertIn("not resource-leveled", result["warnings"][0])
def test_conflict_output_is_bounded(self):
result = schedule.analyze(network([task(str(i), 1, resource="r") for i in range(20)]))
self.assertEqual(result["resource_conflict_count"], 190)
self.assertEqual(len(result["resource_conflicts"]), 100)
self.assertTrue(result["resource_conflicts_truncated"])
def test_adjacent_and_zero_duration_do_not_conflict(self):
result = schedule.analyze(
network(
[
task("A", 2, resource="r"),
task("B", 1, ["A"], resource="r"),
task("M", 0, resource="r"),
]
)
)
self.assertEqual(result["resource_conflicts"], [])
def test_order_independence_and_no_input_mutation(self):
original = copy.deepcopy(self.data)
first = schedule.analyze(self.data)
self.assertEqual(self.data, original)
self.data["tasks"].reverse()
self.assertEqual(first, schedule.analyze(self.data))
def test_float_durations(self):
result = schedule.analyze(network([task("A", 0.1), task("B", 0.2, ["A"])]))
self.assertAlmostEqual(result["finish"], 0.3)
self.assertEqual(result["critical_tasks"], ["A", "B"])
def test_invalid_durations(self):
for value in [-1, True, "2", None, float("nan"), float("inf"), 10**400]:
with self.subTest(value=str(value)), self.assertRaises(ValueError):
schedule.analyze(network([task("A", value)]))
def test_graph_errors(self):
for tasks in [
[],
[task("A", 1), task("A", 2)],
[task("A", 1, ["missing"])],
[task("A", 1, ["A"])],
[task("A", 1, ["B"]), task("B", 1, ["A"])],
[task("A", 1), task("B", 1, ["A", "A"])],
]:
with self.subTest(tasks=tasks), self.assertRaises(ValueError):
schedule.analyze(network(tasks))
def test_unknown_fields_and_malformed_types(self):
samples = [
None,
[],
network([None]),
network([task("", 1)]),
network([task("A", 1)], unit="days"),
network([task("A", 1)], schema_version=True),
network([task("A", 1)], deadline=True),
network([task("A", 1)], calendar="holiday"),
network([task("A", 1, lag=2)]),
network([task("A", 1, resource="")]),
]
for data in samples:
with self.subTest(data=data), self.assertRaises(ValueError):
schedule.analyze(data)
def test_bounds(self):
for data in [
network([task(str(i), 1) for i in range(501)]),
network([task("A", 600000), task("B", 600000)]),
]:
with self.assertRaises(ValueError):
schedule.analyze(data)
def test_cycle_dependent_on_valid_root(self):
with self.assertRaisesRegex(ValueError, "cycle"):
schedule.analyze(
network([task("root", 1), task("A", 1, ["root", "B"]), task("B", 1, ["A"])])
)
def test_cli_success_errors_and_read_only(self):
with tempfile.TemporaryDirectory() as folder:
path = Path(folder) / "input.json"
for content, code in [
(json.dumps(self.data), 0),
('{"tasks":[],"tasks":[]}', 2),
('{"x": NaN}', 2),
("not json", 2),
("[" * 1500, 2),
(" " * (schedule.MAX_BYTES + 1), 2),
]:
path.write_text(content)
result = subprocess.run(
[sys.executable, str(SCRIPT), "--input", str(path), "--json"],
capture_output=True,
text=True,
timeout=10,
)
self.assertEqual(result.returncode, code, result.stderr)
self.assertEqual(path.read_text(), content)
self.assertEqual(list(Path(folder).iterdir()), [path])
if code == 0:
self.assertEqual(json.loads(result.stdout)["finish"], 6)
self.assertEqual(result.stderr, "")
else:
self.assertEqual(result.stdout, "")
self.assertIn("error:", result.stderr)
def test_missing_file(self):
with tempfile.TemporaryDirectory() as folder:
result = subprocess.run(
[sys.executable, str(SCRIPT), "--input", str(Path(folder) / "absent")],
capture_output=True,
text=True,
timeout=10,
)
self.assertEqual(result.returncode, 2)
self.assertNotIn("Traceback", result.stderr)
if __name__ == "__main__":
unittest.main()