|
10 | 10 | from quant_advisor_research.advisory_report import build_advisory_report |
11 | 11 | from quant_advisor_research.archive_backfill import discover_report_paths |
12 | 12 | from quant_advisor_research.publisher import ( |
| 13 | + build_publication_plan, |
13 | 14 | classify_report_path, |
14 | 15 | main as publisher_main, |
15 | 16 | publish_reports, |
@@ -83,6 +84,71 @@ def test_same_day_schema_precedes_generated_at(tmp_path: Path) -> None: |
83 | 84 | assert unique_report_paths_by_content([v6_path, v5_path]) == [v6_path] |
84 | 85 |
|
85 | 86 |
|
| 87 | +@pytest.mark.parametrize( |
| 88 | + ("cadence", "as_of_values"), |
| 89 | + [ |
| 90 | + ("weekly", ("2026-06-15", "2026-06-21")), |
| 91 | + ("monthly", ("2026-06-01", "2026-06-30")), |
| 92 | + ], |
| 93 | +) |
| 94 | +def test_publication_plan_groups_same_canonical_period_variants( |
| 95 | + tmp_path: Path, cadence: str, as_of_values: tuple[str, str] |
| 96 | +) -> None: |
| 97 | + first = build_v5(as_of_values[0]) |
| 98 | + second = build_v5(as_of_values[1]) |
| 99 | + first["cadence"] = second["cadence"] = cadence |
| 100 | + second["recommendations"][0]["reasons"] = ["different semantic content"] |
| 101 | + first_path = write_report(tmp_path / "first.json", first) |
| 102 | + second_path = write_report(tmp_path / "second.json", second) |
| 103 | + |
| 104 | + plan = build_publication_plan([first_path, second_path]) |
| 105 | + reversed_plan = build_publication_plan([second_path, first_path]) |
| 106 | + |
| 107 | + assert len(plan.entries) == 2 |
| 108 | + assert sum(entry.canonical_owner for entry in plan.entries) == 1 |
| 109 | + assert plan.entries[0].canonical_owner is True |
| 110 | + assert ".variant-" in plan.entries[1].json_name |
| 111 | + assert [(entry.json_name, entry.source_path.name) for entry in plan.entries] == [ |
| 112 | + (entry.json_name, entry.source_path.name) for entry in reversed_plan.entries |
| 113 | + ] |
| 114 | + |
| 115 | + |
| 116 | +def test_publication_plan_pins_mandatory_current_as_group_owner_and_first( |
| 117 | + tmp_path: Path, |
| 118 | +) -> None: |
| 119 | + current = build_v5("2026-06-21") |
| 120 | + recovered = build_v6("2026-06-15") |
| 121 | + recovered["summary"]["top_theme_ids"] = ["different-theme"] |
| 122 | + recovered["generated_at"] = "2026-06-16T12:00:00Z" |
| 123 | + recovered["expires_at"] = "2026-06-23T12:00:00Z" |
| 124 | + current_path = write_report(tmp_path / "advisory_report_2026-06-21.json", current) |
| 125 | + recovered_path = write_report(tmp_path / "recovered.json", recovered) |
| 126 | + |
| 127 | + plan = build_publication_plan( |
| 128 | + [current_path], mandatory_current=current_path, recovered_history=[recovered_path] |
| 129 | + ) |
| 130 | + |
| 131 | + assert len(plan.entries) == 2 |
| 132 | + assert plan.entries[0].source_path == current_path |
| 133 | + assert plan.entries[0].canonical_owner is True |
| 134 | + assert ".variant-" in plan.entries[1].json_name |
| 135 | + |
| 136 | + output = tmp_path / "site" |
| 137 | + publish_reports( |
| 138 | + [current_path], |
| 139 | + output, |
| 140 | + site_url="https://example.invalid", |
| 141 | + feed_title="Test", |
| 142 | + mandatory_current=current_path, |
| 143 | + recovered_history=[recovered_path], |
| 144 | + ) |
| 145 | + reports_index = json.loads((output / "reports_index.json").read_text(encoding="utf-8")) |
| 146 | + feed = (output / "feed.xml").read_text(encoding="utf-8") |
| 147 | + assert reports_index["reports"][0]["json"] == "advisory_report_2026-06-21.json" |
| 148 | + assert feed.index("2026-06-21") < feed.index("2026-06-15") |
| 149 | + assert "Tue, 16 Jun 2026 12:00:00 GMT" in feed |
| 150 | + |
| 151 | + |
86 | 152 | def test_invalid_v6_cannot_shadow_valid_v5(tmp_path: Path) -> None: |
87 | 153 | v5_path = write_report(tmp_path / "v5.json", build_v5()) |
88 | 154 | invalid_v6 = build_v6() |
|
0 commit comments