from __future__ import annotations from pathlib import Path import pytest from gyxx_flow.modules.content_marketing.runtime import runtime_paths PROJECT_ROOT = Path(__file__).resolve().parents[1] RUNTIME_ROOT = ( PROJECT_ROOT / "src" / "gyxx_flow" / "modules" / "content_marketing" / "runtime" ) def _source(relative: str) -> str: return (RUNTIME_ROOT / relative).read_text(encoding="utf-8-sig") def test_content_output_resolver_keeps_paths_inside_the_selected_layer( tmp_path: Path, ) -> None: root = tmp_path / "exports" / "content_marketing" relative = runtime_paths.resolve_layer_output( "reports/monthly.md", layer_root=root, field="--output" ) absolute = runtime_paths.resolve_layer_output( root / "summary" / "daily.md", layer_root=root, field="--output" ) assert relative == (root / "reports" / "monthly.md").resolve() assert absolute == (root / "summary" / "daily.md").resolve() with pytest.raises(ValueError, match="--output"): runtime_paths.resolve_layer_output( "../escape.md", layer_root=root, field="--output" ) with pytest.raises(ValueError, match="--output"): runtime_paths.resolve_layer_output( tmp_path / "outside.md", layer_root=root, field="--output" ) def test_dynamic_mapping_uses_normalized_cache_and_read_only_source_seed() -> None: source = _source("feishu_mapping.py") assert 'MAPPING_DIR = PATHS.normalized_root / "mappings"' in source assert 'MAPPING_SEED_PATH = PATHS.config_root /' in source assert 'SELF_MAPPING_SEED_PATH = PATHS.config_root /' in source assert 'MAPPING_PATH = PATHS.config_root /' not in source assert "if data_dir:" not in source def test_v2_outputs_and_checkpoints_use_their_semantic_layers() -> None: assert 'Path("data") / "v2_results"' not in _source("chanmama_scraper.py") assert 'PATHS.normalized_root / "v2_results"' in _source("chanmama_scraper.py") for relative in ( "bilibili_scraper.py", "pgy_xhs_scraper_v2.py", "xingtu_scraper_v2.py", "self_bilibili_scraper.py", "self_douyin_scraper.py", ): assert 'PATHS.normalized_root / "v2_results"' in _source(relative), relative assert 'PATHS.curated_root / "run_all"' in _source("run_all.py") assert 'PATHS.state_root / "checkpoints" / "bilibili"' in _source( "bilibili_scraper.py" ) assert "PATHS.browser_storage_state_file.write_text" in _source( "xingtu_scraper.py" ) assert 'PATHS.state_root / "checkpoints" / "pgy"' in _source( "pgy_xhs_scraper_v2.py" ) for relative in ( "data/tools/check_status.py", "data/tools/retry_failed.py", "data/tools/sync_metrics_to_cmt_notes.py", ): source = _source(relative) assert 'PATHS.normalized_root / "v2_results"' in source, relative assert 'PATHS.raw_root / "v2_results"' not in source, relative def test_transient_content_artifacts_use_tmp_root() -> None: doc_writer = _source("data/tools/feishu_doc_writer.py") report_card = _source("data/tools/daily_report_card.py") assert 'TMP_DIR = PATHS.tmp_root / "feishu_doc_writer"' in doc_writer assert 'PATHS.tmp_root / "daily_report_card"' in report_card assert 'chart_path.with_name(f"{chart_path.stem}_feishu.png")' not in report_card def test_batch_launchers_never_fall_back_to_module_local_var() -> None: for path in sorted((RUNTIME_ROOT / "data" / "tools").glob("*.bat")): source = path.read_text(encoding="utf-8-sig") if "GYXX_DATA_ROOT" not in source: continue assert '%PROJECT_DIR%\\var' not in source, path.name assert '%GYXX_PROJECT_ROOT%\\var' in source, path.name assert "if not defined GYXX_PROJECT_ROOT" in source, path.name @pytest.mark.parametrize( ("relative", "layer_expression"), ( ("data/tools/analyze_comments.py", "PATHS.exports_root"), ("data/tools/analyze_note.py", "PATHS.exports_root"), ("data/tools/collect_note_metrics.py", "PATHS.normalized_root"), ("data/tools/generate_creator_report.py", "PATHS.exports_root"), ), ) def test_cli_output_overrides_are_constrained_to_a_data_layer( relative: str, layer_expression: str ) -> None: source = _source(relative) assert "resolve_layer_output(" in source assert layer_expression in source