文件预览

test_three_laws.py

查看 lobster-novel 技能包中的文件内容。

文件内容

tests/test_three_laws.py

#!/usr/bin/env python3
"""Tests for three_laws — 防幻觉三定律 写前/写后校验."""
import sys, json, tempfile
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "core"))

from agents.three_laws import (
    THREE_LAWS, LAW_LABELS,
    PreWriteValidator, PostWriteValidator,
    LawViolation, ValidationReport,
    format_three_laws_block, _extract_unknown_entities,
)
from core.story_state import StoryState, CharacterState, HookState, ChapterRecord
from core.contract import ChapterCommit, ChapterCommitEvent, StateDelta, EntityDelta


def _make_test_state() -> StoryState:
    state = StoryState(novel_title="测试", volume="V1")
    state.characters["char_林风"] = CharacterState(
        id="char_林风", name="林风", role="主角",
        first_appearance=1, last_appearance=10, status="active",
        state="寻找星辰碎片",
    )
    state.characters["char_冷月"] = CharacterState(
        id="char_冷月", name="冷月", role="导师",
        first_appearance=1, last_appearance=10, status="active",
        state="守护星辰塔",
    )
    state.characters["char_已死"] = CharacterState(
        id="char_已死", name="黑雾尊者", role="反派",
        first_appearance=5, last_appearance=8, status="active",
        state="已被击杀,确认死亡",
    )
    state.chapters[10] = ChapterRecord(
        number=10, title="月食之夜", word_count=2800,
        scene="星辰塔", characters_present=["林风", "冷月"],
        key_events=["月食开始", "封印减弱"],
    )
    return state


def test_three_laws_constants():
    assert len(THREE_LAWS) == 3
    assert "大纲即法律" in THREE_LAWS[0]
    assert "设定即物理" in THREE_LAWS[1]
    assert "发明需识别" in THREE_LAWS[2]
    assert len(LAW_LABELS) == 3
    print("✅ test_three_laws_constants")


def test_law_violation():
    v = LawViolation(law_index=0, severity="P0", category="测试", description="test")
    assert v.law_index == 0
    assert v.severity == "P0"
    print("✅ test_law_violation")


def test_validation_report():
    r = ValidationReport(passed=True)
    assert r.passed
    assert len(r.violations) == 0
    assert r.p0_count == 0
    print("✅ test_validation_report")


def test_validation_report_with_violations():
    v = [LawViolation(law_index=0, severity="P0", category="测试", description="P0 desc"),
         LawViolation(law_index=1, severity="P1", category="测试", description="P1 desc")]
    r = ValidationReport(passed=False, violations=v)
    assert not r.passed
    assert r.p0_count == 1
    assert r.p1_count == 1
    text = r.to_text()
    assert "P0 desc" in text
    assert "大纲即法律" in text
    print("✅ test_validation_report_with_violations")


def test_to_prompt_block():
    v = [LawViolation(law_index=0, severity="P1", category="测试", description="prompt msg",
                      suggestion="fix it")]
    r = ValidationReport(passed=False, violations=v)
    block = r.to_prompt_block()
    assert "三定律预检警告" in block
    assert "prompt msg" in block
    assert "fix it" in block
    print("✅ test_to_prompt_block")


def test_format_three_laws_block():
    block = format_three_laws_block("zh")
    assert "防幻觉三定律" in block
    assert "大纲即法律" in block
    assert block.count("\n") >= 3
    print("✅ test_format_three_laws_block")


def test_extract_unknown_entities():
    known = {"林风"}
    text = "林风说道要去找到星辰碎片。冷月问道塔顶的封印"
    found = _extract_unknown_entities(text, known)
    # 冷月 is in the state but not in `known` for this test
    assert "冷月" in found
    assert "林风" not in found  # already known
    print("✅ test_extract_unknown_entities")


def test_pre_write_law1():
    """PreWriteValidator should flag unknown names in outline."""
    with tempfile.TemporaryDirectory() as tmp:
        state = _make_test_state()
        pv = PreWriteValidator(tmp)
        report = pv.check_outline(11, "林风去讨伐魔王,魔王说要找星辰碎片", state)
        # "魔王" is not in known characters
        unknown_violations = [v for v in report.violations if v.category == "未知角色/地点"]
        assert any("魔王" in v.description for v in unknown_violations), \
            f"Should flag 魔王, got: {[v.description for v in report.violations]}"
    print("✅ test_pre_write_law1")


def test_pre_write_law2_dead_char():
    """PreWriteValidator should flag dead character in outline."""
    with tempfile.TemporaryDirectory() as tmp:
        state = _make_test_state()
        pv = PreWriteValidator(tmp)
        report = pv.check_outline(11, "黑雾尊者复活了", state)
        death_violations = [v for v in report.violations if v.category == "角色状态冲突"]
        assert len(death_violations) >= 1, \
            f"Should flag dead char, got: {[v.description for v in report.violations]}"
    print("✅ test_pre_write_law2_dead_char")


def test_pre_write_law3():
    """PreWriteValidator should flag missing new flag."""
    with tempfile.TemporaryDirectory() as tmp:
        state = _make_test_state()
        pv = PreWriteValidator(tmp)
        # 长大纲(>50 chars)且没有明确的新增标记
        outline = (
            "林风穿过森林来到一片从未见过的山谷。山谷中有废弃的神庙,"
            "庙门上的封印已经松动。冷月说这里可能是星辰碎片最后的沉睡之地。"
            "他们走进去,墙上的壁画讲述了千年前那场战争的故事。"
        )
        report = pv.check_outline(11, outline, state)
        flag_violations = [v for v in report.violations if v.category == "可能遗漏新增标记"]
        assert len(flag_violations) >= 1, (
            f"Should flag missing new flag, got: {[v.description for v in report.violations]}"
        )
    print("✅ test_pre_write_law3")


def test_post_write_new_entities():
    """PostWriteValidator should flag unmarked new entities."""
    with tempfile.TemporaryDirectory() as tmp:
        state = _make_test_state()
        pv = PostWriteValidator(tmp)
        commit = ChapterCommit(
            chapter_number=11,
            chapter_title="测试",
            word_count=1000,
            events=[
                ChapterCommitEvent(event_type="action", description="林风遇到了新角色"),
            ],
            state_delta=StateDelta(
                character_states={"char_林风": "继续前进"},
            ),
            entity_delta=EntityDelta(
                new_characters=[{"name": "白龙", "role": "配角"}],
            ),
            hooks_created=[],
            hooks_resolved=[],
        )
        report = pv.check_commit(11, commit, state)
        new_entity_violations = [v for v in report.violations if v.category == "未标记的新实体"]
        assert len(new_entity_violations) >= 1
    print("✅ test_post_write_new_entities")


def test_post_write_dead_char():
    """PostWriteValidator should flag dead character resurrection."""
    with tempfile.TemporaryDirectory() as tmp:
        state = _make_test_state()
        pv = PostWriteValidator(tmp)
        commit = ChapterCommit(
            chapter_number=11,
            chapter_title="测试",
            word_count=1000,
            events=[ChapterCommitEvent(event_type="action", description="黑雾尊者回来了")],
            state_delta=StateDelta(character_states={"char_已死": "突然复活现身"}),
            entity_delta=EntityDelta(),
            hooks_created=[],
            hooks_resolved=[],
        )
        report = pv.check_commit(11, commit, state)
        dead_violations = [v for v in report.violations if v.category == "角色死而复生"]
        assert len(dead_violations) >= 1
    print("✅ test_post_write_dead_char")


def test_post_write_fake_hook_resolve():
    """PostWriteValidator should flag hook resolution with no matching hook."""
    with tempfile.TemporaryDirectory() as tmp:
        state = _make_test_state()
        pv = PostWriteValidator(tmp)
        commit = ChapterCommit(
            chapter_number=11,
            chapter_title="测试",
            word_count=1000,
            events=[ChapterCommitEvent(event_type="action", description="事件")],
            state_delta=StateDelta(),
            entity_delta=EntityDelta(),
            hooks_created=[],
            hooks_resolved=["不存在的伏笔"],
        )
        report = pv.check_commit(11, commit, state)
        fake_violations = [v for v in report.violations if v.category == "伏笔来源不明"]
        assert len(fake_violations) >= 1
    print("✅ test_post_write_fake_hook_resolve")


def test_pre_write_no_bible_degradation():
    """PreWriteValidator 在无 bible.json 时应正常降级。"""
    with tempfile.TemporaryDirectory() as tmp:
        state = _make_test_state()
        pv = PreWriteValidator(tmp)  # 没有 bible.json
        report = pv.check_outline(11, "林风继续前进", state)
        # 不崩溃就是通过
        assert isinstance(report, ValidationReport)
    print("✅ test_pre_write_no_bible_degradation")


def test_pre_write_chapter_zero():
    """PreWriteValidator 在 chapter=0 时应正常工作。"""
    with tempfile.TemporaryDirectory() as tmp:
        state = _make_test_state()
        pv = PreWriteValidator(tmp)
        report = pv.check_outline(0, "林风说要去星辰塔", state)
        assert isinstance(report, ValidationReport)
    print("✅ test_pre_write_chapter_zero")


def test_extract_names_with_middle_dot():
    """_extract_unknown_entities 应支持带 · 的 5 字名。"""
    known = {"林风"}
    text = "艾琳娜·烬羽说道:星辰碎片在塔顶"
    found = _extract_unknown_entities(text, known)
    assert "艾琳娜·烬羽" in found, f"Should detect 艾琳娜·烬羽, got: {found}"
    print("✅ test_extract_names_with_middle_dot")


def test_clean_chapter_passes():
    """A clean chapter should produce no violations."""
    with tempfile.TemporaryDirectory() as tmp:
        state = _make_test_state()
        pv = PostWriteValidator(tmp)
        commit = ChapterCommit(
            chapter_number=11,
            chapter_title="测试",
            word_count=1000,
            events=[ChapterCommitEvent(event_type="action", description="林风继续走")],
            state_delta=StateDelta(character_states={"char_林风": "继续前进"}),
            entity_delta=EntityDelta(),
            hooks_created=["【新增】白龙——守护星辰塔的远古生物"],
            hooks_resolved=[],
        )
        report = pv.check_commit(11, commit, state)
        assert report.passed, f"Expected clean commit to pass, got violations: {[v.description for v in report.violations]}"
    print("✅ test_clean_chapter_passes")


if __name__ == "__main__":
    test_three_laws_constants()
    test_law_violation()
    test_validation_report()
    test_validation_report_with_violations()
    test_to_prompt_block()
    test_format_three_laws_block()
    test_extract_unknown_entities()
    test_pre_write_law1()
    test_pre_write_law2_dead_char()
    test_pre_write_law3()
    test_post_write_new_entities()
    test_post_write_dead_char()
    test_post_write_fake_hook_resolve()
    test_pre_write_no_bible_degradation()
    test_pre_write_chapter_zero()
    test_extract_names_with_middle_dot()
    test_clean_chapter_passes()
    print("\n🎉 All three_laws tests passed!")