Files
Wyndham-RSVN-0918/scripts/tests/test_booking_desk_event_skill_package.py
T
鲨鱼辣椒 f4aa01e1f9
verify / booking-verify (push) Has been cancelled
完善Proposal配额处理、失败收尾及工作台历史功能
补齐C1正文资料交付和C2配额分类规则,完善Proposal人工任务、固定图片过滤及邮件附件展示。
修正失败终态与恢复上限,增加关闭状态的通知组件,永久保留处理历史并完善筛选分页。
同步相关页面修复、迁移、测试和项目记录。

验证:后台复用同源码clean verify结果1155通过/10条件跳过;前端262项及生产构建通过;敏感资料和提交路径检查通过。
2026-09-10 17:01:39 +08:00

206 lines
11 KiB
Python

"""Determinism and Main-Prompt separation tests for the Booking Skill packager."""
from __future__ import annotations
import importlib.util
import json
import tempfile
import unittest
import zipfile
from pathlib import Path
REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
PACKAGER_PATH = REPOSITORY_ROOT / "scripts" / "package_booking_desk_event_skill.py"
def load_packager():
spec = importlib.util.spec_from_file_location("booking_skill_packager", PACKAGER_PATH)
if spec is None or spec.loader is None:
raise RuntimeError("Booking Skill packager module cannot be loaded")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
class BookingDeskEventSkillPackageTest(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.packager = load_packager()
def test_archive_manifest_are_deterministic_hold_and_prompt_free(self) -> None:
with tempfile.TemporaryDirectory(prefix="booking-skill-package-test-") as temporary_directory:
root = Path(temporary_directory)
source = self.create_source(root / "source")
first = root / "first" / "booking-desk-event.skill"
second = root / "second" / "booking-desk-event.skill"
first_members = self.packager.write_archive(source, first)
second_members = self.packager.write_archive(source, second)
self.assertEqual(first.read_bytes(), second.read_bytes())
self.assertEqual(first_members, second_members)
manifest = self.packager.build_manifest(source, first, first_members)
self.assertEqual("booking-agent-skill-v3-rc4-object-action", manifest["release"])
self.assertEqual("HOLD", manifest["publication_status"])
self.assertEqual("INTERNAL_ONLY", manifest["distribution"])
self.assertEqual("zh-CN", manifest["instruction_language"])
self.assertEqual("Booking Agent Skill V3", manifest["skill_release_version"])
self.assertEqual("booking-business-agent-compact-input-v2", manifest["input_contract"])
self.assertEqual("booking-business-agent-compact-decision-v2", manifest["output_contract"])
self.assertFalse(manifest["main_prompt_packaged"])
self.assertEqual(
self.packager.sha256(source / "prompts/main-prompt.md"),
manifest["main_prompt_sha256"],
)
self.assertEqual(49, manifest["scenario_count"])
self.assertFalse(any("prompts" in member for member in first_members))
with zipfile.ZipFile(first) as archive:
self.assertNotIn("booking-desk-event/prompts/main-prompt.md", archive.namelist())
def test_manifest_check_is_read_only_and_rejects_stale_bytes(self) -> None:
with tempfile.TemporaryDirectory(prefix="booking-skill-manifest-test-") as temporary_directory:
root = Path(temporary_directory)
source = self.create_source(root / "source")
archive = root / "booking-desk-event.skill"
members = self.packager.write_archive(source, archive)
manifest = self.packager.build_manifest(source, archive, members)
path = root / "manifest.json"
self.packager.write_or_check_manifest(path, manifest, False)
before = path.read_bytes()
before_mtime = path.stat().st_mtime_ns
self.packager.write_or_check_manifest(path, manifest, True)
self.assertEqual(before, path.read_bytes())
self.assertEqual(before_mtime, path.stat().st_mtime_ns)
stale = dict(manifest)
stale["publication_status"] = "RELEASED"
with self.assertRaisesRegex(ValueError, "stale or missing"):
self.packager.write_or_check_manifest(path, stale, True)
def test_rejects_any_source_member_outside_the_archive_allowlist(self) -> None:
with tempfile.TemporaryDirectory(prefix="booking-skill-forbidden-test-") as temporary_directory:
source = self.create_source(Path(temporary_directory) / "source")
(source / "scripts").mkdir()
(source / "scripts" / "unexpected.py").write_text("pass\n", encoding="utf-8")
with self.assertRaisesRegex(ValueError, "forbidden top-level"):
self.packager.source_files(source)
def test_rejects_retired_schema_mixed_into_current_package(self) -> None:
with tempfile.TemporaryDirectory(prefix="booking-skill-mixed-schema-test-") as temporary_directory:
source = self.create_source(Path(temporary_directory) / "source")
(source / "schemas/booking-business-agent-compact-decision-v1.schema.json").write_text(
"{}\n", encoding="utf-8")
with self.assertRaisesRegex(ValueError, "only the current compact-v2"):
self.packager.source_files(source)
def test_main_prompt_is_concise_and_keeps_contract_defenses_out(self) -> None:
prompt = (REPOSITORY_ROOT / "agent-skills/booking-desk-event/prompts/main-prompt.md").read_text(encoding="utf-8")
self.assertLessEqual(len(prompt.splitlines()), 5)
self.assertIn("$booking-desk-event", prompt)
self.assertIn("booking-business-agent-compact-decision-v2", prompt)
self.assertIn("必要规则", prompt)
self.assertIn("不调用其他工具", prompt)
self.assertIn("每项只判断一次", prompt)
self.assertIn("立即返回", prompt)
self.assertNotIn("你是 Wyndham-RSVN", prompt)
self.assertNotIn("source_input_hash", prompt)
self.assertNotIn("decision_origin", prompt)
def test_skill_requires_compact_decision_schema_before_output(self) -> None:
skill = (REPOSITORY_ROOT / "agent-skills/booking-desk-event/SKILL.md").read_text(encoding="utf-8")
schema_path = "schemas/booking-business-agent-compact-decision-v2.schema.json"
self.assertIn(f"`{schema_path}`。输出 Schema", skill)
self.assertIn("完整读取", skill)
self.assertIn("输出 Schema 是字段和允许值的唯一权威", skill)
self.assertIn("信息系统在 Layer 5B", skill)
schema = json.loads((REPOSITORY_ROOT / "agent-skills/booking-desk-event" / schema_path)
.read_text(encoding="utf-8"))
self.assertNotIn("source_input_hash", schema["required"])
self.assertNotIn("source_input_hash", schema["properties"])
def test_narrow_qbd_booking_precedence_rules_are_explicit(self) -> None:
root = REPOSITORY_ROOT / "agent-skills/booking-desk-event"
skill = (root / "SKILL.md").read_text(encoding="utf-8")
evidence = (root / "references/01-input-output-evidence.md").read_text(encoding="utf-8")
splitting = (root / "references/02-target-and-source-splitting.md").read_text(encoding="utf-8")
update = (root / "references/11-update-booking.md").read_text(encoding="utf-8")
allotment = (root / "references/15-allotment.md").read_text(encoding="utf-8")
general = (root / "references/90-general-risk-ignored.md").read_text(encoding="utf-8")
self.assertIn("每项只判断一次", skill)
self.assertIn("只执行一次 Schema 字段检查", skill)
self.assertIn("不要创建待办", skill)
self.assertIn("`D1`、`D2`", evidence)
self.assertIn("只使用自己的 `target_ref`", evidence)
self.assertIn("`linked_relationship_refs` 只允许引用 `GROUP_CODE_REPLACEMENT`", evidence)
self.assertIn("`ALLOTMENT_SOURCE_TO_ACTUAL`", evidence)
self.assertIn("只使用该正文的 `body_ref`", evidence)
self.assertIn("只汇总一次", splitting)
self.assertIn("立即冻结", splitting)
self.assertIn("第一道门槛成立后", update)
self.assertIn("重新讨论行类型", update)
self.assertIn("ตัด Allotment <code>", allotment)
self.assertIn("固定视为明确的整块取消", allotment)
self.assertIn("不搜索 actual 决定", allotment)
self.assertIn("不要求与 actual 订单同码", allotment)
self.assertIn("`source_target_ref` 必须填写材料中的 source Tour Code 原值", allotment)
self.assertIn("不得填写 `T1`、`T10`", allotment)
self.assertIn("整块取消与同封物理单元决定可以同时存在", allotment)
self.assertIn("QBD 固定 OP 提醒", general)
self.assertIn("未归类原文", general)
def test_trace_schema_keeps_only_semantic_decision_fields(self) -> None:
schema = json.loads((
REPOSITORY_ROOT
/ "agent-skills/booking-desk-event/schemas/booking-business-agent-compact-decision-v2.schema.json"
).read_text(encoding="utf-8"))
trace_item = schema["$defs"]["traceItem"]
expected = {
"service_type", "service_text", "room_type", "quantity", "source_refs",
}
self.assertEqual(expected, set(trace_item["required"]))
self.assertEqual(expected, set(trace_item["properties"]))
self.assertNotIn("parameters", trace_item["properties"])
self.assertEqual(["GENERAL_SERVICE", "EXTRA_BED"], trace_item["properties"]["service_type"]["enum"])
def test_agent_readable_skill_materials_are_chinese(self) -> None:
skill_root = REPOSITORY_ROOT / "agent-skills/booking-desk-event"
paths = [skill_root / "SKILL.md", skill_root / "agents/openai.yaml"]
paths.extend(sorted((skill_root / "references").glob("*.md")))
self.assertEqual(13, len(paths))
for path in paths:
with self.subTest(path=path.name):
self.assertRegex(path.read_text(encoding="utf-8"), r"[\u4e00-\u9fff]")
def create_source(self, root: Path) -> Path:
for directory in ("agents", "schemas", "references", "fixtures", "prompts"):
(root / directory).mkdir(parents=True, exist_ok=True)
(root / "SKILL.md").write_text(
"---\nname: booking-desk-event\ndescription: A sufficiently long synthetic skill description for tests.\n---\n",
encoding="utf-8",
)
(root / "agents/openai.yaml").write_text("interface:\n display_name: test\n", encoding="utf-8")
(root / "prompts/main-prompt.md").write_text("independent prompt\n", encoding="utf-8")
(root / "manifest.json").write_text(json.dumps({
"publication_status": "HOLD",
"distribution": "INTERNAL_ONLY",
"main_prompt_packaged": False,
}) + "\n", encoding="utf-8")
for name in (
"booking-business-agent-compact-input-v2.schema.json",
"booking-business-agent-compact-decision-v2.schema.json"):
(root / "schemas" / name).write_text("{}\n", encoding="utf-8")
(root / "references/rule.md").write_text("synthetic rule\n", encoding="utf-8")
scenarios = [{"scenario_id": f"scenario-{index}"} for index in range(49)]
(root / "fixtures/scenario-matrix.json").write_text(json.dumps({
"scenario_count": len(scenarios),
"scenarios": scenarios,
}) + "\n", encoding="utf-8")
return root
if __name__ == "__main__":
unittest.main()