lllin000_PaperForge/tests/test_base_preservation.py
Research Assistant 85a77e7039 hotfix(base): stop regenerating Base views on every sync — prevents duplicate views and preserves user column widths
- ensure_base_views now only creates files on first run; subsequent calls
  only update folder filter, leaving views untouched
- merge_base_views gains width preservation for PF views (backup safety)
- Removed deprecated control_dir from test_setup_wizard (5 cases)
- Fixed NoneType stdout in test_doctor_runs
- Added paperforge status reference to README

Obsidian handles view data from frontmatter changes automatically;
PaperForge should not regenerate views on every sync.
2026-05-10 02:59:41 +08:00

326 lines
11 KiB
Python

"""Tests for incremental merge and user-view preservation (Phase 3, Plan 02)."""
import pytest
from paperforge.worker.base_views import (
PAPERFORGE_VIEW_PREFIX,
build_base_views,
ensure_base_views,
merge_base_views,
)
from paperforge.worker.sync import slugify_filename
class TestIncrementalMerge:
@pytest.fixture(autouse=True)
def setup(self, tmp_path):
self.vault = tmp_path / "Vault"
self.vault.mkdir()
resources = self.vault / "03_Resources"
resources.mkdir()
control = resources / "LiteratureControl"
control.mkdir()
literature = resources / "Literature"
literature.mkdir()
lr = control / "library-records"
lr.mkdir()
(lr / "骨科").mkdir()
self.bases = self.vault / "bases"
self.bases.mkdir()
self.paths = {
"vault": self.vault,
"resources": resources,
"control": control,
"literature": literature,
"library_records": lr,
"bases": self.bases,
}
self.config = {"domains": [{"domain": "骨科"}]}
def test_user_custom_view_is_preserved_after_refresh(self):
"""User adds a custom view, then refresh — custom view must be preserved."""
domain_base = self.bases / f"{slugify_filename('骨科')}.base"
ensure_base_views(self.vault, self.paths, self.config, force=False)
content = domain_base.read_text(encoding="utf-8")
assert content.count("type: table") == 8
user_custom = (
content
+ """
- type: table
name: "My Custom Dashboard"
order:
- title
- year
"""
)
domain_base.write_text(user_custom, encoding="utf-8")
ensure_base_views(self.vault, self.paths, self.config, force=False)
refreshed = domain_base.read_text(encoding="utf-8")
assert "My Custom Dashboard" in refreshed, "User custom view was lost on incremental refresh"
assert refreshed.count("type: table") == 9
assert "控制面板" in refreshed
assert "OCR 完成" in refreshed
def test_standard_paperforge_views_are_updated_on_refresh(self):
"""Standard views are replaced with fresh content on each refresh."""
domain_base = self.bases / f"{slugify_filename('骨科')}.base"
ensure_base_views(self.vault, self.paths, self.config, force=False)
content1 = domain_base.read_text(encoding="utf-8")
assert content1.count(PAPERFORGE_VIEW_PREFIX) == 8
ensure_base_views(self.vault, self.paths, self.config, force=False)
content2 = domain_base.read_text(encoding="utf-8")
assert content2.count("type: table") == 8
assert "${LIBRARY_RECORDS}" not in content2
def test_force_flag_does_full_regeneration(self):
"""--force=True bypasses merge and does full regeneration (all views replaced)."""
domain_base = self.bases / f"{slugify_filename('骨科')}.base"
ensure_base_views(self.vault, self.paths, self.config, force=False)
content1 = domain_base.read_text(encoding="utf-8")
user_custom = (
content1
+ """
- type: table
name: "My Custom View"
order:
- title
"""
)
domain_base.write_text(user_custom, encoding="utf-8")
ensure_base_views(self.vault, self.paths, self.config, force=True)
refreshed = domain_base.read_text(encoding="utf-8")
assert "My Custom View" not in refreshed, "force=True should have replaced all views"
assert refreshed.count("type: table") == 8
assert refreshed.count(PAPERFORGE_VIEW_PREFIX) == 8
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.
"""
domain_base = self.bases / f"{slugify_filename('骨科')}.base"
ensure_base_views(self.vault, self.paths, self.config, force=False)
content1 = domain_base.read_text(encoding="utf-8")
ocr_done_filter_old = "ocr_status = 'done'"
ocr_done_filter_modified = "ocr_status = 'done' AND extra_check = true"
modified = content1.replace(ocr_done_filter_old, ocr_done_filter_modified)
domain_base.write_text(modified, encoding="utf-8")
ensure_base_views(self.vault, self.paths, self.config, force=False)
refreshed = domain_base.read_text(encoding="utf-8")
assert ocr_done_filter_modified in refreshed, "User filter modification should be preserved"
# 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")
assert ocr_done_filter_old in force_refreshed
assert ocr_done_filter_modified not in force_refreshed
def test_new_domain_base_is_created_on_first_run(self):
"""First run creates domain base if none exists."""
domain_base = self.bases / f"{slugify_filename('骨科')}.base"
assert not domain_base.exists()
ensure_base_views(self.vault, self.paths, self.config, force=False)
assert domain_base.exists()
content = domain_base.read_text(encoding="utf-8")
assert content.count("type: table") == 8
assert PAPERFORGE_VIEW_PREFIX in content
class TestLiteratureHubBase:
def test_literature_hub_base_created(self, tmp_path):
vault = tmp_path / "Vault"
vault.mkdir()
resources = vault / "03_Resources"
resources.mkdir()
control = resources / "LiteratureControl"
control.mkdir()
literature = resources / "Literature"
literature.mkdir()
library_records = control / "library-records"
library_records.mkdir()
bases = vault / "bases"
bases.mkdir()
paths = {
"vault": vault,
"resources": resources,
"control": control,
"literature": literature,
"library_records": library_records,
"bases": bases,
}
config = {"domains": [{"domain": "骨科"}, {"domain": "运动医学"}]}
ensure_base_views(vault, paths, config, force=False)
hub_base = bases / "Literature Hub.base"
assert hub_base.exists(), "Literature Hub.base not created"
content = hub_base.read_text(encoding="utf-8")
assert content.count("type: table") == 8, f"Expected 8 views, got {content.count('type: table')}"
assert PAPERFORGE_VIEW_PREFIX in content
assert "${LIBRARY_RECORDS}" not in content, "Placeholder should be substituted"
def test_paperforge_base_not_created(self, tmp_path):
"""PaperForge.base (all records) is NOT created (removed in v1.4.1)."""
vault = tmp_path / "Vault"
vault.mkdir()
resources = vault / "03_Resources"
resources.mkdir()
control = resources / "LiteratureControl"
control.mkdir()
literature = resources / "Literature"
literature.mkdir()
library_records = control / "library-records"
library_records.mkdir()
(library_records / "骨科").mkdir()
bases = vault / "bases"
bases.mkdir()
paths = {
"vault": vault,
"resources": resources,
"control": control,
"literature": literature,
"library_records": library_records,
"bases": bases,
}
config = {"domains": [{"domain": "骨科"}]}
ensure_base_views(vault, paths, config, force=False)
pf_base = bases / "PaperForge.base"
assert not pf_base.exists(), "PaperForge.base should not be created"
class TestMergeBaseViews:
def test_merge_base_views_preserves_user_views(self):
"""merge_base_views preserves views without PAPERFORGE_VIEW_PREFIX."""
existing = """
filters:
and:
- file.inFolder("骨科")
views:
- type: table
name: "控制面板"
order:
- file.name
# PAPERFORGE_VIEW: 推荐分析
- type: table
name: "推荐分析"
order:
- year
filter: 'analyze = true'
- type: table
name: "My Custom View"
order:
- title
"""
views = build_base_views("骨科")
result = merge_base_views(existing, views)
assert "My Custom View" in result, "User view was lost"
assert "推荐分析" in result
assert result.count("type: table") >= 8
def test_merge_base_views_first_run_generates_fresh(self):
"""merge_base_views with no existing content generates fresh YAML."""
views = build_base_views("骨科")
result = merge_base_views(None, views)
assert "views:" in result
assert "type: table" in result
assert "properties:" in result
def test_merge_base_views_unknown_placeholder_unchanged(self):
"""Unknown placeholders in content are left unchanged after merge."""
existing = """
filters:
and:
- file.inFolder("骨科")
views:
- type: table
name: "控制面板"
order:
- file.name
"""
views = build_base_views("骨科")
result = merge_base_views(existing, views)
# Only known placeholders should be substituted
assert "${LIBRARY_RECORDS}" not in result or "03_Resources" in result
def test_merge_base_views_preserves_column_widths(self):
"""User-adjusted column widths in PF views survive merge."""
existing = """
filters:
and:
- file.inFolder("骨科")
views:
# PAPERFORGE_VIEW: 控制面板
- type: table
name: "控制面板"
widths:
file.name: 180
title: 400
year: 60
order:
- file.name
- title
- year
# PAPERFORGE_VIEW: OCR 完成
- type: table
name: "OCR 完成"
widths:
year: 55
title: 380
order:
- year
- title
filter: "ocr_status = 'done'"
"""
views = build_base_views("骨科")
result = merge_base_views(existing, views)
assert "widths:" in result
assert "file.name: 180" in result
assert "title: 400" in result
assert "year: 60" in result
assert "year: 55" in result
assert "title: 380" in result
def test_merge_base_views_widths_not_injected_when_none_exist(self):
"""Fresh PF views without old widths should not have widths injected."""
existing = """
filters:
and:
- file.inFolder("骨科")
views:
# PAPERFORGE_VIEW: 控制面板
- type: table
name: "控制面板"
order:
- file.name
- title
"""
views = build_base_views("骨科")
result = merge_base_views(existing, views)
assert "widths:" not in result