fix: opposite_rearm/rearm alerts + /resume unified + canary-pause UX guards

Trei găuri observate în sesiunea 2026-04-21:

A. _handle_tick nu avea branch pentru reason=opposite_rearm (PRIMED_* ↔
   ARMED_opus) sau reason=rearm (PRIMED_* → ARMED_* aceeași direcție). La
   17:45 yellow a trecut FSM-ul PRIMED_BUY→ARMED_SELL corect, dar zero alert
   pe Telegram. Adaugă helper _emit_arm_alert (DRY cu branch-ul arm existent)
   și două branch-uri noi cu kind=opposite_rearm / kind=rearm.

B. Canary drift se curăța doar cu /resume force — user ușor confundă
   /set_interval cu „relansare" și rămâne în drift-pause (cazul 18:09 azi).
   /resume acum curăță user_paused + canary.resume() într-o singură comandă.
   /resume force rămâne alias acceptat (muscle memory legacy).

C. Heartbeat-ul afișa „activ ARMED_SELL" deși detecția era oprită de 3 ore
   (state FSM înghețat). Extract _build_heartbeat_alert care arată
   „⚠️ pauzat (drift)" + „[drift-pause]" când canary.is_paused.

Guard-uri pentru comenzi când canary e paused:
- /set_interval: refuzat cu warn „Trimite /resume"
- /ss: screenshot trimis + body-ul include „⚠️ DETECȚIE OPRITĂ"

11 teste noi (1 critical regression pentru bug-ul A observat azi), plus
actualizarea test-ului /resume existent care aserta vechiul comportament.
Total: 235 passed + 8 scenarii regresie.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-21 22:23:20 +03:00
parent a796e91e90
commit 66ffa4bb9a
4 changed files with 420 additions and 41 deletions

View File

@@ -29,10 +29,11 @@ Când adaugi un frame: copiezi din `logs/fires/` → redenumești `{ts}_{color}.
## Telegram commands (live)
`/ss` `/status` `/pause` `/resume` `/resume force` `/3` (interval min) `/stop`
`/ss` `/status` `/pause` `/resume` `/3` (interval min) `/stop`
- `/resume` clears only user pause; Canary drift requires `/resume force`.
- Drift-pause now emits a single Telegram alert (was silent pre-refactor — root cause of the 2026-04-17 hang).
- `/resume` clears BOTH user pause and canary drift-pause in one shot (`/resume force` still accepted as legacy alias).
- Drift-pause emits a single Telegram alert on transition. While paused, `/set_interval` is refused and `/ss` captions warn that detection is off.
- Heartbeat shows `⚠️ pauzat (drift)` instead of `activ` while canary is paused.
## Operating-hours config

View File

@@ -597,6 +597,51 @@ def _save_annotated_frame(
return None
def _build_heartbeat_alert(
*,
fsm_state: str,
fire_count: int,
uptime_h: float,
canary_paused: bool,
) -> "Alert":
"""Construct the periodic heartbeat Alert.
When canary is drift-paused the title/body reflect it explicitly —
2026-04-21 bug: previously the heartbeat said "activ ARMED_SELL" while
detection had been dead for hours, misleading the user into thinking
the system was running.
"""
title = "⚠️ pauzat (drift)" if canary_paused else "activ"
state_label = f"{fsm_state} [drift-pause]" if canary_paused else fsm_state
body = f"{state_label} | semnale: {fire_count} | {uptime_h:.1f}h"
return Alert(kind="heartbeat", title=title, body=body)
def _emit_arm_alert(
notifier: _NotifierLike,
*,
kind: str,
direction: str,
now: float,
title: str,
snap: Snapshot,
snap_kind: str,
snap_label: str,
) -> None:
"""Build + dispatch an arm-family Alert (arm / opposite_rearm / rearm).
Keeps the three branches in _handle_tick DRY: they differ only in kind,
title, and the snap() label used to save the annotated screenshot.
"""
notifier.send(Alert(
kind=kind,
title=title,
body=f"@ {datetime.fromtimestamp(now).isoformat(timespec='seconds')}",
image_path=snap(snap_kind, snap_label),
direction=direction,
))
def _handle_tick(
fsm: StateMachine,
color: str,
@@ -689,13 +734,45 @@ def _handle_tick(
# ARM: turquoise (BUY) / yellow (SELL) — doar la tranziție nouă IDLE→ARMED
if tr.reason == "arm":
direction = "BUY" if tr.next == State.ARMED_BUY else "SELL"
notifier.send(Alert(
_emit_arm_alert(
notifier,
kind="arm",
title=f"{direction} armat ({color})",
body=f"@ {datetime.fromtimestamp(now).isoformat(timespec='seconds')}",
image_path=snap("arm", f"arm_{direction.lower()}"),
direction=direction,
))
now=now,
title=f"{direction} armat ({color})",
snap=snap,
snap_kind="arm",
snap_label=f"arm_{direction.lower()}",
)
# OPPOSITE REARM: ciclul anterior abandonat de semnal opus
# (PRIMED_BUY→ARMED_SELL pe yellow, sau flip direct ARMED→ARMED opus).
# Setup nou în direcția opusă — alertă cu screenshot.
elif tr.reason == "opposite_rearm":
direction = "BUY" if tr.next == State.ARMED_BUY else "SELL"
_emit_arm_alert(
notifier,
kind="opposite_rearm",
direction=direction,
now=now,
title=f"{direction} re-armat ({color}) — ciclu opus",
snap=snap,
snap_kind="opposite_rearm",
snap_label=f"opposite_rearm_{direction.lower()}",
)
# REARM: PRIMED_* → ARMED_* aceeași direcție (arm-color peste prime).
# Ciclul s-a resetat — prime_ts invalidat, armare proaspătă.
elif tr.reason == "rearm":
direction = "BUY" if tr.next == State.ARMED_BUY else "SELL"
_emit_arm_alert(
notifier,
kind="rearm",
direction=direction,
now=now,
title=f"{direction} re-armat ({color}) — ciclu reluat",
snap=snap,
snap_kind="rearm",
snap_label=f"rearm_{direction.lower()}",
)
# PRIME: dark_green (BUY) / dark_red (SELL) — doar la ARMED→PRIMED
elif tr.reason == "prime":
direction = "BUY" if tr.next == State.PRIMED_BUY else "SELL"
@@ -1065,6 +1142,13 @@ async def _dispatch_command(ctx: RunContext, cmd) -> None:
"""Process a single Command. Exceptions bubble — caller wraps in try/except."""
cfg = ctx.cfg
if cmd.action == "set_interval":
if getattr(ctx.canary, "is_paused", False):
ctx.notifier.send(Alert(
kind="warn",
title="⚠️ Canary drift activ — /set_interval ignorat",
body="Trimite /resume pentru a relua detecția.",
))
return
secs = cmd.value or cfg.telegram.auto_poll_interval_s
ctx.scheduler.start(secs)
ctx.audit.log({"ts": time.time(), "event": "scheduler_started", "reason": "set_interval", "interval_s": secs})
@@ -1123,7 +1207,8 @@ async def _dispatch_command(ctx: RunContext, cmd) -> None:
_save_annotated_frame, frame_ss, ctx.cfg, ctx.fires_dir, "ss", now_ss, ctx.audit,
)
ctx.audit.log({"ts": now_ss, "event": "screenshot_sent", "path": str(path_ss) if path_ss else None})
ctx.notifier.send(Alert(kind="screenshot", title="Screenshot manual", body="", image_path=path_ss))
warn = "⚠️ DETECȚIE OPRITĂ (drift) — trimite /resume" if getattr(ctx.canary, "is_paused", False) else ""
ctx.notifier.send(Alert(kind="screenshot", title="Screenshot manual", body=warn, image_path=path_ss))
elif cmd.action == "pause":
# User manually stops monitoring. Canary drift state is untouched.
if ctx.lifecycle is not None:
@@ -1135,14 +1220,15 @@ async def _dispatch_command(ctx: RunContext, cmd) -> None:
body="Folosește /resume pentru a relua.",
))
elif cmd.action == "resume":
# R2: /resume clears only user_paused. Canary drift requires
# /resume force (value == 1) so the user acknowledges the risk.
# /resume clears BOTH user_paused AND canary drift in one shot.
# /resume force rămâne acceptat ca alias (legacy muscle memory);
# câmpul `force` e păstrat în audit pentru schema compat.
was_drift = bool(getattr(ctx.canary, "is_paused", False))
was_user = bool(ctx.lifecycle.user_paused) if ctx.lifecycle is not None else False
force = cmd.value == 1
if ctx.lifecycle is not None:
ctx.lifecycle.user_paused = False
if force and was_drift:
if was_drift:
ctx.canary.resume()
ctx.audit.log({
"ts": time.time(), "event": "user_resumed",
@@ -1153,33 +1239,25 @@ async def _dispatch_command(ctx: RunContext, cmd) -> None:
ctx.audit.log({"ts": time.time(), "event": "window_focused", "command": "resume", "title": title})
await asyncio.sleep(0.15)
# Adaptive response
if was_drift and not force:
title = "Pauză user eliminată — dar Canary drift activ"
body = (
"Trimite /resume force pentru a anula drift-pause. "
"Recalibrează dacă driftul persistă."
)
elif force and was_drift:
title = "Drift-pause anulat manual (force)"
body = "Dacă driftul persistă, Canary va repauza."
skip_now = None
if ctx.lifecycle is not None:
skip_now = _should_skip(time.time(), ctx.lifecycle, ctx.cfg, ctx.canary)
if skip_now and skip_now.startswith("out_of_window"):
title = "Pauză eliminată — piața e închisă acum"
body = "Monitorizarea va porni la următoarea fereastră."
elif was_drift:
title = "Monitorizare reluată — drift-pause anulat"
body = "Dacă driftul persistă, Canary va repauza la următoarea verificare."
else:
skip_now = None
if ctx.lifecycle is not None:
skip_now = _should_skip(time.time(), ctx.lifecycle, ctx.cfg, ctx.canary)
if skip_now and skip_now.startswith("out_of_window"):
title = "Pauză eliminată — piața e închisă acum"
body = "Monitorizarea va porni la următoarea fereastră."
else:
title = "Monitorizare reluată"
body = ""
title = "Monitorizare reluată"
body = ""
ctx.notifier.send(Alert(kind="status", title=title, body=body))
elif cmd.action == "help":
body = (
"/status — stare FSM, uptime, ultima detecție\n"
"/ss — screenshot acum\n"
"/pause — oprește detecția (heartbeat continuă)\n"
"/resume — reia detecția (doar pauza user)\n"
"/resume force — reia + anulează drift-pause canary\n"
"/resume — reia detecția (șterge user-pause și drift-pause)\n"
"/3 — screenshot automat la fiecare 3 min (sau orice număr)\n"
"/stop — oprește screenshot-urile automate\n"
"/h — acest mesaj"
@@ -1356,8 +1434,13 @@ async def run_live_async(cfg, duration_s=None, capture_stub: bool = False) -> No
heartbeat_due = time.monotonic() + cfg.heartbeat_min * 60
continue
uptime_h = (time.monotonic() - start) / 3600
body = f"{ctx.fsm.state.value} | semnale: {ctx.state.fire_count} | {uptime_h:.1f}h"
notifier.send(Alert(kind="heartbeat", title="activ", body=body))
paused = bool(getattr(canary, "is_paused", False))
notifier.send(_build_heartbeat_alert(
fsm_state=ctx.fsm.state.value,
fire_count=ctx.state.fire_count,
uptime_h=uptime_h,
canary_paused=paused,
))
heartbeat_due = time.monotonic() + cfg.heartbeat_min * 60
async def _detection_loop() -> None:

View File

@@ -567,3 +567,169 @@ def test_state_machine_is_locked_and_record_fire_public_api():
assert fsm.is_locked("BUY", 50.0) is True # within 100s
assert fsm.is_locked("BUY", 150.0) is False # past lockout
assert fsm.is_locked("SELL", 50.0) is False # other direction unaffected
# ---------------------------------------------------------------------------
# opposite_rearm — bug observat 2026-04-21 17:45
# PRIMED_BUY + yellow → ARMED_SELL; reason=opposite_rearm; must emit alert
# with screenshot attached. CRITICAL regression.
# ---------------------------------------------------------------------------
def test_opposite_rearm_primed_buy_to_armed_sell_emits_alert():
"""REGRESSION (2026-04-21): PRIMED_BUY + yellow → ARMED_SELL silently."""
from pathlib import Path
fsm = StateMachine(lockout_s=60)
notif = FakeNotifier()
audit = FakeAudit()
def snap(kind, label):
return Path(f"/tmp/{label}.png")
# Drive to PRIMED_BUY
_handle_tick(fsm, "turquoise", 1.0, notif, audit, first_accepted=False, snapshot=snap)
_handle_tick(fsm, "dark_green", 2.0, notif, audit, first_accepted=False, snapshot=snap)
assert fsm.state == State.PRIMED_BUY
notif.alerts.clear()
# Yellow → ARMED_SELL via opposite_rearm
tr = _handle_tick(fsm, "yellow", 3.0, notif, audit, first_accepted=False, snapshot=snap)
assert tr is not None
assert tr.next == State.ARMED_SELL
assert tr.reason == "opposite_rearm"
assert len(notif.alerts) == 1
a = notif.alerts[0]
assert a.kind == "opposite_rearm"
assert a.direction == "SELL"
assert "yellow" in a.title
assert "opus" in a.title.lower()
assert a.image_path == Path("/tmp/opposite_rearm_sell.png")
def test_opposite_rearm_primed_sell_to_armed_buy_emits_alert():
"""Mirror: PRIMED_SELL + turquoise → ARMED_BUY."""
fsm = StateMachine(lockout_s=60)
notif = FakeNotifier()
audit = FakeAudit()
_handle_tick(fsm, "yellow", 1.0, notif, audit, first_accepted=False)
_handle_tick(fsm, "dark_red", 2.0, notif, audit, first_accepted=False)
assert fsm.state == State.PRIMED_SELL
notif.alerts.clear()
tr = _handle_tick(fsm, "turquoise", 3.0, notif, audit, first_accepted=False)
assert tr is not None
assert tr.next == State.ARMED_BUY
assert tr.reason == "opposite_rearm"
assert len(notif.alerts) == 1
assert notif.alerts[0].kind == "opposite_rearm"
assert notif.alerts[0].direction == "BUY"
def test_opposite_rearm_armed_buy_to_armed_sell_emits_alert():
"""Flip direct ARMED_BUY → ARMED_SELL (fără prime între)."""
fsm = StateMachine(lockout_s=60)
notif = FakeNotifier()
audit = FakeAudit()
_handle_tick(fsm, "turquoise", 1.0, notif, audit, first_accepted=False)
assert fsm.state == State.ARMED_BUY
notif.alerts.clear()
tr = _handle_tick(fsm, "yellow", 2.0, notif, audit, first_accepted=False)
assert tr is not None
assert tr.next == State.ARMED_SELL
assert tr.reason == "opposite_rearm"
assert len(notif.alerts) == 1
assert notif.alerts[0].kind == "opposite_rearm"
assert notif.alerts[0].direction == "SELL"
# ---------------------------------------------------------------------------
# rearm — PRIMED_* + arm-color aceeași direcție → ARMED_* (reset ciclu)
# ---------------------------------------------------------------------------
def test_rearm_primed_buy_to_armed_buy_emits_alert():
from pathlib import Path
fsm = StateMachine(lockout_s=60)
notif = FakeNotifier()
audit = FakeAudit()
def snap(kind, label):
return Path(f"/tmp/{label}.png")
_handle_tick(fsm, "turquoise", 1.0, notif, audit, first_accepted=False, snapshot=snap)
_handle_tick(fsm, "dark_green", 2.0, notif, audit, first_accepted=False, snapshot=snap)
assert fsm.state == State.PRIMED_BUY
notif.alerts.clear()
tr = _handle_tick(fsm, "turquoise", 3.0, notif, audit, first_accepted=False, snapshot=snap)
assert tr is not None
assert tr.next == State.ARMED_BUY
assert tr.reason == "rearm"
assert len(notif.alerts) == 1
a = notif.alerts[0]
assert a.kind == "rearm"
assert a.direction == "BUY"
assert "reluat" in a.title.lower()
assert a.image_path == Path("/tmp/rearm_buy.png")
def test_rearm_primed_sell_to_armed_sell_emits_alert():
fsm = StateMachine(lockout_s=60)
notif = FakeNotifier()
audit = FakeAudit()
_handle_tick(fsm, "yellow", 1.0, notif, audit, first_accepted=False)
_handle_tick(fsm, "dark_red", 2.0, notif, audit, first_accepted=False)
assert fsm.state == State.PRIMED_SELL
notif.alerts.clear()
tr = _handle_tick(fsm, "yellow", 3.0, notif, audit, first_accepted=False)
assert tr is not None
assert tr.next == State.ARMED_SELL
assert tr.reason == "rearm"
assert len(notif.alerts) == 1
assert notif.alerts[0].kind == "rearm"
assert notif.alerts[0].direction == "SELL"
# ---------------------------------------------------------------------------
# _emit_arm_alert helper — unit test
# ---------------------------------------------------------------------------
def test_emit_arm_alert_helper_builds_expected_alert():
from pathlib import Path
from atm.main import _emit_arm_alert
notif = FakeNotifier()
calls: list[tuple[str, str]] = []
def snap(kind, label):
calls.append((kind, label))
return Path(f"/tmp/{label}.png")
_emit_arm_alert(
notif,
kind="opposite_rearm",
direction="SELL",
now=1700000000.0,
title="SELL re-armat (yellow) — ciclu opus",
snap=snap,
snap_kind="opposite_rearm",
snap_label="opposite_rearm_sell",
)
assert len(notif.alerts) == 1
a = notif.alerts[0]
assert a.kind == "opposite_rearm"
assert a.direction == "SELL"
assert a.title == "SELL re-armat (yellow) — ciclu opus"
assert a.image_path == Path("/tmp/opposite_rearm_sell.png")
assert calls == [("opposite_rearm", "opposite_rearm_sell")]

View File

@@ -558,7 +558,7 @@ def _oh_cfg(enabled=True, weekdays=("MON", "TUE", "WED", "THU", "FRI"),
stop_hhmm=stop,
_tz_cache=_ZI(tz) if enabled else None,
)
return types.SimpleNamespace(operating_hours=oh)
return types.SimpleNamespace(operating_hours=oh, window_title=None)
def _fake_canary(paused=False):
@@ -841,8 +841,10 @@ async def test_resume_clears_user_paused_and_canary_when_forced():
@pytest.mark.asyncio
async def test_resume_during_drift_keeps_canary_paused_without_force():
"""R2 #21: plain /resume during drift clears user_paused but NOT canary."""
async def test_resume_plain_also_clears_canary_drift():
"""2026-04-21 decision: /resume (no arg) now clears BOTH user_paused and
canary drift. /resume force remains accepted as legacy alias. Previous
behavior (force required for drift) was a UX trap — see plan doc."""
import atm.main as _main
from atm.commands import Command
@@ -859,15 +861,37 @@ async def test_resume_during_drift_keeps_canary_paused_without_force():
await _main._dispatch_command(ctx, Command(action="resume")) # no force
assert ctx.lifecycle.user_paused is False
assert canary.is_paused is True # still drift-paused
# Message must mention drift
assert canary.is_paused is False # drift cleared without force
# Audit event still records was_drift + force=False for traceability
resumed = [e for e in ctx.audit.events if e.get("event") == "user_resumed"]
assert resumed and resumed[0]["was_drift"] is True and resumed[0]["force"] is False
# Message mentions drift-pause was cleared
status = [a for a in ctx.notifier.alerts if a.kind == "status"]
assert status and ("drift" in (status[0].title + status[0].body).lower())
# Now force
ctx.notifier.alerts.clear()
@pytest.mark.asyncio
async def test_resume_force_alias_still_works():
"""/resume force (value=1) remains accepted — legacy muscle memory."""
import atm.main as _main
from atm.commands import Command
class _Canary:
def __init__(self): self._p = True
@property
def is_paused(self): return self._p
def resume(self): self._p = False
canary = _Canary()
ctx = _dispatch_ctx(canary=canary)
ctx.lifecycle.user_paused = True
await _main._dispatch_command(ctx, Command(action="resume", value=1))
assert ctx.lifecycle.user_paused is False
assert canary.is_paused is False
resumed = [e for e in ctx.audit.events if e.get("event") == "user_resumed"]
assert resumed and resumed[0]["force"] is True
@pytest.mark.asyncio
@@ -923,6 +947,85 @@ async def test_status_command_reports_pause_reason():
assert "pauză manuală" in body or "pauza" in body.lower()
@pytest.mark.asyncio
async def test_set_interval_refused_while_canary_paused():
"""2026-04-21: /set_interval must not start scheduler while canary is drift-paused.
Previously started scheduler silently, misleading the user into thinking
detection was live. Now emits a warn and refuses."""
import atm.main as _main
from atm.commands import Command
canary = types.SimpleNamespace(is_paused=True, resume=lambda: None)
ctx = _dispatch_ctx(canary=canary)
await _main._dispatch_command(ctx, Command(action="set_interval", value=60))
# Scheduler must NOT have started
assert ctx.scheduler.is_running is False
# No scheduler_started audit event
assert not any(e.get("event") == "scheduler_started" for e in ctx.audit.events)
# A warn alert must have been sent referencing /resume
warns = [a for a in ctx.notifier.alerts if a.kind == "warn"]
assert warns
combined = (warns[0].title + warns[0].body).lower()
assert "drift" in combined and "/resume" in combined
@pytest.mark.asyncio
async def test_ss_includes_warn_body_while_canary_paused(monkeypatch, tmp_path):
"""2026-04-21: /ss still captures while canary is paused, but the alert body
must warn that detection is off."""
import atm.main as _main
from atm.commands import Command
canary = types.SimpleNamespace(is_paused=True, resume=lambda: None)
ctx = _dispatch_ctx(canary=canary)
# Bypass window focus + use a simple non-None capture result
fake_frame = object()
ctx = types.SimpleNamespace(**{**ctx.__dict__}) # shallow copy RunContext fields
# Simpler: just override the capture and save functions used
async_capture_called = {"n": 0}
def _capture():
async_capture_called["n"] += 1
return fake_frame
ctx.capture = _capture
ctx.fires_dir = tmp_path
# window_title off so we skip focus branch
ctx.cfg.window_title = None
# stub _save_annotated_frame to return a dummy path
monkeypatch.setattr(_main, "_save_annotated_frame",
lambda *a, **kw: tmp_path / "fake_ss.png")
await _main._dispatch_command(ctx, Command(action="ss"))
screenshots = [a for a in ctx.notifier.alerts if a.kind == "screenshot"]
assert screenshots
assert "DETECȚIE OPRITĂ" in screenshots[0].body or "drift" in screenshots[0].body.lower()
assert "/resume" in screenshots[0].body
@pytest.mark.asyncio
async def test_ss_no_warn_when_canary_healthy(monkeypatch, tmp_path):
"""/ss body must be empty when canary is not paused (no warn noise)."""
import atm.main as _main
from atm.commands import Command
canary = types.SimpleNamespace(is_paused=False, resume=lambda: None)
ctx = _dispatch_ctx(canary=canary)
ctx.capture = lambda: object()
ctx.fires_dir = tmp_path
ctx.cfg.window_title = None
monkeypatch.setattr(_main, "_save_annotated_frame",
lambda *a, **kw: tmp_path / "fake_ss.png")
await _main._dispatch_command(ctx, Command(action="ss"))
screenshots = [a for a in ctx.notifier.alerts if a.kind == "screenshot"]
assert screenshots
assert screenshots[0].body == ""
@pytest.mark.asyncio
async def test_lifecycle_with_drift_then_resume_then_fire(monkeypatch, tmp_path):
"""E2E #16: drift paused → /resume force → dark_red/light_red produce FIRE alert.
@@ -1040,6 +1143,32 @@ def test_heartbeat_suppressed_outside_hours(monkeypatch):
assert sent == []
def test_build_heartbeat_alert_active_when_not_paused():
"""Healthy state → title=activ, body shows fsm.state plainly."""
import atm.main as _main
a = _main._build_heartbeat_alert(
fsm_state="ARMED_BUY", fire_count=2, uptime_h=1.5, canary_paused=False,
)
assert a.kind == "heartbeat"
assert a.title == "activ"
assert "ARMED_BUY" in a.body
assert "[drift-pause]" not in a.body
assert "semnale: 2" in a.body
def test_build_heartbeat_alert_paused_when_canary_drift():
"""2026-04-21: heartbeat must reflect canary drift instead of lying with 'activ'."""
import atm.main as _main
a = _main._build_heartbeat_alert(
fsm_state="ARMED_SELL", fire_count=0, uptime_h=3.2, canary_paused=True,
)
assert a.kind == "heartbeat"
assert "pauzat" in a.title
assert "drift" in a.title.lower()
assert "ARMED_SELL" in a.body
assert "[drift-pause]" in a.body
@pytest.mark.asyncio
async def test_status_compact_active():
"""/status produces compact 2-line format; 'Canary' absent."""