Some checks failed
CI / check (push) Has been cancelled
Capture uncommitted solver robustness work (regularization, domain errors, linear solver lifecycle, tube DP/MSH), web workbench updates, and synced BMAD skills across IDE agent folders before starting BPHX pressure-drop. Co-authored-by: Cursor <cursoragent@cursor.com>
139 lines
6.2 KiB
Python
139 lines
6.2 KiB
Python
#!/usr/bin/env python3
|
|
# /// script
|
|
# requires-python = ">=3.11"
|
|
# ///
|
|
"""Unit tests for resolve_personas.py — pool merge, alias, party resolution."""
|
|
|
|
import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
import resolve_personas as rp # noqa: E402
|
|
|
|
AGENTS = {
|
|
"bmad-agent-analyst": {"name": "Mary", "icon": "📊", "title": "Analyst"},
|
|
"bmad-agent-pm": {"name": "John", "icon": "📋", "title": "PM"},
|
|
}
|
|
|
|
|
|
class TestAlias(unittest.TestCase):
|
|
def test_strips_known_prefixes(self):
|
|
self.assertEqual(rp._alias("bmad-agent-analyst"), "analyst")
|
|
self.assertEqual(rp._alias("bmad-foo"), "foo")
|
|
|
|
def test_passes_through_unprefixed(self):
|
|
self.assertEqual(rp._alias("morpheus"), "morpheus")
|
|
|
|
|
|
class TestBuildPool(unittest.TestCase):
|
|
def test_installed_become_default_room_indexed_every_way(self):
|
|
pool, idx, installed, custom = rp.build_pool(AGENTS, [])
|
|
self.assertEqual(installed, ["bmad-agent-analyst", "bmad-agent-pm"])
|
|
self.assertEqual(custom, [])
|
|
self.assertEqual(idx["analyst"], "bmad-agent-analyst") # alias
|
|
self.assertEqual(idx["mary"], "bmad-agent-analyst") # name (ci)
|
|
self.assertEqual(pool["bmad-agent-analyst"]["source"], "installed")
|
|
|
|
def test_pure_custom_member_stays_out_of_default_room(self):
|
|
pool, _, installed, custom = rp.build_pool(
|
|
AGENTS, [{"code": "morpheus", "name": "Morpheus", "persona": "riddles"}])
|
|
self.assertEqual(custom, ["morpheus"])
|
|
self.assertNotIn("morpheus", installed)
|
|
self.assertEqual(pool["morpheus"]["persona"], "riddles")
|
|
|
|
def test_custom_override_lands_on_installed_slot_not_a_new_face(self):
|
|
pool, _, installed, custom = rp.build_pool(
|
|
AGENTS, [{"code": "analyst", "name": "Mary-Custom", "persona": "p"}])
|
|
self.assertNotIn("analyst", pool)
|
|
self.assertEqual(custom, []) # an override is not a new face
|
|
self.assertEqual(pool["bmad-agent-analyst"]["source"], "custom")
|
|
self.assertEqual(pool["bmad-agent-analyst"]["name"], "Mary-Custom")
|
|
|
|
def test_member_without_code_skipped(self):
|
|
pool, _, _, custom = rp.build_pool(AGENTS, [{"name": "Nameless"}])
|
|
self.assertEqual(custom, [])
|
|
self.assertEqual(set(pool), {"bmad-agent-analyst", "bmad-agent-pm"})
|
|
|
|
def test_custom_rename_does_not_hijack_another_agents_name(self):
|
|
# Override the analyst slot, renaming it to "John" — the PM's name.
|
|
# The PM's name lookup must survive (last-writer-wins would corrupt it).
|
|
_, idx, _, _ = rp.build_pool(AGENTS, [{"code": "analyst", "name": "John"}])
|
|
self.assertEqual(idx["john"], "bmad-agent-pm")
|
|
|
|
def test_brief_carries_model_and_capabilities(self):
|
|
pool, _, _, _ = rp.build_pool(
|
|
AGENTS, [{"code": "neo", "name": "Neo", "model": "opus", "capabilities": ["x"]}])
|
|
brief = rp._brief(pool["neo"])
|
|
self.assertEqual(brief["model"], "opus")
|
|
self.assertEqual(brief["capabilities"], ["x"])
|
|
|
|
def test_non_list_party_members_is_safe(self):
|
|
pool, _, installed, custom = rp.build_pool(AGENTS, "not-a-list")
|
|
self.assertEqual(custom, [])
|
|
self.assertEqual(set(pool), {"bmad-agent-analyst", "bmad-agent-pm"})
|
|
|
|
|
|
class TestResolveParties(unittest.TestCase):
|
|
def setUp(self):
|
|
self.pool, self.idx, _, _ = rp.build_pool(
|
|
AGENTS, [{"code": "shark", "name": "Marcus", "title": "CFO"}])
|
|
|
|
def test_resolves_members_by_alias_and_custom_code(self):
|
|
parties = rp.resolve_parties(
|
|
[{"id": "tank", "name": "Tank", "scene": "hostile",
|
|
"members": ["shark", "analyst"]}], self.pool, self.idx)
|
|
self.assertEqual(len(parties), 1)
|
|
self.assertEqual([m["name"] for m in parties[0]["members"]], ["Marcus", "Mary"])
|
|
self.assertEqual(parties[0]["scene"], "hostile")
|
|
|
|
def test_unknown_member_dropped_silently(self):
|
|
parties = rp.resolve_parties(
|
|
[{"id": "g", "members": ["analyst", "ghost"]}], self.pool, self.idx)
|
|
self.assertEqual([m["name"] for m in parties[0]["members"]], ["Mary"])
|
|
|
|
def test_member_resolution_is_case_insensitive(self):
|
|
# A TOML author naturally writes "Analyst"/"Shark"; the filter accepts
|
|
# them via the lowercase index, so resolution must too (no KeyError).
|
|
parties = rp.resolve_parties(
|
|
[{"id": "g", "members": ["Analyst", "Shark"]}], self.pool, self.idx)
|
|
self.assertEqual([m["name"] for m in parties[0]["members"]], ["Mary", "Marcus"])
|
|
|
|
def test_non_string_member_does_not_crash(self):
|
|
# Malformed members (int, list) must drop silently, never raise.
|
|
parties = rp.resolve_parties(
|
|
[{"id": "g", "members": [123, ["x"], "analyst"]}], self.pool, self.idx)
|
|
self.assertEqual([m["name"] for m in parties[0]["members"]], ["Mary"])
|
|
|
|
def test_open_cast_group_flagged(self):
|
|
parties = rp.resolve_parties(
|
|
[{"id": "rebels", "name": "Rebels", "scene": "the Ghost"}], self.pool, self.idx)
|
|
self.assertTrue(parties[0]["open_cast"])
|
|
self.assertEqual(parties[0]["members"], [])
|
|
|
|
def test_group_without_id_skipped(self):
|
|
self.assertEqual(rp.resolve_parties([{"name": "no id"}], self.pool, self.idx), [])
|
|
|
|
|
|
class TestOverrideMergeFallback(unittest.TestCase):
|
|
"""When party-mode isn't installed, user override TOMLs are read directly."""
|
|
|
|
def test_arrays_append_scalars_override(self):
|
|
import tempfile, os
|
|
with tempfile.TemporaryDirectory() as d:
|
|
custom = Path(d) / "_bmad" / "custom"
|
|
custom.mkdir(parents=True)
|
|
(custom / "bmad-party-mode.toml").write_text(
|
|
'[workflow]\ndefault_party = "a"\n'
|
|
'[[workflow.party_members]]\ncode = "x"\nname = "X"\n')
|
|
(custom / "bmad-party-mode.user.toml").write_text(
|
|
'[workflow]\ndefault_party = "b"\n'
|
|
'[[workflow.party_members]]\ncode = "y"\nname = "Y"\n')
|
|
wf = rp.load_party_overrides(Path(d))
|
|
self.assertEqual(wf["default_party"], "b") # personal wins
|
|
self.assertEqual([m["code"] for m in wf["party_members"]], ["x", "y"]) # appended
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|