Spaces:
Build error
Build error
Download tests/test_learn/test_integration.py from minhtudragon/headroom_3: direct link, hf CLI and curl.
- Browser
- Download file 11.2 kB
-
https://huggingface.co/spaces/minhtudragon/headroom_3/resolve/096375802a63fa6c724f10b3a7438d2c7c01662b/tests/test_learn/test_integration.py
- Command line
-
hf download hf://spaces/minhtudragon/headroom_3@096375802a63fa6c724f10b3a7438d2c7c01662b/tests/test_learn/test_integration.py
-
curl -L -o test_integration.py https://huggingface.co/spaces/minhtudragon/headroom_3/resolve/096375802a63fa6c724f10b3a7438d2c7c01662b/tests/test_learn/test_integration.py
11.2 kB
| """Integration tests for headroom learn — using real session data. | |
| These tests run against actual conversation data on the machine. | |
| They verify the full pipeline: scan → analyze → recommend → write. | |
| Tests are skipped if the required data directories don't exist. | |
| The LLM-based analyzer tests require ANTHROPIC_API_KEY. | |
| Key behaviors tested: | |
| - Empty recommendations don't create files | |
| - Real Codex/Claude Code sessions can be scanned | |
| - Skip logic: no file writes when nothing meaningful is found | |
| - Idempotency: running twice produces same output | |
| """ | |
| from __future__ import annotations | |
| import os | |
| from pathlib import Path | |
| from unittest.mock import patch | |
| import pytest | |
| from headroom.learn.analyzer import SessionAnalyzer | |
| from headroom.learn.models import ( | |
| ProjectInfo, | |
| Recommendation, | |
| RecommendationTarget, | |
| ) | |
| from headroom.learn.scanner import _greedy_path_decode | |
| from headroom.learn.writer import ClaudeCodeWriter, CodexWriter | |
| # ============================================================================= | |
| # Writer Tests (no LLM needed) | |
| # ============================================================================= | |
| class TestSkipWriteLogic: | |
| """Verify files are NOT written when there's nothing to write.""" | |
| def test_empty_recommendations_no_write(self, tmp_path): | |
| """Zero recommendations = zero files written.""" | |
| proj = ProjectInfo( | |
| name="clean-project", | |
| project_path=tmp_path / "proj", | |
| data_path=tmp_path / "data", | |
| ) | |
| (tmp_path / "proj").mkdir() | |
| (tmp_path / "data" / "memory").mkdir(parents=True) | |
| writer = ClaudeCodeWriter() | |
| result = writer.write([], proj, dry_run=False) | |
| assert result.files_written == [] | |
| assert not (tmp_path / "proj" / "CLAUDE.md").exists() | |
| assert not (tmp_path / "data" / "memory" / "MEMORY.md").exists() | |
| def test_codex_empty_no_write(self, tmp_path): | |
| """Codex writer also skips on empty recommendations.""" | |
| proj = ProjectInfo(name="clean", project_path=tmp_path, data_path=tmp_path) | |
| writer = CodexWriter() | |
| result = writer.write([], proj, dry_run=False) | |
| assert result.files_written == [] | |
| assert not (tmp_path / "AGENTS.md").exists() | |
| class TestIdempotency: | |
| """Running learn twice should produce the same output, not duplicate.""" | |
| def test_double_write_replaces_not_appends(self, tmp_path): | |
| proj = ProjectInfo(name="test", project_path=tmp_path, data_path=tmp_path / "data") | |
| (tmp_path / "data" / "memory").mkdir(parents=True) | |
| recs = [ | |
| Recommendation( | |
| target=RecommendationTarget.CONTEXT_FILE, | |
| section="Environment", | |
| content="- Use `uv run python`", | |
| confidence=0.9, | |
| evidence_count=10, | |
| ) | |
| ] | |
| writer = ClaudeCodeWriter() | |
| # First write | |
| writer.write(recs, proj, dry_run=False) | |
| first_content = (tmp_path / "CLAUDE.md").read_text() | |
| # Second write (same recs) | |
| writer.write(recs, proj, dry_run=False) | |
| second_content = (tmp_path / "CLAUDE.md").read_text() | |
| # Content should be identical (replaced, not appended) | |
| assert first_content == second_content | |
| assert second_content.count("uv run python") == 1 | |
| class TestFalsePositiveFiltering: | |
| """Verify that common false positives don't generate recommendations.""" | |
| def test_sed_output_not_error(self): | |
| """sed printing file content with 'Error:' in it isn't a real error.""" | |
| from headroom.learn.scanner import is_error_content | |
| # Normal code output that happens to contain "error" in identifiers | |
| assert not is_error_content("def handle_error(e):\n print('ok')") | |
| def test_real_error_detected(self): | |
| """Actual errors should be detected.""" | |
| from headroom.learn.scanner import is_error_content | |
| assert is_error_content("ModuleNotFoundError: No module named 'flask'") | |
| assert is_error_content( | |
| "FileNotFoundError: [Errno 2] No such file or directory: '/bad/path'" | |
| ) | |
| assert is_error_content("bash: unknown_cmd: command not found") | |
| # ============================================================================= | |
| # Real-World Integration Tests (skipped if data not present) | |
| # ============================================================================= | |
| CLAUDE_DIR = Path.home() / ".claude" / "projects" | |
| CODEX_DIR = Path.home() / ".codex" / "sessions" | |
| HAS_API_KEY = bool(os.environ.get("ANTHROPIC_API_KEY")) | |
| HAS_CODEX_DATA = CODEX_DIR.exists() and ( | |
| any(CODEX_DIR.rglob("*.json")) or any(CODEX_DIR.rglob("*.jsonl")) | |
| ) | |
| class TestClaudeCodeIntegration: | |
| """Integration tests against real Claude Code session data.""" | |
| def test_scanner_discovers_projects(self): | |
| from headroom.learn.scanner import ClaudeCodeScanner | |
| scanner = ClaudeCodeScanner() | |
| projects = scanner.discover_projects() | |
| assert len(projects) > 0 | |
| for p in projects: | |
| assert p.name | |
| assert p.data_path.exists() | |
| def test_scanner_extracts_events(self): | |
| """Scanner should extract events including user messages.""" | |
| from headroom.learn.scanner import ClaudeCodeScanner | |
| scanner = ClaudeCodeScanner() | |
| projects = scanner.discover_projects() | |
| best = max(projects, key=lambda p: len(list(p.data_path.glob("*.jsonl")))) | |
| sessions = scanner.scan_project(best) | |
| assert len(sessions) > 0 | |
| # At least some sessions should have events | |
| sessions_with_events = [s for s in sessions if s.events] | |
| assert len(sessions_with_events) > 0 | |
| def test_full_pipeline_produces_output(self): | |
| """Scan → analyze on real data produces valid output.""" | |
| from headroom.learn.scanner import ClaudeCodeScanner | |
| scanner = ClaudeCodeScanner() | |
| projects = scanner.discover_projects() | |
| best = max(projects, key=lambda p: len(list(p.data_path.glob("*.jsonl")))) | |
| sessions = scanner.scan_project(best) | |
| assert len(sessions) > 0 | |
| analyzer = SessionAnalyzer() | |
| result = analyzer.analyze(best, sessions) | |
| assert result.total_calls > 0 | |
| assert result.total_sessions > 0 | |
| assert 0 <= result.failure_rate <= 0.5 | |
| def test_dry_run_writes_nothing(self): | |
| """Dry run should never create files.""" | |
| from headroom.learn.scanner import ClaudeCodeScanner | |
| scanner = ClaudeCodeScanner() | |
| projects = scanner.discover_projects() | |
| best = max(projects, key=lambda p: len(list(p.data_path.glob("*.jsonl")))) | |
| # Use mock LLM to avoid needing API key | |
| mock_response = { | |
| "context_file_rules": [ | |
| { | |
| "section": "Test", | |
| "content": "- test rule", | |
| "estimated_tokens_saved": 100, | |
| "evidence_count": 2, | |
| } | |
| ], | |
| "memory_file_rules": [], | |
| } | |
| with patch("headroom.learn.analyzer._call_llm", return_value=mock_response): | |
| sessions = scanner.scan_project(best) | |
| result = SessionAnalyzer(model="gpt-4o").analyze(best, sessions) | |
| recs = result.recommendations | |
| writer = ClaudeCodeWriter() | |
| write_result = writer.write(recs, best, dry_run=True) | |
| assert write_result.dry_run is True | |
| for fp in write_result.files_written: | |
| assert "CLAUDE.md" in fp.name or "MEMORY.md" in fp.name | |
| class TestDecodeProjectPath: | |
| """Unit tests for _greedy_path_decode — covers dot-in-path bug (GitHub.nosync).""" | |
| def test_dot_in_directory_name(self, tmp_path): | |
| """Paths with dots (e.g. GitHub.nosync) must decode correctly. | |
| Claude Code encodes '/' and '.' both as '-', so 'GitHub.nosync' | |
| becomes 'GitHub-nosync' in the directory name. | |
| _greedy_path_decode must reconstruct it by trying '.' as a join. | |
| """ | |
| # base = tmp_path, remaining parts = ["GitHub", "nosync", "myproject"] | |
| # which came from encoding "GitHub.nosync/myproject" as "GitHub-nosync-myproject" | |
| (tmp_path / "GitHub.nosync" / "myproject").mkdir(parents=True) | |
| result = _greedy_path_decode(tmp_path, ["GitHub", "nosync", "myproject"]) | |
| assert result == tmp_path / "GitHub.nosync" / "myproject" | |
| def test_hyphen_in_directory_name(self, tmp_path): | |
| """Paths with literal hyphens decode correctly (existing behavior preserved).""" | |
| (tmp_path / "my-project").mkdir() | |
| result = _greedy_path_decode(tmp_path, ["my", "project"]) | |
| assert result == tmp_path / "my-project" | |
| def test_simple_path_no_ambiguity(self, tmp_path): | |
| """Plain paths with no special chars still decode correctly.""" | |
| (tmp_path / "myproject").mkdir() | |
| result = _greedy_path_decode(tmp_path, ["myproject"]) | |
| assert result == tmp_path / "myproject" | |
| def test_dot_preferred_over_slash_when_slash_missing(self, tmp_path): | |
| """When GitHub/nosync doesn't exist but GitHub.nosync does, use dot.""" | |
| # Only create the dot version, not the slash version | |
| (tmp_path / "GitHub.nosync").mkdir() | |
| result = _greedy_path_decode(tmp_path, ["GitHub", "nosync"]) | |
| assert result == tmp_path / "GitHub.nosync" | |
| class TestCodexIntegration: | |
| """Integration tests against real Codex session data.""" | |
| def test_scanner_discovers_sessions(self): | |
| from headroom.learn.scanner import CodexScanner | |
| scanner = CodexScanner() | |
| projects = scanner.discover_projects() | |
| assert len(projects) == 1 # Codex returns one "project" | |
| def test_full_pipeline(self): | |
| """Full pipeline on real Codex data.""" | |
| from headroom.learn.scanner import CodexScanner | |
| scanner = CodexScanner() | |
| projects = scanner.discover_projects() | |
| sessions = scanner.scan_project(projects[0]) | |
| assert len(sessions) > 0 | |
| # Codex has only Bash tool (shell) | |
| all_tools = {tc.name for s in sessions for tc in s.tool_calls} | |
| assert "Bash" in all_tools | |
| def test_codex_writer_targets_agents_md(self, tmp_path): | |
| """Codex writer should target AGENTS.md, not CLAUDE.md.""" | |
| proj = ProjectInfo(name="codex-test", project_path=tmp_path, data_path=tmp_path) | |
| recs = [ | |
| Recommendation( | |
| target=RecommendationTarget.CONTEXT_FILE, | |
| section="Commands", | |
| content="- Use npm run test", | |
| confidence=0.9, | |
| evidence_count=5, | |
| ) | |
| ] | |
| writer = CodexWriter() | |
| result = writer.write(recs, proj, dry_run=True) | |
| for fp in result.files_written: | |
| assert fp.name in ("AGENTS.md", "instructions.md") | |
| assert "CLAUDE.md" not in fp.name | |