Add AST-based code compression and custom model configuration
CodeAwareCompressor:
- Tree-sitter based AST parsing for Python, JS, TS, Go, Rust, Java, C, C++
- Preserves imports, signatures, type annotations, error handlers
- Guarantees syntactically valid output
- Uses tree-sitter-language-pack for broad language support
ContentRouter:
- Intelligent compression orchestrator
- Auto-routes content to optimal compressor based on type detection
- Source hint support for high-confidence routing
Custom Model Configuration:
- HEADROOM_MODEL_LIMITS env var and ~/.headroom/models.json support
- Pattern-based inference for unknown models (opus/sonnet/haiku tiers)
- Support for Claude 4.5, Claude 4, o3, o3-mini
- Graceful fallback - never crashes on unknown models
2026-01-14 13:46:55 -08:00
|
|
|
"""Tests for Code-Aware Compressor using tree-sitter AST parsing.
|
|
|
|
|
|
|
|
|
|
Comprehensive tests covering:
|
|
|
|
|
- CodeCompressorConfig: Configuration validation and defaults
|
|
|
|
|
- CodeAwareCompressor: Core AST-based compression functionality
|
|
|
|
|
- Language detection: Auto-detection from extensions and content
|
|
|
|
|
- Transform interface: apply(), should_apply() methods
|
|
|
|
|
- Syntax preservation: Guarantees valid output syntax
|
|
|
|
|
- Edge cases: Empty content, unavailable dependency, fallbacks
|
|
|
|
|
"""
|
|
|
|
|
|
fix(code): validate Python compressed syntax (#1302)
## Description
Fix a Python code-compression validity gap from #1233 where tree-sitter
parsing could mark compressed output as syntactically valid even when
Python compile-time syntax rules reject it.
This keeps `from __future__ import ...` statements in the
import-preservation bucket so they stay before executable definitions,
and adds Python `compile(..., "exec")` verification after `ast.parse`.
It also keeps the earlier conservative class-method decorator
indentation hardening from this branch.
Refs #1233.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- Treat Python `future_import_statement` nodes as preserved imports.
- Verify Python compressed output with both `ast.parse` and
`compile(..., "exec")`.
- Preserve original source-line indentation for decorators attached to
class methods.
- Add a regression fixture covering `from __future__ import
annotations`, class decorators, property decorators, async methods, and
`match` statements.
- Add a direct regression assertion that future imports stay before
executable definitions.
- Document the user-visible fix in `CHANGELOG.md`.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check`)
- [ ] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ /tmp/headroom-issue-1233-venv/bin/python -m pytest tests/test_transforms/test_code_compressor.py::TestTreeSitterIntegration::test_python_future_import_stays_at_module_start -q
1 passed, 1 warning
$ /tmp/headroom-issue-1233-venv/bin/python -m pytest tests/test_transforms/test_code_compressor.py -q
61 passed, 1 warning
$ uv run --extra dev ruff check headroom/transforms/code_compressor.py tests/test_transforms/test_code_compressor.py
All checks passed!
$ uv run --extra dev ruff format --check headroom/transforms/code_compressor.py tests/test_transforms/test_code_compressor.py
2 files already formatted
$ git diff --check
# no output
```
## Real Behavior Proof
- Environment: macOS, Python 3.11.14, local checkout with `[code]`
dependencies installed in `/tmp/headroom-issue-1233-venv`.
- Exact command / steps: added
`test_python_future_import_stays_at_module_start`, ran it before the fix
to confirm the compressed output failure, then reran the focused test
and full `tests/test_transforms/test_code_compressor.py` after the
patch.
- Observed result: before this patch, the regression fixture produced
compressed Python with `from __future__ import annotations` after
class/function definitions. `result.syntax_valid` was `True`, but
`compile(result.compressed, "<test>", "exec")` failed with `SyntaxError:
from __future__ imports must occur at the beginning of the file`. After
this patch, the focused regression and full code-compressor test file
pass locally, and the regression now directly asserts that the future
import appears before executable definitions.
- Not tested: full repository pytest, `mypy headroom`, and a broad
corpus run over third-party source files.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project style guidelines
- [x] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable
## Screenshots (if applicable)
N/A
## Additional Notes
This PR is now scoped to the stable compile-time failure path in #1233.
The broader syntax-failure rate from the issue may still need
corpus-level follow-up.
Co-authored-by: JD Davis <mxjerrett@gmail.com>
2026-06-24 03:41:14 +08:00
|
|
|
import textwrap
|
Add AST-based code compression and custom model configuration
CodeAwareCompressor:
- Tree-sitter based AST parsing for Python, JS, TS, Go, Rust, Java, C, C++
- Preserves imports, signatures, type annotations, error handlers
- Guarantees syntactically valid output
- Uses tree-sitter-language-pack for broad language support
ContentRouter:
- Intelligent compression orchestrator
- Auto-routes content to optimal compressor based on type detection
- Source hint support for high-confidence routing
Custom Model Configuration:
- HEADROOM_MODEL_LIMITS env var and ~/.headroom/models.json support
- Pattern-based inference for unknown models (opus/sonnet/haiku tiers)
- Support for Claude 4.5, Claude 4, o3, o3-mini
- Graceful fallback - never crashes on unknown models
2026-01-14 13:46:55 -08:00
|
|
|
from unittest.mock import patch
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
from headroom.transforms.code_compressor import (
|
|
|
|
|
CodeAwareCompressor,
|
|
|
|
|
CodeCompressionResult,
|
|
|
|
|
CodeCompressorConfig,
|
|
|
|
|
CodeLanguage,
|
|
|
|
|
DocstringMode,
|
|
|
|
|
detect_language,
|
|
|
|
|
is_tree_sitter_available,
|
|
|
|
|
is_tree_sitter_loaded,
|
|
|
|
|
unload_tree_sitter,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Try to import for availability check
|
|
|
|
|
try:
|
|
|
|
|
import tree_sitter_language_pack # noqa: F401
|
|
|
|
|
|
|
|
|
|
TREE_SITTER_INSTALLED = True
|
|
|
|
|
except ImportError:
|
|
|
|
|
TREE_SITTER_INSTALLED = False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# Test Fixtures
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def default_config():
|
|
|
|
|
"""Default CodeCompressorConfig for testing."""
|
|
|
|
|
return CodeCompressorConfig(
|
|
|
|
|
min_tokens_for_compression=10, # Low threshold for tests
|
|
|
|
|
enable_ccr=False, # Disable CCR for unit tests
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def compressor(default_config):
|
|
|
|
|
"""CodeAwareCompressor instance with default config."""
|
|
|
|
|
return CodeAwareCompressor(default_config)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def tokenizer():
|
|
|
|
|
"""Get a tokenizer for Transform interface tests."""
|
|
|
|
|
from headroom.providers import OpenAIProvider
|
|
|
|
|
from headroom.tokenizer import Tokenizer
|
|
|
|
|
|
|
|
|
|
provider = OpenAIProvider()
|
|
|
|
|
token_counter = provider.get_token_counter("gpt-4o")
|
|
|
|
|
return Tokenizer(token_counter, "gpt-4o")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# Test Data Generators
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def generate_python_code(n_functions: int = 5, n_classes: int = 1) -> str:
|
|
|
|
|
"""Generate Python code for testing."""
|
|
|
|
|
lines = [
|
|
|
|
|
'"""Module with classes and functions."""',
|
|
|
|
|
"",
|
|
|
|
|
"import os",
|
|
|
|
|
"import sys",
|
|
|
|
|
"from typing import Any, Optional, List",
|
|
|
|
|
"from dataclasses import dataclass",
|
|
|
|
|
"",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
for c in range(n_classes):
|
|
|
|
|
lines.extend(
|
|
|
|
|
[
|
|
|
|
|
"@dataclass",
|
|
|
|
|
f"class TestClass{c}:",
|
|
|
|
|
' """A test class with docstring."""',
|
|
|
|
|
" name: str",
|
|
|
|
|
" value: int = 0",
|
|
|
|
|
"",
|
|
|
|
|
" def method(self, arg: Any) -> str:",
|
|
|
|
|
' """Process the argument."""',
|
|
|
|
|
" result = str(arg)",
|
|
|
|
|
" for i in range(10):",
|
|
|
|
|
' result += f"iteration {i}"',
|
|
|
|
|
" return result",
|
|
|
|
|
"",
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
for i in range(n_functions):
|
|
|
|
|
lines.extend(
|
|
|
|
|
[
|
|
|
|
|
f"def function_{i}(arg: Any, optional: Optional[str] = None) -> str:",
|
|
|
|
|
f' """Process argument {i}.',
|
|
|
|
|
"",
|
|
|
|
|
" This is a longer docstring with multiple lines.",
|
|
|
|
|
" It explains what the function does in detail.",
|
|
|
|
|
"",
|
|
|
|
|
" Args:",
|
|
|
|
|
" arg: The argument to process.",
|
|
|
|
|
" optional: An optional parameter.",
|
|
|
|
|
"",
|
|
|
|
|
" Returns:",
|
|
|
|
|
" A string result.",
|
|
|
|
|
' """',
|
|
|
|
|
" result = str(arg)",
|
|
|
|
|
" if optional:",
|
|
|
|
|
" result += optional",
|
|
|
|
|
" for i in range(10):",
|
|
|
|
|
' result += f"iteration {i}"',
|
|
|
|
|
" try:",
|
|
|
|
|
" int(result)",
|
|
|
|
|
" except ValueError:",
|
|
|
|
|
' result = "0"',
|
|
|
|
|
" return result",
|
|
|
|
|
"",
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def generate_javascript_code(n_functions: int = 5) -> str:
|
|
|
|
|
"""Generate JavaScript code for testing."""
|
|
|
|
|
lines = [
|
|
|
|
|
"// Module with various functions",
|
|
|
|
|
'import { something } from "module";',
|
|
|
|
|
'const config = require("./config");',
|
|
|
|
|
"",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
for i in range(n_functions):
|
|
|
|
|
lines.extend(
|
|
|
|
|
[
|
|
|
|
|
"/**",
|
|
|
|
|
f" * Process function {i}",
|
|
|
|
|
" * @param {any} arg - The argument",
|
|
|
|
|
" * @returns {string} The result",
|
|
|
|
|
" */",
|
|
|
|
|
f"function processFunction{i}(arg) {{",
|
|
|
|
|
" let result = String(arg);",
|
|
|
|
|
" for (let j = 0; j < 10; j++) {",
|
|
|
|
|
" result += `iteration ${j}`;",
|
|
|
|
|
" }",
|
|
|
|
|
" try {",
|
|
|
|
|
" JSON.parse(result);",
|
|
|
|
|
" } catch (e) {",
|
|
|
|
|
" console.error(e);",
|
|
|
|
|
" }",
|
|
|
|
|
" return result;",
|
|
|
|
|
"}",
|
|
|
|
|
"",
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
lines.append("export { processFunction0 };")
|
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def generate_go_code(n_functions: int = 3) -> str:
|
|
|
|
|
"""Generate Go code for testing."""
|
|
|
|
|
lines = [
|
|
|
|
|
"package main",
|
|
|
|
|
"",
|
|
|
|
|
'import "fmt"',
|
|
|
|
|
"",
|
|
|
|
|
"// Config holds configuration",
|
|
|
|
|
"type Config struct {",
|
|
|
|
|
" Name string",
|
|
|
|
|
" Value int",
|
|
|
|
|
"}",
|
|
|
|
|
"",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
for i in range(n_functions):
|
|
|
|
|
lines.extend(
|
|
|
|
|
[
|
|
|
|
|
f"// Process{i} processes the input",
|
|
|
|
|
f"func Process{i}(input string) (string, error) {{",
|
|
|
|
|
" result := input",
|
|
|
|
|
" for i := 0; i < 10; i++ {",
|
|
|
|
|
' result = fmt.Sprintf("%s-%d", result, i)',
|
|
|
|
|
" }",
|
|
|
|
|
" if len(result) == 0 {",
|
|
|
|
|
' return "", fmt.Errorf("empty result")',
|
|
|
|
|
" }",
|
|
|
|
|
" return result, nil",
|
|
|
|
|
"}",
|
|
|
|
|
"",
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# TestCodeCompressorConfig
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestCodeCompressorConfig:
|
|
|
|
|
"""Tests for CodeCompressorConfig dataclass."""
|
|
|
|
|
|
|
|
|
|
def test_default_values(self):
|
|
|
|
|
"""Default config values are sensible."""
|
|
|
|
|
config = CodeCompressorConfig()
|
|
|
|
|
|
|
|
|
|
assert config.preserve_imports is True
|
|
|
|
|
assert config.preserve_signatures is True
|
|
|
|
|
assert config.preserve_type_annotations is True
|
|
|
|
|
assert config.preserve_decorators is True
|
|
|
|
|
assert config.docstring_mode == DocstringMode.FIRST_LINE
|
|
|
|
|
assert config.target_compression_rate == 0.2
|
|
|
|
|
assert config.max_body_lines == 5
|
|
|
|
|
assert config.min_tokens_for_compression == 100
|
|
|
|
|
assert config.enable_ccr is True
|
|
|
|
|
|
|
|
|
|
def test_custom_values(self):
|
|
|
|
|
"""Custom config values are applied."""
|
|
|
|
|
config = CodeCompressorConfig(
|
|
|
|
|
preserve_imports=False,
|
|
|
|
|
preserve_signatures=True,
|
|
|
|
|
docstring_mode=DocstringMode.FULL,
|
|
|
|
|
target_compression_rate=0.3,
|
|
|
|
|
max_body_lines=10,
|
|
|
|
|
min_tokens_for_compression=50,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert config.preserve_imports is False
|
|
|
|
|
assert config.preserve_signatures is True
|
|
|
|
|
assert config.docstring_mode == DocstringMode.FULL
|
|
|
|
|
assert config.target_compression_rate == 0.3
|
|
|
|
|
assert config.max_body_lines == 10
|
|
|
|
|
assert config.min_tokens_for_compression == 50
|
|
|
|
|
|
|
|
|
|
def test_docstring_modes(self):
|
|
|
|
|
"""All docstring modes are valid."""
|
|
|
|
|
for mode in DocstringMode:
|
|
|
|
|
config = CodeCompressorConfig(docstring_mode=mode)
|
|
|
|
|
assert config.docstring_mode == mode
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# TestCodeCompressionResult
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestCodeCompressionResult:
|
|
|
|
|
"""Tests for CodeCompressionResult dataclass."""
|
|
|
|
|
|
|
|
|
|
def test_tokens_saved(self):
|
|
|
|
|
"""tokens_saved property calculates correctly."""
|
|
|
|
|
result = CodeCompressionResult(
|
|
|
|
|
compressed="short",
|
|
|
|
|
original="long content here",
|
|
|
|
|
original_tokens=100,
|
|
|
|
|
compressed_tokens=30,
|
|
|
|
|
compression_ratio=0.3,
|
|
|
|
|
language=CodeLanguage.PYTHON,
|
|
|
|
|
syntax_valid=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert result.tokens_saved == 70
|
|
|
|
|
|
|
|
|
|
def test_tokens_saved_no_negative(self):
|
|
|
|
|
"""tokens_saved never returns negative."""
|
|
|
|
|
result = CodeCompressionResult(
|
|
|
|
|
compressed="expanded",
|
|
|
|
|
original="short",
|
|
|
|
|
original_tokens=10,
|
|
|
|
|
compressed_tokens=20,
|
|
|
|
|
compression_ratio=2.0,
|
|
|
|
|
language=CodeLanguage.PYTHON,
|
|
|
|
|
syntax_valid=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert result.tokens_saved == 0
|
|
|
|
|
|
|
|
|
|
def test_savings_percentage(self):
|
|
|
|
|
"""savings_percentage property calculates correctly."""
|
|
|
|
|
result = CodeCompressionResult(
|
|
|
|
|
compressed="short",
|
|
|
|
|
original="long content",
|
|
|
|
|
original_tokens=100,
|
|
|
|
|
compressed_tokens=25,
|
|
|
|
|
compression_ratio=0.25,
|
|
|
|
|
language=CodeLanguage.PYTHON,
|
|
|
|
|
syntax_valid=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert result.savings_percentage == 75.0
|
|
|
|
|
|
|
|
|
|
def test_savings_percentage_zero_original(self):
|
|
|
|
|
"""savings_percentage handles zero original tokens."""
|
|
|
|
|
result = CodeCompressionResult(
|
|
|
|
|
compressed="",
|
|
|
|
|
original="",
|
|
|
|
|
original_tokens=0,
|
|
|
|
|
compressed_tokens=0,
|
|
|
|
|
compression_ratio=1.0,
|
|
|
|
|
language=CodeLanguage.UNKNOWN,
|
|
|
|
|
syntax_valid=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert result.savings_percentage == 0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# TestCodeLanguage
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestCodeLanguage:
|
|
|
|
|
"""Tests for CodeLanguage enum and detection."""
|
|
|
|
|
|
|
|
|
|
def test_all_language_values_are_unique(self):
|
|
|
|
|
"""All language enum values are unique."""
|
|
|
|
|
values = [lang.value for lang in CodeLanguage]
|
|
|
|
|
assert len(values) == len(set(values))
|
|
|
|
|
|
|
|
|
|
def test_detect_python_language(self):
|
|
|
|
|
"""Python language is detected from code patterns."""
|
|
|
|
|
|
|
|
|
|
code = """
|
|
|
|
|
import os
|
|
|
|
|
from typing import List
|
|
|
|
|
|
|
|
|
|
def function(arg: str) -> str:
|
|
|
|
|
return arg
|
|
|
|
|
|
|
|
|
|
class MyClass:
|
|
|
|
|
pass
|
|
|
|
|
"""
|
|
|
|
|
lang, confidence = detect_language(code)
|
|
|
|
|
assert lang == CodeLanguage.PYTHON
|
|
|
|
|
assert confidence > 0.5
|
|
|
|
|
|
|
|
|
|
def test_detect_javascript_language(self):
|
|
|
|
|
"""JavaScript language is detected from code patterns."""
|
|
|
|
|
|
|
|
|
|
code = """
|
|
|
|
|
const express = require('express');
|
|
|
|
|
import { something } from 'module';
|
|
|
|
|
|
|
|
|
|
function handler(req, res) {
|
|
|
|
|
return res.json({ status: 'ok' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default handler;
|
|
|
|
|
"""
|
|
|
|
|
lang, confidence = detect_language(code)
|
|
|
|
|
assert lang in (CodeLanguage.JAVASCRIPT, CodeLanguage.TYPESCRIPT)
|
|
|
|
|
assert confidence > 0.3
|
|
|
|
|
|
|
|
|
|
def test_detect_go_language(self):
|
|
|
|
|
"""Go language is detected from code patterns."""
|
|
|
|
|
|
|
|
|
|
code = """
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import "fmt"
|
|
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
|
fmt.Println("Hello")
|
|
|
|
|
}
|
|
|
|
|
"""
|
|
|
|
|
lang, confidence = detect_language(code)
|
|
|
|
|
assert lang == CodeLanguage.GO
|
|
|
|
|
assert confidence > 0.3
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# TestCodeAwareCompressor
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestCodeAwareCompressor:
|
|
|
|
|
"""Tests for CodeAwareCompressor core functionality."""
|
|
|
|
|
|
|
|
|
|
def test_init_with_default_config(self):
|
|
|
|
|
"""Compressor initializes with default config."""
|
|
|
|
|
compressor = CodeAwareCompressor()
|
|
|
|
|
|
|
|
|
|
assert compressor.config is not None
|
|
|
|
|
assert compressor.config.preserve_imports is True
|
|
|
|
|
|
|
|
|
|
def test_init_with_custom_config(self, default_config):
|
|
|
|
|
"""Compressor initializes with custom config."""
|
|
|
|
|
compressor = CodeAwareCompressor(default_config)
|
|
|
|
|
|
|
|
|
|
assert compressor.config == default_config
|
|
|
|
|
|
|
|
|
|
def test_compress_skips_small_content(self, compressor):
|
|
|
|
|
"""Small content is not compressed."""
|
|
|
|
|
small_code = "def f(): pass"
|
|
|
|
|
result = compressor.compress(small_code)
|
|
|
|
|
|
|
|
|
|
assert result.compressed == small_code
|
|
|
|
|
assert result.compression_ratio == 1.0
|
|
|
|
|
|
|
|
|
|
def test_compress_handles_empty_content(self, compressor):
|
|
|
|
|
"""Empty content returns empty result."""
|
|
|
|
|
result = compressor.compress("")
|
|
|
|
|
|
|
|
|
|
assert result.compressed == ""
|
|
|
|
|
assert result.compression_ratio == 1.0
|
|
|
|
|
assert result.syntax_valid is True
|
|
|
|
|
|
|
|
|
|
def test_compress_with_explicit_language(self, compressor):
|
|
|
|
|
"""Language can be specified explicitly."""
|
|
|
|
|
code = generate_python_code(2)
|
|
|
|
|
result = compressor.compress(code, language="python")
|
|
|
|
|
|
|
|
|
|
# Should detect or use the specified language
|
|
|
|
|
assert result.language == CodeLanguage.PYTHON or result.language == CodeLanguage.UNKNOWN
|
|
|
|
|
|
|
|
|
|
def test_compress_auto_detects_python(self, compressor):
|
|
|
|
|
"""Python code is auto-detected during compression."""
|
|
|
|
|
code = """
|
|
|
|
|
import os
|
|
|
|
|
from typing import List
|
|
|
|
|
|
|
|
|
|
def function(arg: str) -> List[str]:
|
|
|
|
|
return [arg]
|
|
|
|
|
|
|
|
|
|
class MyClass:
|
|
|
|
|
pass
|
|
|
|
|
"""
|
|
|
|
|
result = compressor.compress(code)
|
|
|
|
|
# Should detect Python (if tree-sitter available) or return UNKNOWN
|
|
|
|
|
assert result.language in (CodeLanguage.PYTHON, CodeLanguage.UNKNOWN)
|
|
|
|
|
|
|
|
|
|
def test_compress_auto_detects_javascript(self, compressor):
|
|
|
|
|
"""JavaScript code is auto-detected during compression."""
|
|
|
|
|
code = """
|
|
|
|
|
const express = require('express');
|
|
|
|
|
import { something } from 'module';
|
|
|
|
|
|
|
|
|
|
function handler(req, res) {
|
|
|
|
|
return res.json({ status: 'ok' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default handler;
|
|
|
|
|
"""
|
|
|
|
|
result = compressor.compress(code)
|
|
|
|
|
assert result.language in (
|
|
|
|
|
CodeLanguage.JAVASCRIPT,
|
|
|
|
|
CodeLanguage.TYPESCRIPT,
|
|
|
|
|
CodeLanguage.UNKNOWN,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_compress_auto_detects_go(self, compressor):
|
|
|
|
|
"""Go code is auto-detected during compression."""
|
|
|
|
|
code = """
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import "fmt"
|
|
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
|
fmt.Println("Hello")
|
|
|
|
|
}
|
|
|
|
|
"""
|
|
|
|
|
result = compressor.compress(code)
|
|
|
|
|
assert result.language in (CodeLanguage.GO, CodeLanguage.UNKNOWN)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# TestFallbackCompression
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestFallbackCompression:
|
|
|
|
|
"""Tests for fallback compression when tree-sitter unavailable."""
|
|
|
|
|
|
|
|
|
|
def test_fallback_when_tree_sitter_unavailable(self, default_config):
|
|
|
|
|
"""Uses fallback compression when tree-sitter is not installed."""
|
|
|
|
|
with patch(
|
|
|
|
|
"headroom.transforms.code_compressor._check_tree_sitter_available",
|
|
|
|
|
return_value=False,
|
|
|
|
|
):
|
|
|
|
|
compressor = CodeAwareCompressor(default_config)
|
|
|
|
|
code = generate_python_code(5)
|
|
|
|
|
|
|
|
|
|
result = compressor.compress(code)
|
|
|
|
|
|
|
|
|
|
# Should still return a result (fallback compression)
|
|
|
|
|
assert result is not None
|
2026-03-26 11:11:00 -07:00
|
|
|
# Kompress fallback does NOT guarantee syntax validity
|
|
|
|
|
# If Kompress is unavailable, returns original (valid)
|
|
|
|
|
# If Kompress IS available, syntax_valid=False (cannot guarantee)
|
Add AST-based code compression and custom model configuration
CodeAwareCompressor:
- Tree-sitter based AST parsing for Python, JS, TS, Go, Rust, Java, C, C++
- Preserves imports, signatures, type annotations, error handlers
- Guarantees syntactically valid output
- Uses tree-sitter-language-pack for broad language support
ContentRouter:
- Intelligent compression orchestrator
- Auto-routes content to optimal compressor based on type detection
- Source hint support for high-confidence routing
Custom Model Configuration:
- HEADROOM_MODEL_LIMITS env var and ~/.headroom/models.json support
- Pattern-based inference for unknown models (opus/sonnet/haiku tiers)
- Support for Claude 4.5, Claude 4, o3, o3-mini
- Graceful fallback - never crashes on unknown models
2026-01-14 13:46:55 -08:00
|
|
|
|
|
|
|
|
def test_fallback_preserves_structure(self, default_config):
|
Integrate DynamicContentDetector into CacheAligner (Phase 1)
- Add DynamicContentDetector integration for comprehensive dynamic content
detection (20+ patterns vs previous 4 date patterns)
- New detection: UUIDs, API keys, JWT tokens, Unix timestamps, request/trace
IDs, hex hashes (MD5/SHA1/SHA256), version numbers, high-entropy strings
- Add CacheAlignerConfig options: use_dynamic_detector, detection_tiers,
extra_dynamic_labels, entropy_threshold
- Maintain backward compatibility with legacy date-only mode
- Add 25 new comprehensive tests for Phase 1 functionality
- Fix code compressor fallback test to properly mock LLMLingua availability
Expected cache hit improvement: 30-50% by extracting more dynamic content
2026-01-19 22:56:20 -08:00
|
|
|
"""Fallback compression preserves basic structure when no compressor available.
|
|
|
|
|
|
2026-03-26 11:11:00 -07:00
|
|
|
When both tree-sitter and Kompress are unavailable, the fallback
|
Integrate DynamicContentDetector into CacheAligner (Phase 1)
- Add DynamicContentDetector integration for comprehensive dynamic content
detection (20+ patterns vs previous 4 date patterns)
- New detection: UUIDs, API keys, JWT tokens, Unix timestamps, request/trace
IDs, hex hashes (MD5/SHA1/SHA256), version numbers, high-entropy strings
- Add CacheAlignerConfig options: use_dynamic_detector, detection_tiers,
extra_dynamic_labels, entropy_threshold
- Maintain backward compatibility with legacy date-only mode
- Add 25 new comprehensive tests for Phase 1 functionality
- Fix code compressor fallback test to properly mock LLMLingua availability
Expected cache hit improvement: 30-50% by extracting more dynamic content
2026-01-19 22:56:20 -08:00
|
|
|
returns the original code unchanged - preserving all structure.
|
|
|
|
|
"""
|
|
|
|
|
with (
|
|
|
|
|
patch(
|
|
|
|
|
"headroom.transforms.code_compressor._check_tree_sitter_available",
|
|
|
|
|
return_value=False,
|
|
|
|
|
),
|
|
|
|
|
patch(
|
2026-03-26 11:11:00 -07:00
|
|
|
"headroom.transforms.kompress_compressor.is_kompress_available",
|
Integrate DynamicContentDetector into CacheAligner (Phase 1)
- Add DynamicContentDetector integration for comprehensive dynamic content
detection (20+ patterns vs previous 4 date patterns)
- New detection: UUIDs, API keys, JWT tokens, Unix timestamps, request/trace
IDs, hex hashes (MD5/SHA1/SHA256), version numbers, high-entropy strings
- Add CacheAlignerConfig options: use_dynamic_detector, detection_tiers,
extra_dynamic_labels, entropy_threshold
- Maintain backward compatibility with legacy date-only mode
- Add 25 new comprehensive tests for Phase 1 functionality
- Fix code compressor fallback test to properly mock LLMLingua availability
Expected cache hit improvement: 30-50% by extracting more dynamic content
2026-01-19 22:56:20 -08:00
|
|
|
return_value=False,
|
|
|
|
|
),
|
Add AST-based code compression and custom model configuration
CodeAwareCompressor:
- Tree-sitter based AST parsing for Python, JS, TS, Go, Rust, Java, C, C++
- Preserves imports, signatures, type annotations, error handlers
- Guarantees syntactically valid output
- Uses tree-sitter-language-pack for broad language support
ContentRouter:
- Intelligent compression orchestrator
- Auto-routes content to optimal compressor based on type detection
- Source hint support for high-confidence routing
Custom Model Configuration:
- HEADROOM_MODEL_LIMITS env var and ~/.headroom/models.json support
- Pattern-based inference for unknown models (opus/sonnet/haiku tiers)
- Support for Claude 4.5, Claude 4, o3, o3-mini
- Graceful fallback - never crashes on unknown models
2026-01-14 13:46:55 -08:00
|
|
|
):
|
|
|
|
|
compressor = CodeAwareCompressor(default_config)
|
|
|
|
|
code = generate_python_code(3)
|
|
|
|
|
|
|
|
|
|
result = compressor.compress(code)
|
|
|
|
|
|
Integrate DynamicContentDetector into CacheAligner (Phase 1)
- Add DynamicContentDetector integration for comprehensive dynamic content
detection (20+ patterns vs previous 4 date patterns)
- New detection: UUIDs, API keys, JWT tokens, Unix timestamps, request/trace
IDs, hex hashes (MD5/SHA1/SHA256), version numbers, high-entropy strings
- Add CacheAlignerConfig options: use_dynamic_detector, detection_tiers,
extra_dynamic_labels, entropy_threshold
- Maintain backward compatibility with legacy date-only mode
- Add 25 new comprehensive tests for Phase 1 functionality
- Fix code compressor fallback test to properly mock LLMLingua availability
Expected cache hit improvement: 30-50% by extracting more dynamic content
2026-01-19 22:56:20 -08:00
|
|
|
# With no compressor available, original code is returned unchanged
|
|
|
|
|
# This preserves all imports and class/function signatures
|
Add AST-based code compression and custom model configuration
CodeAwareCompressor:
- Tree-sitter based AST parsing for Python, JS, TS, Go, Rust, Java, C, C++
- Preserves imports, signatures, type annotations, error handlers
- Guarantees syntactically valid output
- Uses tree-sitter-language-pack for broad language support
ContentRouter:
- Intelligent compression orchestrator
- Auto-routes content to optimal compressor based on type detection
- Source hint support for high-confidence routing
Custom Model Configuration:
- HEADROOM_MODEL_LIMITS env var and ~/.headroom/models.json support
- Pattern-based inference for unknown models (opus/sonnet/haiku tiers)
- Support for Claude 4.5, Claude 4, o3, o3-mini
- Graceful fallback - never crashes on unknown models
2026-01-14 13:46:55 -08:00
|
|
|
assert "import os" in result.compressed
|
|
|
|
|
assert "def function_" in result.compressed
|
Integrate DynamicContentDetector into CacheAligner (Phase 1)
- Add DynamicContentDetector integration for comprehensive dynamic content
detection (20+ patterns vs previous 4 date patterns)
- New detection: UUIDs, API keys, JWT tokens, Unix timestamps, request/trace
IDs, hex hashes (MD5/SHA1/SHA256), version numbers, high-entropy strings
- Add CacheAlignerConfig options: use_dynamic_detector, detection_tiers,
extra_dynamic_labels, entropy_threshold
- Maintain backward compatibility with legacy date-only mode
- Add 25 new comprehensive tests for Phase 1 functionality
- Fix code compressor fallback test to properly mock LLMLingua availability
Expected cache hit improvement: 30-50% by extracting more dynamic content
2026-01-19 22:56:20 -08:00
|
|
|
# Compression ratio should be 1.0 (no compression)
|
|
|
|
|
assert result.compression_ratio == 1.0
|
Add AST-based code compression and custom model configuration
CodeAwareCompressor:
- Tree-sitter based AST parsing for Python, JS, TS, Go, Rust, Java, C, C++
- Preserves imports, signatures, type annotations, error handlers
- Guarantees syntactically valid output
- Uses tree-sitter-language-pack for broad language support
ContentRouter:
- Intelligent compression orchestrator
- Auto-routes content to optimal compressor based on type detection
- Source hint support for high-confidence routing
Custom Model Configuration:
- HEADROOM_MODEL_LIMITS env var and ~/.headroom/models.json support
- Pattern-based inference for unknown models (opus/sonnet/haiku tiers)
- Support for Claude 4.5, Claude 4, o3, o3-mini
- Graceful fallback - never crashes on unknown models
2026-01-14 13:46:55 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# TestTransformInterface
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestTransformInterface:
|
|
|
|
|
"""Tests for Transform interface (apply, should_apply)."""
|
|
|
|
|
|
|
|
|
|
def test_should_apply_returns_false_for_small_content(self, default_config, tokenizer):
|
|
|
|
|
"""should_apply returns False for small content."""
|
|
|
|
|
config = CodeCompressorConfig(min_tokens_for_compression=1000)
|
|
|
|
|
compressor = CodeAwareCompressor(config)
|
|
|
|
|
messages = [{"role": "user", "content": "def f(): pass"}]
|
|
|
|
|
|
|
|
|
|
assert not compressor.should_apply(messages, tokenizer)
|
|
|
|
|
|
|
|
|
|
def test_should_apply_returns_bool_for_large_code(self, default_config, tokenizer):
|
|
|
|
|
"""should_apply returns boolean for large code content."""
|
|
|
|
|
compressor = CodeAwareCompressor(default_config)
|
|
|
|
|
code = generate_python_code(20)
|
|
|
|
|
messages = [{"role": "tool", "tool_call_id": "call_1", "content": code}]
|
|
|
|
|
|
|
|
|
|
# Should return True if there's code content to process
|
|
|
|
|
result = compressor.should_apply(messages, tokenizer)
|
|
|
|
|
assert isinstance(result, bool)
|
|
|
|
|
|
|
|
|
|
def test_apply_returns_transform_result(self, default_config, tokenizer):
|
|
|
|
|
"""apply() returns proper TransformResult."""
|
|
|
|
|
compressor = CodeAwareCompressor(default_config)
|
|
|
|
|
code = generate_python_code(10)
|
|
|
|
|
messages = [{"role": "tool", "tool_call_id": "call_1", "content": code}]
|
|
|
|
|
|
|
|
|
|
result = compressor.apply(messages, tokenizer)
|
|
|
|
|
|
|
|
|
|
assert result is not None
|
|
|
|
|
assert result.tokens_before > 0
|
|
|
|
|
assert len(result.messages) == 1
|
|
|
|
|
|
|
|
|
|
def test_apply_passes_through_non_code_messages(self, default_config, tokenizer):
|
|
|
|
|
"""apply() passes through non-code messages unchanged."""
|
|
|
|
|
compressor = CodeAwareCompressor(default_config)
|
|
|
|
|
messages = [
|
|
|
|
|
{"role": "user", "content": "Hello"},
|
|
|
|
|
{"role": "assistant", "content": "Hi there!"},
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
result = compressor.apply(messages, tokenizer)
|
|
|
|
|
|
|
|
|
|
assert result.messages[0]["content"] == "Hello"
|
|
|
|
|
assert result.messages[1]["content"] == "Hi there!"
|
|
|
|
|
|
|
|
|
|
def test_name_property(self, compressor):
|
|
|
|
|
"""Compressor has correct name."""
|
|
|
|
|
assert compressor.name == "code_aware_compressor"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# TestEdgeCases
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestEdgeCases:
|
|
|
|
|
"""Edge case tests for CodeAwareCompressor."""
|
|
|
|
|
|
|
|
|
|
def test_whitespace_only_content(self, compressor):
|
|
|
|
|
"""Whitespace-only content is handled gracefully."""
|
|
|
|
|
result = compressor.compress(" \n\t\n ")
|
|
|
|
|
|
|
|
|
|
assert result.compression_ratio == 1.0
|
|
|
|
|
assert result.syntax_valid is True
|
|
|
|
|
|
|
|
|
|
def test_unicode_content(self, default_config):
|
|
|
|
|
"""Unicode in code is handled correctly."""
|
|
|
|
|
compressor = CodeAwareCompressor(default_config)
|
|
|
|
|
code = '''
|
|
|
|
|
def greet(name: str) -> str:
|
|
|
|
|
"""Greet the user in multiple languages."""
|
|
|
|
|
return f"Hello, {name}! \u4f60\u597d! \u3053\u3093\u306b\u3061\u306f!"
|
|
|
|
|
'''
|
|
|
|
|
result = compressor.compress(code)
|
|
|
|
|
|
|
|
|
|
# Should handle unicode without crashing
|
|
|
|
|
assert result is not None
|
|
|
|
|
|
|
|
|
|
def test_very_long_function(self, default_config):
|
|
|
|
|
"""Very long functions are compressed."""
|
|
|
|
|
compressor = CodeAwareCompressor(default_config)
|
|
|
|
|
lines = ["def very_long_function():"]
|
|
|
|
|
lines.append(' """A very long function."""')
|
|
|
|
|
for i in range(100):
|
|
|
|
|
lines.append(f" x_{i} = {i}")
|
|
|
|
|
lines.append(" return x_99")
|
|
|
|
|
code = "\n".join(lines)
|
|
|
|
|
|
|
|
|
|
result = compressor.compress(code)
|
|
|
|
|
|
|
|
|
|
# Should compress the long function body
|
|
|
|
|
assert result.compression_ratio < 1.0 or "tree_sitter" not in str(
|
|
|
|
|
is_tree_sitter_available()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_nested_functions(self, default_config):
|
|
|
|
|
"""Nested functions are handled."""
|
|
|
|
|
compressor = CodeAwareCompressor(default_config)
|
|
|
|
|
code = """
|
|
|
|
|
def outer():
|
|
|
|
|
def inner():
|
|
|
|
|
return "inner"
|
|
|
|
|
return inner()
|
|
|
|
|
"""
|
|
|
|
|
result = compressor.compress(code)
|
|
|
|
|
|
|
|
|
|
assert result is not None
|
2026-03-26 12:04:30 -07:00
|
|
|
# syntax_valid requires tree-sitter; without it, validation is skipped
|
|
|
|
|
if is_tree_sitter_available():
|
|
|
|
|
assert result.syntax_valid is True
|
Add AST-based code compression and custom model configuration
CodeAwareCompressor:
- Tree-sitter based AST parsing for Python, JS, TS, Go, Rust, Java, C, C++
- Preserves imports, signatures, type annotations, error handlers
- Guarantees syntactically valid output
- Uses tree-sitter-language-pack for broad language support
ContentRouter:
- Intelligent compression orchestrator
- Auto-routes content to optimal compressor based on type detection
- Source hint support for high-confidence routing
Custom Model Configuration:
- HEADROOM_MODEL_LIMITS env var and ~/.headroom/models.json support
- Pattern-based inference for unknown models (opus/sonnet/haiku tiers)
- Support for Claude 4.5, Claude 4, o3, o3-mini
- Graceful fallback - never crashes on unknown models
2026-01-14 13:46:55 -08:00
|
|
|
|
|
|
|
|
def test_syntax_errors_in_input(self, default_config):
|
|
|
|
|
"""Syntax errors in input don't crash the compressor."""
|
|
|
|
|
compressor = CodeAwareCompressor(default_config)
|
|
|
|
|
# Invalid Python syntax
|
|
|
|
|
code = """
|
|
|
|
|
def broken(
|
|
|
|
|
# Missing closing paren
|
|
|
|
|
"""
|
|
|
|
|
# Should not raise
|
|
|
|
|
result = compressor.compress(code, language="python")
|
|
|
|
|
assert result is not None
|
|
|
|
|
|
|
|
|
|
def test_mixed_language_content(self, default_config):
|
|
|
|
|
"""Mixed language content (like markdown with code) is handled."""
|
|
|
|
|
compressor = CodeAwareCompressor(default_config)
|
|
|
|
|
content = """
|
|
|
|
|
# Documentation
|
|
|
|
|
|
|
|
|
|
Here is some code:
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
def example():
|
|
|
|
|
pass
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
And some more text.
|
|
|
|
|
"""
|
|
|
|
|
# Should not crash
|
|
|
|
|
result = compressor.compress(content)
|
|
|
|
|
assert result is not None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# TestMemoryManagement
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestMemoryManagement:
|
|
|
|
|
"""Tests for memory management functions."""
|
|
|
|
|
|
|
|
|
|
def test_is_tree_sitter_available_returns_bool(self):
|
|
|
|
|
"""is_tree_sitter_available returns a boolean."""
|
|
|
|
|
result = is_tree_sitter_available()
|
|
|
|
|
assert isinstance(result, bool)
|
|
|
|
|
|
|
|
|
|
def test_is_tree_sitter_loaded_returns_false_initially(self):
|
|
|
|
|
"""is_tree_sitter_loaded returns False when no parsers loaded."""
|
|
|
|
|
# Clear any loaded parsers first
|
|
|
|
|
unload_tree_sitter()
|
|
|
|
|
assert is_tree_sitter_loaded() is False
|
|
|
|
|
|
|
|
|
|
def test_unload_returns_false_when_nothing_loaded(self):
|
|
|
|
|
"""unload_tree_sitter returns False when nothing to unload."""
|
|
|
|
|
# Ensure nothing is loaded
|
|
|
|
|
unload_tree_sitter()
|
|
|
|
|
result = unload_tree_sitter()
|
|
|
|
|
assert result is False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# Integration Tests (only run if tree-sitter is installed)
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.skipif(not TREE_SITTER_INSTALLED, reason="tree-sitter-languages not installed")
|
|
|
|
|
class TestTreeSitterIntegration:
|
|
|
|
|
"""Integration tests that require actual tree-sitter installation.
|
|
|
|
|
|
|
|
|
|
These tests verify actual AST parsing and compression behavior.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def test_actual_python_compression(self):
|
|
|
|
|
"""Test actual compression of Python code."""
|
|
|
|
|
config = CodeCompressorConfig(
|
|
|
|
|
min_tokens_for_compression=10,
|
|
|
|
|
enable_ccr=False,
|
|
|
|
|
)
|
|
|
|
|
compressor = CodeAwareCompressor(config)
|
|
|
|
|
code = generate_python_code(5)
|
|
|
|
|
|
|
|
|
|
result = compressor.compress(code, language="python")
|
|
|
|
|
|
|
|
|
|
# Should achieve compression
|
|
|
|
|
assert result.compression_ratio < 1.0
|
|
|
|
|
assert result.syntax_valid is True
|
|
|
|
|
assert result.language == CodeLanguage.PYTHON
|
|
|
|
|
|
|
|
|
|
def test_actual_javascript_compression(self):
|
|
|
|
|
"""Test actual compression of JavaScript code."""
|
|
|
|
|
config = CodeCompressorConfig(
|
|
|
|
|
min_tokens_for_compression=10,
|
|
|
|
|
enable_ccr=False,
|
|
|
|
|
)
|
|
|
|
|
compressor = CodeAwareCompressor(config)
|
|
|
|
|
code = generate_javascript_code(5)
|
|
|
|
|
|
|
|
|
|
result = compressor.compress(code, language="javascript")
|
|
|
|
|
|
|
|
|
|
assert result.compression_ratio < 1.0
|
|
|
|
|
assert result.syntax_valid is True
|
|
|
|
|
assert result.language == CodeLanguage.JAVASCRIPT
|
|
|
|
|
|
|
|
|
|
def test_actual_go_compression(self):
|
|
|
|
|
"""Test Go code is processed (compression may fall back due to nested structures)."""
|
|
|
|
|
config = CodeCompressorConfig(
|
|
|
|
|
min_tokens_for_compression=10,
|
|
|
|
|
enable_ccr=False,
|
|
|
|
|
)
|
|
|
|
|
compressor = CodeAwareCompressor(config)
|
|
|
|
|
code = generate_go_code(3)
|
|
|
|
|
|
|
|
|
|
result = compressor.compress(code, language="go")
|
|
|
|
|
|
|
|
|
|
# Go code is processed and returns valid output
|
|
|
|
|
# Note: compression_ratio may be 1.0 if compression produces invalid syntax
|
|
|
|
|
# and falls back to original (Go has complex nested brace handling)
|
|
|
|
|
assert result.syntax_valid is True
|
|
|
|
|
assert result.language == CodeLanguage.GO
|
|
|
|
|
assert result.compressed # Some output is produced
|
|
|
|
|
|
fix(code): compress class member containers (#1334)
## Description
CodeAwareCompressor used the same `body_node_types` config to find both
executable function bodies and class/impl member containers. That works
when those AST nodes happen to match, but it misses member containers
such as Java `class_body`, C++ `field_declaration_list`, and Rust
`declaration_list`, so class methods were returned essentially
uncompressed.
This adds an optional `class_body_node_types` override for class/impl
member containers and uses it only in class compression. It also skips
anonymous punctuation tokens while reconstructing class bodies and keeps
same-line C++ class semicolons attached to the compressed class
declaration.
Closes #1318
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- Added `LangConfig.class_body_node_types` for languages whose
class/impl member container differs from executable method-body nodes.
- Configured class member containers for JavaScript, TypeScript, Java,
C++, and Rust.
- Updated `_compress_class_ast` to use class-member containers, skip
anonymous punctuation children, and preserve C++ `};` output without
creating stray top-level semicolons.
- Added regression coverage proving class/impl methods compress for
JavaScript, TypeScript, Java, C++, and Rust.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ PYTHONPATH=. /tmp/headroom-1319-venv/bin/python -m pytest tests/test_transforms/test_code_compressor.py -q
collected 71 items
tests/test_transforms/test_code_compressor.py .......................... [ 36%]
............................................. [100%]
71 passed, 1 warning in 0.36s
$ /tmp/headroom-1319-venv/bin/python -m ruff check .
All checks passed!
$ /tmp/headroom-1319-venv/bin/python -m ruff format --check .
965 files already formatted
$ PYTHONPATH=. /tmp/headroom-1319-venv/bin/python -m mypy headroom
headroom/proxy/server.py:1186: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs [annotation-unchecked]
headroom/proxy/server.py:1257: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs [annotation-unchecked]
headroom/proxy/server.py:1261: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs [annotation-unchecked]
pyproject.toml: note: unused section(s): module = ['mlx.*']
Success: no issues found in 394 source files
```
## Real Behavior Proof
- Environment: macOS arm64, Python 3.14.5, branch
`fix-code-compressor-class-members`, tree-sitter grammar pack installed
in `/tmp/headroom-1319-venv`, repo imported with `PYTHONPATH=.`.
- Exact command / steps: Reproduced class-method compression with
`CodeAwareCompressor(CodeCompressorConfig(enable_ccr=False,
min_tokens_for_compression=1, max_body_lines=1))` for Java/C++/Rust
before the fix, then reran the pytest/ruff/mypy commands listed above
after the patch.
- Observed result: Java/C++/Rust class methods now compress below 1.0
while `syntax_valid` remains true; C++ output preserves `};`; regression
coverage also verifies JavaScript/TypeScript class member containers.
- Not tested: Full repository pytest suite; local `uv run` editable
builds are blocked on this machine by native C++ header failures in
optional/native dependencies (`hnswlib` / Rust `esaxx-rs`), so
validation used a lightweight venv with `PYTHONPATH=.`.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
## Screenshots (if applicable)
N/A
## Additional Notes
Documentation and CHANGELOG updates are not applicable for this narrow
bug fix. The pytest warning shown above is from running without
`pytest-asyncio` in the lightweight verification venv (`asyncio_mode`
config is unknown there); it is unrelated to this change.
2026-06-23 15:41:36 -04:00
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
(
|
|
|
|
|
"language",
|
|
|
|
|
"code",
|
|
|
|
|
"expected_signature",
|
|
|
|
|
"expected_omitted_lines",
|
|
|
|
|
"expected_removed_line",
|
|
|
|
|
"expected_closing",
|
|
|
|
|
),
|
|
|
|
|
[
|
|
|
|
|
(
|
|
|
|
|
"javascript",
|
|
|
|
|
(
|
|
|
|
|
"class Calc {\n"
|
|
|
|
|
" compute(x) {\n"
|
|
|
|
|
" let a = x + 1;\n"
|
|
|
|
|
" let b = a * 2;\n"
|
|
|
|
|
" let c = b - 3;\n"
|
|
|
|
|
" return c;\n"
|
|
|
|
|
" }\n"
|
|
|
|
|
"}\n"
|
|
|
|
|
),
|
|
|
|
|
"compute(x) {",
|
|
|
|
|
3,
|
|
|
|
|
"return c;",
|
|
|
|
|
"}\n}",
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
"typescript",
|
|
|
|
|
(
|
|
|
|
|
"class Calc {\n"
|
|
|
|
|
" compute(x: number): number {\n"
|
|
|
|
|
" let a = x + 1;\n"
|
|
|
|
|
" let b = a * 2;\n"
|
|
|
|
|
" let c = b - 3;\n"
|
|
|
|
|
" return c;\n"
|
|
|
|
|
" }\n"
|
|
|
|
|
"}\n"
|
|
|
|
|
),
|
|
|
|
|
"compute(x: number): number {",
|
|
|
|
|
3,
|
|
|
|
|
"return c;",
|
|
|
|
|
"}\n}",
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
"java",
|
|
|
|
|
(
|
|
|
|
|
"public class Calc {\n"
|
|
|
|
|
" public int compute(int x) {\n"
|
|
|
|
|
" int a = x + 1;\n"
|
|
|
|
|
" int b = a * 2;\n"
|
|
|
|
|
" int c = b - 3;\n"
|
|
|
|
|
" int d = c / 4;\n"
|
|
|
|
|
" int e = d + 5;\n"
|
|
|
|
|
" return e;\n"
|
|
|
|
|
" }\n"
|
|
|
|
|
"}\n"
|
|
|
|
|
),
|
|
|
|
|
"public int compute(int x) {",
|
|
|
|
|
5,
|
|
|
|
|
"return e;",
|
|
|
|
|
"}\n}",
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
"cpp",
|
|
|
|
|
(
|
|
|
|
|
"class Calc {\n"
|
|
|
|
|
"public:\n"
|
|
|
|
|
" int compute(int x) {\n"
|
|
|
|
|
" int a = x + 1;\n"
|
|
|
|
|
" int b = a * 2;\n"
|
|
|
|
|
" int c = b - 3;\n"
|
|
|
|
|
" int d = c / 4;\n"
|
|
|
|
|
" int e = d + 5;\n"
|
|
|
|
|
" return e;\n"
|
|
|
|
|
" }\n"
|
|
|
|
|
"};\n"
|
|
|
|
|
),
|
|
|
|
|
"int compute(int x) {",
|
|
|
|
|
5,
|
|
|
|
|
"return e;",
|
|
|
|
|
"};",
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
"rust",
|
|
|
|
|
(
|
|
|
|
|
"impl Calc {\n"
|
|
|
|
|
" pub fn compute(&self, x: i32) -> i32 {\n"
|
|
|
|
|
" let a = x + 1;\n"
|
|
|
|
|
" let b = a * 2;\n"
|
|
|
|
|
" let c = b - 3;\n"
|
|
|
|
|
" let d = c / 4;\n"
|
|
|
|
|
" let e = d + 5;\n"
|
|
|
|
|
" e\n"
|
|
|
|
|
" }\n"
|
|
|
|
|
"}\n"
|
|
|
|
|
),
|
|
|
|
|
"pub fn compute(&self, x: i32) -> i32 {",
|
|
|
|
|
5,
|
|
|
|
|
" e\n",
|
|
|
|
|
"}\n}",
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
def test_compresses_methods_inside_class_member_containers(
|
|
|
|
|
self,
|
|
|
|
|
language,
|
|
|
|
|
code,
|
|
|
|
|
expected_signature,
|
|
|
|
|
expected_omitted_lines,
|
|
|
|
|
expected_removed_line,
|
|
|
|
|
expected_closing,
|
|
|
|
|
):
|
|
|
|
|
"""Class/impl member containers are distinct from executable method bodies."""
|
|
|
|
|
config = CodeCompressorConfig(
|
|
|
|
|
min_tokens_for_compression=1,
|
|
|
|
|
max_body_lines=1,
|
|
|
|
|
enable_ccr=False,
|
|
|
|
|
)
|
|
|
|
|
compressor = CodeAwareCompressor(config)
|
|
|
|
|
|
|
|
|
|
result = compressor.compress(code, language=language)
|
|
|
|
|
|
|
|
|
|
assert result.language == CodeLanguage(language)
|
|
|
|
|
assert result.syntax_valid is True
|
|
|
|
|
assert result.compression_ratio < 1.0
|
|
|
|
|
assert expected_signature in result.compressed
|
|
|
|
|
assert f"// [{expected_omitted_lines} lines omitted]" in result.compressed
|
|
|
|
|
assert expected_removed_line not in result.compressed
|
|
|
|
|
assert result.compressed.endswith(expected_closing)
|
|
|
|
|
|
Add AST-based code compression and custom model configuration
CodeAwareCompressor:
- Tree-sitter based AST parsing for Python, JS, TS, Go, Rust, Java, C, C++
- Preserves imports, signatures, type annotations, error handlers
- Guarantees syntactically valid output
- Uses tree-sitter-language-pack for broad language support
ContentRouter:
- Intelligent compression orchestrator
- Auto-routes content to optimal compressor based on type detection
- Source hint support for high-confidence routing
Custom Model Configuration:
- HEADROOM_MODEL_LIMITS env var and ~/.headroom/models.json support
- Pattern-based inference for unknown models (opus/sonnet/haiku tiers)
- Support for Claude 4.5, Claude 4, o3, o3-mini
- Graceful fallback - never crashes on unknown models
2026-01-14 13:46:55 -08:00
|
|
|
def test_imports_preserved(self):
|
|
|
|
|
"""Imports are preserved in compressed output."""
|
|
|
|
|
config = CodeCompressorConfig(
|
|
|
|
|
preserve_imports=True,
|
|
|
|
|
min_tokens_for_compression=10,
|
|
|
|
|
enable_ccr=False,
|
|
|
|
|
)
|
|
|
|
|
compressor = CodeAwareCompressor(config)
|
|
|
|
|
code = generate_python_code(5)
|
|
|
|
|
|
|
|
|
|
result = compressor.compress(code, language="python")
|
|
|
|
|
|
|
|
|
|
assert "import os" in result.compressed
|
|
|
|
|
assert "from typing import" in result.compressed
|
|
|
|
|
|
|
|
|
|
def test_signatures_preserved(self):
|
|
|
|
|
"""Function signatures are preserved."""
|
|
|
|
|
config = CodeCompressorConfig(
|
|
|
|
|
preserve_signatures=True,
|
|
|
|
|
min_tokens_for_compression=10,
|
|
|
|
|
enable_ccr=False,
|
|
|
|
|
)
|
|
|
|
|
compressor = CodeAwareCompressor(config)
|
|
|
|
|
code = generate_python_code(3)
|
|
|
|
|
|
|
|
|
|
result = compressor.compress(code, language="python")
|
|
|
|
|
|
|
|
|
|
# Should preserve function signatures
|
|
|
|
|
assert "def function_" in result.compressed
|
|
|
|
|
assert "arg:" in result.compressed or "(arg" in result.compressed
|
|
|
|
|
|
|
|
|
|
def test_error_handlers_preserved(self):
|
|
|
|
|
"""Module-level try/except blocks are preserved."""
|
|
|
|
|
config = CodeCompressorConfig(
|
|
|
|
|
min_tokens_for_compression=10,
|
|
|
|
|
enable_ccr=False,
|
|
|
|
|
)
|
|
|
|
|
compressor = CodeAwareCompressor(config)
|
|
|
|
|
# Code with module-level try/except (not inside functions)
|
|
|
|
|
code = '''
|
|
|
|
|
import os
|
|
|
|
|
|
|
|
|
|
def setup():
|
|
|
|
|
"""Setup function."""
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
from optional_module import feature
|
|
|
|
|
except ImportError:
|
|
|
|
|
feature = None
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
|
"""Main function with long body."""
|
|
|
|
|
result = []
|
|
|
|
|
for i in range(100):
|
|
|
|
|
result.append(i)
|
|
|
|
|
return result
|
|
|
|
|
'''
|
|
|
|
|
result = compressor.compress(code, language="python")
|
|
|
|
|
|
|
|
|
|
# Module-level error handlers should be preserved
|
|
|
|
|
assert "try:" in result.compressed or "except" in result.compressed
|
|
|
|
|
|
|
|
|
|
def test_syntax_verification(self):
|
|
|
|
|
"""Output syntax is verified as valid."""
|
|
|
|
|
config = CodeCompressorConfig(
|
|
|
|
|
min_tokens_for_compression=10,
|
|
|
|
|
enable_ccr=False,
|
|
|
|
|
)
|
|
|
|
|
compressor = CodeAwareCompressor(config)
|
|
|
|
|
code = generate_python_code(5)
|
|
|
|
|
|
|
|
|
|
result = compressor.compress(code, language="python")
|
|
|
|
|
|
|
|
|
|
# Verify the compressed output is valid Python
|
|
|
|
|
assert result.syntax_valid is True
|
|
|
|
|
|
|
|
|
|
# Should be parseable
|
|
|
|
|
try:
|
|
|
|
|
compile(result.compressed, "<test>", "exec")
|
|
|
|
|
except SyntaxError:
|
|
|
|
|
pytest.fail("Compressed output has invalid Python syntax")
|
|
|
|
|
|
fix(code): validate Python compressed syntax (#1302)
## Description
Fix a Python code-compression validity gap from #1233 where tree-sitter
parsing could mark compressed output as syntactically valid even when
Python compile-time syntax rules reject it.
This keeps `from __future__ import ...` statements in the
import-preservation bucket so they stay before executable definitions,
and adds Python `compile(..., "exec")` verification after `ast.parse`.
It also keeps the earlier conservative class-method decorator
indentation hardening from this branch.
Refs #1233.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- Treat Python `future_import_statement` nodes as preserved imports.
- Verify Python compressed output with both `ast.parse` and
`compile(..., "exec")`.
- Preserve original source-line indentation for decorators attached to
class methods.
- Add a regression fixture covering `from __future__ import
annotations`, class decorators, property decorators, async methods, and
`match` statements.
- Add a direct regression assertion that future imports stay before
executable definitions.
- Document the user-visible fix in `CHANGELOG.md`.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check`)
- [ ] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ /tmp/headroom-issue-1233-venv/bin/python -m pytest tests/test_transforms/test_code_compressor.py::TestTreeSitterIntegration::test_python_future_import_stays_at_module_start -q
1 passed, 1 warning
$ /tmp/headroom-issue-1233-venv/bin/python -m pytest tests/test_transforms/test_code_compressor.py -q
61 passed, 1 warning
$ uv run --extra dev ruff check headroom/transforms/code_compressor.py tests/test_transforms/test_code_compressor.py
All checks passed!
$ uv run --extra dev ruff format --check headroom/transforms/code_compressor.py tests/test_transforms/test_code_compressor.py
2 files already formatted
$ git diff --check
# no output
```
## Real Behavior Proof
- Environment: macOS, Python 3.11.14, local checkout with `[code]`
dependencies installed in `/tmp/headroom-issue-1233-venv`.
- Exact command / steps: added
`test_python_future_import_stays_at_module_start`, ran it before the fix
to confirm the compressed output failure, then reran the focused test
and full `tests/test_transforms/test_code_compressor.py` after the
patch.
- Observed result: before this patch, the regression fixture produced
compressed Python with `from __future__ import annotations` after
class/function definitions. `result.syntax_valid` was `True`, but
`compile(result.compressed, "<test>", "exec")` failed with `SyntaxError:
from __future__ imports must occur at the beginning of the file`. After
this patch, the focused regression and full code-compressor test file
pass locally, and the regression now directly asserts that the future
import appears before executable definitions.
- Not tested: full repository pytest, `mypy headroom`, and a broad
corpus run over third-party source files.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project style guidelines
- [x] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable
## Screenshots (if applicable)
N/A
## Additional Notes
This PR is now scoped to the stable compile-time failure path in #1233.
The broader syntax-failure rate from the issue may still need
corpus-level follow-up.
Co-authored-by: JD Davis <mxjerrett@gmail.com>
2026-06-24 03:41:14 +08:00
|
|
|
def test_python_future_import_stays_at_module_start(self):
|
|
|
|
|
"""Compressed Python keeps future imports before executable statements."""
|
|
|
|
|
config = CodeCompressorConfig(
|
|
|
|
|
min_tokens_for_compression=10,
|
|
|
|
|
target_compression_rate=0.2,
|
|
|
|
|
max_body_lines=3,
|
|
|
|
|
enable_ccr=False,
|
|
|
|
|
)
|
|
|
|
|
compressor = CodeAwareCompressor(config)
|
|
|
|
|
code = textwrap.dedent(
|
|
|
|
|
"""
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
from typing import Any, Callable, Iterable
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def traced(label: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
|
|
|
def decorate(fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
|
|
|
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
|
|
|
return await fn(*args, **kwargs)
|
|
|
|
|
|
|
|
|
|
return wrapper
|
|
|
|
|
|
|
|
|
|
return decorate
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(slots=True)
|
|
|
|
|
class Event:
|
|
|
|
|
kind: str
|
|
|
|
|
payload: dict[str, Any]
|
|
|
|
|
retries: int = 0
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def important(self) -> bool:
|
|
|
|
|
return self.kind in {"error", "retry"} or self.retries > 2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class EventRouter:
|
|
|
|
|
def __init__(self, sinks: dict[str, Callable[[Event], Any]]) -> None:
|
|
|
|
|
self.sinks = sinks
|
|
|
|
|
self.history: list[tuple[str, bool]] = []
|
|
|
|
|
|
|
|
|
|
@traced("route")
|
|
|
|
|
async def route(self, events: Iterable[Event]) -> list[str]:
|
|
|
|
|
accepted: list[str] = []
|
|
|
|
|
for event in events:
|
|
|
|
|
match event:
|
|
|
|
|
case Event(kind="error", payload={"code": code, "message": msg}, retries=r) if r > 1:
|
|
|
|
|
destination = "pager"
|
|
|
|
|
accepted.append(f"{destination}:{code}:{msg}")
|
|
|
|
|
case Event(kind=kind, payload=payload) if (route := payload.get("route")):
|
|
|
|
|
destination = str(route)
|
|
|
|
|
accepted.append(f"{destination}:{kind}")
|
|
|
|
|
case _:
|
|
|
|
|
destination = "dead_letter"
|
|
|
|
|
accepted.append(destination)
|
|
|
|
|
|
|
|
|
|
self.history.append((destination, event.important))
|
|
|
|
|
|
|
|
|
|
return [item for item in accepted if item]
|
|
|
|
|
"""
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
result = compressor.compress(code, language="python")
|
|
|
|
|
|
|
|
|
|
assert result.syntax_valid is True
|
|
|
|
|
future_import_index = result.compressed.index("from __future__ import annotations")
|
|
|
|
|
first_executable_index = min(
|
|
|
|
|
result.compressed.index("@dataclass"),
|
|
|
|
|
result.compressed.index("def traced"),
|
|
|
|
|
result.compressed.index("class EventRouter"),
|
|
|
|
|
)
|
|
|
|
|
assert future_import_index < first_executable_index
|
|
|
|
|
try:
|
|
|
|
|
compile(result.compressed, "<test>", "exec")
|
|
|
|
|
except SyntaxError as exc:
|
|
|
|
|
pytest.fail(f"Compressed output has invalid Python syntax: {exc}\n{result.compressed}")
|
|
|
|
|
|
Add AST-based code compression and custom model configuration
CodeAwareCompressor:
- Tree-sitter based AST parsing for Python, JS, TS, Go, Rust, Java, C, C++
- Preserves imports, signatures, type annotations, error handlers
- Guarantees syntactically valid output
- Uses tree-sitter-language-pack for broad language support
ContentRouter:
- Intelligent compression orchestrator
- Auto-routes content to optimal compressor based on type detection
- Source hint support for high-confidence routing
Custom Model Configuration:
- HEADROOM_MODEL_LIMITS env var and ~/.headroom/models.json support
- Pattern-based inference for unknown models (opus/sonnet/haiku tiers)
- Support for Claude 4.5, Claude 4, o3, o3-mini
- Graceful fallback - never crashes on unknown models
2026-01-14 13:46:55 -08:00
|
|
|
def test_tree_sitter_loaded_after_compression(self):
|
|
|
|
|
"""Parser is loaded after compression."""
|
|
|
|
|
config = CodeCompressorConfig(
|
|
|
|
|
min_tokens_for_compression=10,
|
|
|
|
|
enable_ccr=False,
|
|
|
|
|
)
|
|
|
|
|
compressor = CodeAwareCompressor(config)
|
|
|
|
|
|
|
|
|
|
# Ensure clean state
|
|
|
|
|
unload_tree_sitter()
|
|
|
|
|
assert is_tree_sitter_loaded() is False
|
|
|
|
|
|
|
|
|
|
# Compress should load parser
|
|
|
|
|
code = generate_python_code(3)
|
|
|
|
|
compressor.compress(code, language="python")
|
|
|
|
|
|
|
|
|
|
assert is_tree_sitter_loaded() is True
|
|
|
|
|
|
|
|
|
|
def test_unload_clears_parsers(self):
|
|
|
|
|
"""unload_tree_sitter clears loaded parsers."""
|
|
|
|
|
config = CodeCompressorConfig(
|
|
|
|
|
min_tokens_for_compression=10,
|
|
|
|
|
enable_ccr=False,
|
|
|
|
|
)
|
|
|
|
|
compressor = CodeAwareCompressor(config)
|
|
|
|
|
|
|
|
|
|
# Load a parser
|
|
|
|
|
code = generate_python_code(3)
|
|
|
|
|
compressor.compress(code, language="python")
|
|
|
|
|
assert is_tree_sitter_loaded() is True
|
|
|
|
|
|
|
|
|
|
# Unload
|
|
|
|
|
result = unload_tree_sitter()
|
|
|
|
|
assert result is True
|
|
|
|
|
assert is_tree_sitter_loaded() is False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# TestDocstringModes
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.skipif(not TREE_SITTER_INSTALLED, reason="tree-sitter-languages not installed")
|
|
|
|
|
class TestDocstringModes:
|
|
|
|
|
"""Tests for different docstring handling modes."""
|
|
|
|
|
|
|
|
|
|
def test_docstring_mode_full(self):
|
|
|
|
|
"""FULL mode preserves entire docstrings."""
|
|
|
|
|
config = CodeCompressorConfig(
|
|
|
|
|
docstring_mode=DocstringMode.FULL,
|
|
|
|
|
min_tokens_for_compression=10,
|
|
|
|
|
enable_ccr=False,
|
|
|
|
|
)
|
|
|
|
|
compressor = CodeAwareCompressor(config)
|
|
|
|
|
code = generate_python_code(2)
|
|
|
|
|
|
|
|
|
|
result = compressor.compress(code, language="python")
|
|
|
|
|
|
|
|
|
|
# Should preserve full docstrings
|
|
|
|
|
assert "Args:" in result.compressed or "Returns:" in result.compressed
|
|
|
|
|
|
|
|
|
|
def test_docstring_mode_first_line(self):
|
|
|
|
|
"""FIRST_LINE mode keeps only first line of docstring."""
|
|
|
|
|
config = CodeCompressorConfig(
|
|
|
|
|
docstring_mode=DocstringMode.FIRST_LINE,
|
|
|
|
|
min_tokens_for_compression=10,
|
|
|
|
|
enable_ccr=False,
|
|
|
|
|
)
|
|
|
|
|
compressor = CodeAwareCompressor(config)
|
|
|
|
|
code = generate_python_code(2)
|
|
|
|
|
|
|
|
|
|
result = compressor.compress(code, language="python")
|
|
|
|
|
|
|
|
|
|
# Multi-line docstring details should be removed
|
|
|
|
|
# This is implementation-dependent
|
|
|
|
|
assert result.compressed is not None
|
|
|
|
|
|
|
|
|
|
def test_docstring_mode_remove(self):
|
|
|
|
|
"""REMOVE mode removes all docstrings."""
|
|
|
|
|
config = CodeCompressorConfig(
|
|
|
|
|
docstring_mode=DocstringMode.REMOVE,
|
|
|
|
|
min_tokens_for_compression=10,
|
|
|
|
|
max_body_lines=2, # Low threshold to trigger compression
|
|
|
|
|
enable_ccr=False,
|
|
|
|
|
)
|
|
|
|
|
compressor = CodeAwareCompressor(config)
|
|
|
|
|
# Larger function to trigger body compression
|
|
|
|
|
code = '''
|
|
|
|
|
def example():
|
|
|
|
|
"""This docstring should be removed."""
|
|
|
|
|
x = 1
|
|
|
|
|
y = 2
|
|
|
|
|
z = 3
|
|
|
|
|
result = x + y + z
|
|
|
|
|
for i in range(10):
|
|
|
|
|
result += i
|
|
|
|
|
return result
|
|
|
|
|
'''
|
|
|
|
|
result = compressor.compress(code, language="python")
|
|
|
|
|
|
|
|
|
|
# Docstring should be removed when REMOVE mode is active
|
|
|
|
|
assert "This docstring should be removed" not in result.compressed
|
2026-03-03 14:18:04 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# TestSemanticSymbolImportance
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _payment_processing_code() -> str:
|
|
|
|
|
"""Python code with varying symbol importance for testing."""
|
|
|
|
|
return '''
|
|
|
|
|
import os
|
|
|
|
|
from typing import List, Optional
|
|
|
|
|
|
|
|
|
|
def process_payment(order, config):
|
|
|
|
|
"""Process a payment through the pipeline."""
|
|
|
|
|
validated = validate_order(order)
|
|
|
|
|
if not validated.is_valid:
|
|
|
|
|
return PaymentResult(status='failed')
|
|
|
|
|
charge = charge_customer(order.customer, order.total)
|
|
|
|
|
receipt = generate_receipt(charge)
|
|
|
|
|
send_confirmation(order.customer.email, receipt)
|
|
|
|
|
update_inventory(order.items)
|
|
|
|
|
log_transaction(charge.transaction_id)
|
|
|
|
|
notify_warehouse(order)
|
|
|
|
|
return PaymentResult(status='success', receipt=receipt)
|
|
|
|
|
|
|
|
|
|
def validate_order(order):
|
|
|
|
|
"""Validate an order before processing."""
|
|
|
|
|
if not order.items:
|
|
|
|
|
return ValidationResult(False, ['No items'])
|
|
|
|
|
total = sum(item.price for item in order.items)
|
|
|
|
|
if total <= 0:
|
|
|
|
|
return ValidationResult(False, ['Invalid total'])
|
|
|
|
|
if not order.customer:
|
|
|
|
|
return ValidationResult(False, ['No customer'])
|
|
|
|
|
return ValidationResult(True, [])
|
|
|
|
|
|
|
|
|
|
def charge_customer(customer, amount):
|
|
|
|
|
"""Charge the customer."""
|
|
|
|
|
gateway = get_payment_gateway()
|
|
|
|
|
response = gateway.charge(customer.card, amount)
|
|
|
|
|
if not response.success:
|
|
|
|
|
raise PaymentError(response.error)
|
|
|
|
|
return response
|
|
|
|
|
|
|
|
|
|
def generate_receipt(charge):
|
|
|
|
|
"""Generate a receipt for the charge."""
|
|
|
|
|
template = load_template('receipt')
|
|
|
|
|
return template.render(charge=charge)
|
|
|
|
|
|
|
|
|
|
def _format_log_entry(entry):
|
|
|
|
|
"""Format a log entry for internal use. Never called."""
|
|
|
|
|
timestamp = entry.get('ts', '')
|
|
|
|
|
level = entry.get('level', 'INFO')
|
|
|
|
|
message = entry.get('msg', '')
|
|
|
|
|
source = entry.get('source', 'unknown')
|
|
|
|
|
formatted = f'[{timestamp}] {level}: {message} ({source})'
|
|
|
|
|
return formatted.strip()
|
|
|
|
|
|
|
|
|
|
def _dead_helper():
|
|
|
|
|
"""Never called anywhere in this file."""
|
|
|
|
|
x = 1
|
|
|
|
|
y = 2
|
|
|
|
|
z = 3
|
|
|
|
|
result = x + y + z
|
|
|
|
|
for i in range(100):
|
|
|
|
|
result += i
|
|
|
|
|
return result
|
|
|
|
|
'''
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.skipif(not TREE_SITTER_INSTALLED, reason="tree-sitter-languages not installed")
|
|
|
|
|
class TestSemanticSymbolImportance:
|
|
|
|
|
"""Tests for semantic symbol importance analysis and variable compression."""
|
|
|
|
|
|
|
|
|
|
def _make_compressor(self, **overrides):
|
|
|
|
|
defaults = {
|
|
|
|
|
"min_tokens_for_compression": 10,
|
|
|
|
|
"max_body_lines": 3,
|
|
|
|
|
"enable_ccr": False,
|
|
|
|
|
"semantic_analysis": True,
|
|
|
|
|
}
|
|
|
|
|
defaults.update(overrides)
|
|
|
|
|
return CodeAwareCompressor(CodeCompressorConfig(**defaults))
|
|
|
|
|
|
|
|
|
|
def test_symbol_scores_populated(self):
|
|
|
|
|
"""Compression result includes symbol importance scores."""
|
|
|
|
|
compressor = self._make_compressor()
|
|
|
|
|
result = compressor.compress(_payment_processing_code(), language="python")
|
|
|
|
|
|
|
|
|
|
assert result.symbol_scores
|
|
|
|
|
assert "process_payment" in result.symbol_scores
|
|
|
|
|
assert "validate_order" in result.symbol_scores
|
|
|
|
|
assert "_dead_helper" in result.symbol_scores
|
|
|
|
|
|
|
|
|
|
def test_called_functions_score_higher_than_dead_code(self):
|
|
|
|
|
"""Functions called by others score higher than unused functions."""
|
|
|
|
|
compressor = self._make_compressor()
|
|
|
|
|
result = compressor.compress(_payment_processing_code(), language="python")
|
|
|
|
|
|
|
|
|
|
# validate_order is called by process_payment — should score higher
|
|
|
|
|
assert result.symbol_scores["validate_order"] > result.symbol_scores["_dead_helper"]
|
|
|
|
|
assert result.symbol_scores["charge_customer"] > result.symbol_scores["_dead_helper"]
|
|
|
|
|
|
|
|
|
|
def test_public_symbols_score_higher_than_private(self):
|
|
|
|
|
"""Public functions (no leading _) score higher than private ones."""
|
|
|
|
|
compressor = self._make_compressor()
|
|
|
|
|
code = '''
|
|
|
|
|
def public_func():
|
|
|
|
|
"""A public function."""
|
|
|
|
|
x = 1
|
|
|
|
|
y = 2
|
|
|
|
|
z = 3
|
|
|
|
|
result = x + y + z
|
|
|
|
|
for i in range(10):
|
|
|
|
|
result += i
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
def _private_func():
|
|
|
|
|
"""A private function."""
|
|
|
|
|
x = 1
|
|
|
|
|
y = 2
|
|
|
|
|
z = 3
|
|
|
|
|
result = x + y + z
|
|
|
|
|
for i in range(10):
|
|
|
|
|
result += i
|
|
|
|
|
return result
|
|
|
|
|
'''
|
|
|
|
|
result = compressor.compress(code, language="python")
|
|
|
|
|
|
|
|
|
|
assert result.symbol_scores["public_func"] > result.symbol_scores["_private_func"]
|
|
|
|
|
|
|
|
|
|
def test_dead_code_compressed_to_signature_only(self):
|
|
|
|
|
"""Functions with score < 0.1 are compressed to signature + docstring only."""
|
|
|
|
|
compressor = self._make_compressor()
|
|
|
|
|
result = compressor.compress(_payment_processing_code(), language="python")
|
|
|
|
|
|
|
|
|
|
# _dead_helper has 0 references, private → score 0.0
|
|
|
|
|
assert result.symbol_scores["_dead_helper"] < 0.1
|
|
|
|
|
|
|
|
|
|
# Body should be fully omitted
|
|
|
|
|
assert "_dead_helper" in result.compressed
|
|
|
|
|
# Should NOT contain body content
|
|
|
|
|
assert "range(100)" not in result.compressed
|
|
|
|
|
|
|
|
|
|
def test_referenced_functions_keep_more_body(self):
|
|
|
|
|
"""Higher-scored functions get more body lines from the budget."""
|
|
|
|
|
# Use a generous target rate so there IS budget to distribute
|
|
|
|
|
compressor = self._make_compressor(target_compression_rate=0.7)
|
|
|
|
|
result = compressor.compress(_payment_processing_code(), language="python")
|
|
|
|
|
|
|
|
|
|
compressed = result.compressed
|
|
|
|
|
# With 70% target, high-scoring functions should retain body
|
|
|
|
|
# while low-scoring ones get less. validate_order is referenced
|
|
|
|
|
# and public (high score) so should keep some body.
|
|
|
|
|
# _dead_helper has lowest score so should get least body.
|
|
|
|
|
# Count body lines per function as a proxy for retention
|
|
|
|
|
lines = compressed.split("\n")
|
|
|
|
|
in_validate = False
|
|
|
|
|
in_dead = False
|
|
|
|
|
validate_body = 0
|
|
|
|
|
dead_body = 0
|
|
|
|
|
for line in lines:
|
|
|
|
|
if "def validate_order" in line:
|
|
|
|
|
in_validate = True
|
|
|
|
|
in_dead = False
|
|
|
|
|
continue
|
|
|
|
|
elif "def _dead_helper" in line:
|
|
|
|
|
in_dead = True
|
|
|
|
|
in_validate = False
|
|
|
|
|
continue
|
|
|
|
|
elif line.startswith("def ") or (line.startswith("class ") and ":" in line):
|
|
|
|
|
in_validate = False
|
|
|
|
|
in_dead = False
|
|
|
|
|
continue
|
|
|
|
|
if in_validate and line.strip() and not line.strip().startswith('"""'):
|
|
|
|
|
validate_body += 1
|
|
|
|
|
if in_dead and line.strip() and not line.strip().startswith('"""'):
|
|
|
|
|
dead_body += 1
|
|
|
|
|
|
|
|
|
|
assert validate_body >= dead_body
|
|
|
|
|
|
|
|
|
|
def test_omitted_comment_includes_calls(self):
|
|
|
|
|
"""Omitted comment includes call information when available."""
|
|
|
|
|
compressor = self._make_compressor()
|
|
|
|
|
result = compressor.compress(_payment_processing_code(), language="python")
|
|
|
|
|
|
|
|
|
|
# process_payment calls validate_order, charge_customer, generate_receipt
|
|
|
|
|
# These should appear in the omitted comment
|
|
|
|
|
compressed = result.compressed
|
|
|
|
|
if "lines omitted" in compressed:
|
|
|
|
|
# Find omitted comments and check for calls info
|
|
|
|
|
for line in compressed.split("\n"):
|
|
|
|
|
if "process_payment" not in line and "lines omitted" in line:
|
|
|
|
|
continue
|
|
|
|
|
if "lines omitted; calls:" in line:
|
|
|
|
|
assert "validate_order" in line or "charge_customer" in line
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
def test_semantic_analysis_disabled(self):
|
|
|
|
|
"""When semantic_analysis=False, all functions get uniform compression."""
|
|
|
|
|
compressor_with = self._make_compressor(semantic_analysis=True)
|
|
|
|
|
compressor_without = self._make_compressor(semantic_analysis=False)
|
|
|
|
|
|
|
|
|
|
code = _payment_processing_code()
|
|
|
|
|
result_with = compressor_with.compress(code, language="python")
|
|
|
|
|
result_without = compressor_without.compress(code, language="python")
|
|
|
|
|
|
|
|
|
|
# Without semantic analysis, no symbol scores
|
|
|
|
|
assert result_without.symbol_scores == {}
|
|
|
|
|
|
|
|
|
|
# With semantic analysis, dead code is compressed more aggressively
|
|
|
|
|
# _dead_helper body should NOT appear with semantic analysis
|
|
|
|
|
assert "range(100)" not in result_with.compressed
|
|
|
|
|
# But with uniform compression (no semantic), body lines ARE kept
|
|
|
|
|
assert "x = 1" in result_without.compressed
|
|
|
|
|
|
|
|
|
|
def test_summary_includes_semantic_info(self):
|
|
|
|
|
"""Summary includes semantic analysis information."""
|
|
|
|
|
compressor = self._make_compressor()
|
|
|
|
|
result = compressor.compress(_payment_processing_code(), language="python")
|
|
|
|
|
|
|
|
|
|
summary = result.summary
|
|
|
|
|
if result.symbol_scores:
|
|
|
|
|
low_count = sum(1 for s in result.symbol_scores.values() if s < 0.1)
|
|
|
|
|
if low_count > 0:
|
|
|
|
|
assert "low-importance" in summary
|
|
|
|
|
|
|
|
|
|
def test_dunder_methods_get_boost(self):
|
|
|
|
|
"""Dunder methods (__init__, etc.) get importance boost."""
|
|
|
|
|
compressor = self._make_compressor()
|
|
|
|
|
code = '''
|
|
|
|
|
class MyClass:
|
|
|
|
|
"""A class."""
|
|
|
|
|
def __init__(self, value):
|
|
|
|
|
"""Initialize."""
|
|
|
|
|
self.value = value
|
|
|
|
|
self.processed = False
|
|
|
|
|
self.results = []
|
|
|
|
|
self.cache = {}
|
|
|
|
|
self.errors = []
|
|
|
|
|
for i in range(10):
|
|
|
|
|
self.results.append(i)
|
|
|
|
|
|
|
|
|
|
def _setup_cache(self):
|
|
|
|
|
"""Internal setup."""
|
|
|
|
|
x = 1
|
|
|
|
|
y = 2
|
|
|
|
|
z = 3
|
|
|
|
|
result = x + y + z
|
|
|
|
|
for i in range(10):
|
|
|
|
|
result += i
|
|
|
|
|
return result
|
|
|
|
|
'''
|
|
|
|
|
result = compressor.compress(code, language="python")
|
|
|
|
|
|
|
|
|
|
# __init__ should score higher than _setup_cache
|
|
|
|
|
if "__init__" in result.symbol_scores and "_setup_cache" in result.symbol_scores:
|
|
|
|
|
assert result.symbol_scores["__init__"] > result.symbol_scores["_setup_cache"]
|
|
|
|
|
|
|
|
|
|
def test_javascript_importance(self):
|
|
|
|
|
"""Symbol importance works for JavaScript code."""
|
|
|
|
|
compressor = self._make_compressor()
|
|
|
|
|
code = """
|
|
|
|
|
import { db } from './database';
|
|
|
|
|
|
|
|
|
|
function processUser(userId) {
|
|
|
|
|
const user = fetchUser(userId);
|
|
|
|
|
const profile = buildProfile(user);
|
|
|
|
|
sendNotification(user.email, profile);
|
|
|
|
|
logAction('process', userId);
|
|
|
|
|
updateMetrics('user_processed');
|
|
|
|
|
return { user, profile };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function fetchUser(id) {
|
|
|
|
|
const result = db.query('SELECT * FROM users WHERE id = ?', [id]);
|
|
|
|
|
if (!result) {
|
|
|
|
|
throw new Error('User not found');
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildProfile(user) {
|
|
|
|
|
const prefs = loadPreferences(user.id);
|
|
|
|
|
return { ...user, preferences: prefs };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function _internalDebug(msg) {
|
|
|
|
|
const ts = Date.now();
|
|
|
|
|
const formatted = `[${ts}] DEBUG: ${msg}`;
|
|
|
|
|
console.log(formatted);
|
|
|
|
|
return formatted;
|
|
|
|
|
}
|
|
|
|
|
"""
|
|
|
|
|
result = compressor.compress(code, language="javascript")
|
|
|
|
|
|
|
|
|
|
assert result.symbol_scores
|
|
|
|
|
# fetchUser is called by processUser — should score higher than _internalDebug
|
|
|
|
|
if "fetchUser" in result.symbol_scores and "_internalDebug" in result.symbol_scores:
|
|
|
|
|
assert result.symbol_scores["fetchUser"] > result.symbol_scores["_internalDebug"]
|
|
|
|
|
|
|
|
|
|
def test_syntax_still_valid_with_importance(self):
|
|
|
|
|
"""Compressed output with importance remains syntactically valid."""
|
|
|
|
|
compressor = self._make_compressor()
|
|
|
|
|
result = compressor.compress(_payment_processing_code(), language="python")
|
|
|
|
|
|
|
|
|
|
assert result.syntax_valid is True
|
|
|
|
|
|
|
|
|
|
# Should be parseable as Python
|
|
|
|
|
try:
|
|
|
|
|
compile(result.compressed, "<test>", "exec")
|
|
|
|
|
except SyntaxError:
|
|
|
|
|
pytest.fail("Semantic compression produced invalid Python syntax")
|
|
|
|
|
|
|
|
|
|
def test_empty_code_no_crash(self):
|
|
|
|
|
"""Importance analysis handles empty code gracefully."""
|
|
|
|
|
compressor = self._make_compressor()
|
|
|
|
|
result = compressor.compress("", language="python")
|
|
|
|
|
|
|
|
|
|
assert result.symbol_scores == {}
|
|
|
|
|
|
|
|
|
|
def test_config_default_semantic_analysis_enabled(self):
|
|
|
|
|
"""semantic_analysis is True by default in config."""
|
|
|
|
|
config = CodeCompressorConfig()
|
|
|
|
|
assert config.semantic_analysis is True
|
fix(code): verify a real parse in tree-sitter availability check (#1231) (#1299)
## Description
`is_tree_sitter_available()` / `_check_tree_sitter_available()` in
`headroom/transforms/code_compressor.py` return `True` based on
importing `tree_sitter_language_pack` alone, without ever constructing a
parser or attempting a parse. When the installed pack/parser combination
is ABI-incompatible, `get_parser`/`parse` raises at runtime; the caller
catches it and silently falls back to the lossy text compressor, while
the availability flag and startup banner still report code-aware as on.
This is the defensive half that the `<1.0` pin in #1234 does not cover:
if that cap is ever lifted, the availability signal silently lies again.
Follow-up to #1231.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- Make `_check_tree_sitter_available()` construct a parser and parse a
tiny snippet, returning `True` only if it yields a real `module` AST
instead of trusting an import.
- Add `_tree_sitter_importable()` for the cheap import-only probe, and
use it to guard parser construction so the real-parse check cannot
recurse.
- Add tests asserting the check is `False` when parsing raises and
`True` on a real parse, plus that AST compression runs for python/rust
without falling back.
## Testing
- [ ] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
# pytest tests/test_transforms/test_code_compressor.py -> passed locally (tree-sitter-language-pack 0.13.0)
# ruff check . and ruff format --check . pass locally on the rebased branch.
# Full pytest suite / mypy not run locally; left to CI.
```
## Real Behavior Proof
- Environment: local repo on tree-sitter-language-pack 0.13.0,
tree-sitter 0.25.2, Python 3.12, Linux
- Exact command / steps: call `is_tree_sitter_available()`, then run
`pytest tests/test_transforms/test_code_compressor.py`
- Observed result: with a working pack the probe parses and returns
`True` (code-aware runs, strategy `CODE_AWARE` rather than the kompress
fallback); the new
`test_check_tree_sitter_available_false_when_parse_broken` confirms that
when parsing raises the check now returns `False` instead of the old
import-only `True`, so the lossy fallback is no longer entered silently.
- Not tested: reproducing the specific ABI-incompatible 1.x pack combo
against a live install (covered instead by a mocked broken parse in the
test)
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
2026-06-23 12:00:57 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# Regression: tree-sitter ABI mismatch (real AST must run, no silent fallback)
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.skipif(not TREE_SITTER_INSTALLED, reason="tree-sitter grammar pack not installed")
|
|
|
|
|
class TestRealASTRuns:
|
|
|
|
|
"""Guards against the regression where the code-aware compressor silently
|
|
|
|
|
fell back to a lossy stripper because ``_get_parser`` built a stock
|
|
|
|
|
``tree_sitter.Parser`` and assigned it a foreign grammar-pack ``Language``
|
|
|
|
|
(raising ``TypeError`` that was swallowed into a fallback).
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def _compressor(self):
|
|
|
|
|
return CodeAwareCompressor(
|
|
|
|
|
CodeCompressorConfig(
|
|
|
|
|
min_tokens_for_compression=10,
|
|
|
|
|
enable_ccr=False,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_get_parser_returns_stock_node_api(self):
|
|
|
|
|
"""The parser must yield nodes with the stock tree_sitter property API
|
|
|
|
|
that the tree-walking code relies on (``.type``/``.children``/...)."""
|
|
|
|
|
from headroom.transforms.code_compressor import _get_parser
|
|
|
|
|
|
|
|
|
|
parser = _get_parser("python")
|
|
|
|
|
tree = parser.parse(b"def foo(x):\n return x + 1\n")
|
|
|
|
|
root = tree.root_node
|
|
|
|
|
|
|
|
|
|
# Property access (NOT method calls) — the old pack binding exposed
|
|
|
|
|
# methods like ``.kind()`` which would break every call site.
|
|
|
|
|
assert root.type == "module"
|
|
|
|
|
assert root.child_count >= 1
|
|
|
|
|
assert isinstance(root.children, list)
|
|
|
|
|
|
|
|
|
|
func = root.children[0]
|
|
|
|
|
assert func.type == "function_definition"
|
|
|
|
|
assert isinstance(func.start_byte, int)
|
|
|
|
|
assert isinstance(func.end_byte, int)
|
|
|
|
|
# start_point must be index-able like a (row, col) tuple.
|
|
|
|
|
assert func.start_point[0] == 0
|
|
|
|
|
assert b"def foo" in func.text
|
|
|
|
|
|
|
|
|
|
def test_check_tree_sitter_available_verifies_real_parse(self):
|
|
|
|
|
"""``_check_tree_sitter_available`` must only return True when an actual
|
|
|
|
|
parse succeeds — not merely when the package imports."""
|
|
|
|
|
import headroom.transforms.code_compressor as cc
|
|
|
|
|
|
|
|
|
|
cc._tree_sitter_available = None # reset memoized result
|
|
|
|
|
assert cc._check_tree_sitter_available() is True
|
|
|
|
|
|
|
|
|
|
def test_check_tree_sitter_available_false_when_parse_broken(self):
|
|
|
|
|
"""If parsing raises (e.g. the old foreign-Language bug), availability
|
|
|
|
|
must report False instead of green-lighting the broken path."""
|
|
|
|
|
import headroom.transforms.code_compressor as cc
|
|
|
|
|
|
|
|
|
|
cc._tree_sitter_available = None
|
|
|
|
|
with patch.object(cc, "_get_parser", side_effect=TypeError("boom")):
|
|
|
|
|
assert cc._check_tree_sitter_available() is False
|
|
|
|
|
cc._tree_sitter_available = None # reset for other tests
|
|
|
|
|
|
|
|
|
|
def test_ast_runs_for_python_no_fallback(self):
|
|
|
|
|
"""A supported language must be compressed via real AST, not the
|
|
|
|
|
UNKNOWN-language Kompress fallback."""
|
|
|
|
|
result = self._compressor().compress(_payment_processing_code(), language="python")
|
|
|
|
|
|
|
|
|
|
# The fallback path forces language=UNKNOWN and syntax_valid=False.
|
|
|
|
|
# Real AST keeps the detected language and guarantees valid syntax.
|
|
|
|
|
assert result.language == CodeLanguage.PYTHON
|
|
|
|
|
assert result.syntax_valid is True
|
|
|
|
|
compile(result.compressed, "<test>", "exec")
|
|
|
|
|
|
|
|
|
|
def test_ast_preserves_structure_for_python(self):
|
|
|
|
|
"""AST output retains signatures/scopes/imports (unlike the old
|
|
|
|
|
whitespace garble)."""
|
|
|
|
|
code = (
|
|
|
|
|
"import math\n"
|
|
|
|
|
"\n"
|
|
|
|
|
"def compute(values):\n"
|
|
|
|
|
" total = 0\n"
|
|
|
|
|
" for v in values:\n"
|
|
|
|
|
" total += v * v\n"
|
|
|
|
|
" total -= 1\n"
|
|
|
|
|
" total *= 2\n"
|
|
|
|
|
" total //= 3\n"
|
|
|
|
|
" return math.sqrt(total)\n"
|
|
|
|
|
)
|
|
|
|
|
result = self._compressor().compress(code, language="python")
|
|
|
|
|
|
|
|
|
|
assert result.language == CodeLanguage.PYTHON
|
|
|
|
|
assert result.syntax_valid is True
|
|
|
|
|
# Structure markers survive compression.
|
|
|
|
|
assert "import math" in result.compressed
|
|
|
|
|
assert "def compute(values):" in result.compressed
|
|
|
|
|
# Output is still valid Python.
|
|
|
|
|
compile(result.compressed, "<test>", "exec")
|
|
|
|
|
|
fix(code): slice tree-sitter byte offsets as UTF-8 (#1332)
## Description
CodeAwareCompressor was slicing Python strings with tree-sitter
`start_byte` / `end_byte` offsets directly. That works for ASCII-only
files, but it corrupts slices after non-ASCII source text such as CJK
characters or emoji because tree-sitter offsets are UTF-8 byte offsets
while Python string indexes are character offsets.
This caused code-aware compression to produce invalid intermediate
Python and then safely fall back to the original file, resulting in 0%
compression on affected files.
Closes #1319
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- Added `_slice_code_bytes()` in
`headroom/transforms/code_compressor.py` to slice source text using
UTF-8 byte offsets.
- Updated `_get_node_text()` to use byte-safe slicing.
- Routed the other direct tree-sitter byte-offset slices through the
same helper.
- Added regression tests in
`tests/test_transforms/test_code_compressor.py`:
- `test_get_node_text_uses_utf8_byte_offsets`
- `test_ast_compresses_python_after_non_ascii_source`
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ .venv/bin/python -m pytest tests/test_transforms/test_code_compressor.py
68 passed, 1 warning
$ .venv/bin/python -m ruff check headroom/transforms/code_compressor.py tests/test_transforms/test_code_compressor.py
All checks passed!
$ .venv/bin/python -m ruff format --check headroom/transforms/code_compressor.py tests/test_transforms/test_code_compressor.py
2 files already formatted
$ git diff --check
# no output
$ /tmp/headroom-1319-venv/bin/python -m mypy headroom
headroom/proxy/server.py:1186: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs [annotation-unchecked]
headroom/proxy/server.py:1257: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs [annotation-unchecked]
headroom/proxy/server.py:1261: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs [annotation-unchecked]
pyproject.toml: note: unused section(s): module = ['mlx.*']
Success: no issues found in 394 source files
```
## Real Behavior Proof
- Environment: macOS arm64, Python 3.14.5, tree-sitter 0.25.2,
tree-sitter-language-pack 0.13.0
- Exact command / steps: On `main`, ran a local reproducer with a Python
source string containing a CJK docstring before a second function;
called `_get_node_text()` on the second tree-sitter function node; ran a
full `CodeAwareCompressor.compress(...)` repro with non-ASCII module
text before an import and a compressible function; re-ran both repros on
this branch.
- Observed result: Before fix, `_get_node_text()` returned the wrong
slice (`'nd():\n return 2\n'` instead of `'def second():\n return 2'`)
and full compression fell back to the original file with
`compression_ratio: 1.0`; after fix, `_get_node_text()` returns the full
expected function slice and full compression succeeds with
`compression_ratio < 1.0`, `syntax_valid: True`, and does not return the
original.
- Not tested: Full repository test suite; live proxy/provider
integrations; Windows/Linux platform-specific behavior.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
## Screenshots (if applicable)
N/A
## Additional Notes
- Documentation was not updated because this is an internal bug fix with
no user-facing API or behavior change beyond restoring intended
compression.
- `CHANGELOG.md` was not updated because the fix is narrow and
issue-scoped; maintainers can advise if they want a changelog entry.
- The fix is intentionally small and targeted: it only changes how
tree-sitter byte offsets are converted back into Python source text,
without changing compression heuristics or language behavior.
2026-06-23 16:03:44 -04:00
|
|
|
def test_get_node_text_uses_utf8_byte_offsets(self):
|
|
|
|
|
"""tree-sitter byte offsets must not be sliced as Python str indexes."""
|
|
|
|
|
from headroom.transforms.code_compressor import _get_node_text, _get_parser
|
|
|
|
|
|
|
|
|
|
code = 'def first():\n """中文占位"""\n return 1\n\ndef second():\n return 2\n'
|
|
|
|
|
root = _get_parser("python").parse(code.encode("utf-8")).root_node
|
|
|
|
|
functions = [node for node in root.children if node.type == "function_definition"]
|
|
|
|
|
|
|
|
|
|
assert _get_node_text(functions[1], code) == "def second():\n return 2"
|
|
|
|
|
|
|
|
|
|
def test_ast_compresses_python_after_non_ascii_source(self):
|
|
|
|
|
"""CJK/emoji before a later function must not corrupt downstream slices."""
|
|
|
|
|
compressor = CodeAwareCompressor(
|
|
|
|
|
CodeCompressorConfig(
|
|
|
|
|
min_tokens_for_compression=1,
|
|
|
|
|
max_body_lines=2,
|
|
|
|
|
enable_ccr=False,
|
|
|
|
|
semantic_analysis=False,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
code = (
|
|
|
|
|
"def first():\n"
|
|
|
|
|
' """中文占位 with emoji 🔥."""\n'
|
|
|
|
|
" return 1\n"
|
|
|
|
|
"\n"
|
|
|
|
|
"def second():\n"
|
|
|
|
|
" values = []\n"
|
|
|
|
|
" for i in range(10):\n"
|
|
|
|
|
" values.append(i)\n"
|
|
|
|
|
" values.append(i * 2)\n"
|
|
|
|
|
" values.append(i * 3)\n"
|
|
|
|
|
" values.append(i * 4)\n"
|
|
|
|
|
" return sum(values)\n"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
result = compressor.compress(code, language="python")
|
|
|
|
|
|
|
|
|
|
assert result.language == CodeLanguage.PYTHON
|
|
|
|
|
assert result.syntax_valid is True
|
|
|
|
|
assert result.compression_ratio < 1.0
|
|
|
|
|
assert "def second():" in result.compressed
|
|
|
|
|
assert "中文占位" in result.compressed
|
|
|
|
|
compile(result.compressed, "<test>", "exec")
|
|
|
|
|
|
fix(code): verify a real parse in tree-sitter availability check (#1231) (#1299)
## Description
`is_tree_sitter_available()` / `_check_tree_sitter_available()` in
`headroom/transforms/code_compressor.py` return `True` based on
importing `tree_sitter_language_pack` alone, without ever constructing a
parser or attempting a parse. When the installed pack/parser combination
is ABI-incompatible, `get_parser`/`parse` raises at runtime; the caller
catches it and silently falls back to the lossy text compressor, while
the availability flag and startup banner still report code-aware as on.
This is the defensive half that the `<1.0` pin in #1234 does not cover:
if that cap is ever lifted, the availability signal silently lies again.
Follow-up to #1231.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- Make `_check_tree_sitter_available()` construct a parser and parse a
tiny snippet, returning `True` only if it yields a real `module` AST
instead of trusting an import.
- Add `_tree_sitter_importable()` for the cheap import-only probe, and
use it to guard parser construction so the real-parse check cannot
recurse.
- Add tests asserting the check is `False` when parsing raises and
`True` on a real parse, plus that AST compression runs for python/rust
without falling back.
## Testing
- [ ] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
# pytest tests/test_transforms/test_code_compressor.py -> passed locally (tree-sitter-language-pack 0.13.0)
# ruff check . and ruff format --check . pass locally on the rebased branch.
# Full pytest suite / mypy not run locally; left to CI.
```
## Real Behavior Proof
- Environment: local repo on tree-sitter-language-pack 0.13.0,
tree-sitter 0.25.2, Python 3.12, Linux
- Exact command / steps: call `is_tree_sitter_available()`, then run
`pytest tests/test_transforms/test_code_compressor.py`
- Observed result: with a working pack the probe parses and returns
`True` (code-aware runs, strategy `CODE_AWARE` rather than the kompress
fallback); the new
`test_check_tree_sitter_available_false_when_parse_broken` confirms that
when parsing raises the check now returns `False` instead of the old
import-only `True`, so the lossy fallback is no longer entered silently.
- Not tested: reproducing the specific ABI-incompatible 1.x pack combo
against a live install (covered instead by a mocked broken parse in the
test)
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
2026-06-23 12:00:57 +08:00
|
|
|
def test_ast_runs_for_rust_no_fallback(self):
|
|
|
|
|
"""A second supported language (Rust) also runs through real AST."""
|
|
|
|
|
code = (
|
|
|
|
|
"pub fn add(a: i64, b: i64) -> i64 {\n"
|
|
|
|
|
" let mut acc = a;\n"
|
|
|
|
|
" acc += b;\n"
|
|
|
|
|
" acc -= 0;\n"
|
|
|
|
|
" acc\n"
|
|
|
|
|
"}\n"
|
|
|
|
|
)
|
|
|
|
|
result = self._compressor().compress(code, language="rust")
|
|
|
|
|
|
|
|
|
|
assert result.language == CodeLanguage.RUST
|
|
|
|
|
assert result.syntax_valid is True
|
|
|
|
|
# Signature is preserved verbatim.
|
|
|
|
|
assert "pub fn add(a: i64, b: i64) -> i64" in result.compressed
|