fix(reconcile): symbol을 슬롯 생성 시 함께 저장 + NULL 슬롯 backfill

reconcile_universe 시그니처를 set[str] → dict[str, str] (portfolio_id→symbol)로 변경.
기존 symbol=NULL 슬롯은 다음 reconcile 실행 시 자동 backfill.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 21:08:46 +09:00
co-authored by Claude Sonnet 4.6
parent 221a16342e
commit 24a6017457
29 changed files with 92 additions and 68 deletions
@@ -24,7 +24,7 @@ def _wire(max_active):
def test_activates_asset_slot():
repo, svc, intents = _wire(max_active=2)
repo.reconcile_universe(INST, {"pf-a", "pf-b"})
repo.reconcile_universe(INST, {"pf-a": "SYM", "pf-b": "SYM"})
d = intents.submit_buy(BuyIntent(intent_id="b1", portfolio_id="pf-a"))
assert d.status == "admitted"
slot = repo.find_slot_by_portfolio(INST, "pf-a")
@@ -34,7 +34,7 @@ def test_activates_asset_slot():
def test_activates_by_slot_id():
# dcy-020/REQ-061: slot_id is the canonical buy contract key.
repo, svc, intents = _wire(max_active=2)
repo.reconcile_universe(INST, {"pf-a"})
repo.reconcile_universe(INST, {"pf-a": "SYM"})
slot = repo.find_slot_by_portfolio(INST, "pf-a")
d = intents.submit_buy(BuyIntent(intent_id="b1", slot_id=slot.slot_id))
assert d.status == "admitted" and d.slot_id == slot.slot_id
@@ -45,14 +45,14 @@ def test_activates_by_slot_id():
def test_unknown_slot_for_bad_slot_id():
from uuid import uuid4
repo, svc, intents = _wire(max_active=2)
repo.reconcile_universe(INST, {"pf-a"})
repo.reconcile_universe(INST, {"pf-a": "SYM"})
d = intents.submit_buy(BuyIntent(intent_id="b1", slot_id=uuid4()))
assert d.status == "rejected" and d.reason == "unknown_slot"
def test_already_active_is_rejected():
repo, svc, intents = _wire(max_active=2)
repo.reconcile_universe(INST, {"pf-a", "pf-b"})
repo.reconcile_universe(INST, {"pf-a": "SYM", "pf-b": "SYM"})
intents.submit_buy(BuyIntent(intent_id="b1", portfolio_id="pf-a"))
d2 = intents.submit_buy(BuyIntent(intent_id="b2", portfolio_id="pf-a"))
assert d2.status == "rejected" and d2.reason == "already_active"
@@ -60,7 +60,7 @@ def test_already_active_is_rejected():
def test_capacity_full_at_M_no_eviction():
repo, svc, intents = _wire(max_active=1)
repo.reconcile_universe(INST, {"pf-a", "pf-b"})
repo.reconcile_universe(INST, {"pf-a": "SYM", "pf-b": "SYM"})
d1 = intents.submit_buy(BuyIntent(intent_id="b1", portfolio_id="pf-a"))
assert d1.status == "admitted"
d2 = intents.submit_buy(BuyIntent(intent_id="b2", portfolio_id="pf-b"))
@@ -71,7 +71,7 @@ def test_capacity_full_at_M_no_eviction():
def test_unknown_portfolio_when_no_asset_slot():
repo, svc, intents = _wire(max_active=2)
repo.reconcile_universe(INST, {"pf-a"})
repo.reconcile_universe(INST, {"pf-a": "SYM"})
# pf-c is resolvable to a symbol but has no reconciled asset slot
d = intents.submit_buy(BuyIntent(intent_id="b1", portfolio_id="pf-c"))
assert d.status == "rejected" and d.reason == "unknown_portfolio"
@@ -80,7 +80,7 @@ def test_unknown_portfolio_when_no_asset_slot():
def test_over_cap_blocks_new_keeps_existing():
# M=2: fill 2, then lower M to 1 (over-cap) -> new rejected, existing kept
repo, svc, intents = _wire(max_active=2)
repo.reconcile_universe(INST, {"pf-a", "pf-b", "pf-c"})
repo.reconcile_universe(INST, {"pf-a": "SYM", "pf-b": "SYM", "pf-c": "SYM"})
intents.submit_buy(BuyIntent(intent_id="b1", portfolio_id="pf-a"))
intents.submit_buy(BuyIntent(intent_id="b2", portfolio_id="pf-b"))
assert repo.count_active(INST) == 2
@@ -94,7 +94,7 @@ def test_over_cap_blocks_new_keeps_existing():
def test_no_cap_admits_without_capacity_check():
# BAT-030: max_active_slots=None -> no capacity cap (asset-centric only path)
repo, svc, intents = _wire(max_active=None)
repo.reconcile_universe(INST, {"pf-a", "pf-b", "pf-c"})
repo.reconcile_universe(INST, {"pf-a": "SYM", "pf-b": "SYM", "pf-c": "SYM"})
for i, pid in enumerate(sorted(_SYMS)):
d = intents.submit_buy(BuyIntent(intent_id=f"b{i}", portfolio_id=pid))
assert d.status == "admitted"