chore: commit remaining workspace updates

Agent:
- lifecycle: run tracking, middleware hardening, langgraph setup
- tests: agent lifecycle + langgraph setup coverage

Backend:
- async_job_runner: resilience hardening, tests
- agent_conversations: run lifecycle integration
- translate: scheduler + orchestrator SQL adjustments
- schemas/services: agent_lifecycle model extensions

Frontend:
- TaskDrawer: UX improvements
- TaskLogPanel/Viewer: safety hardening, i18n (en/ru)
- FilterBar: report filters contract + tests
- Reports page: layout adjustments

Specs:
- 036-agent-test-stabilization: runs contract, modules, events
- 037-superset-baseline-engine: catalog schema, testing API, modules
- 038-dashboard-scenario-model: scenario schema, capture profile, modules
- 039-dashboard-scenario-ui: screen models, release verification UX, modules
- dashboard-verification-usecases: new cross-cutting spec
This commit is contained in:
2026-07-17 19:11:09 +03:00
parent fdb6541372
commit 31b9a19a0c
65 changed files with 2621 additions and 194 deletions

View File

@@ -254,3 +254,131 @@ def test_run_later_with_zero_delay(runner, event_loop):
event_loop.call_soon_threadsafe(event_loop.stop)
thread.join(timeout=2)
# #endregion test_run_later_with_zero_delay
# -- Tests: dispatch() --------------------------------------------------------
# #region test_dispatch_executes_coroutine [C:2] [TYPE Function]
# @BRIEF dispatch() submits coroutine to event loop without blocking — returns None immediately.
def test_dispatch_executes_coroutine(event_loop):
"""dispatch() is fire-and-forget: returns None immediately, coroutine runs on event loop."""
runner = AsyncJobRunner(event_loop)
results = []
async def track(value):
results.append(value)
thread = threading.Thread(target=event_loop.run_forever, daemon=True)
thread.start()
try:
ret = runner.dispatch(track(42))
# dispatch() returns immediately with None
assert ret is None, f"dispatch() should return None, got {ret}"
# Allow time for the coroutine to execute on the event loop
time.sleep(0.3)
assert results == [42], f"Expected [42], got {results}"
finally:
event_loop.call_soon_threadsafe(event_loop.stop)
thread.join(timeout=2)
# #endregion test_dispatch_executes_coroutine
# #region test_dispatch_returns_immediately [C:2] [TYPE Function]
# @BRIEF dispatch() does NOT wait for completion — returns before coroutine finishes.
def test_dispatch_returns_immediately(event_loop):
"""dispatch() returns before the coroutine completes (fire-and-forget semantics)."""
runner = AsyncJobRunner(event_loop)
started = threading.Event()
completed = threading.Event()
async def slow_task():
started.set()
await asyncio.sleep(1.0)
completed.set()
thread = threading.Thread(target=event_loop.run_forever, daemon=True)
thread.start()
try:
ret = runner.dispatch(slow_task())
# dispatch() returns immediately — coroutine hasn't finished yet
assert ret is None
# Coroutine should have started
assert started.wait(timeout=0.5), "Coroutine did not start"
# But not yet completed (fire-and-forget, no blocking)
assert not completed.is_set(), "dispatch() should not wait for completion"
finally:
event_loop.call_soon_threadsafe(event_loop.stop)
thread.join(timeout=2)
# #endregion test_dispatch_returns_immediately
# #region test_dispatch_logs_exception [C:2] [TYPE Function]
# @BRIEF dispatch() catches coroutine exceptions and logs via EXPLORE — does NOT propagate.
def test_dispatch_logs_exception(event_loop):
"""dispatch() logs coroutine exceptions but does NOT raise to the caller."""
from unittest.mock import patch
runner = AsyncJobRunner(event_loop)
async def failing_coro():
raise ValueError("background failure")
thread = threading.Thread(target=event_loop.run_forever, daemon=True)
thread.start()
try:
with patch("ss_tools.shared.cot_logger.log") as mock_log:
ret = runner.dispatch(failing_coro())
# dispatch() returns None even when coroutine fails
assert ret is None
# Allow time for the guarded wrapper to catch and log
time.sleep(0.3)
# Verify EXPLORE was logged with error info
explore_calls = [
call for call in mock_log.call_args_list
if call[0][1] == "EXPLORE"
]
assert len(explore_calls) >= 1, (
f"Expected at least one EXPLORE log call, got {len(explore_calls)}. "
f"All calls: {[c[0][1] for c in mock_log.call_args_list]}"
)
# Verify error was logged
error_call = explore_calls[0]
assert "Fire-and-forget coroutine failed" in error_call[0][2], \
f"Expected 'Fire-and-forget coroutine failed' in intent, got: {error_call[0][2]}"
assert error_call.kwargs.get("error") is not None, \
"Expected error kwarg to be set on EXPLORE log"
finally:
event_loop.call_soon_threadsafe(event_loop.stop)
thread.join(timeout=2)
# #endregion test_dispatch_logs_exception
# #region test_dispatch_does_not_block_caller [C:2] [TYPE Function]
# @BRIEF dispatch() caller thread is not blocked — confirms no future.result() call.
def test_dispatch_does_not_block_caller(event_loop):
"""dispatch() caller can continue immediately (no blocking TimeoutError risk)."""
runner = AsyncJobRunner(event_loop)
async def very_slow():
await asyncio.sleep(10.0) # would block for 10s if using run()
thread = threading.Thread(target=event_loop.run_forever, daemon=True)
thread.start()
try:
import time
start = time.monotonic()
ret = runner.dispatch(very_slow())
elapsed = time.monotonic() - start
# dispatch() must return in well under 1 second (not 10s or 300s timeout)
assert elapsed < 0.5, (
f"dispatch() took {elapsed:.2f}s — it should return immediately, "
f"not block on the coroutine"
)
assert ret is None
finally:
event_loop.call_soon_threadsafe(event_loop.stop)
thread.join(timeout=2)
# #endregion test_dispatch_does_not_block_caller
# #endregion Test.AsyncJobRunner