chore: reorganize into polyglot monorepo (workshop)
- Move bigmind/ -> mcp/bigmind/ - Move webscraper/ -> mcp/webscraper/ - Move mss-failsafe/ -> java/mss-failsafe/ - Move Wellmann-Shop/ -> java/wellmann-shop/ (normalize to kebab-case) - Add .roo/ IDE config files to tracking - Add plans/REPO_STRATEGY.md (monorepo strategy document) - Expand .gitignore: Java/Maven, Node/TS, coverage, uv.lock - Rewrite README.md as navigation index - Update .roo/mcp.json webscraper path to mcp/webscraper/
This commit is contained in:
@@ -0,0 +1,328 @@
|
||||
"""Tests for the Achievement Gallery — profile_builder.compute_achievements().
|
||||
|
||||
All tests use the temp_db fixture (auto-use in conftest.py) which wires
|
||||
BIGMIND_DB_PATH + BIGMIND_USER to a fresh SQLite file per test.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from bigmind import memory_store
|
||||
from bigmind.profile_builder import compute_achievements, build_profile_data
|
||||
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
def _uid():
|
||||
user = memory_store.get_or_create_user(memory_store.get_current_username())
|
||||
return user["id"]
|
||||
|
||||
|
||||
def _close_session(session_id: str, has_tier2: bool = False):
|
||||
"""Close a session with a one-liner summary."""
|
||||
memory_store.close_session(
|
||||
session_id=session_id,
|
||||
one_liner="test session",
|
||||
topics="test",
|
||||
outcome="ok",
|
||||
importance=5,
|
||||
)
|
||||
if has_tier2:
|
||||
memory_store.save_session_summary(session_id, summary="detailed summary")
|
||||
|
||||
|
||||
# ── TestComputeAchievements ───────────────────────────────────────────────────
|
||||
|
||||
class TestComputeAchievements:
|
||||
|
||||
def test_returns_list_of_expected_ids(self):
|
||||
uid = _uid()
|
||||
achievements = compute_achievements(uid)
|
||||
ids = {a["id"] for a in achievements}
|
||||
expected = {
|
||||
"first_breath", "first_thought", "eureka", "honest_mind",
|
||||
"scholar", "deep_knowledge", "scientist", "veteran",
|
||||
"on_fire", "storyteller", "night_owl", "speed_thinker",
|
||||
"first_handshake", "birthday", "shared_mind",
|
||||
"frugal_mind", "quarter_million", "token_millionaire", "sniper",
|
||||
}
|
||||
assert expected == ids
|
||||
|
||||
def test_all_locked_for_empty_db(self):
|
||||
"""Fresh DB: most achievements locked, except First Handshake (hardcoded)."""
|
||||
uid = _uid()
|
||||
achievements = compute_achievements(uid)
|
||||
by_id = {a["id"]: a for a in achievements}
|
||||
|
||||
# First Handshake is always unlocked (hardcoded to 2026-03-31)
|
||||
assert by_id["first_handshake"]["unlocked"] is True
|
||||
assert by_id["first_handshake"]["unlocked_at"] == "2026-03-31"
|
||||
|
||||
# Everything else locked
|
||||
for aid in ["first_breath", "first_thought", "eureka", "honest_mind",
|
||||
"scholar", "veteran", "on_fire", "storyteller", "night_owl",
|
||||
"speed_thinker", "frugal_mind", "quarter_million",
|
||||
"token_millionaire", "sniper"]:
|
||||
assert by_id[aid]["unlocked"] is False, f"{aid} should be locked"
|
||||
|
||||
# Shared Mind is always locked (Phase 3 not yet)
|
||||
assert by_id["shared_mind"]["unlocked"] is False
|
||||
|
||||
# ── First Breath ──────────────────────────────────────────────────────────
|
||||
|
||||
def test_first_breath_unlocks_after_first_session(self):
|
||||
uid = _uid()
|
||||
sid = memory_store.create_session(uid)
|
||||
_close_session(sid)
|
||||
ach = {a["id"]: a for a in compute_achievements(uid)}
|
||||
assert ach["first_breath"]["unlocked"] is True
|
||||
assert ach["first_breath"]["unlocked_at"] is not None
|
||||
|
||||
def test_first_breath_locked_with_only_open_session(self):
|
||||
"""Open (unclosed) session does NOT unlock First Breath."""
|
||||
uid = _uid()
|
||||
memory_store.create_session(uid) # not closed
|
||||
ach = {a["id"]: a for a in compute_achievements(uid)}
|
||||
assert ach["first_breath"]["unlocked"] is False
|
||||
|
||||
# ── First Thought / Eureka / Honest Mind ─────────────────────────────────
|
||||
|
||||
def test_first_thought_unlocks_on_first_hypothesis(self):
|
||||
uid = _uid()
|
||||
sid = memory_store.create_session(uid)
|
||||
memory_store.add_hypothesis(uid, sid, "test hypothesis", confidence=0.7)
|
||||
ach = {a["id"]: a for a in compute_achievements(uid)}
|
||||
assert ach["first_thought"]["unlocked"] is True
|
||||
|
||||
def test_eureka_locked_until_confirmed(self):
|
||||
uid = _uid()
|
||||
sid = memory_store.create_session(uid)
|
||||
hid = memory_store.add_hypothesis(uid, sid, "will be confirmed")
|
||||
ach = {a["id"]: a for a in compute_achievements(uid)}
|
||||
assert ach["eureka"]["unlocked"] is False # still open
|
||||
|
||||
memory_store.resolve_hypothesis(hid, uid, "confirmed", "yes it was true")
|
||||
ach = {a["id"]: a for a in compute_achievements(uid)}
|
||||
assert ach["eureka"]["unlocked"] is True
|
||||
|
||||
def test_honest_mind_locked_until_refuted(self):
|
||||
uid = _uid()
|
||||
sid = memory_store.create_session(uid)
|
||||
hid = memory_store.add_hypothesis(uid, sid, "will be refuted")
|
||||
ach = {a["id"]: a for a in compute_achievements(uid)}
|
||||
assert ach["honest_mind"]["unlocked"] is False
|
||||
|
||||
memory_store.resolve_hypothesis(hid, uid, "refuted", "nope, was wrong")
|
||||
ach = {a["id"]: a for a in compute_achievements(uid)}
|
||||
assert ach["honest_mind"]["unlocked"] is True
|
||||
|
||||
# ── Scholar ───────────────────────────────────────────────────────────────
|
||||
|
||||
def test_scholar_locks_below_25_facts(self):
|
||||
uid = _uid()
|
||||
for i in range(24):
|
||||
memory_store.store_fact(uid, "test", f"fact number {i}")
|
||||
ach = {a["id"]: a for a in compute_achievements(uid)}
|
||||
assert ach["scholar"]["unlocked"] is False
|
||||
assert "24" in ach["scholar"]["condition"]
|
||||
|
||||
def test_scholar_unlocks_at_25_facts(self):
|
||||
uid = _uid()
|
||||
for i in range(25):
|
||||
memory_store.store_fact(uid, "test", f"fact number {i}")
|
||||
ach = {a["id"]: a for a in compute_achievements(uid)}
|
||||
assert ach["scholar"]["unlocked"] is True
|
||||
assert ach["scholar"]["unlocked_at"] is not None
|
||||
|
||||
def test_deep_knowledge_unlocks_at_100_facts(self):
|
||||
uid = _uid()
|
||||
for i in range(100):
|
||||
memory_store.store_fact(uid, "test", f"fact number {i}")
|
||||
ach = {a["id"]: a for a in compute_achievements(uid)}
|
||||
assert ach["deep_knowledge"]["unlocked"] is True
|
||||
# Scholar should also be unlocked
|
||||
assert ach["scholar"]["unlocked"] is True
|
||||
|
||||
# ── Scientist ─────────────────────────────────────────────────────────────
|
||||
|
||||
def test_scientist_unlocks_at_10_hypotheses(self):
|
||||
uid = _uid()
|
||||
sid = memory_store.create_session(uid)
|
||||
for i in range(9):
|
||||
memory_store.add_hypothesis(uid, sid, f"hypothesis {i}")
|
||||
ach = {a["id"]: a for a in compute_achievements(uid)}
|
||||
assert ach["scientist"]["unlocked"] is False
|
||||
|
||||
memory_store.add_hypothesis(uid, sid, "hypothesis 9")
|
||||
ach = {a["id"]: a for a in compute_achievements(uid)}
|
||||
assert ach["scientist"]["unlocked"] is True
|
||||
assert ach["scientist"]["unlocked_at"] is not None
|
||||
|
||||
# ── Veteran ───────────────────────────────────────────────────────────────
|
||||
|
||||
def test_veteran_unlocks_at_50_sessions(self):
|
||||
uid = _uid()
|
||||
for _ in range(50):
|
||||
sid = memory_store.create_session(uid)
|
||||
_close_session(sid)
|
||||
ach = {a["id"]: a for a in compute_achievements(uid)}
|
||||
assert ach["veteran"]["unlocked"] is True
|
||||
assert ach["veteran"]["unlocked_at"] is not None
|
||||
|
||||
def test_veteran_locks_at_49_sessions(self):
|
||||
uid = _uid()
|
||||
for _ in range(49):
|
||||
sid = memory_store.create_session(uid)
|
||||
_close_session(sid)
|
||||
ach = {a["id"]: a for a in compute_achievements(uid)}
|
||||
assert ach["veteran"]["unlocked"] is False
|
||||
|
||||
# ── On Fire ───────────────────────────────────────────────────────────────
|
||||
|
||||
def test_on_fire_locked_below_5_sessions_per_day(self):
|
||||
uid = _uid()
|
||||
for _ in range(4):
|
||||
sid = memory_store.create_session(uid)
|
||||
_close_session(sid)
|
||||
ach = {a["id"]: a for a in compute_achievements(uid)}
|
||||
assert ach["on_fire"]["unlocked"] is False
|
||||
|
||||
def test_on_fire_unlocks_at_5_sessions_same_day(self):
|
||||
uid = _uid()
|
||||
for _ in range(5):
|
||||
sid = memory_store.create_session(uid)
|
||||
_close_session(sid)
|
||||
ach = {a["id"]: a for a in compute_achievements(uid)}
|
||||
assert ach["on_fire"]["unlocked"] is True
|
||||
|
||||
# ── Storyteller ───────────────────────────────────────────────────────────
|
||||
|
||||
def test_storyteller_requires_20_tier2_sessions(self):
|
||||
uid = _uid()
|
||||
# 19 sessions with tier2
|
||||
for _ in range(19):
|
||||
sid = memory_store.create_session(uid)
|
||||
_close_session(sid, has_tier2=True)
|
||||
ach = {a["id"]: a for a in compute_achievements(uid)}
|
||||
assert ach["storyteller"]["unlocked"] is False
|
||||
|
||||
sid = memory_store.create_session(uid)
|
||||
_close_session(sid, has_tier2=True)
|
||||
ach = {a["id"]: a for a in compute_achievements(uid)}
|
||||
assert ach["storyteller"]["unlocked"] is True
|
||||
|
||||
# ── Speed Thinker ─────────────────────────────────────────────────────────
|
||||
|
||||
def test_speed_thinker_unlocks_same_day_confirm(self):
|
||||
uid = _uid()
|
||||
sid = memory_store.create_session(uid)
|
||||
hid = memory_store.add_hypothesis(uid, sid, "quick thought")
|
||||
memory_store.resolve_hypothesis(hid, uid, "confirmed", "yes!")
|
||||
ach = {a["id"]: a for a in compute_achievements(uid)}
|
||||
assert ach["speed_thinker"]["unlocked"] is True
|
||||
|
||||
# ── Token achievements ────────────────────────────────────────────────────
|
||||
|
||||
def test_frugal_mind_unlocks_on_first_token_save(self):
|
||||
uid = _uid()
|
||||
sid = memory_store.create_session(uid)
|
||||
memory_store.log_token_save(sid, uid, "saved tokens by grep", 5000, "grep")
|
||||
ach = {a["id"]: a for a in compute_achievements(uid)}
|
||||
assert ach["frugal_mind"]["unlocked"] is True
|
||||
assert ach["frugal_mind"]["unlocked_at"] is not None
|
||||
|
||||
def test_quarter_million_unlocks_at_250k(self):
|
||||
uid = _uid()
|
||||
sid = memory_store.create_session(uid)
|
||||
memory_store.log_token_save(sid, uid, "big save", 250_000, "grep")
|
||||
ach = {a["id"]: a for a in compute_achievements(uid)}
|
||||
assert ach["quarter_million"]["unlocked"] is True
|
||||
|
||||
def test_token_millionaire_unlocks_at_1m(self):
|
||||
uid = _uid()
|
||||
sid = memory_store.create_session(uid)
|
||||
memory_store.log_token_save(sid, uid, "huge save", 1_000_000, "memory_hit")
|
||||
ach = {a["id"]: a for a in compute_achievements(uid)}
|
||||
assert ach["token_millionaire"]["unlocked"] is True
|
||||
|
||||
def test_sniper_requires_single_save_over_500k(self):
|
||||
uid = _uid()
|
||||
sid = memory_store.create_session(uid)
|
||||
# Multiple saves that total > 500k but none individual exceeds it
|
||||
memory_store.log_token_save(sid, uid, "save 1", 300_000, "grep")
|
||||
memory_store.log_token_save(sid, uid, "save 2", 300_000, "grep")
|
||||
ach = {a["id"]: a for a in compute_achievements(uid)}
|
||||
assert ach["sniper"]["unlocked"] is False # no single save > 500k
|
||||
|
||||
memory_store.log_token_save(sid, uid, "sniper shot", 600_000, "grep")
|
||||
ach = {a["id"]: a for a in compute_achievements(uid)}
|
||||
assert ach["sniper"]["unlocked"] is True
|
||||
|
||||
# ── Birthday ──────────────────────────────────────────────────────────────
|
||||
|
||||
def test_birthday_locked_shows_countdown(self):
|
||||
uid = _uid()
|
||||
sid = memory_store.create_session(uid)
|
||||
_close_session(sid)
|
||||
ach = {a["id"]: a for a in compute_achievements(uid)}
|
||||
bday = ach["birthday"]
|
||||
assert bday["unlocked"] is False
|
||||
assert bday["extra"] is not None
|
||||
assert "In " in bday["extra"] or "day" in bday["extra"]
|
||||
|
||||
# ── Hardcoded achievements ─────────────────────────────────────────────────
|
||||
|
||||
def test_first_handshake_always_unlocked(self):
|
||||
uid = _uid()
|
||||
ach = {a["id"]: a for a in compute_achievements(uid)}
|
||||
assert ach["first_handshake"]["unlocked"] is True
|
||||
assert ach["first_handshake"]["unlocked_at"] == "2026-03-31"
|
||||
|
||||
def test_shared_mind_always_locked(self):
|
||||
uid = _uid()
|
||||
ach = {a["id"]: a for a in compute_achievements(uid)}
|
||||
assert ach["shared_mind"]["unlocked"] is False
|
||||
|
||||
# ── Achievement structure ─────────────────────────────────────────────────
|
||||
|
||||
def test_all_achievements_have_required_keys(self):
|
||||
uid = _uid()
|
||||
achievements = compute_achievements(uid)
|
||||
for a in achievements:
|
||||
assert "id" in a
|
||||
assert "icon" in a
|
||||
assert "name" in a
|
||||
assert "description" in a
|
||||
assert "unlocked" in a
|
||||
assert "unlocked_at" in a
|
||||
assert "condition" in a
|
||||
|
||||
def test_unlocked_achievement_has_no_extra_for_non_birthday(self):
|
||||
"""Non-birthday unlocked achievements should not have 'extra' countdown text."""
|
||||
uid = _uid()
|
||||
sid = memory_store.create_session(uid)
|
||||
_close_session(sid)
|
||||
ach = {a["id"]: a for a in compute_achievements(uid)}
|
||||
fb = ach["first_breath"]
|
||||
assert fb["unlocked"] is True
|
||||
assert fb.get("extra") is None
|
||||
|
||||
# ── build_profile_data integration ────────────────────────────────────────
|
||||
|
||||
def test_build_profile_data_includes_achievements(self):
|
||||
uid = _uid()
|
||||
data = build_profile_data(uid)
|
||||
assert "achievements" in data
|
||||
assert isinstance(data["achievements"], list)
|
||||
assert len(data["achievements"]) > 0
|
||||
|
||||
def test_build_profile_data_achievement_count_correct(self):
|
||||
uid = _uid()
|
||||
# Add one session so first_breath and on_fire can unlock
|
||||
sid = memory_store.create_session(uid)
|
||||
_close_session(sid)
|
||||
data = build_profile_data(uid)
|
||||
unlocked = [a for a in data["achievements"] if a["unlocked"]]
|
||||
# At minimum: first_breath + first_handshake = 2
|
||||
assert len(unlocked) >= 2
|
||||
|
||||
|
||||
Reference in New Issue
Block a user