test_resolve_personas.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. #!/usr/bin/env python3
  2. # /// script
  3. # requires-python = ">=3.11"
  4. # ///
  5. """Unit tests for resolve_personas.py — pool merge, alias, party resolution."""
  6. import sys
  7. import unittest
  8. from pathlib import Path
  9. sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
  10. import resolve_personas as rp # noqa: E402
  11. AGENTS = {
  12. "bmad-agent-analyst": {"name": "Mary", "icon": "📊", "title": "Analyst"},
  13. "bmad-agent-pm": {"name": "John", "icon": "📋", "title": "PM"},
  14. }
  15. class TestAlias(unittest.TestCase):
  16. def test_strips_known_prefixes(self):
  17. self.assertEqual(rp._alias("bmad-agent-analyst"), "analyst")
  18. self.assertEqual(rp._alias("bmad-foo"), "foo")
  19. def test_passes_through_unprefixed(self):
  20. self.assertEqual(rp._alias("morpheus"), "morpheus")
  21. class TestBuildPool(unittest.TestCase):
  22. def test_installed_become_default_room_indexed_every_way(self):
  23. pool, idx, installed, custom = rp.build_pool(AGENTS, [])
  24. self.assertEqual(installed, ["bmad-agent-analyst", "bmad-agent-pm"])
  25. self.assertEqual(custom, [])
  26. self.assertEqual(idx["analyst"], "bmad-agent-analyst") # alias
  27. self.assertEqual(idx["mary"], "bmad-agent-analyst") # name (ci)
  28. self.assertEqual(pool["bmad-agent-analyst"]["source"], "installed")
  29. def test_pure_custom_member_stays_out_of_default_room(self):
  30. pool, _, installed, custom = rp.build_pool(
  31. AGENTS, [{"code": "morpheus", "name": "Morpheus", "persona": "riddles"}])
  32. self.assertEqual(custom, ["morpheus"])
  33. self.assertNotIn("morpheus", installed)
  34. self.assertEqual(pool["morpheus"]["persona"], "riddles")
  35. def test_custom_override_lands_on_installed_slot_not_a_new_face(self):
  36. pool, _, installed, custom = rp.build_pool(
  37. AGENTS, [{"code": "analyst", "name": "Mary-Custom", "persona": "p"}])
  38. self.assertNotIn("analyst", pool)
  39. self.assertEqual(custom, []) # an override is not a new face
  40. self.assertEqual(pool["bmad-agent-analyst"]["source"], "custom")
  41. self.assertEqual(pool["bmad-agent-analyst"]["name"], "Mary-Custom")
  42. def test_member_without_code_skipped(self):
  43. pool, _, _, custom = rp.build_pool(AGENTS, [{"name": "Nameless"}])
  44. self.assertEqual(custom, [])
  45. self.assertEqual(set(pool), {"bmad-agent-analyst", "bmad-agent-pm"})
  46. def test_custom_rename_does_not_hijack_another_agents_name(self):
  47. # Override the analyst slot, renaming it to "John" — the PM's name.
  48. # The PM's name lookup must survive (last-writer-wins would corrupt it).
  49. _, idx, _, _ = rp.build_pool(AGENTS, [{"code": "analyst", "name": "John"}])
  50. self.assertEqual(idx["john"], "bmad-agent-pm")
  51. def test_brief_carries_model_and_capabilities(self):
  52. pool, _, _, _ = rp.build_pool(
  53. AGENTS, [{"code": "neo", "name": "Neo", "model": "opus", "capabilities": ["x"]}])
  54. brief = rp._brief(pool["neo"])
  55. self.assertEqual(brief["model"], "opus")
  56. self.assertEqual(brief["capabilities"], ["x"])
  57. def test_non_list_party_members_is_safe(self):
  58. pool, _, installed, custom = rp.build_pool(AGENTS, "not-a-list")
  59. self.assertEqual(custom, [])
  60. self.assertEqual(set(pool), {"bmad-agent-analyst", "bmad-agent-pm"})
  61. class TestResolveParties(unittest.TestCase):
  62. def setUp(self):
  63. self.pool, self.idx, _, _ = rp.build_pool(
  64. AGENTS, [{"code": "shark", "name": "Marcus", "title": "CFO"}])
  65. def test_resolves_members_by_alias_and_custom_code(self):
  66. parties = rp.resolve_parties(
  67. [{"id": "tank", "name": "Tank", "scene": "hostile",
  68. "members": ["shark", "analyst"]}], self.pool, self.idx)
  69. self.assertEqual(len(parties), 1)
  70. self.assertEqual([m["name"] for m in parties[0]["members"]], ["Marcus", "Mary"])
  71. self.assertEqual(parties[0]["scene"], "hostile")
  72. def test_unknown_member_dropped_silently(self):
  73. parties = rp.resolve_parties(
  74. [{"id": "g", "members": ["analyst", "ghost"]}], self.pool, self.idx)
  75. self.assertEqual([m["name"] for m in parties[0]["members"]], ["Mary"])
  76. def test_member_resolution_is_case_insensitive(self):
  77. # A TOML author naturally writes "Analyst"/"Shark"; the filter accepts
  78. # them via the lowercase index, so resolution must too (no KeyError).
  79. parties = rp.resolve_parties(
  80. [{"id": "g", "members": ["Analyst", "Shark"]}], self.pool, self.idx)
  81. self.assertEqual([m["name"] for m in parties[0]["members"]], ["Mary", "Marcus"])
  82. def test_non_string_member_does_not_crash(self):
  83. # Malformed members (int, list) must drop silently, never raise.
  84. parties = rp.resolve_parties(
  85. [{"id": "g", "members": [123, ["x"], "analyst"]}], self.pool, self.idx)
  86. self.assertEqual([m["name"] for m in parties[0]["members"]], ["Mary"])
  87. def test_open_cast_group_flagged(self):
  88. parties = rp.resolve_parties(
  89. [{"id": "rebels", "name": "Rebels", "scene": "the Ghost"}], self.pool, self.idx)
  90. self.assertTrue(parties[0]["open_cast"])
  91. self.assertEqual(parties[0]["members"], [])
  92. def test_group_without_id_skipped(self):
  93. self.assertEqual(rp.resolve_parties([{"name": "no id"}], self.pool, self.idx), [])
  94. class TestOverrideMergeFallback(unittest.TestCase):
  95. """When party-mode isn't installed, user override TOMLs are read directly."""
  96. def test_arrays_append_scalars_override(self):
  97. import tempfile, os
  98. with tempfile.TemporaryDirectory() as d:
  99. custom = Path(d) / "_bmad" / "custom"
  100. custom.mkdir(parents=True)
  101. (custom / "bmad-party-mode.toml").write_text(
  102. '[workflow]\ndefault_party = "a"\n'
  103. '[[workflow.party_members]]\ncode = "x"\nname = "X"\n')
  104. (custom / "bmad-party-mode.user.toml").write_text(
  105. '[workflow]\ndefault_party = "b"\n'
  106. '[[workflow.party_members]]\ncode = "y"\nname = "Y"\n')
  107. wf = rp.load_party_overrides(Path(d))
  108. self.assertEqual(wf["default_party"], "b") # personal wins
  109. self.assertEqual([m["code"] for m in wf["party_members"]], ["x", "y"]) # appended
  110. if __name__ == "__main__":
  111. unittest.main()