328 lines
17 KiB
Python
328 lines
17 KiB
Python
#!/usr/bin/env python3
|
||
"""AI 味案例卡离线合同测试;不连接数据库、不调用模型。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import sys
|
||
import tempfile
|
||
import unittest
|
||
from pathlib import Path
|
||
from unittest.mock import patch
|
||
|
||
PROJECT_ROOT = Path(__file__).resolve().parents[3]
|
||
SCRIPT_DIR = PROJECT_ROOT / "muse" / "lifecycle" / "quality" / "humanization" / "skills" / "记录机器味案例" / "scripts"
|
||
sys.path.insert(0, str(SCRIPT_DIR))
|
||
|
||
import yaml
|
||
from capture_cases import (
|
||
CAPTURE_CLI_COMMANDS,
|
||
LIFECYCLE_COMMANDS_FORBIDDEN,
|
||
PATTERNS,
|
||
CaseCardError,
|
||
_parser,
|
||
annotate_card,
|
||
build_case_card,
|
||
build_inventory,
|
||
build_revalidation_report,
|
||
capture_feedback,
|
||
capture_file,
|
||
confirm_card,
|
||
load_bundle,
|
||
main,
|
||
project_sample,
|
||
revalidate_card,
|
||
text_sha256,
|
||
validate_card,
|
||
)
|
||
|
||
|
||
class CaptureCasesTest(unittest.TestCase):
|
||
def test_source_hash_is_derived_from_supplied_text(self):
|
||
with self.assertRaises(CaseCardError):
|
||
build_case_card(
|
||
text="值得注意的是。", source_sha256="0" * 64,
|
||
source_kind="existing_work", source_license="owned", source_ref="x.txt",
|
||
work_ref="work-a", start=0, end=6, pattern=PATTERNS[0],
|
||
)
|
||
|
||
def test_inventory_is_deterministic_and_keeps_only_hash_for_research_sources(self):
|
||
with tempfile.TemporaryDirectory() as tmp:
|
||
root = Path(tmp)
|
||
(root / "b.txt").write_text("值得注意的是。\n", encoding="utf-8")
|
||
(root / "a.txt").write_text("嘴角微微上扬。\n", encoding="utf-8")
|
||
report = build_inventory(root, source_root_ref="fixture", generated_on="2026-08-13")
|
||
self.assertEqual("ai-flavor-inventory-v1", report["schema_version"])
|
||
self.assertEqual({"books": 2, "cards": 2},
|
||
{key: report["totals"][key] for key in ("books", "cards")})
|
||
self.assertEqual(2, len({card["id"] for card in report["cards"]}))
|
||
self.assertTrue(all(card["state"] == "shadow" and not card["excerpt"] for card in report["cards"]))
|
||
self.assertTrue(all(not card["source"]["source_ref"].startswith("/") for card in report["cards"]))
|
||
|
||
def test_backfill_research_only_is_hash_only_and_replayable(self):
|
||
with tempfile.TemporaryDirectory() as tmp:
|
||
path = Path(tmp) / "work.txt"
|
||
path.write_text("第一行。值得注意的是,门外下雨了。\n", encoding="utf-8")
|
||
cards = capture_file(path, work_ref="work-a")
|
||
self.assertEqual(1, len(cards))
|
||
card = cards[0]
|
||
self.assertEqual("shadow", card["state"])
|
||
self.assertEqual("", card["excerpt"])
|
||
self.assertEqual("", card["context"])
|
||
self.assertEqual(text_sha256("值得注意的是,门外下雨了。"), card["source"]["excerpt_sha256"])
|
||
self.assertEqual(1, card["source"]["location"]["line_start"])
|
||
validate_card(card)
|
||
|
||
def test_owned_backfill_keeps_context_and_location(self):
|
||
with tempfile.TemporaryDirectory() as tmp:
|
||
path = Path(tmp) / "owned.txt"
|
||
path.write_text("她抬头。嘴角微微上扬。\n", encoding="utf-8")
|
||
cards = capture_file(path, work_ref="work-owned", source_license="owned")
|
||
self.assertEqual(1, len(cards))
|
||
self.assertEqual("嘴角微微上扬。", cards[0]["excerpt"])
|
||
self.assertIn("她抬头", cards[0]["context"])
|
||
self.assertEqual(1, cards[0]["source"]["location"]["line_start"])
|
||
|
||
def test_duplicate_scan_pattern_is_deduplicated(self):
|
||
pattern = PATTERNS[0]
|
||
with tempfile.TemporaryDirectory() as tmp:
|
||
path = Path(tmp) / "same.txt"
|
||
path.write_text("值得注意的是。", encoding="utf-8")
|
||
cards = capture_file(path, patterns=[pattern, pattern], source_license="owned", work_ref="work-same")
|
||
self.assertEqual(1, len(cards))
|
||
|
||
def test_feedback_is_bound_to_creation_run(self):
|
||
card = capture_feedback("候选正文", work_ref="work-12", run_ref="run-7", issue="段尾空泛")
|
||
self.assertEqual("live_feedback", card["capture_mode"])
|
||
self.assertEqual("creation_feedback", card["source"]["kind"])
|
||
self.assertEqual("run-7", card["feedback"]["run_ref"])
|
||
self.assertEqual("shadow", card["state"])
|
||
|
||
def test_unlicensed_card_cannot_be_confirmed_or_store_text(self):
|
||
with tempfile.TemporaryDirectory() as tmp:
|
||
path = Path(tmp) / "third-party.txt"
|
||
path.write_text("值得注意的是。", encoding="utf-8")
|
||
card = capture_file(path, work_ref="work-third")[0]
|
||
with self.assertRaises(CaseCardError):
|
||
confirm_card(card, reviewer="u", note="不能确认")
|
||
broken = dict(card)
|
||
broken["excerpt"] = "偷偷保存的原文"
|
||
with self.assertRaises(CaseCardError):
|
||
validate_card(broken)
|
||
|
||
def test_revalidate_unchanged_source_is_verified(self):
|
||
with tempfile.TemporaryDirectory() as tmp:
|
||
path = Path(tmp) / "work.txt"
|
||
path.write_text("第一行。值得注意的是,门外下雨了。\n", encoding="utf-8")
|
||
card = capture_file(path, work_ref="work-a", source_license="owned")[0]
|
||
result = revalidate_card(card, source_path=path, checked_on="2026-08-14")
|
||
self.assertEqual("verified", result["status"])
|
||
self.assertEqual(card["source"]["source_sha256"], result["actual_source_sha256"])
|
||
self.assertEqual("2026-08-14", result["checked_on"])
|
||
|
||
def test_revalidate_modified_source_is_stale_and_does_not_mutate_card(self):
|
||
with tempfile.TemporaryDirectory() as tmp:
|
||
path = Path(tmp) / "work.txt"
|
||
path.write_text("值得注意的是。\n", encoding="utf-8")
|
||
card = capture_file(path, work_ref="work-a", source_license="owned")[0]
|
||
original_state = card["state"]
|
||
path.write_text("已经改成另一版正文。\n", encoding="utf-8")
|
||
result = revalidate_card(card, source_path=path)
|
||
self.assertEqual("stale", result["status"])
|
||
self.assertEqual("source_hash_mismatch", result["reason"])
|
||
self.assertEqual("shadow", original_state)
|
||
self.assertEqual("shadow", card["state"])
|
||
|
||
def test_revalidate_same_source_but_tampered_anchor_is_card_mismatch(self):
|
||
with tempfile.TemporaryDirectory() as tmp:
|
||
path = Path(tmp) / "work.txt"
|
||
path.write_text("值得注意的是。", encoding="utf-8")
|
||
card = capture_file(path, work_ref="work-a", source_license="owned")[0]
|
||
tampered = dict(card)
|
||
tampered["source"] = dict(card["source"])
|
||
tampered["source"]["surface_location"] = dict(card["source"]["surface_location"])
|
||
tampered["source"]["surface_location"]["char_start"] = 1
|
||
result = revalidate_card(tampered, source_path=path)
|
||
self.assertEqual("card_mismatch", result["status"])
|
||
self.assertEqual("card_surface_mismatch", result["reason"])
|
||
|
||
def test_revalidate_unavailable_source_is_fail_closed(self):
|
||
with tempfile.TemporaryDirectory() as tmp:
|
||
path = Path(tmp) / "work.txt"
|
||
path.write_text("值得注意的是。", encoding="utf-8")
|
||
card = capture_file(path, work_ref="work-a")[0]
|
||
result = revalidate_card(card, source_path=Path(tmp) / "missing" / "work.txt")
|
||
self.assertEqual("unavailable", result["status"])
|
||
self.assertEqual("source_not_found", result["reason"])
|
||
|
||
def test_revalidate_report_deduplicates_source_reads_and_exposes_counts(self):
|
||
with tempfile.TemporaryDirectory() as tmp:
|
||
path = Path(tmp) / "work.txt"
|
||
path.write_text("值得注意的是。嘴角微微上扬。", encoding="utf-8")
|
||
cards = capture_file(path, work_ref="work-a", source_license="owned")
|
||
report = build_revalidation_report(cards, source_root=Path(tmp), checked_on="2026-08-14")
|
||
self.assertEqual(2, report["totals"]["cards"])
|
||
self.assertEqual(2, report["totals"]["verified"])
|
||
self.assertTrue(report["usable"])
|
||
|
||
def test_lifecycle_receipt_wrapper_can_feed_next_step(self):
|
||
with tempfile.TemporaryDirectory() as tmp:
|
||
path = Path(tmp) / "owned.txt"
|
||
path.write_text("值得注意的是。", encoding="utf-8")
|
||
card = annotate_card(capture_file(path, work_ref="work-a", source_license="owned")[0], label="sf")
|
||
wrapper = Path(tmp) / "annotated.json"
|
||
wrapper.write_text(yaml.safe_dump({"card": card, "persistence": {"status": "offline"}}, allow_unicode=True), encoding="utf-8")
|
||
loaded = load_bundle([wrapper])
|
||
self.assertEqual([card["id"]], [item["id"] for item in loaded])
|
||
|
||
def test_confirmation_requires_verified_receipt(self):
|
||
with tempfile.TemporaryDirectory() as tmp:
|
||
path = Path(tmp) / "owned.txt"
|
||
path.write_text("值得注意的是。", encoding="utf-8")
|
||
shadow = capture_file(path, work_ref="work-a", source_license="owned")[0]
|
||
annotated = annotate_card(shadow, label="sf", carrier="narration")
|
||
with self.assertRaises(CaseCardError):
|
||
confirm_card(annotated, reviewer="human", note="无功能")
|
||
verification = {"cards": [revalidate_card(annotated, source_path=path)]}
|
||
canonical = confirm_card(annotated, reviewer="human", note="无功能", verification=verification)
|
||
self.assertEqual("canonical", canonical["state"])
|
||
|
||
def test_canonical_unknown_carrier_cannot_project_to_sample(self):
|
||
with tempfile.TemporaryDirectory() as tmp:
|
||
path = Path(tmp) / "owned.txt"
|
||
path.write_text("值得注意的是。", encoding="utf-8")
|
||
shadow = capture_file(path, work_ref="work-a", source_license="owned")[0]
|
||
annotated = annotate_card(shadow, label="sf", carrier="unknown")
|
||
verification = {"cards": [revalidate_card(annotated, source_path=path)]}
|
||
canonical = confirm_card(annotated, reviewer="human", note="功能已确认", verification=verification)
|
||
with self.assertRaises(CaseCardError):
|
||
project_sample(canonical, verification=verification)
|
||
|
||
def test_canonical_card_projects_to_sample_only_after_review(self):
|
||
with tempfile.TemporaryDirectory() as tmp:
|
||
path = Path(tmp) / "owned.txt"
|
||
path.write_text("值得注意的是。", encoding="utf-8")
|
||
shadow = capture_file(path, work_ref="work-a", source_license="owned")[0]
|
||
annotated = annotate_card(shadow, label="sf", carrier="narration")
|
||
with self.assertRaises(CaseCardError):
|
||
project_sample(annotated, verification={})
|
||
verification = {"cards": [revalidate_card(annotated, source_path=path)]}
|
||
canonical = confirm_card(annotated, reviewer="human", note="上下文无功能", verification=verification)
|
||
sample = project_sample(canonical, verification=verification)
|
||
self.assertEqual("sample-" + canonical["id"], sample["id"])
|
||
self.assertEqual(canonical["id"], sample["case_card_id"])
|
||
self.assertEqual("hand_written", sample["source"])
|
||
self.assertEqual("narration", sample["carrier"])
|
||
|
||
def test_shipped_fixtures_pass_the_same_validator(self):
|
||
root = SCRIPT_DIR.parent / "references" / "fixtures"
|
||
for name in ("backfill-hash-only.yaml", "canonical-samples.yaml"):
|
||
data = yaml.safe_load((root / name).read_text(encoding="utf-8"))
|
||
for card in data["cards"]:
|
||
validate_card(card)
|
||
|
||
def test_shipped_rule_seed_is_candidate_only(self):
|
||
root = SCRIPT_DIR.parent / "references" / "fixtures"
|
||
data = yaml.safe_load((root / "rule-candidates.yaml").read_text(encoding="utf-8"))
|
||
self.assertTrue(data["rules"])
|
||
self.assertTrue(all(rule["status"] == "candidate" for rule in data["rules"]))
|
||
|
||
def test_revalidate_cli_is_persist_by_default_contract(self):
|
||
import capture_cases
|
||
parser = capture_cases._parser()
|
||
args = parser.parse_args(["revalidate", "cards.yaml", "--output", "receipt.json"])
|
||
self.assertFalse(args.offline)
|
||
|
||
def test_inventory_cli_automatically_calls_persistence(self):
|
||
with tempfile.TemporaryDirectory() as tmp:
|
||
root = Path(tmp) / "works"
|
||
root.mkdir()
|
||
(root / "work.txt").write_text("值得注意的是。", encoding="utf-8")
|
||
output = Path(tmp) / "inventory.json"
|
||
with patch("capture_cases._persist_detection", return_value={"status": "written"}) as persist:
|
||
rc = main(["inventory", str(root), "--output", str(output)])
|
||
self.assertEqual(0, rc)
|
||
persist.assert_called_once()
|
||
|
||
def test_inventory_cli_offline_is_explicit_escape_hatch(self):
|
||
with tempfile.TemporaryDirectory() as tmp:
|
||
root = Path(tmp) / "works"
|
||
root.mkdir()
|
||
(root / "work.txt").write_text("值得注意的是。", encoding="utf-8")
|
||
output = Path(tmp) / "inventory.json"
|
||
with patch("capture_cases._persist_detection") as persist:
|
||
rc = main(["inventory", str(root), "--output", str(output), "--offline"])
|
||
self.assertEqual(0, rc)
|
||
persist.assert_not_called()
|
||
|
||
def test_scan_cli_automatically_calls_persistence(self):
|
||
with tempfile.TemporaryDirectory() as tmp:
|
||
root = Path(tmp)
|
||
source = root / "work.txt"
|
||
source.write_text("值得注意的是。", encoding="utf-8")
|
||
output = root / "cards.yaml"
|
||
with patch("capture_cases._persist_detection", return_value={"status": "written"}) as persist:
|
||
rc = main(["scan", str(source), "--work-ref", "work-a", "--output", str(output)])
|
||
self.assertEqual(0, rc)
|
||
persist.assert_called_once()
|
||
|
||
def test_feedback_cli_automatically_calls_persistence(self):
|
||
with tempfile.TemporaryDirectory() as tmp:
|
||
root = Path(tmp)
|
||
source = root / "candidate.txt"
|
||
source.write_text("候选正文。", encoding="utf-8")
|
||
output = root / "feedback.yaml"
|
||
with patch("capture_cases._persist_detection", return_value={"status": "written"}) as persist:
|
||
rc = main([
|
||
"feedback", "--text-file", str(source), "--work-ref", "work-a",
|
||
"--run-ref", "run-a", "--issue", "段尾空泛", "--output", str(output),
|
||
])
|
||
self.assertEqual(0, rc)
|
||
persist.assert_called_once()
|
||
|
||
def test_revalidate_single_card_writes_inventory_sidecar_before_persistence(self):
|
||
with tempfile.TemporaryDirectory() as tmp:
|
||
root = Path(tmp)
|
||
source = root / "work.txt"
|
||
source.write_text("值得注意的是。", encoding="utf-8")
|
||
card = capture_file(source, work_ref="work-a", source_license="owned")[0]
|
||
cards_path = root / "card.yaml"
|
||
cards_path.write_text(yaml.safe_dump({"schema_version": "ai-flavor-case-v1", "cards": [card]}, allow_unicode=True), encoding="utf-8")
|
||
receipt_path = root / "receipt.json"
|
||
with patch("capture_cases._persist_detection", return_value={"status": "written"}) as persist:
|
||
rc = main([
|
||
"revalidate", str(cards_path), "--source-root", str(root),
|
||
"--output", str(receipt_path), "--checked-on", "2026-08-14",
|
||
])
|
||
self.assertEqual(0, rc)
|
||
persist.assert_called_once()
|
||
self.assertTrue((root / "card.inventory.json").is_file())
|
||
|
||
def test_revalidate_single_card_offline_does_not_write_database(self):
|
||
with tempfile.TemporaryDirectory() as tmp:
|
||
root = Path(tmp)
|
||
source = root / "work.txt"
|
||
source.write_text("值得注意的是。", encoding="utf-8")
|
||
card = capture_file(source, work_ref="work-a", source_license="owned")[0]
|
||
cards_path = root / "card.yaml"
|
||
cards_path.write_text(yaml.safe_dump({"schema_version": "ai-flavor-case-v1", "cards": [card]}, allow_unicode=True), encoding="utf-8")
|
||
receipt_path = root / "receipt.json"
|
||
with patch("capture_cases._persist_detection") as persist:
|
||
rc = main([
|
||
"revalidate", str(cards_path), "--source-root", str(root),
|
||
"--output", str(receipt_path), "--offline",
|
||
])
|
||
self.assertEqual(0, rc)
|
||
persist.assert_not_called()
|
||
|
||
def test_cli_surface_excludes_lifecycle_commands(self):
|
||
"""采集 CLI 不得暴露 confirm/project/activate;生命周期归 promote。"""
|
||
parser = _parser()
|
||
sub = next(a for a in parser._actions if getattr(a, "choices", None))
|
||
commands = set(sub.choices)
|
||
self.assertEqual(commands, set(CAPTURE_CLI_COMMANDS))
|
||
self.assertTrue(commands.isdisjoint(LIFECYCLE_COMMANDS_FORBIDDEN))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|