426 lines
14 KiB
Python
426 lines
14 KiB
Python
import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
MAIN_LOOP_SCRIPTS = ROOT / "skills" / "cook-it-through" / "scripts"
|
|
sys.path.insert(0, str(MAIN_LOOP_SCRIPTS))
|
|
|
|
from main_loop_scheduler import (
|
|
FeatureId,
|
|
FeatureRecord,
|
|
FeatureIntegrationId,
|
|
Scheduler,
|
|
SchedulerError,
|
|
TicketId,
|
|
TicketRecord,
|
|
parse_dependencies,
|
|
)
|
|
|
|
|
|
class SchedulerIdentityTests(unittest.TestCase):
|
|
def test_dependency_parser_accepts_only_canonical_qualified_identities(self):
|
|
owner = TicketId.parse("feature-a/04")
|
|
|
|
self.assertEqual(parse_dependencies("None", owner), ())
|
|
self.assertEqual(
|
|
parse_dependencies(
|
|
"feature-a/01; feature-b/03; feature-c@integrated",
|
|
owner,
|
|
),
|
|
(
|
|
TicketId.parse("feature-a/01"),
|
|
TicketId.parse("feature-b/03"),
|
|
FeatureIntegrationId.parse("feature-c@integrated"),
|
|
),
|
|
)
|
|
self.assertEqual(str(FeatureId.parse("feature-a")), "feature-a")
|
|
self.assertEqual(str(TicketId.parse("feature-a/01")), "feature-a/01")
|
|
self.assertEqual(
|
|
str(FeatureIntegrationId.parse("feature-a@integrated")),
|
|
"feature-a@integrated",
|
|
)
|
|
|
|
invalid_values = (
|
|
"01",
|
|
"01 - First",
|
|
"feature-a/01, feature-b/03",
|
|
"None - can start immediately",
|
|
"feature-a/1",
|
|
"Feature-A/01",
|
|
)
|
|
for raw in invalid_values:
|
|
with self.subTest(raw=raw), self.assertRaisesRegex(
|
|
SchedulerError,
|
|
"invalid dependency",
|
|
):
|
|
parse_dependencies(raw, owner)
|
|
|
|
with self.assertRaisesRegex(
|
|
SchedulerError,
|
|
"duplicate dependency feature-a/01",
|
|
):
|
|
parse_dependencies("feature-a/01; feature-a/01", owner)
|
|
|
|
with self.assertRaisesRegex(
|
|
SchedulerError,
|
|
"self dependency feature-a/04",
|
|
):
|
|
parse_dependencies("feature-a/04", owner)
|
|
|
|
|
|
class GlobalSchedulerTests(unittest.TestCase):
|
|
def test_scheduler_rejects_an_invalid_ticket_status_with_qualified_identity(self):
|
|
feature = FeatureRecord(
|
|
id=FeatureId.parse("alpha"),
|
|
tickets=(
|
|
TicketRecord(
|
|
id=TicketId.parse("alpha/01"),
|
|
slug="invalid",
|
|
status="done",
|
|
dependencies=(),
|
|
),
|
|
),
|
|
)
|
|
|
|
with self.assertRaisesRegex(
|
|
SchedulerError,
|
|
"alpha/01: invalid status done",
|
|
):
|
|
Scheduler((feature,))
|
|
|
|
def test_scheduler_rejects_impossible_integration_state(self):
|
|
unresolved_integrated = FeatureRecord(
|
|
id=FeatureId.parse("alpha"),
|
|
tickets=(
|
|
TicketRecord(
|
|
id=TicketId.parse("alpha/01"),
|
|
slug="unfinished",
|
|
status="ready-for-agent",
|
|
dependencies=(),
|
|
),
|
|
),
|
|
integrated=True,
|
|
)
|
|
with self.assertRaisesRegex(
|
|
SchedulerError,
|
|
"alpha@integrated: unsatisfied ticket alpha/01",
|
|
):
|
|
Scheduler((unresolved_integrated,))
|
|
|
|
alpha = FeatureRecord(
|
|
id=FeatureId.parse("alpha"),
|
|
tickets=(
|
|
TicketRecord(
|
|
id=TicketId.parse("alpha/01"),
|
|
slug="pending",
|
|
status="resolved",
|
|
dependencies=(),
|
|
),
|
|
),
|
|
)
|
|
beta = FeatureRecord(
|
|
id=FeatureId.parse("beta"),
|
|
tickets=(
|
|
TicketRecord(
|
|
id=TicketId.parse("beta/01"),
|
|
slug="done",
|
|
status="resolved",
|
|
dependencies=(),
|
|
),
|
|
),
|
|
integrated=True,
|
|
)
|
|
with self.assertRaisesRegex(
|
|
SchedulerError,
|
|
"beta@integrated: earlier integration is pending: alpha@integrated",
|
|
):
|
|
Scheduler((alpha, beta))
|
|
|
|
def test_claimable_tickets_use_queue_priority_without_a_feature_head_barrier(self):
|
|
alpha = FeatureRecord(
|
|
id=FeatureId.parse("alpha"),
|
|
tickets=(
|
|
TicketRecord(
|
|
id=TicketId.parse("alpha/01"),
|
|
slug="alpha-ticket",
|
|
status="claimed",
|
|
dependencies=(),
|
|
),
|
|
),
|
|
)
|
|
beta = FeatureRecord(
|
|
id=FeatureId.parse("beta"),
|
|
tickets=(
|
|
TicketRecord(
|
|
id=TicketId.parse("beta/02"),
|
|
slug="later-number",
|
|
status="ready-for-agent",
|
|
dependencies=(),
|
|
),
|
|
TicketRecord(
|
|
id=TicketId.parse("beta/01"),
|
|
slug="earlier-number",
|
|
status="ready-for-agent",
|
|
dependencies=(),
|
|
),
|
|
),
|
|
)
|
|
|
|
scheduler = Scheduler((alpha, beta))
|
|
|
|
self.assertEqual(
|
|
scheduler.ticket_frontier,
|
|
(TicketId.parse("beta/01"), TicketId.parse("beta/02")),
|
|
)
|
|
|
|
def test_ticket_and_integration_dependencies_have_distinct_satisfaction_rules(self):
|
|
beta_id = FeatureId.parse("beta")
|
|
alpha_ticket = TicketRecord(
|
|
id=TicketId.parse("alpha/01"),
|
|
slug="consumer",
|
|
status="ready-for-agent",
|
|
dependencies=(
|
|
TicketId.parse("beta/01"),
|
|
FeatureIntegrationId.parse("beta@integrated"),
|
|
),
|
|
)
|
|
beta = FeatureRecord(
|
|
id=beta_id,
|
|
tickets=(
|
|
TicketRecord(
|
|
id=TicketId.parse("beta/01"),
|
|
slug="provider",
|
|
status="skipped",
|
|
dependencies=(),
|
|
),
|
|
),
|
|
)
|
|
alpha = FeatureRecord(
|
|
id=FeatureId.parse("alpha"),
|
|
tickets=(alpha_ticket,),
|
|
)
|
|
|
|
before_integration = Scheduler((beta, alpha))
|
|
self.assertEqual(before_integration.ticket_frontier, ())
|
|
self.assertEqual(
|
|
before_integration.integration_dependencies(alpha_ticket.id),
|
|
(FeatureIntegrationId.parse("beta@integrated"),),
|
|
)
|
|
self.assertEqual(
|
|
before_integration.unsatisfied_dependencies(alpha_ticket.id),
|
|
(FeatureIntegrationId.parse("beta@integrated"),),
|
|
)
|
|
|
|
after_integration = Scheduler(
|
|
(
|
|
FeatureRecord(
|
|
id=beta.id,
|
|
tickets=beta.tickets,
|
|
integrated=True,
|
|
),
|
|
alpha,
|
|
)
|
|
)
|
|
self.assertEqual(after_integration.ticket_frontier, (alpha_ticket.id,))
|
|
|
|
resolved_only = Scheduler(
|
|
(
|
|
FeatureRecord(
|
|
id=beta.id,
|
|
tickets=(
|
|
TicketRecord(
|
|
id=TicketId.parse("beta/01"),
|
|
slug="provider",
|
|
status="resolved",
|
|
dependencies=(),
|
|
),
|
|
),
|
|
),
|
|
FeatureRecord(
|
|
id=alpha.id,
|
|
tickets=(
|
|
TicketRecord(
|
|
id=alpha_ticket.id,
|
|
slug=alpha_ticket.slug,
|
|
status=alpha_ticket.status,
|
|
dependencies=(TicketId.parse("beta/01"),),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
)
|
|
self.assertEqual(resolved_only.ticket_frontier, (alpha_ticket.id,))
|
|
|
|
def test_graph_validation_reports_missing_targets_and_cross_feature_cycles(self):
|
|
missing_feature = FeatureRecord(
|
|
id=FeatureId.parse("alpha"),
|
|
tickets=(
|
|
TicketRecord(
|
|
id=TicketId.parse("alpha/01"),
|
|
slug="missing-feature",
|
|
status="ready-for-agent",
|
|
dependencies=(TicketId.parse("missing/01"),),
|
|
),
|
|
),
|
|
)
|
|
with self.assertRaisesRegex(
|
|
SchedulerError,
|
|
"alpha/01: dependency feature not queued: missing",
|
|
):
|
|
Scheduler((missing_feature,))
|
|
|
|
beta = FeatureRecord(
|
|
id=FeatureId.parse("beta"),
|
|
tickets=(
|
|
TicketRecord(
|
|
id=TicketId.parse("beta/01"),
|
|
slug="present",
|
|
status="ready-for-agent",
|
|
dependencies=(),
|
|
),
|
|
),
|
|
)
|
|
missing_ticket = FeatureRecord(
|
|
id=FeatureId.parse("alpha"),
|
|
tickets=(
|
|
TicketRecord(
|
|
id=TicketId.parse("alpha/01"),
|
|
slug="missing-ticket",
|
|
status="ready-for-agent",
|
|
dependencies=(TicketId.parse("beta/99"),),
|
|
),
|
|
),
|
|
)
|
|
with self.assertRaisesRegex(
|
|
SchedulerError,
|
|
"alpha/01: dependency ticket not found: beta/99",
|
|
):
|
|
Scheduler((alpha := missing_ticket, beta))
|
|
|
|
cycle_alpha = FeatureRecord(
|
|
id=alpha.id,
|
|
tickets=(
|
|
TicketRecord(
|
|
id=TicketId.parse("alpha/01"),
|
|
slug="cycle-a",
|
|
status="ready-for-agent",
|
|
dependencies=(TicketId.parse("beta/02"),),
|
|
),
|
|
),
|
|
)
|
|
cycle_beta = FeatureRecord(
|
|
id=beta.id,
|
|
tickets=(
|
|
TicketRecord(
|
|
id=TicketId.parse("beta/02"),
|
|
slug="cycle-b",
|
|
status="ready-for-agent",
|
|
dependencies=(
|
|
FeatureIntegrationId.parse("alpha@integrated"),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
with self.assertRaisesRegex(
|
|
SchedulerError,
|
|
"dependency cycle: alpha/01 -> beta/02 -> alpha@integrated -> alpha/01",
|
|
):
|
|
Scheduler((cycle_alpha, cycle_beta))
|
|
|
|
def test_ready_integration_frontier_does_not_hide_later_ticket_frontier(self):
|
|
alpha = FeatureRecord(
|
|
id=FeatureId.parse("alpha"),
|
|
tickets=(
|
|
TicketRecord(
|
|
id=TicketId.parse("alpha/01"),
|
|
slug="done",
|
|
status="resolved",
|
|
dependencies=(),
|
|
),
|
|
),
|
|
)
|
|
beta = FeatureRecord(
|
|
id=FeatureId.parse("beta"),
|
|
tickets=(
|
|
TicketRecord(
|
|
id=TicketId.parse("beta/01"),
|
|
slug="independent",
|
|
status="ready-for-agent",
|
|
dependencies=(),
|
|
),
|
|
),
|
|
)
|
|
|
|
scheduler = Scheduler((alpha, beta))
|
|
|
|
self.assertEqual(
|
|
scheduler.integration_frontier,
|
|
FeatureIntegrationId.parse("alpha@integrated"),
|
|
)
|
|
self.assertEqual(
|
|
scheduler.ticket_frontier,
|
|
(TicketId.parse("beta/01"),),
|
|
)
|
|
self.assertEqual(scheduler.feature_state(alpha.id), "ready-to-integrate")
|
|
self.assertEqual(scheduler.feature_state(beta.id), "queued")
|
|
|
|
def test_ticket_input_order_does_not_change_frontier_or_error_order(self):
|
|
tickets = (
|
|
TicketRecord(
|
|
id=TicketId.parse("alpha/10"),
|
|
slug="ten",
|
|
status="ready-for-agent",
|
|
dependencies=(),
|
|
),
|
|
TicketRecord(
|
|
id=TicketId.parse("alpha/02"),
|
|
slug="two",
|
|
status="ready-for-agent",
|
|
dependencies=(),
|
|
),
|
|
)
|
|
forward = Scheduler(
|
|
(FeatureRecord(id=FeatureId.parse("alpha"), tickets=tickets),)
|
|
)
|
|
reverse = Scheduler(
|
|
(FeatureRecord(id=FeatureId.parse("alpha"), tickets=tickets[::-1]),)
|
|
)
|
|
expected = (TicketId.parse("alpha/02"), TicketId.parse("alpha/10"))
|
|
self.assertEqual(forward.ticket_frontier, expected)
|
|
self.assertEqual(reverse.ticket_frontier, expected)
|
|
|
|
invalid_tickets = (
|
|
TicketRecord(
|
|
id=TicketId.parse("alpha/02"),
|
|
slug="second-error",
|
|
status="ready-for-agent",
|
|
dependencies=(TicketId.parse("missing/02"),),
|
|
),
|
|
TicketRecord(
|
|
id=TicketId.parse("alpha/01"),
|
|
slug="first-error",
|
|
status="ready-for-agent",
|
|
dependencies=(TicketId.parse("missing/01"),),
|
|
),
|
|
)
|
|
messages = []
|
|
for order in (invalid_tickets, invalid_tickets[::-1]):
|
|
with self.assertRaises(SchedulerError) as raised:
|
|
Scheduler(
|
|
(FeatureRecord(id=FeatureId.parse("alpha"), tickets=order),)
|
|
)
|
|
messages.append(str(raised.exception))
|
|
self.assertEqual(
|
|
messages,
|
|
[
|
|
"alpha/01: dependency feature not queued: missing",
|
|
"alpha/01: dependency feature not queued: missing",
|
|
],
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|