
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x11cd7f230>

    def test_lock_on_flat_account_arms_day_lock(monkeypatch):
        """Bug fbb62441d227 (operator 2026-07-30 'i locked, and many position were sold without vetoed'):
        the LOCK half on a flat account is NO LONGER a no-op — it arms the account-scoped day-lock so the
        NEXT entry on this account is protected from the moment it lands (the entire 13:43Z lock-no-leg
        gap the operator complained about). No PositionLocks row is written (no symbol to key on), but
        the day-lock IS — that's the durable veto. UNLOCK still clears both."""
        import rtrader.database as database
        monkeypatch.setattr(database, "lock_position",
                            lambda *a, **k: (_ for _ in ()).throw(AssertionError("must not lock flat")))
        monkeypatch.setattr(mse, "_broker_for", lambda a: None)
        # Track the day-lock writes/clears through a stub DB + collection (matches the production
        # wiring: set_providers(db_factory=lambda: db); db[name] returns the collection).
        class _Coll:
            def __init__(self): self.docs = {}
    
            def update_one(self, q, update, upsert=False):
                key = (q["account_nick"], q["trading_day"])
                doc = self.docs.get(key, {})
                doc.update(update["$set"])
                self.docs[key] = doc
    
                class _R:
                    upserted_id = None
    
                return _R()
    
            def find_one(self, q):
                return self.docs.get((q["account_nick"], q["trading_day"]))
    
            def delete_one(self, q):
                self.docs.pop((q["account_nick"], q["trading_day"]), None)
    
                class _R:
                    deleted_count = 1
    
                return _R()
    
        class _FakeDB:
            def __init__(self): self.colls = {}
            def __getitem__(self, name): return self.colls.setdefault(name, _Coll())
    
        db = _FakeDB()
        mse.set_providers(broker_provider=lambda a: None, db_factory=lambda: db)
        monkeypatch.setattr(mse, "_record_decision", lambda d: None)
        monkeypatch.setattr(mse, "_vehicle", lambda: ("TNA", "TZA"))
        monkeypatch.setattr(mse, "_held_leg_3way",
                            lambda broker, bull, bear, retry_unknown=True: (None, None, 0, "flat"))
>       dec = mse.lock_sleeve_leg("miaoyong", DAY, NOW, lock=True, hard=True)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

tests/services/test_permanent_lock.py:292: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

account_nick = 'miaoyong', day = datetime.date(2026, 7, 24)
now_et = datetime.datetime(2026, 7, 24, 15, 56, tzinfo=zoneinfo.ZoneInfo(key='America/New_York'))

    def lock_sleeve_leg(account_nick: str, day: _date, now_et: datetime, *, lock: bool,
                        hard: bool = False, origin: str = MANUAL_ORIGIN) -> Dict[str, Any]:
        """OPERATOR LOCK/UNLOCK of ONE account's CURRENTLY-HELD sleeve leg (the 🔒 LOCK ALL button).
    
        Writes TWO lock artefacts so the veto survives an engine-driven PositionLocks rewrite (bug
        fbb62441d227 — operator 2026-07-30 "i locked, and many position were sold without vetoed"):
    
          1. ``ManualHoldLocksDay`` — the ACCOUNT-SCOPED, DAY-PERSISTENT veto (``manual_hold_active``'s
             tier-1 source of truth). Keyed by ``(account_nick, trading_day)``; ARMABLE WHILE FLAT; survives
             boost's post-buy ``_lock_position`` overwrite, the sleeve's own entry bookkeeping, and the
             EOD force-unlock.
          2. ``PositionLocks{reason:"manual"}`` — the per-symbol row the /positions padlock still writes
             (legacy tier-2 fallback + dashboard visibility). The day-lock tier is what matters; this row is
             a courtesy.
    
        A lock whose reason is not the sleeve's own 'market_state_etf' suppresses the crest profit-take and
        the rollover-flip flatten.
    
        There is no second lock mechanism and no new veto — this is only a control for the existing one,
        reachable from the panel the operator actually runs the sleeve from (bug c55ff0c207).
    
        Scope is the SLEEVE LEG ONLY (operator 2026-07-16): the held TNA/TZA position, never the account's
        other holdings. The DAY-LOCK armable-while-flat path is the explicit exception (bug fbb62441d227
        spec: locked means the engine may not sell this account today regardless of position changes):
        when the account is FLAT and the operator clicks LOCK, the day-lock is STILL written so the next
        entry is protected from the moment it lands.
    
        Deliberately does NOT take ``execute``: a lock places no broker order. It is a DB veto flag, so it
        must work even when MARKET_STATE_ETF_EXECUTE_BROKER is off (DRY-RUN) — an operator locking a
        position in a log-only window still means "do not sell this". Unlocking is the exact inverse and
        hands the leg back to normal sleeve management.
    
        LEG DETECTION IS 3-WAY (bug e0c4a45ec60c) — the bulk positions() read this used to do laundered a
        degraded/None read into "flat" via ``or []``, so a blipped read answered an operator's 🔒 LOCK with
        'no_leg' ("nothing to protect") on a leg that WAS held — the sleeve then stayed free to sell the very
        position the operator had just moved to protect. That is strictly worse than the sibling BOOST defect
        it shares a button row with (bug 597b46e6c0cd): a dropped boost costs an unplaced order, a dropped
        LOCK silently withdraws protection while reporting success-shaped no-op.
    
        UNKNOWN CROSS-CHECKS THE LEDGER (as boost does, and unlike trim_account): a lock places NO order, so
        acting on a ledger-only leg is cheap and one-directional — worst case we write a veto row for a
        symbol that turns out to be flat, which vetoes nothing. Weigh that against dropping a real lock and
        the fallback is obviously right. A definitive 404 'flat' still does NOT get the second opinion.
    
        DAY-LOCK ARMED ON FLAT (bug fbb62441d227 spec): when ``lock=True`` and the account is confirmed
        FLAT, the function STILL writes the ManualHoldLocksDay row (action='armed_day_lock') so the next
        entry on this account is protected. The legacy PositionLocks row is intentionally not written —
        there is no symbol to key it on, and a sym-less row would be a bug surface. UNLOCK on a flat account
        STILL clears the day-lock and any leftover PositionLocks rows (live 2026-07-24 PERM UNLOCK incident:
        an operator closed three legs on the website and three PERM UNLOCK clicks each returned no_leg
        leaving stale hard rows behind — clearing foreign rows for BOTH vehicles regardless of held state).
        """
        from rtrader.database import lock_position
    
        bull, bear = _vehicle()
        dbf = _db_factory()
>       broker = _broker_for(account_nick, book=exit_book_for(account_nick))
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E       TypeError: test_lock_on_flat_account_arms_day_lock.<locals>.<lambda>() got an unexpected keyword argument 'book'

rtrader/services/market_state_etf.py:4592: TypeError
