feat(ack): refine intake and validation workflow
This commit is contained in:
@@ -185,6 +185,33 @@ class AckDeliveryValidationTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual(validate_delivery.validate_builtin(contract), [])
|
||||
|
||||
def test_default_validation_profile_requires_deploy_and_health_check(self) -> None:
|
||||
contract = valid_contract()
|
||||
contract["defaultProfile"] = "local-validation"
|
||||
contract["profiles"]["local-validation"] = {
|
||||
"stopAt": "validation_ready",
|
||||
"steps": [
|
||||
{"id": "build-local", "action": "build", "artifact": "service-deb"},
|
||||
{
|
||||
"id": "deploy-local",
|
||||
"action": "deploy",
|
||||
"artifact": "service-deb",
|
||||
"environment": "test-server",
|
||||
},
|
||||
{
|
||||
"id": "health-local",
|
||||
"action": "health-check",
|
||||
"environment": "test-server",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
self.assertEqual(validate_delivery.validate_builtin(contract), [])
|
||||
|
||||
contract["profiles"]["local-validation"]["steps"].pop()
|
||||
errors = validate_delivery.validate_builtin(contract)
|
||||
self.assertTrue(any("必须全部完成 health-check" in item for item in errors))
|
||||
|
||||
def test_publish_and_health_check_require_prior_steps(self) -> None:
|
||||
contract = valid_contract()
|
||||
steps = contract["profiles"]["review"]["steps"]
|
||||
|
||||
@@ -239,7 +239,8 @@ tasks: []
|
||||
self.assertIn("审核通过前的唯一协作区", content)
|
||||
self.assertIn("不创建或刷新 `tasks.yaml` 中的 ACK 任务", content)
|
||||
self.assertIn("base:record:write", content)
|
||||
self.assertIn("fixLogic", content)
|
||||
self.assertIn("problemStatement", content)
|
||||
self.assertIn("不在收件箱写修复逻辑", content)
|
||||
self.assertNotIn("--lark-cli", content)
|
||||
self.assertNotIn("--executable", content)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
+62
-1
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
@@ -47,6 +48,66 @@ class AckSkillContentTests(unittest.TestCase):
|
||||
self.assertIn("禁止根据持久化 receipt 自动复用", content)
|
||||
self.assertIn("launcher 身份证明", content)
|
||||
|
||||
def test_worker_reuse_requires_idle_state_and_verified_history_reset(self) -> None:
|
||||
skill = (REPO_ROOT / "skills" / "ack" / "SKILL.md").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
adapter = (
|
||||
REPO_ROOT / "skills" / "ack" / "references" / "orca-adapter.md"
|
||||
).read_text(encoding="utf-8")
|
||||
|
||||
self.assertIn("空闲", skill)
|
||||
self.assertIn("清理历史消息", skill)
|
||||
self.assertIn("无法确认清理成功时创建 fresh worker", skill)
|
||||
self.assertIn("角色、profile、worktree", adapter)
|
||||
self.assertIn("不得复用仍在工作", adapter)
|
||||
self.assertIn("或运行状态不明的 worker", adapter)
|
||||
|
||||
def test_coordinator_reclaims_only_verified_task_terminals_at_run_end(self) -> None:
|
||||
skill = (REPO_ROOT / "skills" / "ack" / "SKILL.md").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
kickoff = (
|
||||
REPO_ROOT / "skills" / "ack" / "references" / "kickoff.md"
|
||||
).read_text(encoding="utf-8")
|
||||
|
||||
self.assertIn("整轮任务完成", skill)
|
||||
self.assertIn("回收所有只属于 `verified` 任务的 worker", skill)
|
||||
self.assertIn("终端,并核对关闭回执", skill)
|
||||
self.assertIn("不设置 TTL", skill)
|
||||
self.assertIn("blocked", kickoff)
|
||||
self.assertIn("failed_retest", kickoff)
|
||||
self.assertIn("leftover", kickoff)
|
||||
|
||||
def test_environment_failures_are_reported_without_consuming_retest_rounds(self) -> None:
|
||||
skill = (REPO_ROOT / "skills" / "ack" / "SKILL.md").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
optimization = (
|
||||
REPO_ROOT / "skills" / "ack" / "references" / "optimization-method.md"
|
||||
).read_text(encoding="utf-8")
|
||||
schema = json.loads(
|
||||
(REPO_ROOT / "skills" / "ack" / "templates" / "tasks.schema.json")
|
||||
.read_text(encoding="utf-8")
|
||||
)
|
||||
|
||||
self.assertIn("环境失败不占复验轮次", skill)
|
||||
self.assertIn("userAction", optimization)
|
||||
self.assertIn("environmentIncidents", schema["definitions"]["task"]["properties"]["dispatch"]["properties"])
|
||||
|
||||
def test_validation_ready_hands_off_a_deployed_test_environment(self) -> None:
|
||||
skill = (REPO_ROOT / "skills" / "ack" / "SKILL.md").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
delivery = (
|
||||
REPO_ROOT / "skills" / "ack" / "references" / "delivery.md"
|
||||
).read_text(encoding="utf-8")
|
||||
|
||||
self.assertIn("不能停在", skill)
|
||||
self.assertIn("`verified` 却声称整轮 ACK 已结束", skill)
|
||||
self.assertIn("validation_ready", delivery)
|
||||
self.assertIn("访问地址和用户下一步", delivery)
|
||||
|
||||
def test_ack_knowledge_resources_and_version_are_present(self) -> None:
|
||||
ack_dir = REPO_ROOT / "skills" / "ack"
|
||||
|
||||
@@ -67,7 +128,7 @@ class AckSkillContentTests(unittest.TestCase):
|
||||
):
|
||||
self.assertTrue((ack_dir / relative_path).is_file(), relative_path)
|
||||
version = (ack_dir / "VERSION").read_text(encoding="utf-8").strip()
|
||||
self.assertEqual(version, "0.14.1")
|
||||
self.assertEqual(version, "0.15.0")
|
||||
self.assertIn(
|
||||
f'ackVersion: "{version}"',
|
||||
(ack_dir / "examples" / "tasks.example.yaml").read_text(encoding="utf-8"),
|
||||
|
||||
@@ -403,6 +403,41 @@ class AckTaskValidationTests(unittest.TestCase):
|
||||
"delivery run 只能引用 verified 任务",
|
||||
)
|
||||
|
||||
def test_validation_ready_delivery_run_does_not_require_pull_request(self) -> None:
|
||||
board = valid_manual_routing_board()
|
||||
board["project"]["deliveryFile"] = "docs/ack/delivery.yaml"
|
||||
board["tasks"][0]["status"] = "verified"
|
||||
board["deliveryRuns"] = [
|
||||
{
|
||||
"id": "DR-local-1",
|
||||
"profile": "local-validation",
|
||||
"taskIds": ["T-1"],
|
||||
"status": "validation_ready",
|
||||
"sourceRevision": "a" * 64,
|
||||
"configRevision": "b" * 64,
|
||||
"pullRequest": None,
|
||||
"artifacts": [
|
||||
{
|
||||
"id": "local-service",
|
||||
"type": "file",
|
||||
"reference": "music-pilot",
|
||||
"digest": "sha256:" + "c" * 64,
|
||||
}
|
||||
],
|
||||
"deployments": [
|
||||
{
|
||||
"environment": "local-8080",
|
||||
"result": "succeeded",
|
||||
"evidence": "HTTP 200 and preflight passed",
|
||||
}
|
||||
],
|
||||
"evidence": ["http://127.0.0.1:8080 ready for user validation"],
|
||||
"updatedAt": "2026-08-03T23:10:00+08:00",
|
||||
}
|
||||
]
|
||||
|
||||
self.assert_board_accepted_in_all_modes(board)
|
||||
|
||||
def test_delivery_runs_and_delivery_file_must_appear_together(self) -> None:
|
||||
board = valid_manual_routing_board()
|
||||
board["deliveryRuns"] = []
|
||||
@@ -1111,6 +1146,62 @@ class AckTaskValidationTests(unittest.TestCase):
|
||||
"dispatch.rounds: round 必须从 1 连续递增且不重复",
|
||||
)
|
||||
|
||||
def test_environment_incidents_do_not_consume_round_budget(self) -> None:
|
||||
board = valid_knowledge_board()
|
||||
board["tasks"][0]["status"] = "fixed_by_dev"
|
||||
board["tasks"][0]["dispatch"] = {
|
||||
"rounds": [],
|
||||
"environmentIncidents": [
|
||||
{
|
||||
"id": f"T-1-ENV-{index}",
|
||||
"attemptId": f"T-1-A{index}",
|
||||
"role": "test",
|
||||
"phase": "browser",
|
||||
"status": "resolved",
|
||||
"summary": "browser runtime was unavailable",
|
||||
"evidence": "browser executable lookup returned no result",
|
||||
"impact": "interactive acceptance signals were not evaluated",
|
||||
"recoveryAction": "launch a network-enabled fresh Test worker",
|
||||
"userAction": "none; Coordinator continues the recovery",
|
||||
"reportedAt": "2026-08-03T20:00:00+08:00",
|
||||
"resolvedAt": "2026-08-03T20:05:00+08:00",
|
||||
}
|
||||
for index in range(1, 5)
|
||||
],
|
||||
}
|
||||
|
||||
self.assert_board_accepted_in_all_modes(board)
|
||||
|
||||
def test_environment_incidents_require_actionable_reporting(self) -> None:
|
||||
board = valid_knowledge_board()
|
||||
board["tasks"][0]["dispatch"] = {
|
||||
"rounds": [],
|
||||
"environmentIncidents": [
|
||||
{
|
||||
"id": "WRONG-ENV-9",
|
||||
"role": "observer",
|
||||
"phase": "unknown",
|
||||
"status": "resolved",
|
||||
"summary": "",
|
||||
"evidence": "",
|
||||
"impact": "",
|
||||
"recoveryAction": "",
|
||||
"userAction": "",
|
||||
"reportedAt": "",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
self.assert_board_rejected_in_all_modes(
|
||||
board,
|
||||
"environmentIncidents[0].id: 应为 T-1-ENV-1",
|
||||
"environmentIncidents[0].role: 必须是 coordinator/developer/test",
|
||||
"environmentIncidents[0].phase: 非法环境阶段",
|
||||
"environmentIncidents[0].summary: 必须是非空字符串",
|
||||
"environmentIncidents[0].userAction: 必须是非空字符串",
|
||||
"environmentIncidents[0]: resolved 必须填写 resolvedAt",
|
||||
)
|
||||
|
||||
def test_leftover_reason_must_be_nonempty_string_in_all_modes(self) -> None:
|
||||
board = {
|
||||
"version": 1,
|
||||
|
||||
@@ -602,6 +602,32 @@ class ReceiptValidationTests(unittest.TestCase):
|
||||
)
|
||||
)
|
||||
|
||||
def test_receipt_accepts_project_and_board_bound_launch_fingerprint(self) -> None:
|
||||
routing = valid_orchestration()
|
||||
receipt = valid_receipt(routing)
|
||||
receipt["projectRoot"] = "/repo/demo"
|
||||
receipt["boardHash"] = worker_profiles.canonical_sha256({"tasks": []})
|
||||
receipt["launchFingerprint"] = worker_profiles.canonical_sha256({
|
||||
"protocolVersion": 1,
|
||||
"backend": "orca",
|
||||
"projectRoot": receipt["projectRoot"],
|
||||
"boardHash": receipt["boardHash"],
|
||||
"profileId": receipt["profileId"],
|
||||
"profileHash": receipt["profileHash"],
|
||||
"createdFor": receipt["createdFor"],
|
||||
"worktree": receipt["worktree"],
|
||||
"requested": receipt["requested"],
|
||||
"slot": receipt["slot"],
|
||||
})
|
||||
receipt["receiptHash"] = worker_profiles.receipt_hash(receipt)
|
||||
|
||||
self.assertEqual(
|
||||
worker_profiles.validate_worker_receipt(
|
||||
receipt, orchestration=routing, task_ids={"TASK-001"},
|
||||
),
|
||||
[],
|
||||
)
|
||||
|
||||
def test_receipt_slot_is_bounded_and_bound_into_launch_fingerprint(self) -> None:
|
||||
routing = valid_orchestration()
|
||||
invalid = valid_receipt(routing)
|
||||
|
||||
Reference in New Issue
Block a user