Initial commit
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
"""Fix 5 PR7 — kanban queue-depth surface + stuck-session alerts.
|
||||
|
||||
Covers:
|
||||
- Kanban payload surfacing: ``_serialize_session`` includes
|
||||
``pending_queue_depth`` + ``pending_work_item_ids``.
|
||||
- Stuck-queue runtime event fires at the depth threshold (once, not
|
||||
on every subsequent enqueue).
|
||||
- ``check_stuck_focused_sessions`` emits one event per offender whose
|
||||
focus has been held past ``threshold_minutes``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
import unittest
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
from opc.core.models import (
|
||||
CompanyMemberSession,
|
||||
DelegationRoleSession,
|
||||
DelegationRun,
|
||||
DelegationWorkItem,
|
||||
Phase,
|
||||
)
|
||||
from opc.database.store import OPCStore
|
||||
from opc.layer2_organization import phase_hooks # noqa: F401 — registers hooks
|
||||
from opc.layer2_organization.phase_hooks import (
|
||||
STUCK_QUEUE_DEPTH_THRESHOLD,
|
||||
check_stuck_focused_sessions,
|
||||
)
|
||||
|
||||
|
||||
# ── Kanban payload surfacing ─────────────────────────────────────────────
|
||||
|
||||
|
||||
class SerializeSessionSurfacesQueueDepthTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
from opc.layer2_organization.company_runtime import CompanyRuntime
|
||||
|
||||
async def _noop(*_a, **_kw):
|
||||
return None
|
||||
|
||||
self.runtime = CompanyRuntime(
|
||||
org_engine=None,
|
||||
communication=None,
|
||||
store=None,
|
||||
save_runtime_session=_noop,
|
||||
emit_runtime_event=_noop,
|
||||
)
|
||||
self.role_session_id = "role-runtime::run::cto"
|
||||
self.role_session = DelegationRoleSession(
|
||||
role_session_id=self.role_session_id,
|
||||
run_id="run", role_id="cto",
|
||||
pending_work_item_ids=["wi-a", "wi-b", "wi-c"],
|
||||
)
|
||||
self.runtime.role_sessions[self.role_session_id] = self.role_session
|
||||
self.member = CompanyMemberSession(
|
||||
member_session_id="m1",
|
||||
role_session_id=self.role_session_id,
|
||||
role_id="cto",
|
||||
)
|
||||
|
||||
def test_serialize_includes_queue_depth_and_ids(self) -> None:
|
||||
payload = self.runtime._serialize_session(self.member)
|
||||
self.assertEqual(payload["pending_queue_depth"], 3)
|
||||
self.assertEqual(
|
||||
payload["pending_work_item_ids"], ["wi-a", "wi-b", "wi-c"]
|
||||
)
|
||||
|
||||
def test_serialize_with_no_role_session_returns_empty_queue(self) -> None:
|
||||
orphan = CompanyMemberSession(
|
||||
member_session_id="m2",
|
||||
role_session_id="role-runtime::run::unknown",
|
||||
role_id="unknown",
|
||||
)
|
||||
payload = self.runtime._serialize_session(orphan)
|
||||
self.assertEqual(payload["pending_queue_depth"], 0)
|
||||
self.assertEqual(payload["pending_work_item_ids"], [])
|
||||
|
||||
|
||||
# ── Stuck-queue runtime event ────────────────────────────────────────────
|
||||
|
||||
|
||||
class StuckQueueRuntimeEventTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def asyncSetUp(self) -> None:
|
||||
self._tmpdir = tempfile.TemporaryDirectory()
|
||||
self.store = OPCStore(db_path=Path(self._tmpdir.name) / "store.db")
|
||||
await self.store.initialize()
|
||||
self.store.role_serial_queue_enabled = True
|
||||
self.sid = "role-runtime::run-pr7::cto"
|
||||
await self.store.save_delegation_role_session(
|
||||
DelegationRoleSession(
|
||||
role_session_id=self.sid,
|
||||
run_id="run-pr7",
|
||||
role_id="cto",
|
||||
focused_work_item_id="wi-busy",
|
||||
status="running",
|
||||
)
|
||||
)
|
||||
|
||||
async def asyncTearDown(self) -> None:
|
||||
await self.store.close()
|
||||
self._tmpdir.cleanup()
|
||||
|
||||
async def _enqueue_wid(self, wid: str) -> None:
|
||||
item = DelegationWorkItem(
|
||||
work_item_id=wid,
|
||||
run_id="run-pr7", cell_id="c",
|
||||
role_id="cto", seat_id="s",
|
||||
role_runtime_session_id=self.sid,
|
||||
manager_role_id="ceo", manager_seat_id="seat::ceo",
|
||||
title="t", phase=Phase.READY,
|
||||
)
|
||||
await self.store.save_delegation_work_item(item)
|
||||
|
||||
async def _count_events(self, event_type: str) -> int:
|
||||
async with self.store._db.execute(
|
||||
"SELECT COUNT(*) FROM runtime_events WHERE event_type=?",
|
||||
(event_type,),
|
||||
) as cursor:
|
||||
(count,) = await cursor.fetchone()
|
||||
return count
|
||||
|
||||
async def test_event_fires_exactly_at_threshold_crossing(self) -> None:
|
||||
# Enqueue threshold items — only the one that takes us TO the
|
||||
# threshold should fire the event.
|
||||
for i in range(STUCK_QUEUE_DEPTH_THRESHOLD):
|
||||
await self._enqueue_wid(f"wi-{i}")
|
||||
self.assertEqual(
|
||||
await self._count_events("stuck_session_queue_depth"), 1
|
||||
)
|
||||
|
||||
async def test_event_does_not_fire_below_threshold(self) -> None:
|
||||
for i in range(STUCK_QUEUE_DEPTH_THRESHOLD - 1):
|
||||
await self._enqueue_wid(f"wi-{i}")
|
||||
self.assertEqual(
|
||||
await self._count_events("stuck_session_queue_depth"), 0
|
||||
)
|
||||
|
||||
async def test_event_does_not_refire_past_threshold(self) -> None:
|
||||
# Enqueue enough to cross the threshold, then another. The hook
|
||||
# fires only at the crossing (``depth == threshold``), so
|
||||
# subsequent enqueues do not re-fire.
|
||||
for i in range(STUCK_QUEUE_DEPTH_THRESHOLD + 2):
|
||||
await self._enqueue_wid(f"wi-{i}")
|
||||
self.assertEqual(
|
||||
await self._count_events("stuck_session_queue_depth"), 1
|
||||
)
|
||||
|
||||
|
||||
# ── Stuck-focus sweep ────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class CheckStuckFocusedSessionsTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def asyncSetUp(self) -> None:
|
||||
self._tmpdir = tempfile.TemporaryDirectory()
|
||||
self.store = OPCStore(db_path=Path(self._tmpdir.name) / "store.db")
|
||||
await self.store.initialize()
|
||||
self.run_id = "run-pr7-stuck"
|
||||
# DelegationRun has to exist so list_delegation_runs returns it.
|
||||
await self.store.save_delegation_run(
|
||||
DelegationRun(
|
||||
run_id=self.run_id,
|
||||
project_id="proj1",
|
||||
session_id="sess",
|
||||
)
|
||||
)
|
||||
|
||||
async def asyncTearDown(self) -> None:
|
||||
await self.store.close()
|
||||
self._tmpdir.cleanup()
|
||||
|
||||
async def _seed_session(
|
||||
self,
|
||||
*,
|
||||
role_id: str,
|
||||
focused: str,
|
||||
minutes_ago: int,
|
||||
) -> str:
|
||||
sid = f"role-runtime::{self.run_id}::{role_id}"
|
||||
session = DelegationRoleSession(
|
||||
role_session_id=sid,
|
||||
run_id=self.run_id,
|
||||
role_id=role_id,
|
||||
focused_work_item_id=focused,
|
||||
status="running",
|
||||
)
|
||||
# Backdate updated_at so the session looks stale.
|
||||
await self.store.save_delegation_role_session(session)
|
||||
backdated = (datetime.now() - timedelta(minutes=minutes_ago)).isoformat()
|
||||
for table in ("role_runtime_sessions", "delegation_role_sessions"):
|
||||
await self.store._db.execute(
|
||||
f"UPDATE {table} SET updated_at = ? WHERE role_session_id = ?",
|
||||
(backdated, sid),
|
||||
)
|
||||
await self.store._db.commit()
|
||||
return sid
|
||||
|
||||
async def test_stuck_session_emits_event(self) -> None:
|
||||
await self._seed_session(role_id="cto", focused="wi-x", minutes_ago=20)
|
||||
emitted = await check_stuck_focused_sessions(
|
||||
self.store, run_id=self.run_id, threshold_minutes=10,
|
||||
)
|
||||
self.assertEqual(len(emitted), 1)
|
||||
self.assertEqual(emitted[0]["focused_work_item_id"], "wi-x")
|
||||
self.assertGreaterEqual(emitted[0]["focused_for_minutes"], 10)
|
||||
# Event row persisted.
|
||||
async with self.store._db.execute(
|
||||
"SELECT COUNT(*) FROM runtime_events WHERE event_type=?",
|
||||
("stuck_session_focused",),
|
||||
) as cursor:
|
||||
(count,) = await cursor.fetchone()
|
||||
self.assertEqual(count, 1)
|
||||
|
||||
async def test_fresh_session_does_not_emit(self) -> None:
|
||||
await self._seed_session(role_id="cmo", focused="wi-y", minutes_ago=1)
|
||||
emitted = await check_stuck_focused_sessions(
|
||||
self.store, run_id=self.run_id, threshold_minutes=10,
|
||||
)
|
||||
self.assertEqual(len(emitted), 0)
|
||||
|
||||
async def test_unfocused_session_never_emits(self) -> None:
|
||||
await self._seed_session(role_id="coo", focused="", minutes_ago=60)
|
||||
emitted = await check_stuck_focused_sessions(
|
||||
self.store, run_id=self.run_id, threshold_minutes=10,
|
||||
)
|
||||
self.assertEqual(len(emitted), 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user