feat(ack): refine intake and validation workflow

This commit is contained in:
2026-08-04 10:52:37 +08:00
parent b9c82b5520
commit 5018a1801d
32 changed files with 1602 additions and 302 deletions
+250 -1
View File
@@ -38,8 +38,95 @@ project:
tasks: []
"""
CLARIFIED_BOARD = """version: 1
project:
name: demo
bugIntake:
provider: feishu-base
workflow: clarified-writeback-v1
profile: tenant-b
baseToken: bascnDemo
tableId: tblDemo
viewId: vewReady
fields:
title: 标题
details: 详细描述
problemStatement: 问题说明
expectedOutcome: 期望效果
acceptance: 验收标准
intakeStatus: 处理状态
ackTaskId: ACK任务ID
attachments: 附件
updatedAt: 更新时间
tasks: []
"""
class FeishuBugIntakeUnitTests(unittest.TestCase):
def test_clarified_workflow_normalizes_only_source_and_clarification_fields(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
board_path = Path(temp_dir) / "tasks.yaml"
board_path.write_text(CLARIFIED_BOARD, encoding="utf-8")
config = feishu_bug_intake.config_from_board(
feishu_bug_intake.load_board(board_path)
)
row = [
"Bug", "用户描述", "问题说明", "期望效果", "1. 可观察结果",
"待审核", "", [], "2026-08-03T12:00:00Z",
]
with mock.patch.object(feishu_bug_intake, "profile_check"), mock.patch.object(
feishu_bug_intake, "fetch_pages", return_value=[("recA", row)]
):
record = feishu_bug_intake.fetch(config, None)["records"][0]
self.assertEqual(record["details"], "用户描述")
self.assertEqual(record["expectedOutcome"], "期望效果")
self.assertNotIn("fixLogic", record)
self.assertNotIn("priority", record)
def test_clarified_draft_contract_rejects_fix_logic(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
path = Path(temp_dir) / "draft.json"
path.write_text(json.dumps({
"problemStatement": "问题说清楚",
"expectedOutcome": "期望说清楚",
"acceptance": ["结果可从界面观察"],
}), encoding="utf-8")
draft = feishu_bug_intake.load_draft(path, "clarified-writeback-v1")
self.assertEqual(set(draft), {"problemStatement", "expectedOutcome", "acceptance"})
path.write_text(json.dumps({**draft, "fixLogic": "不应出现"}), encoding="utf-8")
with self.assertRaisesRegex(feishu_bug_intake.IntakeError, "exactly"):
feishu_bug_intake.load_draft(path, "clarified-writeback-v1")
def test_schema_plan_preserves_legacy_fields_and_adds_new_contract(self) -> None:
config = {
"workflow": "clarified-writeback-v1",
"profile": "tenant-b",
"baseToken": "bascnDemo",
"tableId": "tblDemo",
"viewId": "vewReady",
}
fields = [
{"id": "a", "name": "标题", "type": "text"},
{"id": "b", "name": "详细描述", "type": "text"},
{"id": "c", "name": "附件", "type": "attachment"},
{"id": "d", "name": "验收标准", "type": "text"},
{"id": "e", "name": "更新时间", "type": "updated_at"},
{"id": "f", "name": "期望结果", "type": "text"},
]
with mock.patch.object(feishu_bug_intake, "field_list", return_value=fields):
plan = feishu_bug_intake.schema_plan(config)
self.assertEqual(plan["missingFields"], ["问题说明", "期望效果", "处理状态", "ACK任务ID"])
self.assertIn("期望结果", plan["legacyFieldsPreserved"])
self.assertEqual(plan["typeConflicts"], [])
self.assertEqual(plan["target"]["tableId"], "tblDemo")
changed_target = {**config, "tableId": "tblOther"}
self.assertNotEqual(
feishu_bug_intake.schema_fingerprint(config, fields),
feishu_bug_intake.schema_fingerprint(changed_target, fields),
)
with self.assertRaisesRegex(feishu_bug_intake.IntakeError, "requires clarified"):
feishu_bug_intake.schema_plan({**config, "workflow": "read-only-v1"})
def make_fake_cli(self, root: Path) -> tuple[Path, Path]:
log_path = root / "calls.jsonl"
fake = root / "lark-cli"
@@ -73,9 +160,12 @@ class FeishuBugIntakeUnitTests(unittest.TestCase):
return code, stdout.getvalue(), stderr.getvalue()
def test_reader_exposes_check_and_fetch_commands(self) -> None:
with self.assertRaises(SystemExit) as exited, contextlib.redirect_stdout(io.StringIO()):
output = io.StringIO()
with self.assertRaises(SystemExit) as exited, contextlib.redirect_stdout(output):
feishu_bug_intake.main(["--help"])
self.assertEqual(exited.exception.code, 0)
self.assertIn("mark-imported", output.getvalue())
self.assertNotIn("confirm", output.getvalue())
def test_fetch_uses_mocked_trusted_executable_and_official_wire_shapes(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
@@ -214,6 +304,165 @@ class FeishuBugIntakeUnitTests(unittest.TestCase):
}
self.assertEqual(feishu_bug_intake.matrix_from_response(response, fields), (["recA"], [["Bug"]]))
def test_matrix_reorders_same_field_projection(self) -> None:
response = {
"ok": True,
"data": {
"fields": ["fldExpected", "fldTitle"],
"record_id_list": ["recA"],
"data": [["expected", "Bug"]],
},
}
self.assertEqual(
feishu_bug_intake.matrix_from_response(
response,
["fldTitle", "fldExpected"],
),
(["recA"], [["Bug", "expected"]]),
)
def test_matrix_rejects_different_field_projection_with_diagnostics(self) -> None:
response = {
"ok": True,
"data": {
"fields": ["fldActual"],
"record_id_list": ["recA"],
"data": [["actual"]],
},
}
with self.assertRaisesRegex(
feishu_bug_intake.IntakeError,
"expected=.*fldTitle.*actual=.*fldActual",
):
feishu_bug_intake.matrix_from_response(response, ["fldTitle"])
def test_missing_priority_mapping_is_normalized_as_enrichment(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
board_path = Path(temp_dir) / "tasks.yaml"
board_path.write_text(
BOARD.replace(" priority: fldPriority\n", ""),
encoding="utf-8",
)
config = feishu_bug_intake.config_from_board(
feishu_bug_intake.load_board(board_path)
)
row = [
"Bug", "actual", "expected", "steps", "acceptance", [],
"2026-08-01T12:00:00Z",
]
with mock.patch.object(feishu_bug_intake, "profile_check"), mock.patch.object(
feishu_bug_intake, "fetch_pages", return_value=[("recA", row)]
):
payload = feishu_bug_intake.fetch(config, None)
self.assertEqual(payload["records"][0]["priority"], "")
self.assertIn("priority", payload["records"][0]["enrichmentRequired"])
def test_reviewed_workflow_requires_priority_mapping(self) -> None:
board = feishu_bug_intake.load_yaml_subset(
BOARD.replace(
" profile: tenant-b\n",
" workflow: reviewed-writeback-v1\n profile: tenant-b\n",
).replace(
" acceptance: fldAcceptance\n",
" acceptance: fldAcceptance\n fixLogic: fldFixLogic\n",
).replace(" priority: fldPriority\n", "")
)
with self.assertRaisesRegex(feishu_bug_intake.IntakeError, "fixLogic and priority"):
feishu_bug_intake.config_from_board(board)
def test_fully_blank_clarified_row_is_skipped_without_key_error(self) -> None:
config = feishu_bug_intake.config_from_board(
feishu_bug_intake.load_yaml_subset(CLARIFIED_BOARD)
)
row = ["", "", "", "", "", "", "", [], ""]
with mock.patch.object(feishu_bug_intake, "profile_check"), mock.patch.object(
feishu_bug_intake, "fetch_pages", return_value=[("recA", row)]
):
payload = feishu_bug_intake.fetch(config, None)
self.assertEqual(payload["records"], [])
self.assertEqual(payload["warnings"][0]["code"], "blank_record_skipped")
def test_mark_imported_binds_confirmed_record_to_existing_task(self) -> None:
source_ref = "feishu-base:sha256:" + "a" * 64
revision = "sha256:" + "b" * 64
task = {
"id": "BUG-001",
"title": "Bug",
"description": "问题说明",
"actual": "用户描述",
"expected": "期望效果",
"acceptanceCriteria": ["结果可观察"],
"source": {
"kind": "feishu-base",
"workflow": "clarified-writeback-v1",
"ref": source_ref,
"recordId": "recA",
"updatedAt": "2026-08-04T10:00:00Z",
"approvedRevision": revision,
},
}
task["source"]["approvedPayloadHash"] = feishu_bug_intake.approval_payload_hash(task)
board = {"tasks": [task]}
config = feishu_bug_intake.config_from_board(
feishu_bug_intake.load_yaml_subset(CLARIFIED_BOARD)
)
confirmed = {
"recordId": "recA", "sourceRef": source_ref,
"draftRevision": revision, "intakeStatus": "已确认", "ackTaskId": "",
"title": "Bug", "details": "用户描述",
"problemStatement": "问题说明", "expectedOutcome": "期望效果",
"acceptance": "1. 结果可观察", "updatedAt": "2026-08-04T10:00:00Z",
}
imported = {
**confirmed, "intakeStatus": "已导入", "ackTaskId": "BUG-001",
}
with mock.patch.object(
feishu_bug_intake, "review_record", return_value=confirmed,
), mock.patch.object(
feishu_bug_intake, "validate_task_board", return_value=[],
), mock.patch.object(feishu_bug_intake, "profile_check"), mock.patch.object(
feishu_bug_intake, "run_cli", return_value={"ok": True},
) as run_cli, mock.patch.object(
feishu_bug_intake, "fetch", return_value={"records": [imported]},
):
result = feishu_bug_intake.mark_imported(
board, config, "recA", source_ref, revision, "BUG-001",
)
self.assertTrue(result["ok"])
patch = json.loads(run_cli.call_args.args[0][run_cli.call_args.args[0].index("--json") + 1])
self.assertEqual(patch, {"处理状态": "已导入", "ACK任务ID": "BUG-001"})
tampered = json.loads(json.dumps(board, ensure_ascii=False))
tampered["tasks"][0]["description"] = "未批准的改写"
tampered["tasks"][0]["source"]["approvedPayloadHash"] = (
feishu_bug_intake.approval_payload_hash(tampered["tasks"][0])
)
with mock.patch.object(
feishu_bug_intake, "review_record", return_value=confirmed,
), mock.patch.object(
feishu_bug_intake, "validate_task_board", return_value=[],
), mock.patch.object(feishu_bug_intake, "run_cli") as blocked_write:
with self.assertRaisesRegex(
feishu_bug_intake.IntakeError, "does not match the approved",
):
feishu_bug_intake.mark_imported(
tampered, config, "recA", source_ref, revision, "BUG-001",
)
blocked_write.assert_not_called()
with mock.patch.object(
feishu_bug_intake, "validate_task_board", return_value=["invalid"],
), mock.patch.object(feishu_bug_intake, "review_record") as blocked_read:
with self.assertRaisesRegex(feishu_bug_intake.IntakeError, "board is invalid"):
feishu_bug_intake.mark_imported(
board, config, "recA", source_ref, revision, "BUG-001",
)
blocked_read.assert_not_called()
def test_optional_fix_logic_field_is_normalized_for_preapproval_review(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
board_path = Path(temp_dir) / "tasks.yaml"