"""URI resolver + 强验证测试(T12,OV3)。 OV3 裁定:可追溯性标准未操作性定义(URI 格式不一致、无 resolver、QA#8 可作弊) → 统一 URI + resolver + 强验证。 本文件测试:URI 解析、provenance 往返、结构化源内定位、批量强验证(防 QA 作弊)。 """ from __future__ import annotations import pytest from genesis.data_models import ( CellComment, CellValue, ExcelTable, ParsedTemplate, Provenance, RuleDocument, SheetType, StructuredSource, ) from genesis.parsers.provenance import build_source_uri from genesis.parsers.resolver import ( SourceRef, URIError, ValidationResult, parse_source_uri, provenance_to_uri, resolve_source_uri, validate_source_uris, ) # ---------- 构造小源 ---------- def _cell(file_name: str, sheet: str, row: int, col: str, value: str) -> CellValue: return CellValue( value=value, provenance=Provenance( file_name=file_name, sheet_name=sheet, row=row, column=col, column_header="x" ), ) def _source_with(cells: list[CellValue]) -> StructuredSource: table = ExcelTable( name="機能一覧", detected_type=SheetType.FUNCTION, extraction_method="structured", headers=["v"], rows=[{"v": c} for c in cells], ) return StructuredSource( tables=[table], template=ParsedTemplate(file_name="t.docx", sections=[], placeholders={}, styles={}), rule_docs=[], image_analyses=[], existing_system=None, comments=[], ) # ---------- URI 解析 ---------- def test_parse_source_uri(): ref = parse_source_uri("要求.xlsx#機能一覧!C3") assert ref.file_name == "要求.xlsx" assert ref.sheet_name == "機能一覧" assert ref.cell_ref == "C3" def test_parse_source_uri_rejects_missing_hash(): with pytest.raises(URIError): parse_source_uri("要求.xlsx機能一覧!C3") def test_parse_source_uri_rejects_missing_bang(): with pytest.raises(URIError): parse_source_uri("要求.xlsx#機能一覧") def test_parse_source_uri_rejects_empty_file(): with pytest.raises(URIError): parse_source_uri("#機能一覧!C3") def test_parse_source_uri_rejects_empty_sheet_or_cell(): with pytest.raises(URIError): parse_source_uri("f.xlsx#!C3") with pytest.raises(URIError): parse_source_uri("f.xlsx#Sheet!") def test_resolve_skips_cell_without_provenance(): """CellValue.provenance 为 None 时不匹配任何 URI(_matches 防御分支)。""" orphan = CellValue(value="x", provenance=None) source = _source_with([orphan]) assert resolve_source_uri("f.xlsx#機能一覧!C3", source) is None # ---------- provenance 往返 ---------- def test_provenance_to_uri_roundtrip(): prov = Provenance(file_name="f.xlsx", sheet_name="S", row=3, column="C", column_header="h") uri = provenance_to_uri(prov) assert uri == "f.xlsx#S!C3" ref = parse_source_uri(uri) assert ref == SourceRef(file_name="f.xlsx", sheet_name="S", cell_ref="C3") def test_build_and_parse_roundtrip(): uri = build_source_uri("要求.xlsx", "機能一覧", "A5") ref = parse_source_uri(uri) assert ref == SourceRef(file_name="要求.xlsx", sheet_name="機能一覧", cell_ref="A5") # ---------- 结构化源内定位 ---------- def test_resolve_finds_existing_cell(): cell = _cell("f.xlsx", "機能一覧", 3, "C", "登録") source = _source_with([cell]) found = resolve_source_uri("f.xlsx#機能一覧!C3", source) assert found is not None assert found.value == "登録" def test_resolve_returns_none_for_missing(): cell = _cell("f.xlsx", "機能一覧", 3, "C", "登録") source = _source_with([cell]) assert resolve_source_uri("f.xlsx#機能一覧!D9", source) is None def test_resolve_distinguishes_sheet(): cell = _cell("f.xlsx", "機能一覧", 3, "C", "登録") source = _source_with([cell]) # 不同 Sheet 的同 cell_ref 不应命中 assert resolve_source_uri("f.xlsx#画面一覧!C3", source) is None # ---------- 批量强验证(防 QA 作弊) ---------- def test_validate_source_uris_split(): cell = _cell("f.xlsx", "機能一覧", 3, "C", "登録") source = _source_with([cell]) uris = ["f.xlsx#機能一覧!C3", "f.xlsx#機能一覧!D9", "invalid-uri"] result = validate_source_uris(uris, source) assert isinstance(result, ValidationResult) assert result.resolved == ["f.xlsx#機能一覧!C3"] assert result.unresolved == ["f.xlsx#機能一覧!D9", "invalid-uri"] def test_validate_rejects_malformed_separately(): """格式错误的 URI 应归入 unresolved(QA#8 不可靠 URI 无效,防作弊)。""" cell = _cell("f.xlsx", "機能一覧", 3, "C", "登録") source = _source_with([cell]) result = validate_source_uris(["garbage-without-hash"], source) assert result.resolved == [] assert result.unresolved == ["garbage-without-hash"]