diff --git a/claude_code_log/converter.py b/claude_code_log/converter.py index 74c4307b..aa409b7e 100644 --- a/claude_code_log/converter.py +++ b/claude_code_log/converter.py @@ -4113,6 +4113,9 @@ def convert_jsonl_to( shown_sessions: Optional[set[str]] = None window_rows: Optional[Dict[str, SessionCacheData]] = None record_window = True + if input_path.is_dir() and cache_manager is None and generate_individual_sessions: + # The session writer needs rows even when no render cache was requested. + window_rows = _build_session_data_from_messages(loaded) if windowed: assert output_path is not None if input_path.is_dir(): @@ -5540,10 +5543,12 @@ def _generate_individual_session_files( or not session_file_path.exists() ) else: - # Fallback: no cache, or a format that tracks its own - # freshness (JSON) rather than the shared version marker. + # Without a cache there is no source fingerprint for an existing + # session page; the renderer's version check alone misses appends. + # JSON tracks its own version field when a cache is present. should_regenerate_session = ( - renderer.is_outdated(session_file_path) + cache_manager is None + or renderer.is_outdated(session_file_path) or from_date is not None or to_date is not None or not session_file_path.exists() diff --git a/test/test_date_filtered_runs.py b/test/test_date_filtered_runs.py index e8e9a97b..3e0fd78e 100644 --- a/test/test_date_filtered_runs.py +++ b/test/test_date_filtered_runs.py @@ -182,6 +182,42 @@ def _missing(project: Path, shape: str) -> List[str]: return missing +def test_no_cache_hierarchy_writes_session_pages_linked_from_index( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """An unfiltered no-cache run must not link to absent session pages.""" + project = _build_project(tmp_path) + monkeypatch.setenv("CLAUDE_CODE_LOG_CACHE_PATH", str(tmp_path / "cache.db")) + + process_projects_hierarchy(project.parent, jobs=1, use_cache=False) + + index = (project.parent / "index.html").read_text(encoding="utf-8") + for sid in SESSIONS: + page = project / f"session-{sid}.html" + assert f"session-{sid}.html" in index + assert page.is_file(), f"index links to missing {page.name}" + assert f"first {sid}" in page.read_text(encoding="utf-8") + assert not (tmp_path / "cache.db").exists() + + +def test_no_cache_hierarchy_refreshes_existing_session_page_after_append( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """An uncached rerun must refresh a session page when its source grows.""" + project = _build_project(tmp_path) + monkeypatch.setenv("CLAUDE_CODE_LOG_CACHE_PATH", str(tmp_path / "cache.db")) + process_projects_hierarchy(project.parent, jobs=1, use_cache=False) + page = project / "session-sess-alpha.html" + late = "LATE-UNCACHED-TURN" + assert late not in page.read_text(encoding="utf-8") + + _append(project / "sess-alpha.jsonl", _turn("sess-alpha", 2, 2, late)) + process_projects_hierarchy(project.parent, jobs=1, use_cache=False) + + assert late in page.read_text(encoding="utf-8") + assert not (tmp_path / "cache.db").exists() + + # The hierarchy pass converts only a project with a changed file, so it # reaches a filtered run through an append; a project converted on its own # reaches it with or without one. diff --git a/test/test_output_explicit.py b/test/test_output_explicit.py index 211e1b26..d84caf73 100644 --- a/test/test_output_explicit.py +++ b/test/test_output_explicit.py @@ -367,7 +367,11 @@ def test_grown_session_regenerates_without_cache(self, tmp_path: Path): r2 = runner.invoke(main, [str(proj), "--no-cache"]) assert r2.exit_code == 0, r2.output - assert "skipping regeneration" not in r2.output + # Session pages may be current; only the combined page must regenerate. + assert ( + "combined_transcripts.html is current, skipping regeneration" + not in r2.output + ) second = combined.read_text(encoding="utf-8") assert "SECOND_dir_marker" in second, ( "stale combined HTML served on a --no-cache directory re-run"