feat(v2): T05-c 运行补充说明只能形成草稿(AC-26)
运行时的"本次预审补充说明"只影响本次运行(run_instruction 落在运行对象上, 属 T07 的 ScreeningRun 字段)。要变成持久知识只有一条路:显式提议为草稿, 且走正常审核。本批实现卡片侧的那一半。 新增 knowledge_cards.draft_from_run_instruction(),三条约束: 1. 只产草稿——运行中说过的话绝不直接生效(有测试断言无已发布版本产生) 2. **不得携带工具/权限/引擎字段**:allowed_tools / tools / permissions / engine / run_instruction 出现即拒,扩大工具授权必须走独立提议与审核 3. 记录 derived_from_run,便于回溯知识来自哪次运行 另:运行指令不能声明为 history_derived(历史提炼须提供来源事件或证据), hard 门禁对运行衍生的卡片同样生效。 未完成:运行对象上的 run_instruction 字段与"只影响本次运行"的服务端实现属 T07, 本批只交付卡片侧,不宣称 AC-26 已整体闭环。 测试:test_knowledge_cards.py 增至 25 项(新增 5 项)。 全量 315 项 OK(310 → 315);verify.py 10/10 PASS。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -220,5 +220,47 @@ class EffectiveFilterTests(KnowledgeCardTestCase):
|
||||
self.assertIn("不是项目知识卡片", str(ctx.exception))
|
||||
|
||||
|
||||
class RunInstructionScopeTests(KnowledgeCardTestCase):
|
||||
"""AC-26:运行补充说明只影响当前运行;持久化只能形成草稿,且不得扩大工具权限。"""
|
||||
|
||||
def draft_from_run(self, body, actor="demo-manager"):
|
||||
with store.transaction(self.db_path) as db:
|
||||
return cards.draft_from_run_instruction(db, "DEMO-A", "run-1", body, actor)
|
||||
|
||||
def test_instruction_becomes_a_draft_never_a_published_card(self):
|
||||
draft = self.draft_from_run(rule_body(id="corporate.from_run"))
|
||||
self.assertEqual(draft["status"], "draft")
|
||||
with store.transaction(self.db_path) as db:
|
||||
published = [row for row in store.listing(db, "component_version")
|
||||
if row.get("component_id") == "corporate.from_run"]
|
||||
self.assertEqual(published, [], "运行中说过的话不得直接成为生效知识")
|
||||
|
||||
def test_instruction_cannot_widen_tool_permissions(self):
|
||||
for field in ("allowed_tools", "tools", "permissions", "engine", "run_instruction"):
|
||||
with self.subTest(field=field):
|
||||
body = rule_body(id="corporate.tool_grab")
|
||||
body[field] = ["terminal"]
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
self.draft_from_run(body)
|
||||
self.assertIn("不能持久化工具权限", str(ctx.exception))
|
||||
|
||||
def test_instruction_cannot_claim_history_derived_origin(self):
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
self.draft_from_run(rule_body(id="corporate.fake_history", origin="history_derived"))
|
||||
self.assertIn("不能声明为历史提炼", str(ctx.exception))
|
||||
|
||||
def test_draft_records_which_run_it_came_from(self):
|
||||
mock = self.draft_from_run(rule_body(id="corporate.traced"))
|
||||
with store.transaction(self.db_path) as db:
|
||||
row = store.get(db, "component_draft", mock["id"])
|
||||
self.assertEqual(row["derived_from_run"], "run-1")
|
||||
self.assertEqual(row["proposed_via"], "run_instruction")
|
||||
|
||||
def test_hard_gate_still_applies_to_run_derived_cards(self):
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
self.draft_from_run(rule_body(id="corporate.run_hard", effect="hard"))
|
||||
self.assertIn("正式制度或授权依据", str(ctx.exception))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user