ouroboros/tests/test_log_forwarding.py
Ouroboros f4ad64b928 Show a peer's message in the receiver's block and offer Stop on a tool-only direct turn
Owner 5=A asked for a task-authored message to be visible where it lands:
the receiving task's timeline. The event reached events.jsonl and the Logs
projection, but the chat reducer rendered it invisible and the supervisor
never pushed it live. The row now carries a bounded preview, is pushed to
the live log stream, and renders as a visible compact row in the receiver's
block.

A direct turn that only called tools offered no Stop until the model wrote
a narration row, because the host-attested marker rode progress frames only.
The turn's own queue proxy now stamps the marker on its tool frames (the
same by-value rule that carries the lane fact), and the client grants Stop
from any stamped log frame; a turn that neither narrates nor calls tools
keeps no block to hang a Stop on. The lane key is declared in ChatOutbound.

Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
2026-09-15 23:30:00 +03:00

635 lines
27 KiB
Python

"""Live log-delivery contract: explicit audience + exactly-once.
Covers the fix for the Main-chat leak of Project task trees (issue #296
residual): a Project child's diagnostic event carrying only its own task_id
must reach the browser addressed to the Project thread (chat_id + the
``project_thread`` stamp), a Main task's event must carry its explicit chat,
and one persisted event must produce exactly ONE live frame with the
PRODUCTION log sink installed — the pre-fix suite stubbed the sink out and
asserted an exactly-once the production wiring violated.
"""
import inspect
import json
import pathlib
from types import SimpleNamespace
import pytest
REPO = pathlib.Path(__file__).resolve().parent.parent
WORKER_BASELINE_TYPES = frozenset(
{"tool_call", "llm_round", "task_checkpoint", "task_done", "llm_usage"}
)
# One producer publishes the SAME type twice (durable append + live
# emit_log_event sibling); the forwarded append copy is suppressed.
WORKER_SAME_TYPE_PAIRS = frozenset({
"provider_incomplete_response", "llm_empty_response", "provider_body_error",
"review_cycles_exhausted", "plan_review_advisory_open",
})
# Every one of these has a dedicated ctx.bridge.push_log at its supervisor
# handler (events.py / cognitive_operations.py / gateway/tasks.py /
# post_task_checkpoint.py), so the raw server-process sink copy would be the
# second delivery of the same event.
SERVER_HANDLER_PUSHED_TYPES = frozenset({
"budget_scope_paused", "task_metrics_event", "review_late_result",
"task_cost_finalized", "skill_exec_finished", "skill_exec_failed",
"task_cancel_cascade_noop", "task_cancel_cascade_error",
"task_model_wait",
})
@pytest.fixture
def production_sink(tmp_path):
"""Install the REAL server-process log sink for the test's lifetime."""
from ouroboros.utils import set_log_sink
from supervisor.events import make_server_log_sink
frames = []
running: dict = {}
bridge = SimpleNamespace(push_log=lambda e: frames.append(e))
set_log_sink(make_server_log_sink(bridge, tmp_path, running=running))
try:
yield SimpleNamespace(
frames=frames, running=running, bridge=bridge, drive_root=tmp_path
)
finally:
set_log_sink(None)
def _ctx(sink):
from supervisor import state as supervisor_state
return SimpleNamespace(
DRIVE_ROOT=sink.drive_root,
RUNNING=sink.running,
append_jsonl=supervisor_state.append_jsonl,
bridge=sink.bridge,
)
def _bind_project(tmp_path, root_task_id="root1"):
from ouroboros.projects_registry import bind_task_to_project, create_project
project = create_project(tmp_path, "leaktest", name="Leak Test")
bind_task_to_project(
tmp_path, root_task_id, "leaktest", origin={"absent": "system"}
)
return int(project["chat_id"])
def test_worker_log_sink_suppresses_double_published_types():
from supervisor.workers import WORKER_LOG_SINK_SUPPRESSED_TYPES, worker_main
assert WORKER_LOG_SINK_SUPPRESSED_TYPES == WORKER_BASELINE_TYPES | WORKER_SAME_TYPE_PAIRS
src = inspect.getsource(worker_main)
assert "set_log_sink" in src
assert "emit_log_event" in src
assert "WORKER_LOG_SINK_SUPPRESSED_TYPES" in src
def test_server_suppressed_types_cover_worker_set_plus_handler_pushes():
from supervisor.workers import (
SERVER_LOG_SINK_SUPPRESSED_TYPES,
WORKER_LOG_SINK_SUPPRESSED_TYPES,
)
# The server process runs the worker producers too (direct chat, BGC), so
# its suppression is a strict superset; the extras are exactly the types
# whose supervisor handler performs the dedicated push.
assert WORKER_LOG_SINK_SUPPRESSED_TYPES < SERVER_LOG_SINK_SUPPRESSED_TYPES
assert (
SERVER_LOG_SINK_SUPPRESSED_TYPES - WORKER_LOG_SINK_SUPPRESSED_TYPES
== SERVER_HANDLER_PUSHED_TYPES
)
def test_handle_log_event_exactly_once_with_production_sink(production_sink):
"""task_checkpoint: handler push + persist, and the persisted append's
sink copy is suppressed — ONE frame total (the claim the old suite made
with the sink stubbed out, now proved against the production wiring)."""
from supervisor import events as ev
ctx = _ctx(production_sink)
events_file = production_sink.drive_root / "logs" / "events.jsonl"
ev._handle_log_event(
{"type": "log_event", "data": {"type": "task_received", "task_id": "t1"}}, ctx
)
assert [e.get("type") for e in production_sink.frames] == ["task_received"]
assert not events_file.exists()
ev._handle_log_event(
{"type": "log_event", "data": {"type": "task_checkpoint", "task_id": "t1", "round": 1}},
ctx,
)
checkpoints = [e for e in production_sink.frames if e.get("type") == "task_checkpoint"]
assert len(checkpoints) == 1
lines = [ln for ln in events_file.read_text(encoding="utf-8").splitlines() if ln.strip()]
assert len(lines) == 1 and json.loads(lines[0])["type"] == "task_checkpoint"
def test_project_child_error_routes_to_project_thread(production_sink, monkeypatch):
"""The reported leak: a child's llm_api_error carries only its task_id;
the RUNNING row supplies lineage, the binding supplies the chat, and the
broadcast choke stamps project_thread so Main rejects the frame even
before it learns the project."""
from supervisor import events as ev
from supervisor import message_bus
project_chat = _bind_project(production_sink.drive_root)
production_sink.running["child1"] = {
"task": {"id": "child1", "parent_task_id": "root1", "root_task_id": "root1"}
}
monkeypatch.setattr(message_bus, "DATA_DIR", production_sink.drive_root)
bridge = message_bus.LocalChatBridge()
ws_frames = []
bridge._broadcast_fn = ws_frames.append
ctx = _ctx(production_sink)
ctx.bridge = bridge
ev._handle_log_event(
{"type": "log_event", "data": {"type": "llm_api_error", "task_id": "child1",
"error": "APIConnectionError('Connection error.')"}},
ctx,
)
assert len(ws_frames) == 1
frame = ws_frames[0]
assert frame["type"] == "log"
assert frame["chat_id"] == project_chat
assert frame["project_thread"] is True
assert frame["data"]["root_task_id"] == "root1"
def test_main_task_event_gets_explicit_chat_id(production_sink):
from supervisor import events as ev
production_sink.running["m1"] = {"task": {"id": "m1", "chat_id": 1}}
ctx = _ctx(production_sink)
ev._handle_log_event(
{"type": "log_event", "data": {"type": "llm_api_error", "task_id": "m1"}}, ctx
)
assert production_sink.frames[-1]["chat_id"] == 1
def test_addressing_is_honest_and_transport_suppresses_a2a(production_sink):
"""chat_id=0 is a REAL session (Skill Review panel), never 'missing'; an
explicit chat_id on the event is preserved; an A2A row is stamped HONESTLY
(the durable row keeps the true audience) and the broadcast choke drops
the frame; transport-shaped ids pass through as the task row says."""
from supervisor import events as ev
from supervisor import message_bus
from supervisor.events import _address_task_event
# Explicit 0 on the event survives (no project binding around).
payload = {"type": "x", "task_id": "t", "chat_id": 0}
_address_task_event({}, None, payload)
assert payload["chat_id"] == 0
# Task row chat_id 0 is stamped as the honest address.
payload = {"type": "x", "task_id": "t"}
_address_task_event({"t": {"task": {"id": "t", "chat_id": 0}}}, None, payload)
assert payload["chat_id"] == 0
# An A2A row is stamped honestly...
payload = {"type": "x", "task_id": "t"}
_address_task_event({"t": {"task": {"id": "t", "chat_id": -5}}}, None, payload)
assert payload["chat_id"] == -5
# ...and the addressed frame never reaches the browser (end to end).
bridge = message_bus.LocalChatBridge()
ws_frames = []
bridge._broadcast_fn = ws_frames.append
ctx = _ctx(production_sink)
ctx.bridge = bridge
production_sink.running["a2a1"] = {"task": {"id": "a2a1", "chat_id": -7}}
ev._handle_log_event(
{"type": "log_event", "data": {"type": "llm_api_error", "task_id": "a2a1"}}, ctx
)
assert ws_frames == []
# External-transport ids ride through explicitly.
payload = {"type": "x", "task_id": "t"}
_address_task_event({"t": {"task": {"id": "t", "chat_id": 197422551}}}, None, payload)
assert payload["chat_id"] == 197422551
# No row, no registry entry: the event stays unaddressed (legacy 0 frame).
payload = {"type": "x", "task_id": "unknown"}
_address_task_event({}, None, payload)
assert "chat_id" not in payload
def test_addressing_precedence_is_pinned():
"""The owner-decided precedence order: a Project binding OVERRIDES an
explicit chat_id already on the event (post-hoc bound tasks keep their
original Main chat on the row), and an explicit chat_id — the real
Skill-Review 0 included — OVERRIDES the task row; a None value is absence,
not an address."""
from supervisor.events import _address_task_event
# Binding wins over an explicit stale chat_id (post-hoc bound task).
import supervisor.log_addressing as la
original = la.resolve_project_chat
la.resolve_project_chat = lambda *a, **k: 777
try:
payload = {"type": "x", "task_id": "t", "chat_id": 1}
_address_task_event({}, None, payload)
assert payload["chat_id"] == 777
finally:
la.resolve_project_chat = original
# Explicit 0 beats a RUNNING row carrying chat_id 1 (M2 mutation guard).
payload = {"type": "x", "task_id": "t", "chat_id": 0}
_address_task_event({"t": {"task": {"id": "t", "chat_id": 1}}}, None, payload)
assert payload["chat_id"] == 0
# A None chat_id is absence: the task row supplies the address.
payload = {"type": "x", "task_id": "t", "chat_id": None}
_address_task_event({"t": {"task": {"id": "t", "chat_id": 1}}}, None, payload)
assert payload["chat_id"] == 1
def test_production_sink_reads_live_running_table(tmp_path, monkeypatch):
"""server.py installs the sink WITHOUT ``running=`` — that branch must
read the live supervisor.workers.RUNNING table (M8 mutation guard)."""
from ouroboros.utils import append_jsonl, set_log_sink
from supervisor import workers as workers_mod
from supervisor.events import make_server_log_sink
frames = []
bridge = SimpleNamespace(push_log=lambda e: frames.append(e))
monkeypatch.setattr(
workers_mod, "RUNNING", {"live1": {"task": {"id": "live1", "chat_id": 1}}}
)
set_log_sink(make_server_log_sink(bridge, tmp_path))
try:
(tmp_path / "logs").mkdir(exist_ok=True)
append_jsonl(tmp_path / "logs" / "events.jsonl", {"type": "task_error", "task_id": "live1"})
finally:
set_log_sink(None)
assert frames and frames[-1]["chat_id"] == 1
def test_project_direct_turn_frame_is_stamped(production_sink, monkeypatch):
"""A direct turn in a PROJECT room: the registry entry carries the project
chat, and the broadcast choke stamps project_thread on the frame."""
from supervisor import message_bus
from supervisor.active_activity import get_direct_activity_registry
from ouroboros.projects_registry import create_project
from ouroboros.utils import append_jsonl
project_chat = int(create_project(production_sink.drive_root, "direct-room")["chat_id"])
monkeypatch.setattr(message_bus, "DATA_DIR", production_sink.drive_root)
bridge = message_bus.LocalChatBridge()
ws_frames = []
bridge._broadcast_fn = ws_frames.append
monkeypatch.setattr(production_sink, "bridge", bridge)
# Rebuild the sink over the real bridge so the frame crosses push_log.
from ouroboros.utils import set_log_sink
from supervisor.events import make_server_log_sink
set_log_sink(make_server_log_sink(bridge, production_sink.drive_root, running={}))
registry = get_direct_activity_registry()
registry.register("dturn1", project_chat, kind="direct_chat", project_id="direct-room")
try:
logs = production_sink.drive_root / "logs"
logs.mkdir(exist_ok=True)
append_jsonl(logs / "events.jsonl", {"type": "llm_api_error", "task_id": "dturn1"})
finally:
registry.unregister("dturn1")
set_log_sink(None)
assert ws_frames and ws_frames[-1]["chat_id"] == project_chat
assert ws_frames[-1]["project_thread"] is True
def test_direct_turn_event_addressed_from_activity_registry(production_sink):
"""Direct/ephemeral turns run in the server process and are never in
RUNNING; the DirectActivityRegistry entry supplies their chat."""
from supervisor.active_activity import get_direct_activity_registry
from ouroboros.utils import append_jsonl
registry = get_direct_activity_registry()
registry.register("turn1", 42, kind="direct_chat")
try:
logs = production_sink.drive_root / "logs"
logs.mkdir(exist_ok=True)
append_jsonl(logs / "events.jsonl", {"type": "llm_api_error", "task_id": "turn1"})
finally:
registry.unregister("turn1")
assert production_sink.frames[-1]["chat_id"] == 42
def test_sink_streams_only_log_files(production_sink):
"""The append_jsonl sink streams runtime LOG files only: chat.jsonl has
its own live channel, and state/memory/receipt stores are durable data,
not a log feed."""
from ouroboros.utils import append_jsonl
root = production_sink.drive_root
(root / "logs").mkdir(exist_ok=True)
(root / "state").mkdir(exist_ok=True)
append_jsonl(root / "logs" / "supervisor.jsonl", {"type": "queue_restored"})
append_jsonl(root / "logs" / "chat.jsonl", {"direction": "in", "text": "hi"})
append_jsonl(root / "state" / "usage_attempts.jsonl", {"type": "reserved"})
append_jsonl(root / "notes.jsonl", {"type": "misc"})
assert [e.get("type") for e in production_sink.frames] == ["queue_restored"]
def test_llm_usage_single_addressed_frame(production_sink, monkeypatch):
"""llm_usage is delivered by its handler's explicit push (addressed), and
the raw sink copy of the same persisted row is suppressed — one frame."""
from supervisor import events as ev
from supervisor import message_bus
project_chat = _bind_project(production_sink.drive_root, root_task_id="uroot")
production_sink.running["uchild"] = {
"task": {"id": "uchild", "parent_task_id": "uroot", "root_task_id": "uroot"}
}
monkeypatch.setattr(message_bus, "DATA_DIR", production_sink.drive_root)
bridge = message_bus.LocalChatBridge()
ws_frames = []
bridge._broadcast_fn = ws_frames.append
ctx = _ctx(production_sink)
ctx.bridge = bridge
ctx.update_budget_from_usage = lambda usage: None
ev._handle_llm_usage(
{"type": "llm_usage", "task_id": "uchild", "usage": {"prompt_tokens": 10, "cost": 0.01}},
ctx,
)
log_frames = [f for f in ws_frames if f.get("type") == "log"]
assert len(log_frames) == 1
assert log_frames[0]["chat_id"] == project_chat
assert log_frames[0]["project_thread"] is True
# The durable row carries the same explicit audience (additive fields).
rows = [
json.loads(ln)
for ln in (production_sink.drive_root / "logs" / "events.jsonl")
.read_text(encoding="utf-8").splitlines() if ln.strip()
]
assert rows[-1]["type"] == "llm_usage" and rows[-1]["chat_id"] == project_chat
def test_budget_pause_frame_is_addressed_from_popped_row(production_sink, monkeypatch):
"""_handle_budget_pause pops the RUNNING row before publishing; the
addressing must use that popped row (a plain RUNNING lookup deterministically
misses here — triad finding, round 1)."""
from supervisor import events as ev
from supervisor import message_bus
project_chat = _bind_project(production_sink.drive_root, root_task_id="broot")
monkeypatch.setattr(message_bus, "DATA_DIR", production_sink.drive_root)
bridge = message_bus.LocalChatBridge()
ws_frames = []
bridge._broadcast_fn = ws_frames.append
ctx = _ctx(production_sink)
ctx.bridge = bridge
ctx.PENDING = []
ctx.WORKERS = {}
ctx.sort_pending = lambda: None
ctx.persist_queue_snapshot = lambda reason="": None
production_sink.running["bchild"] = {
"task": {"id": "bchild", "parent_task_id": "broot", "root_task_id": "broot"}
}
ev._handle_budget_pause(
{
"type": "budget_pause",
"task_id": "bchild",
"resource_limit": {"replay_safe": True, "physical_calls": 0, "scope": "global"},
},
ctx,
)
frames = [f for f in ws_frames if f.get("type") == "log"]
assert len(frames) == 1
assert frames[0]["chat_id"] == project_chat
assert frames[0]["project_thread"] is True
def test_cascade_incident_durable_and_live_share_one_event(tmp_path, monkeypatch):
"""_record_cascade_incident must persist and push ONE event object: a
second timestamp would defeat the Logs panel's backfill/live dedupe."""
from ouroboros.gateway import tasks as gw_tasks
from supervisor import message_bus
from supervisor import queue as supervisor_queue
(tmp_path / "logs").mkdir()
monkeypatch.setattr(supervisor_queue, "DRIVE_ROOT", tmp_path)
frames = []
monkeypatch.setattr(
message_bus, "_BRIDGE", SimpleNamespace(push_log=lambda e: frames.append(e))
)
gw_tasks._record_cascade_incident("c1", "task_cancel_cascade_noop")
rows = [
json.loads(ln)
for ln in (tmp_path / "logs" / "supervisor.jsonl").read_text(encoding="utf-8").splitlines()
if ln.strip()
]
assert len(rows) == 1 and len(frames) == 1
assert frames[0] == rows[0]
def test_push_log_never_broadcasts_a2a_frames(monkeypatch, tmp_path):
"""A2A synthetic chats are machine traffic; a log frame explicitly
addressed to one must not reach the browser socket."""
from supervisor import message_bus
monkeypatch.setattr(message_bus, "DATA_DIR", tmp_path)
bridge = message_bus.LocalChatBridge()
frames = []
bridge._broadcast_fn = frames.append
bridge.push_log({"type": "tool_call", "task_id": "t", "chat_id": -5})
bridge.push_log({"type": "tool_call", "task_id": "t"})
assert [f["chat_id"] for f in frames] == [0]
def test_turn_event_queue_stamps_by_value_at_the_producer():
"""A direct/ephemeral turn's chat authority (its DirectActivityRegistry
entry) dies with the turn while its queued events are drained LATER, so
the address must ride the event itself: the turn-scoped queue proxy stamps
the turn chat onto the turn's own still-unaddressed task-scoped payloads —
live log_event envelopes and returned top-level events alike."""
from supervisor.workers import _TurnEventQueue
captured = []
proxy = _TurnEventQueue(SimpleNamespace(put=captured.append, put_nowait=captured.append), "turn1", 42)
# Live emit during the turn (loop_llm_call shape): data gains the chat.
proxy.put_nowait({"type": "log_event", "data": {"type": "llm_round_error", "task_id": "turn1"}})
assert captured[-1]["data"]["chat_id"] == 42
# Returned top-level event (llm_usage shape) is stamped the same way.
assert proxy.stamp({"type": "llm_usage", "task_id": "turn1", "usage": {}})["chat_id"] == 42
# The lane fact rides the same events by the same rule (a stamped
# task_done keeps its own value; other tasks' events are untouched).
assert captured[-1]["data"]["_is_direct_chat"] is True
assert "cancelable" not in captured[-1]["data"] # not a work frame
assert proxy.stamp({"type": "task_done", "task_id": "turn1", "_is_direct_chat": False})["_is_direct_chat"] is False
tool = proxy.stamp({"type": "log_event", "data": {"type": "tool_call_started", "task_id": "turn1", "tool": "read_file"}})
assert tool["data"]["cancelable"] is True and tool["data"]["_is_direct_chat"] is True
# Another task's event and an already-addressed event are left alone.
other = {"type": "log_event", "data": {"type": "x", "task_id": "other"}}
assert "chat_id" not in proxy.stamp(other)["data"] and "_is_direct_chat" not in proxy.stamp(other)["data"]
zero = {"type": "log_event", "data": {"type": "x", "task_id": "turn1", "chat_id": 0}}
assert proxy.stamp(zero)["data"]["chat_id"] == 0
# _run_chat_task installs the proxy around agent.handle_task.
import inspect
from supervisor.workers import _run_chat_task
src = inspect.getsource(_run_chat_task)
assert "_TurnEventQueue" in src and "agent._event_queue = turn_queue" in src
def test_llm_usage_explicit_zero_survives_running_row(production_sink, monkeypatch):
"""An llm_usage event carrying an explicit chat_id=0 (Skill Review) keeps
it even when the RUNNING row says chat 1 (precedence: explicit beats row)."""
from supervisor import events as ev
from supervisor import message_bus
monkeypatch.setattr(message_bus, "DATA_DIR", production_sink.drive_root)
bridge = message_bus.LocalChatBridge()
ws_frames = []
bridge._broadcast_fn = ws_frames.append
ctx = _ctx(production_sink)
ctx.bridge = bridge
ctx.update_budget_from_usage = lambda usage: None
production_sink.running["z1"] = {"task": {"id": "z1", "chat_id": 1}}
ev._handle_llm_usage(
{"type": "llm_usage", "task_id": "z1", "chat_id": 0, "usage": {"prompt_tokens": 1}},
ctx,
)
frames = [f for f in ws_frames if f.get("type") == "log"]
assert len(frames) == 1 and frames[0]["chat_id"] == 0
def test_review_cycles_exhausted_queue_less_caller_gets_one_live_frame(tmp_path, monkeypatch):
"""A queue-less server-process caller (the HTTP skill-review ctx has
event_queue=None) still owes the ONE live frame: the sink copy of the
durable append is suppressed, so the producer pushes the addressed
sibling through the bridge itself."""
from ouroboros.review_cycles import emit_review_cycles_exhausted
from supervisor import message_bus
(tmp_path / "logs").mkdir()
frames = []
monkeypatch.setattr(
message_bus, "_BRIDGE", SimpleNamespace(push_log=lambda e: frames.append(e))
)
emit_review_cycles_exhausted(
None, tmp_path, surface="skill_review", task_id="api_skill_review",
cycles_paid=2, cap=2, enforcement="blocking",
)
rows = [
json.loads(ln)
for ln in (tmp_path / "logs" / "events.jsonl").read_text(encoding="utf-8").splitlines()
if ln.strip()
]
assert len(rows) == 1 and rows[0]["type"] == "review_cycles_exhausted"
assert len(frames) == 1 and frames[0]["type"] == "review_cycles_exhausted"
def test_task_metrics_carries_turn_stamped_chat(production_sink):
"""_handle_task_metrics rebuilds its payload; the TurnEventQueue by-value
chat stamp on the incoming event must survive into the pushed frame."""
from supervisor import events as ev
ctx = _ctx(production_sink)
ev._handle_task_metrics(
{"type": "task_metrics", "task_id": "turnX", "chat_id": 42, "duration_sec": 1.0},
ctx,
)
frames = [e for e in production_sink.frames if e.get("type") == "task_metrics_event"]
assert len(frames) == 1 and frames[0]["chat_id"] == 42
def test_logs_js_backfills_all_streams_and_dedupes_without_dropping_preconnect():
src = (REPO / "web" / "modules" / "logs.js").read_text(encoding="utf-8")
for stream in ("'events'", "'tools'", "'progress'", "'supervisor'"):
assert stream in src, f"backfill must include the {stream} log stream"
# Exact-duplicate guard collapses backfill/live overlap…
assert "renderedLogKeys" in src
# …and backfill reruns on reconnect…
assert "ws.on('open'" in src
# …without a load-time timestamp skip that could drop the pre-connect window.
assert "loadStart" not in src
@pytest.mark.parametrize("write_succeeds", [True, False])
@pytest.mark.parametrize("sink_installed", [True, False])
def test_llm_call_failure_reaches_the_live_log_exactly_once(
tmp_path, production_sink, monkeypatch, write_succeeds, sink_installed,
):
"""#355: one failure remains visible with or without its durable write/sink.
A successful append with the production sink sends the only live frame.
A failed append or absent sink falls back to the existing live queue, with
the same timestamp and complete evidence, without inventing persistence.
"""
import queue as queue_mod
from ouroboros import loop_llm_call, utils
from ouroboros.loop_llm_call import _LlmErrorContext, _record_llm_call_error
from supervisor.events import _handle_log_event
src = inspect.getsource(_record_llm_call_error)
assert '"llm_round_error"' not in src
assert '"type": "llm_api_error"' in src
if not sink_installed:
utils.set_log_sink(None)
monkeypatch.setattr(loop_llm_call, "utc_now_iso", lambda: "2026-09-07T13:05:00Z")
production_sink.running["m1"] = {"task": {"id": "m1", "chat_id": 42}}
live = queue_mod.Queue()
events_file = tmp_path / "logs/events.jsonl"
attempted_rows = []
append = loop_llm_call.append_jsonl
def append_observed(path, row):
attempted_rows.append(dict(row))
return append(path, row)
monkeypatch.setattr(loop_llm_call, "append_jsonl", append_observed)
if not write_succeeds:
write = utils._write_fd_fully
def refuse_event_write(fd, data, path):
if pathlib.Path(path) == events_file:
raise OSError("controlled journal write refusal")
return write(fd, data, path)
monkeypatch.setattr(utils, "_write_fd_fully", refuse_event_write)
ctx = _LlmErrorContext(
task_id="m1", task_type="task", execution_id="exec-1", round_id="round-1",
llm_call_id="call-1", round_idx=1, attempt=0, model="provider/model",
request_ref={"manifest_ref": {"path": "request/ref"}},
drive_logs=tmp_path / "logs", event_queue=live,
accumulated_usage={}, context_fit_event_fields={"selected_context_mode": "test-context"},
)
class ProviderError(RuntimeError):
status_code = 503
_record_llm_call_error(ProviderError("upstream unavailable"), ctx)
[error_row] = attempted_rows
assert error_row["type"] == "llm_api_error"
queued = []
while not live.empty():
queued.append(live.get_nowait())
assert len(queued) == (0 if write_succeeds and sink_installed else 1)
if queued:
assert queued[0] == {"type": "log_event", "data": error_row}
_handle_log_event(queued[0], _ctx(production_sink))
[frame] = production_sink.frames
assert frame == {**error_row, "chat_id": 42}
assert frame["ts"] == "2026-09-07T13:05:00Z"
assert frame["execution_id"] == "exec-1" and frame["llm_call_id"] == "call-1"
assert frame["status_code"] == 503 and frame["request_ref"] == {"path": "request/ref"}
assert frame["selected_context_mode"] == "test-context"
rows = [json.loads(line) for line in events_file.read_text(encoding="utf-8").splitlines()]
assert rows == ([error_row] if write_succeeds else [])