| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138 |
- #!/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()
|