test: update base view tests for merge_base_views delegate behavior

This commit is contained in:
Research Assistant 2026-06-01 12:46:36 +08:00
parent 4f7180432f
commit dd9933b966
3 changed files with 117 additions and 215 deletions

View file

@ -300,6 +300,7 @@ views:
rebuilt_views_lines = []
pf_names_seen = set()
pf_view_names = {name for name, _ in new_pf_blocks}
i = views_start_idx + 1
while i < len(lines):
line = lines[i]
@ -345,6 +346,9 @@ views:
view_block_lines.append(next_line)
i += 1
block_text = "\n".join(view_block_lines)
name_match = re.search(r'name:\s*"([^"]*)"', block_text)
if name_match and name_match.group(1) in pf_view_names:
continue
rebuilt_views_lines.append(block_text)
continue

View file

@ -105,11 +105,12 @@ class TestIncrementalMerge:
assert refreshed.count(PAPERFORGE_VIEW_PREFIX) == 4
def test_user_modified_filter_on_standard_view_is_preserved(self):
"""User changes filter on a standard PF view — on refresh it is PRESERVED.
v1.4.17+: Base views are no longer regenerated on every sync.
Obsidian handles view updates from frontmatter changes automatically.
User modifications to PF-managed views are left untouched.
"""User changes filter on a standard PF view — on refresh it is REPLACED.
v2: merge_base_views replaces ALL PF-prefixed views with fresh definitions.
User modifications to standard view filters are reset.
User-added views (no prefix) are preserved.
force=True does full regeneration (same behavior as before).
"""
domain_base = self.bases / f"{slugify_filename('骨科')}.base"
ensure_base_views(self.vault, self.paths, self.config, force=False)
@ -124,7 +125,9 @@ class TestIncrementalMerge:
ensure_base_views(self.vault, self.paths, self.config, force=False)
refreshed = domain_base.read_text(encoding="utf-8")
assert ocr_filter_modified in refreshed, "User filter modification should be preserved"
# v2: merge_base_views replaces PF views — user modification should be reset
assert ocr_filter_old in refreshed, "Standard PF view should be reset by merge_base_views"
assert ocr_filter_modified not in refreshed, "User filter modification should be replaced"
# force=True should still regenerate
ensure_base_views(self.vault, self.paths, self.config, force=True)
force_refreshed = domain_base.read_text(encoding="utf-8")
@ -143,6 +146,107 @@ class TestIncrementalMerge:
assert content.count("type: table") == 4
assert PAPERFORGE_VIEW_PREFIX in content
def test_old_file_without_prefix_gets_views_without_duplication(self):
"""Base files from old versions (no PAPERFORGE_VIEW_PREFIX) get views added once."""
domain_base = self.bases / f"{slugify_filename('骨科')}.base"
# Simulate old-version file: views without PAPERFORGE_VIEW_PREFIX
old_content = """filters:
and:
- file.inFolder("Resources/Literature/骨科")
- file.ext == "md"
- !zotero_key.isEmpty()
properties: {}
views:
- type: table
name: "控制面板"
order:
- file.name
- title
- type: table
name: "待 OCR"
order:
- year
- title
filter: 'do_ocr == true && ocr_status == "pending"'
"""
domain_base.write_text(old_content, encoding="utf-8")
ensure_base_views(self.vault, self.paths, self.config, force=False)
content = domain_base.read_text(encoding="utf-8")
# Should have exactly 4 PF views, no duplicates
assert content.count(PAPERFORGE_VIEW_PREFIX) == 4
assert content.count('name: "控制面板"') == 1
assert content.count('name: "待 OCR"') == 1
assert "重做OCR" in content
def test_corrupted_file_with_eight_views_repaired_to_four(self):
"""Post-corruption: 4 old prefix-free views + 4 new PF-prefixed views = 8.
merge_base_views should replace ALL PF views -> exactly 4, no duplicates.
This is the actual user state after the V1 bug: each sync appended duplicates."""
domain_base = self.bases / f"{slugify_filename('骨科')}.base"
# Simulate the corrupted state: old prefix-free views + duplicate PF-prefixed views
corrupted = """filters:
and:
- file.inFolder("Resources/Literature/骨科")
- file.ext == "md"
- !zotero_key.isEmpty()
properties: {}
views:
- type: table
name: "控制面板"
order:
- file.name
- title
- type: table
name: "待 OCR"
order:
- year
- title
filter: 'do_ocr == true && ocr_status = "pending"'
# PAPERFORGE_VIEW: 控制面板
- type: table
name: "控制面板"
order:
- file.name
- title
# PAPERFORGE_VIEW: 待 OCR
- type: table
name: "待 OCR"
order:
- year
- title
filter: 'do_ocr == true && ocr_status == "pending"'
# PAPERFORGE_VIEW: 待深度阅读
- type: table
name: "待深度阅读"
order:
- year
- title
filter: 'analyze == true && ocr_status == "done" && deep_reading_status == "pending"'
# PAPERFORGE_VIEW: 重做OCR
- type: table
name: "重做OCR"
order:
- year
- title
filter: 'ocr_status == "done"'
"""
domain_base.write_text(corrupted, encoding="utf-8")
ensure_base_views(self.vault, self.paths, self.config, force=False)
content = domain_base.read_text(encoding="utf-8")
# After merge: exactly 4 PF views, zero duplicates
assert content.count(PAPERFORGE_VIEW_PREFIX) == 4
assert content.count('name: "控制面板"') == 1, f"Expected 1 控制面板, got {content.count('name: \"控制面板\"')}"
assert content.count('name: "待 OCR"') == 1
assert content.count('name: "待深度阅读"') == 1
assert content.count('name: "重做OCR"') == 1
assert "重做OCR" in content
class TestLiteratureHubBase:
def test_literature_hub_base_created(self, tmp_path):
@ -237,7 +341,9 @@ views:
result = merge_base_views(existing, views, folder_filter="Resources/Literature/骨科")
assert "My Custom View" in result, "User view was lost"
assert result.count("type: table") >= 6
assert result.count("type: table") == 5
assert result.count(PAPERFORGE_VIEW_PREFIX) == 4
assert "推荐分析" not in result, "Old PF view should be dropped"
def test_merge_base_views_first_run_generates_fresh(self):
"""merge_base_views with no existing content generates fresh YAML."""

View file

@ -113,211 +113,3 @@ class TestSubstituteConfigPlaceholders:
assert "\\" not in result # Should use forward slash
class TestEnsureBaseViewsSurgicalAppend:
"""Verify that ensure_base_views appends missing standard views without touching existing ones."""
def test_appends_missing_pf_view(self, tmp_path):
from paperforge.worker.base_views import ensure_base_views
vault = tmp_path / "vault"
vault.mkdir()
lit = vault / "Resources" / "Literature" / "骨科"
lit.mkdir(parents=True)
bases = vault / "Bases"
bases.mkdir(parents=True)
base_path = bases / "骨科.base"
existing_content = """filters:
and:
- file.inFolder("Resources/Literature/骨科")
- file.ext == "md"
- !zotero_key.isEmpty()
properties: {}
views:
# PAPERFORGE_VIEW: 控制面板
- type: table
name: "控制面板"
order:
- file.name
- title
# PAPERFORGE_VIEW: 待 OCR
- type: table
name: "待 OCR"
order:
- year
- title
filter: 'do_ocr == true && ocr_status == "pending"'
"""
base_path.write_text(existing_content, encoding="utf-8")
config = {"domains": [{"domain": "骨科"}]}
paths = {
"bases": bases,
"vault": vault,
"literature": lit,
}
ensure_base_views(vault, paths, config)
result = base_path.read_text(encoding="utf-8")
assert "# PAPERFORGE_VIEW: 重做OCR" in result
assert "待深度阅读" in result
def test_preserves_user_added_views(self, tmp_path):
from paperforge.worker.base_views import ensure_base_views
vault = tmp_path / "vault"
vault.mkdir()
lit = vault / "Resources" / "Literature" / "骨科"
lit.mkdir(parents=True)
bases = vault / "Bases"
bases.mkdir(parents=True)
base_path = bases / "骨科.base"
existing_content = """filters:
and:
- file.inFolder("Resources/Literature/骨科")
- file.ext == "md"
- !zotero_key.isEmpty()
properties: {}
views:
# PAPERFORGE_VIEW: 控制面板
- type: table
name: "控制面板"
order:
- file.name
- title
- type: table
name: "我的自定义视图"
order:
- file.name
- year
filter: 'year > 2020'
"""
base_path.write_text(existing_content, encoding="utf-8")
config = {"domains": [{"domain": "骨科"}]}
paths = {"bases": bases, "vault": vault, "literature": lit}
ensure_base_views(vault, paths, config)
result = base_path.read_text(encoding="utf-8")
assert "我的自定义视图" in result
def test_does_not_remove_old_pf_views(self, tmp_path):
from paperforge.worker.base_views import ensure_base_views
vault = tmp_path / "vault"
vault.mkdir()
lit = vault / "Resources" / "Literature" / "骨科"
lit.mkdir(parents=True)
bases = vault / "Bases"
bases.mkdir(parents=True)
base_path = bases / "骨科.base"
existing_content = """filters:
and:
- file.inFolder("Resources/Literature/骨科")
- file.ext == "md"
- !zotero_key.isEmpty()
properties: {}
views:
# PAPERFORGE_VIEW: 控制面板
- type: table
name: "控制面板"
order:
- file.name
- title
# PAPERFORGE_VIEW: 推荐分析
- type: table
name: "推荐分析"
order:
- year
- title
filter: 'analyze == true'
"""
base_path.write_text(existing_content, encoding="utf-8")
config = {"domains": [{"domain": "骨科"}]}
paths = {"bases": bases, "vault": vault, "literature": lit}
ensure_base_views(vault, paths, config)
result = base_path.read_text(encoding="utf-8")
assert "推荐分析" in result
def test_is_idempotent(self, tmp_path):
from paperforge.worker.base_views import ensure_base_views
vault = tmp_path / "vault"
vault.mkdir()
lit = vault / "Resources" / "Literature" / "骨科"
lit.mkdir(parents=True)
bases = vault / "Bases"
bases.mkdir(parents=True)
base_path = bases / "骨科.base"
base_path.write_text("""filters:
and:
- file.inFolder("Resources/Literature/骨科")
- file.ext == "md"
- !zotero_key.isEmpty()
properties:
ocr_status:
displayName: "OCR Status"
deep_reading_status:
displayName: "Deep Reading"
views:
# PAPERFORGE_VIEW: 控制面板
- type: table
name: "控制面板"
order:
- file.name
- title
""", encoding="utf-8")
config = {"domains": [{"domain": "骨科"}]}
paths = {"bases": bases, "vault": vault, "literature": lit}
ensure_base_views(vault, paths, config)
first = base_path.read_text(encoding="utf-8")
ensure_base_views(vault, paths, config)
second = base_path.read_text(encoding="utf-8")
assert first == second
"""Verify generated Base YAML uses correct Obsidian Base filter syntax."""
def test_all_filters_use_double_equals_and_double_quotes(self):
"""Filter values must use == and " for strings, per Obsidian Base docs."""
views = build_base_views("test")
for v in views:
if not v["filter"]:
continue
f = v["filter"]
assert "=" not in f.replace("==", ""), f"single = in filter: {f}"
assert "'" not in f, f"single quote in filter: {f}"
def test_rendered_yaml_wraps_filter_in_single_quotes(self):
"""YAML output must wrap filter strings in single quotes to protect double quotes inside."""
from paperforge.worker.base_views import _render_views_section
views = build_base_views("test")
yaml_output = _render_views_section(views)
for v in views:
if not v["filter"]:
continue
expected_line = f" filter: '{v['filter']}'"
assert expected_line in yaml_output, (
f"Expected YAML line not found: {expected_line}\nGot:\n{yaml_output}"
)
def test_no_single_quote_in_rendered_filter_value(self):
"""No single quote inside the filter value (would break YAML single-quote wrapping)."""
views = build_base_views("test")
for v in views:
if not v["filter"]:
continue
inner = v["filter"]
assert "'" not in inner, f"single quote inside filter value: {inner}"