328 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()