Files
wehub-resource-sync c3749daf48
Tests / test-linux (3.13) (push) Failing after 0s
Tests / test-linux (3.11) (push) Failing after 1s
Tests / lint (push) Failing after 0s
Tests / test-linux (3.9) (push) Failing after 1s
Docker / build (push) Failing after 1s
Docker / build-gpu (push) Failing after 2s
Tests / test-windows (push) Has been cancelled
Tests / test-macos (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:03:03 +08:00

571 lines
20 KiB
Python

"""Tests for mempalace.onboarding."""
import os
from unittest.mock import patch
from mempalace.onboarding import (
DEFAULT_WINGS,
_ask,
_ask_embedding_model,
_ask_mode,
_ask_people,
_ask_projects,
_ask_wings,
_auto_detect,
_generate_aaak_bootstrap,
_header,
_hr,
_warn_ambiguous,
_yn,
quick_setup,
run_onboarding,
)
# Force UTF-8 for Windows (source file contains Unicode symbols like hearts/stars)
os.environ["PYTHONUTF8"] = "1"
# ── DEFAULT_WINGS ───────────────────────────────────────────────────────
def test_default_wings_has_expected_keys():
assert "work" in DEFAULT_WINGS
assert "personal" in DEFAULT_WINGS
assert "combo" in DEFAULT_WINGS
def test_default_wings_work_has_projects():
assert "projects" in DEFAULT_WINGS["work"]
def test_default_wings_personal_has_family():
assert "family" in DEFAULT_WINGS["personal"]
def test_default_wings_combo_has_both():
wings = DEFAULT_WINGS["combo"]
assert "family" in wings
assert "work" in wings
def test_default_wings_values_are_lists():
for mode, wings in DEFAULT_WINGS.items():
assert isinstance(wings, list), f"{mode} wings should be a list"
assert len(wings) >= 3, f"{mode} should have at least 3 wings"
# ── _warn_ambiguous ─────────────────────────────────────────────────────
def test_warn_ambiguous_flags_common_words():
people = [
{"name": "Grace", "relationship": "friend"},
{"name": "Riley", "relationship": "daughter"},
]
result = _warn_ambiguous(people)
assert "Grace" in result
# Riley is not a common English word
assert "Riley" not in result
def test_warn_ambiguous_empty_list():
result = _warn_ambiguous([])
assert result == []
def test_warn_ambiguous_no_ambiguous_names():
people = [
{"name": "Riley", "relationship": "daughter"},
{"name": "Devon", "relationship": "friend"},
]
result = _warn_ambiguous(people)
assert result == []
def test_warn_ambiguous_multiple_hits():
people = [
{"name": "Grace", "relationship": "friend"},
{"name": "May", "relationship": "aunt"},
{"name": "Joy", "relationship": "sister"},
]
result = _warn_ambiguous(people)
assert "Grace" in result
assert "May" in result
assert "Joy" in result
# ── quick_setup ─────────────────────────────────────────────────────────
def test_quick_setup_creates_registry(tmp_path):
registry = quick_setup(
mode="personal",
people=[{"name": "Riley", "relationship": "daughter", "context": "personal"}],
projects=["MemPalace"],
config_dir=tmp_path,
)
assert "Riley" in registry.people
assert "MemPalace" in registry.projects
assert registry.mode == "personal"
def test_quick_setup_work_mode(tmp_path):
registry = quick_setup(
mode="work",
people=[{"name": "Alice", "relationship": "colleague", "context": "work"}],
projects=["Acme"],
config_dir=tmp_path,
)
assert registry.mode == "work"
assert "Alice" in registry.people
assert "Acme" in registry.projects
def test_quick_setup_empty(tmp_path):
registry = quick_setup(mode="personal", people=[], config_dir=tmp_path)
assert len(registry.people) == 0
assert len(registry.projects) == 0
def test_quick_setup_saves_to_disk(tmp_path):
quick_setup(
mode="personal",
people=[{"name": "Riley", "relationship": "daughter", "context": "personal"}],
config_dir=tmp_path,
)
assert (tmp_path / "entity_registry.json").exists()
# ── _generate_aaak_bootstrap ───────────────────────────────────────────
def test_generate_aaak_bootstrap_creates_files(tmp_path):
people = [
{"name": "Riley", "relationship": "daughter", "context": "personal"},
{"name": "Devon", "relationship": "friend", "context": "personal"},
]
projects = ["MemPalace"]
wings = ["family", "creative"]
_generate_aaak_bootstrap(people, projects, wings, "personal", config_dir=tmp_path)
assert (tmp_path / "aaak_entities.md").exists()
assert (tmp_path / "critical_facts.md").exists()
def test_generate_aaak_bootstrap_entities_content(tmp_path):
people = [{"name": "Riley", "relationship": "daughter", "context": "personal"}]
projects = ["MemPalace"]
wings = ["family"]
_generate_aaak_bootstrap(people, projects, wings, "personal", config_dir=tmp_path)
content = (tmp_path / "aaak_entities.md").read_text(encoding="utf-8")
assert "Riley" in content
assert "RIL" in content # entity code
assert "MemPalace" in content
def test_generate_aaak_bootstrap_facts_content(tmp_path):
people = [
{"name": "Alice", "relationship": "colleague", "context": "work"},
]
projects = ["Acme"]
wings = ["projects"]
_generate_aaak_bootstrap(people, projects, wings, "work", config_dir=tmp_path)
content = (tmp_path / "critical_facts.md").read_text(encoding="utf-8")
assert "Alice" in content
assert "Acme" in content
assert "work" in content.lower()
def test_generate_aaak_bootstrap_empty_people(tmp_path):
_generate_aaak_bootstrap([], [], ["general"], "personal", config_dir=tmp_path)
assert (tmp_path / "aaak_entities.md").exists()
assert (tmp_path / "critical_facts.md").exists()
def test_generate_aaak_bootstrap_collision(tmp_path):
"""Two people with same 3-letter code get different codes."""
people = [
{"name": "Alice", "relationship": "friend", "context": "work"},
{"name": "Alison", "relationship": "coworker", "context": "work"},
]
_generate_aaak_bootstrap(people, [], ["work"], "work", config_dir=tmp_path)
content = (tmp_path / "aaak_entities.md").read_text(encoding="utf-8")
assert "ALI" in content
assert "ALIS" in content
def test_generate_aaak_bootstrap_no_relationship(tmp_path):
"""Person without relationship string still generates entry."""
people = [{"name": "Bob", "context": "work"}]
_generate_aaak_bootstrap(people, [], ["work"], "work", config_dir=tmp_path)
content = (tmp_path / "aaak_entities.md").read_text(encoding="utf-8")
assert "BOB=Bob" in content
# ── _hr, _header ──────────────────────────────────────────────────────
def test_hr_prints_line(capsys):
_hr()
out = capsys.readouterr().out
assert "─" in out
def test_header_prints_banner(capsys):
_header("Test Title")
out = capsys.readouterr().out
assert "Test Title" in out
assert "=" in out
# ── _ask ──────────────────────────────────────────────────────────────
def test_ask_with_default_uses_default():
with patch("builtins.input", return_value=""):
result = _ask("prompt", default="fallback")
assert result == "fallback"
def test_ask_with_default_uses_input():
with patch("builtins.input", return_value="custom"):
result = _ask("prompt", default="fallback")
assert result == "custom"
def test_ask_no_default():
with patch("builtins.input", return_value="answer"):
result = _ask("prompt")
assert result == "answer"
# ── _yn ───────────────────────────────────────────────────────────────
def test_yn_default_yes_empty_input():
with patch("builtins.input", return_value=""):
assert _yn("continue?") is True
def test_yn_default_no_empty_input():
with patch("builtins.input", return_value=""):
assert _yn("continue?", default="n") is False
def test_yn_explicit_yes():
with patch("builtins.input", return_value="yes"):
assert _yn("continue?", default="n") is True
def test_yn_explicit_no():
with patch("builtins.input", return_value="no"):
assert _yn("continue?") is False
# ── _ask_mode ─────────────────────────────────────────────────────────
def test_ask_mode_work():
with patch("builtins.input", return_value="1"):
assert _ask_mode() == "work"
def test_ask_mode_personal():
with patch("builtins.input", return_value="2"):
assert _ask_mode() == "personal"
def test_ask_mode_combo():
with patch("builtins.input", return_value="3"):
assert _ask_mode() == "combo"
def test_ask_mode_retries_on_bad_input():
with patch("builtins.input", side_effect=["x", "bad", "1"]):
assert _ask_mode() == "work"
# ── _ask_people ───────────────────────────────────────────────────────
def test_ask_people_personal_mode():
with patch("builtins.input", side_effect=["Alice, daughter", "", "done"]):
people, aliases = _ask_people("personal")
assert len(people) == 1
assert people[0]["name"] == "Alice"
assert people[0]["relationship"] == "daughter"
def test_ask_people_work_mode():
with patch("builtins.input", side_effect=["Bob, manager", "", "done"]):
people, aliases = _ask_people("work")
assert len(people) == 1
assert people[0]["name"] == "Bob"
assert people[0]["context"] == "work"
def test_ask_people_combo_mode():
with patch(
"builtins.input",
side_effect=[
"Alice, daughter",
"",
"done", # personal
"Bob, boss",
"done", # work
],
):
people, aliases = _ask_people("combo")
assert len(people) == 2
def test_ask_people_with_nickname():
with patch("builtins.input", side_effect=["Alice, daughter", "Ali", "done"]):
people, aliases = _ask_people("personal")
assert aliases == {"Ali": "Alice"}
def test_ask_people_empty_name_skipped():
with patch("builtins.input", side_effect=["", "done"]):
people, aliases = _ask_people("personal")
assert len(people) == 0
# ── _ask_projects ─────────────────────────────────────────────────────
def test_ask_projects_personal_returns_empty():
result = _ask_projects("personal")
assert result == []
def test_ask_projects_work_mode():
with patch("builtins.input", side_effect=["Acme", "BigCo", "done"]):
result = _ask_projects("work")
assert result == ["Acme", "BigCo"]
def test_ask_projects_empty_entry_stops():
with patch("builtins.input", side_effect=["Acme", ""]):
result = _ask_projects("work")
assert result == ["Acme"]
# ── _ask_wings ────────────────────────────────────────────────────────
def test_ask_wings_accept_defaults():
with patch("builtins.input", return_value=""):
result = _ask_wings("work")
assert result == DEFAULT_WINGS["work"]
def test_ask_wings_custom():
with patch("builtins.input", return_value="alpha, beta, gamma"):
result = _ask_wings("personal")
assert result == ["alpha", "beta", "gamma"]
# ── _auto_detect ──────────────────────────────────────────────────────
def test_auto_detect_no_files(tmp_path):
result = _auto_detect(str(tmp_path), [])
assert result == []
def test_auto_detect_filters_known(tmp_path):
known = [{"name": "Alice"}]
fake_detected = {
"people": [
{"name": "Alice", "confidence": 0.9, "signals": ["test"]},
{"name": "Bob", "confidence": 0.8, "signals": ["test"]},
],
"projects": [],
"uncertain": [],
}
with (
patch("mempalace.onboarding.scan_for_detection", return_value=["file.txt"]),
patch("mempalace.onboarding.detect_entities", return_value=fake_detected),
):
result = _auto_detect(str(tmp_path), known)
names = [p["name"] for p in result]
assert "Alice" not in names
assert "Bob" in names
def test_auto_detect_filters_low_confidence(tmp_path):
fake_detected = {
"people": [{"name": "Bob", "confidence": 0.5, "signals": ["test"]}],
"projects": [],
"uncertain": [],
}
with (
patch("mempalace.onboarding.scan_for_detection", return_value=["file.txt"]),
patch("mempalace.onboarding.detect_entities", return_value=fake_detected),
):
result = _auto_detect(str(tmp_path), [])
assert len(result) == 0
def test_auto_detect_handles_exception(tmp_path):
with patch("mempalace.onboarding.scan_for_detection", side_effect=Exception("boom")):
result = _auto_detect(str(tmp_path), [])
assert result == []
# ── run_onboarding ────────────────────────────────────────────────────
def test_run_onboarding_basic_flow(tmp_path):
"""Test the full onboarding flow with minimal mocking."""
with (
patch("mempalace.onboarding._ask_mode", return_value="work"),
patch("mempalace.onboarding._ask_embedding_model", return_value="embeddinggemma"),
patch(
"mempalace.onboarding._ask_people",
return_value=([{"name": "Bob", "relationship": "boss", "context": "work"}], {}),
),
patch("mempalace.onboarding._ask_projects", return_value=["Acme"]),
patch("mempalace.onboarding._ask_wings", return_value=["projects", "team"]),
patch("mempalace.onboarding._yn", return_value=False),
patch("mempalace.onboarding._warn_ambiguous", return_value=[]),
):
registry = run_onboarding(directory=".", config_dir=tmp_path, auto_detect=False)
assert "Bob" in registry.people
assert "Acme" in registry.projects
def test_run_onboarding_with_ambiguous_names(tmp_path):
"""Onboarding prints a warning for ambiguous names."""
with (
patch("mempalace.onboarding._ask_mode", return_value="personal"),
patch("mempalace.onboarding._ask_embedding_model", return_value="embeddinggemma"),
patch(
"mempalace.onboarding._ask_people",
return_value=([{"name": "Grace", "relationship": "friend", "context": "personal"}], {}),
),
patch("mempalace.onboarding._ask_projects", return_value=[]),
patch("mempalace.onboarding._ask_wings", return_value=["family"]),
patch("mempalace.onboarding._yn", return_value=False),
):
registry = run_onboarding(directory=".", config_dir=tmp_path, auto_detect=False)
assert "Grace" in registry.people
# ── _ask_embedding_model ──────────────────────────────────────────────
def test_ask_embedding_model_defaults_to_multilingual():
"""Default is multilingual: empty input (just Enter) accepts the default."""
with patch("builtins.input", return_value=""):
assert _ask_embedding_model() == "embeddinggemma"
def test_ask_embedding_model_explicit_yes():
with patch("builtins.input", return_value="y"):
assert _ask_embedding_model() == "embeddinggemma"
def test_ask_embedding_model_explicit_no():
"""User opts down to English-only — choice is recorded as 'minilm'."""
with patch("builtins.input", return_value="n"):
assert _ask_embedding_model() == "minilm"
# ── run_onboarding persists embedding_model ────────────────────────────
def test_run_onboarding_persists_multilingual_choice(tmp_path):
"""When user picks multilingual, embedding_model is written to config.json
so subsequent loads pick up embeddinggemma without re-prompting."""
import json
from mempalace.config import MempalaceConfig
with (
patch("mempalace.onboarding._ask_mode", return_value="work"),
patch("mempalace.onboarding._ask_embedding_model", return_value="embeddinggemma"),
patch(
"mempalace.onboarding._ask_people",
return_value=([{"name": "Bob", "relationship": "boss", "context": "work"}], {}),
),
patch("mempalace.onboarding._ask_projects", return_value=[]),
patch("mempalace.onboarding._ask_wings", return_value=["projects"]),
patch("mempalace.onboarding._yn", return_value=False),
patch("mempalace.onboarding._warn_ambiguous", return_value=[]),
):
run_onboarding(directory=".", config_dir=tmp_path, auto_detect=False)
config_file = tmp_path / "config.json"
assert config_file.exists(), "onboarding must write config.json"
data = json.loads(config_file.read_text())
assert data["embedding_model"] == "embeddinggemma"
assert MempalaceConfig(config_dir=tmp_path).embedding_model == "embeddinggemma"
def test_run_onboarding_persists_minilm_choice(tmp_path):
"""When user opts down to English-only, the choice is still persisted —
we want the config to be explicit, not silently fall back to the hard
default. This way changing the hard default later won't silently shift
these users."""
import json
with (
patch("mempalace.onboarding._ask_mode", return_value="work"),
patch("mempalace.onboarding._ask_embedding_model", return_value="minilm"),
patch("mempalace.onboarding._ask_people", return_value=([], {})),
patch("mempalace.onboarding._ask_projects", return_value=[]),
patch("mempalace.onboarding._ask_wings", return_value=["projects"]),
patch("mempalace.onboarding._yn", return_value=False),
patch("mempalace.onboarding._warn_ambiguous", return_value=[]),
):
run_onboarding(directory=".", config_dir=tmp_path, auto_detect=False)
data = json.loads((tmp_path / "config.json").read_text())
assert data["embedding_model"] == "minilm"
# ── quick_setup writes embedding_model when provided ───────────────────
def test_quick_setup_writes_embedding_model_when_provided(tmp_path):
"""Programmatic setup honors the explicit embedding_model arg."""
import json
quick_setup(
mode="work",
people=[{"name": "Bob", "relationship": "boss", "context": "work"}],
config_dir=tmp_path,
embedding_model="embeddinggemma",
)
data = json.loads((tmp_path / "config.json").read_text())
assert data["embedding_model"] == "embeddinggemma"
def test_quick_setup_leaves_config_alone_when_no_model_provided(tmp_path):
"""Back-compat: callers that don't pass embedding_model don't get a
surprise config.json write."""
quick_setup(
mode="work",
people=[{"name": "Bob", "relationship": "boss", "context": "work"}],
config_dir=tmp_path,
)
assert not (tmp_path / "config.json").exists()
# ── MempalaceConfig.set_embedding_model ────────────────────────────────
def test_set_embedding_model_persists_and_reloads(tmp_path):
"""Setter writes to config.json and a fresh MempalaceConfig reads it back."""
from mempalace.config import MempalaceConfig
MempalaceConfig(config_dir=tmp_path).set_embedding_model("embeddinggemma")
assert MempalaceConfig(config_dir=tmp_path).embedding_model == "embeddinggemma"
MempalaceConfig(config_dir=tmp_path).set_embedding_model("MiniLM")
assert MempalaceConfig(config_dir=tmp_path).embedding_model == "minilm"