test_agent_profile.py
9.27 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
"""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("<FEISHU_BASE_TOKEN_REDACTED_PLACEHOLDER>", 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_id>/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()