fix(consult): limit career direction and single-flight chart warm
The career checklist now says the direction section only covers strength, resistance, timing, and three events. Warm builds one packet per cache key and skips when its own cap is full. A three-repeat biography run still has one career answer that names a direction from a planet nature, so this is not accepted. Not pushed.
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
@@ -302,3 +303,125 @@ def test_warm_response_does_not_include_the_packet():
|
||||
assert warmed["success"] is False
|
||||
assert "packet" not in warmed
|
||||
assert warmed["error_type"]
|
||||
|
||||
|
||||
def _fictional_body(today: str, minute: int) -> dict:
|
||||
return {
|
||||
"year": 1902,
|
||||
"month": 3,
|
||||
"day": 3,
|
||||
"hour": 1,
|
||||
"minute": minute,
|
||||
"second": 0,
|
||||
"lat": 1,
|
||||
"lon": 2,
|
||||
"tz": 0,
|
||||
"ayanamsa": "raman",
|
||||
"node_mode": "mean",
|
||||
"today": today,
|
||||
}
|
||||
|
||||
|
||||
def _drop_cache(identity: dict) -> None:
|
||||
path = cache_dir() / f"{cache_key(identity)}.json"
|
||||
path.unlink(missing_ok=True)
|
||||
path.with_suffix(".json.tmp").unlink(missing_ok=True)
|
||||
|
||||
|
||||
def test_one_warm_at_a_time_and_same_key_user_waits_for_that_build(monkeypatch):
|
||||
"""H2: warms share a cap of 1; a question for the key being warmed builds once."""
|
||||
calls = {"n": 0}
|
||||
calls_lock = threading.Lock()
|
||||
entered = threading.Event()
|
||||
release = threading.Event()
|
||||
|
||||
def blocking(identity):
|
||||
with calls_lock:
|
||||
calls["n"] += 1
|
||||
entered.set()
|
||||
assert release.wait(timeout=5)
|
||||
return {"marker": identity["reference_date"]}
|
||||
|
||||
monkeypatch.setattr("consult_full_data_cache.build_full_data_packet", blocking)
|
||||
|
||||
bodies = [_fictional_body(f"1904-01-0{index}", 11) for index in (1, 2, 3)]
|
||||
identities = [identity_from_body(body) for body in bodies]
|
||||
for identity in identities:
|
||||
_drop_cache(identity)
|
||||
results: list[dict | None] = [None, None, None]
|
||||
|
||||
def run_warm(index: int) -> None:
|
||||
results[index] = warm_consult_packet(bodies[index])
|
||||
|
||||
threads = [threading.Thread(target=run_warm, args=(index,)) for index in range(3)]
|
||||
try:
|
||||
for thread in threads:
|
||||
thread.start()
|
||||
assert entered.wait(timeout=5)
|
||||
deadline = time.time() + 3
|
||||
while time.time() < deadline and sum(item is not None for item in results) < 2:
|
||||
time.sleep(0.02)
|
||||
assert calls["n"] == 1
|
||||
assert sum(item is not None and item.get("skipped") is True for item in results) == 2
|
||||
for item in results:
|
||||
if item is not None:
|
||||
assert "packet" not in item
|
||||
|
||||
same = _fictional_body("1904-02-02", 12)
|
||||
same_identity = identity_from_body(same)
|
||||
_drop_cache(same_identity)
|
||||
# The cap is still held by the first warm. A same-key question must join
|
||||
# that pattern on its own key: start a warm, then the question.
|
||||
release.set()
|
||||
for thread in threads:
|
||||
thread.join(timeout=5)
|
||||
assert calls["n"] == 1
|
||||
built = [item for item in results if item and item.get("skipped") is not True]
|
||||
assert len(built) == 1
|
||||
assert built[0]["success"] is True
|
||||
assert built[0]["hit"] is False
|
||||
finally:
|
||||
release.set()
|
||||
for thread in threads:
|
||||
thread.join(timeout=5)
|
||||
for identity in identities:
|
||||
_drop_cache(identity)
|
||||
|
||||
entered.clear()
|
||||
release.clear()
|
||||
calls["n"] = 0
|
||||
warm_box: dict = {}
|
||||
user_box: dict = {}
|
||||
same = _fictional_body("1904-02-02", 12)
|
||||
same_identity = identity_from_body(same)
|
||||
_drop_cache(same_identity)
|
||||
|
||||
def run_same_warm() -> None:
|
||||
warm_box["result"] = warm_consult_packet(same)
|
||||
|
||||
def run_user() -> None:
|
||||
user_box["result"] = get_or_build(same_identity)
|
||||
|
||||
warm_thread = threading.Thread(target=run_same_warm)
|
||||
user_thread = threading.Thread(target=run_user)
|
||||
try:
|
||||
warm_thread.start()
|
||||
assert entered.wait(timeout=5)
|
||||
user_thread.start()
|
||||
time.sleep(0.3)
|
||||
assert calls["n"] == 1
|
||||
assert user_thread.is_alive()
|
||||
release.set()
|
||||
warm_thread.join(timeout=5)
|
||||
user_thread.join(timeout=5)
|
||||
assert calls["n"] == 1
|
||||
assert warm_box["result"]["success"] is True
|
||||
assert warm_box["result"].get("skipped") is not True
|
||||
assert "packet" not in warm_box["result"]
|
||||
assert user_box["result"]["hit"] is True
|
||||
assert user_box["result"]["packet"]["marker"] == "1904-02-02"
|
||||
finally:
|
||||
release.set()
|
||||
warm_thread.join(timeout=5)
|
||||
user_thread.join(timeout=5)
|
||||
_drop_cache(same_identity)
|
||||
|
||||
Reference in New Issue
Block a user