feat(b0): 注入透明可控版后端 — PUT override(pin/排除/recent_n) + 选择函数加参 + draft 同读
- selection.select_relevant_entities 加 pinned/excluded frozenset 入参;新理由 author_pin(末位) - assemble 加 override 关键字参(recent_n 覆盖回看章数 + pin/排除);GET/PUT/draft 同读同一覆盖(不变量 #6 作者兜底) - 新 domain/injection_repo.py(InjectionOverride/EntityRef/SqlInjectionOverrideRepo,upsert 只 flush) - 新表 chapter_injection(迁移 ad2c4c663daf,唯一 project_id+chapter_no;复用 outline 行已否决) - PUT/GET injection 端点 + InjectionOverrideRequest/回显 pinned/excluded;recent_n 1..20 校验 422 - 单测:selection pin/排除/exclude>pin + assemble override + 端点 PUT/404/422;regen TS 客户端 - 门禁绿:ruff/format · mypy 163 · alembic 无漂移 · pytest 476 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -200,6 +200,99 @@ def test_union_dedups_reasons_sorted() -> None:
|
||||
]
|
||||
|
||||
|
||||
# ---- author override: pin / exclude / recent_n (B0 可控版) ----
|
||||
|
||||
|
||||
def test_pin_forces_inclusion_with_author_pin_reason() -> None:
|
||||
# 路人甲零自动理由——pin 强制纳入并加 author_pin。
|
||||
outline = OutlineView(volume=1, chapter_no=1, beats={})
|
||||
trace = select_relevant_entities(
|
||||
outline=outline,
|
||||
characters=[char("路人甲", role="配角")],
|
||||
world_entities=[],
|
||||
recent_digests=[],
|
||||
pinned=frozenset({("character", "路人甲")}),
|
||||
)
|
||||
selected = [e for e in trace.selected if e.name == "路人甲"]
|
||||
assert len(selected) == 1
|
||||
assert selected[0].reasons == ["author_pin"]
|
||||
|
||||
|
||||
def test_pin_appends_author_pin_to_existing_reasons() -> None:
|
||||
outline = OutlineView(volume=1, chapter_no=1, beats={})
|
||||
trace = select_relevant_entities(
|
||||
outline=outline,
|
||||
characters=[char("林动", role="主角")],
|
||||
world_entities=[],
|
||||
recent_digests=[],
|
||||
pinned=frozenset({("character", "林动")}),
|
||||
)
|
||||
main = next(e for e in trace.selected if e.name == "林动")
|
||||
assert main.reasons == ["main_character", "author_pin"] # 规范顺序,author_pin 末位
|
||||
|
||||
|
||||
def test_exclude_removes_auto_selected_entity() -> None:
|
||||
# 主角林动本会自动入选——excluded 强制剔除。
|
||||
outline = OutlineView(volume=1, chapter_no=1, beats={})
|
||||
trace = select_relevant_entities(
|
||||
outline=outline,
|
||||
characters=[char("林动", role="主角")],
|
||||
world_entities=[],
|
||||
recent_digests=[],
|
||||
excluded=frozenset({("character", "林动")}),
|
||||
)
|
||||
assert all(e.name != "林动" for e in trace.selected)
|
||||
|
||||
|
||||
def test_exclude_wins_over_pin() -> None:
|
||||
# 同一实体同时 pin + exclude(前端不会,但防御):排除优先。
|
||||
outline = OutlineView(volume=1, chapter_no=1, beats={})
|
||||
trace = select_relevant_entities(
|
||||
outline=outline,
|
||||
characters=[char("林动", role="主角")],
|
||||
world_entities=[],
|
||||
recent_digests=[],
|
||||
pinned=frozenset({("character", "林动")}),
|
||||
excluded=frozenset({("character", "林动")}),
|
||||
)
|
||||
assert all(e.name != "林动" for e in trace.selected)
|
||||
|
||||
|
||||
async def test_assemble_override_pins_and_excludes() -> None:
|
||||
from ww_core.domain.injection_repo import EntityRef, InjectionOverride
|
||||
|
||||
repos = build_repos(
|
||||
characters=[char("林动", role="主角"), char("路人乙", role="龙套")],
|
||||
spec=ProjectSpecView(title="书"),
|
||||
)
|
||||
override = InjectionOverride(
|
||||
pinned=[EntityRef(kind="character", name="路人乙")],
|
||||
excluded=[EntityRef(kind="character", name="林动")],
|
||||
)
|
||||
ctx = await assemble(repos, PROJECT, 1, override=override)
|
||||
names = {e.name for e in ctx.selection.selected}
|
||||
assert "路人乙" in names # pin 强制纳入
|
||||
assert "林动" not in names # exclude 强制剔除
|
||||
|
||||
|
||||
async def test_assemble_override_recent_n_limits_digests() -> None:
|
||||
from ww_core.domain.injection_repo import InjectionOverride
|
||||
|
||||
digests = [DigestView(chapter_no=n, facts={"entities": [f"实体{n}"]}) for n in range(1, 6)]
|
||||
world = [WorldEntityView(type="设定", name=f"实体{n}") for n in range(1, 6)]
|
||||
repos = build_repos(
|
||||
outline={10: OutlineView(volume=1, chapter_no=10, beats={})},
|
||||
world=world,
|
||||
digests=digests,
|
||||
spec=ProjectSpecView(title="书"),
|
||||
)
|
||||
# recent_n=1 → 只回看最近一章(第5章)的实体5 命中 recent_digest。
|
||||
ctx = await assemble(repos, PROJECT, 10, override=InjectionOverride(recent_n=1))
|
||||
names = {e.name for e in ctx.selection.selected}
|
||||
assert "实体5" in names
|
||||
assert "实体1" not in names
|
||||
|
||||
|
||||
# ---- render_cards ----
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user