"""U-004 tests: migrated agent personas compile into valid-shaped subagent definitions and deploy correctly. No real `claude` calls here -- see src/test_agent_e2e.py (RUN_REAL_AGENT_E2E=1) for the actual A-2 validation. Run: python3 -m unittest src.test_agent_profile -v """ import json import os import tempfile import unittest from src import agent_profile, schema class RenderAgentDefinitionTest(unittest.TestCase): def test_all_three_org_nodes_render(self): for org_node in agent_profile.ORG_NODES: with self.subTest(org_node=org_node): content = agent_profile.render_agent_definition(org_node) self.assertTrue(content.startswith("---\n")) self.assertIn("name: " + org_node, content) self.assertIn("memory: project", content) self.assertIn("model:", content) self.assertIn("skills:", content) self.assertIn("tools:", content) def test_validator_has_disallowed_tools_from_agents_md_contract(self): content = agent_profile.render_agent_definition("validator") self.assertIn("disallowedTools:", content) self.assertIn("- Edit", content) self.assertIn("- Write", content) def test_validator_references_unknown_scope_defect_sweep_skill(self): # B (2026-08-06): unknown-scope-defect-sweep must actually be wired # into the deployed skills: frontmatter, not just exist on disk. content = agent_profile.render_agent_definition("validator") self.assertIn("- unknown-scope-defect-sweep", content) def test_validator_disallowed_tools_has_no_scoped_bash_pattern(self): # 2026-07-31: confirmed for real that any "Bash(...)" entry in a # subagent's disallowedTools silently disables Bash *entirely*, not # just that pattern -- must never reappear in the actual frontmatter # (see agent_profile.py's FRONTMATTER["validator"] comment for the # full story; the migrated AGENTS.md body is allowed to *mention* # "Bash(...)" in prose explaining why it's no longer used there). content = agent_profile.render_agent_definition("validator") frontmatter = content.split("---", 2)[1] self.assertNotIn("Bash(", frontmatter) self.assertIn("- Bash", frontmatter) def test_planner_and_coder_have_no_disallowed_tools(self): for org_node in ("planner", "coder"): content = agent_profile.render_agent_definition(org_node) self.assertNotIn("disallowedTools:", content) def test_validator_body_includes_both_soul_and_agents_md(self): content = agent_profile.render_agent_definition("validator") self.assertIn("QA Agent", content) # from SOUL.md self.assertIn("AGENTS.md — QA Profile 运行契约", content) # from AGENTS.md def test_no_leaked_feishu_secret_or_personal_path(self): for org_node in agent_profile.ORG_NODES: content = agent_profile.render_agent_definition(org_node) # real value intentionally not reproduced here -- see # .claude/agent-memory/coder/feedback_hermes_skill_migration_secret_redaction.md self.assertNotIn("", content) self.assertNotIn("/Users/yibo/Projects/lemonAI/graphagent", content) # the original told Coder to itself run this; the migrated version is # allowed to *mention* the flag only as a prohibition, not an instruction self.assertNotIn("优先使用 `claude --dangerously-skip-permissions`", content) self.assertNotIn("优先使用 claude --dangerously-skip-permissions", content) class NewRoleWithoutCodeChangeTest(unittest.TestCase): """H (2026-08-06), requirements doc acceptance standard 1: a brand-new role, defined purely by adding a roles//role.json, is recognized by these UNMODIFIED functions (render_agent_definition/ deploy_agent_definitions/record_agent_profiles) via the roles_dir override -- no agent_profile.py/schema.py edit involved. Points identity.soul_md_path at a real, already-existing repo file (agents/planner/SOUL.md) rather than fabricating a new one -- this standard is about the roles/ discovery + rendering mechanism, not about proving a new identity file can be written (unrelated concern).""" def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.addCleanup(self.tmp.cleanup) role_dir = os.path.join(self.tmp.name, "researcher") os.makedirs(role_dir) role = { "role_id": "researcher", "description": "A brand-new role added with zero Python/schema changes.", "migrated_from_profile": "planning", "identity": {"soul_md_path": "agents/planner/SOUL.md", "agents_md_path": None}, "context": {"memory": "project"}, "model": "claude-sonnet-5", "skills": ["planning-requirement-expansion"], "applications": {"mcp_servers": []}, "workflows": {"dynamic_planning": "generic"}, "permissions": {"tools": ["Read", "Grep"], "disallowed_tools": [], "notes": []}, "acceptance_criteria": {"ref": "docs/sdd/06-验收与测试锚点.md"}, } with open(os.path.join(role_dir, "role.json"), "w", encoding="utf-8") as f: json.dump(role, f) def test_discovered_alongside_the_real_three_roles(self): discovered = agent_profile._discover_role_ids(self.tmp.name) self.assertIn("researcher", discovered) def test_render_agent_definition_recognizes_it(self): content = agent_profile.render_agent_definition("researcher", roles_dir=self.tmp.name) self.assertIn("name: researcher", content) self.assertIn("- planning-requirement-expansion", content) def test_deploy_agent_definitions_writes_it_out(self): with tempfile.TemporaryDirectory() as target: agent_profile.deploy_agent_definitions(target, roles_dir=self.tmp.name) path = os.path.join(target, ".claude", "agents", "researcher.md") self.assertTrue(os.path.isfile(path)) def test_record_agent_profiles_writes_a_row(self): conn = schema.connect(":memory:") try: schema.create_schema(conn) agent_profile.record_agent_profiles(conn, "deadbeef", roles_dir=self.tmp.name) row = conn.execute( "SELECT migrated_from_profile FROM agent_profile WHERE org_node = 'researcher'" ).fetchone() self.assertEqual(row, ("planning",)) finally: conn.close() def test_unknown_role_still_rejected(self): with self.assertRaises(ValueError): agent_profile.render_agent_definition("no-such-role", roles_dir=self.tmp.name) class DeployAgentDefinitionsTest(unittest.TestCase): def test_writes_three_files_into_target_claude_agents(self): with tempfile.TemporaryDirectory() as tmp: agent_profile.deploy_agent_definitions(tmp) out_dir = os.path.join(tmp, ".claude", "agents") for org_node in agent_profile.ORG_NODES: path = os.path.join(out_dir, org_node + ".md") self.assertTrue(os.path.isfile(path)) with open(path, encoding="utf-8") as f: self.assertTrue(f.read().startswith("---\n")) class RecordAgentProfilesTest(unittest.TestCase): def setUp(self): self.conn = schema.connect(":memory:") schema.create_schema(self.conn) def tearDown(self): self.conn.close() def test_inserts_one_row_per_org_node(self): agent_profile.record_agent_profiles(self.conn, "deadbeef") rows = self.conn.execute( "SELECT org_node, migrated_from_profile, current_git_commit_sha FROM agent_profile ORDER BY org_node" ).fetchall() self.assertEqual(len(rows), 4) by_node = {r[0]: r for r in rows} self.assertEqual(by_node["planner"][1], "planning") self.assertEqual(by_node["coder"][1], "code") self.assertEqual(by_node["validator"][1], "qa") # planner_reviewer has no legacy Hermes profile to migrate from # (role.json's migrated_from_profile: null) -- see # schema.migrate_relax_agent_profile_migrated_from_profile. self.assertIsNone(by_node["planner_reviewer"][1]) for row in rows: self.assertEqual(row[2], "deadbeef") def test_validator_has_agents_file_path_others_do_not(self): agent_profile.record_agent_profiles(self.conn, "deadbeef") rows = dict( self.conn.execute("SELECT org_node, agents_file_path FROM agent_profile").fetchall() ) self.assertIsNone(rows["planner"]) self.assertIsNone(rows["coder"]) self.assertIsNotNone(rows["validator"]) def test_rerun_upserts_instead_of_duplicating(self): agent_profile.record_agent_profiles(self.conn, "sha-1") agent_profile.record_agent_profiles(self.conn, "sha-2") count = self.conn.execute("SELECT COUNT(*) FROM agent_profile").fetchone()[0] self.assertEqual(count, 4) sha = self.conn.execute( "SELECT current_git_commit_sha FROM agent_profile WHERE org_node = 'planner'" ).fetchone()[0] self.assertEqual(sha, "sha-2") if __name__ == "__main__": unittest.main()